← all walkthroughs

Sink

Linux· Insane· Credential Access· Privilege Escalation
owned
2026-07-14
time to own
22m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The attacker enumerated three exposed services on <retired-instance-ip> — SSH (22), a Flask notes application proxied through HAProxy (5000), and Gitea 1.12.6 (3000) — then exploited a CL-TE HTTP request-smuggling vulnerability between the mismatched HAProxy 1.9.10 and Gunicorn 20.0.0 pair to capture the site administrator's live session cookie.

The hijacked session exposed admin notes that stored Gitea login credentials in plaintext; those credentials unlocked a repository whose commit history contained an accidentally committed RSA private key for the local account 'marcus', yielding shell access and the user flag.

From that foothold, the attacker queried the internal LocalStack AWS emulator and found that CloudWatch log events had recorded SecretsManager secret values reused verbatim as the OS password for a second local account, 'david'.

As david, the attacker located an encrypted deployment archive and used david's unrestricted KMS Decrypt access to recover it with the RSAES_OAEP_SHA_256 algorithm; the unpacked archive contained a root server credential, completing full system compromise.

Attack path — how the box was taken

Mapped three exposed services and identified the HAProxy→Gunicorn proxy chain, then Exploited CL-TE request smuggling to hijack the administrator's active session, then Read administrator notes via the hijacked session to recover plaintext Gitea credentials, then Extracted an accidentally committed SSH private key for 'marcus' from Gitea repository history, then Authenticated as 'marcus' with the stolen SSH key and captured the user flag, then Harvested SecretsManager credentials for 'david' from LocalStack CloudWatch logs, then Used david's unrestricted KMS access to decrypt a deployment archive containing the root credential, then Used the recovered server credential to obtain a root shell and capture the root flag.

1EnumerationService fingerprinting and banner grabbing
Mapped three exposed services and identified the HAProxy→Gunicorn proxy chain
A version-detection scan against <retired-instance-ip> revealed SSH on port 22 (OpenSSH 8.2p1), a Gitea 1.12.6 instance on port 3000, and a Flask/Gunicorn notes application on port 5000. HTTP response headers on port 5000 — specifically 'Via: haproxy' and 'Server: gunicorn/20.0.0' — exposed the exact software versions on both sides of the proxy, flagging the chain for HTTP desynchronisation testing.
Exact commands 2
Version-detect the three open ports.
nmap -sV -p 22,3000,5000 --open $TARGET
Confirm 'Via: haproxy' and 'Server: gunicorn/20.0.0' response headers, establishing the two-layer proxy chain and its exact versions.
curl -sI http://$TARGET:5000/
2ExploitationCL-TE HTTP Request Smuggling — session capture (CWE-444)
Exploited CL-TE request smuggling to hijack the administrator's active session
HAProxy 1.9.10 resolves request framing using Content-Length and silently ignores a Transfer-Encoding header that has been obfuscated with an embedded vertical-tab byte (0x0B). Gunicorn 20.0.0 honours the Transfer-Encoding header regardless, treating the request as chunked. By sending a single HTTP request that HAProxy forwarded in full by Content-Length while Gunicorn terminated early on the chunked zero-block, the attacker left a partial, attacker-controlled HTTP prefix queued on the backend connection. The next request to arrive on that connection — the administrator's periodic GET /notes/delete job — was prepended with that prefix, causing Gunicorn to echo the admin's full request (including Cookie header) back in the attacker-controlled response body. This yielded the administrator's live session cookie.
Hijacked admin session cookie beginning 'session=[REDACTED: recovered credential]' recovered; timed observation confirmed periodic admin cleanup job on the target connection.
3Credential AccessCredential harvesting from compromised admin session (T1552.001)
Read administrator notes via the hijacked session to recover plaintext Gitea credentials
With the captured admin session cookie the attacker authenticated to the port-5000 notes app as the administrator and retrieved private notes. One note stored Gitea login credentials in plaintext. These credentials granted authenticated access to the Gitea instance on port 3000.
Admin notes endpoint read with hijacked cookie; Gitea username and password recovered for subsequent repository enumeration.
4Credential Theft from Version ControlExposed private key in version control history (T1552.004)
Extracted an accidentally committed SSH private key for 'marcus' from Gitea repository history
Authenticated to Gitea with the recovered credentials, the attacker browsed the Key_Management repository and found that a developer had committed an RSA-3072 SSH private key for the local user 'marcus' in commit b01a6b7 at path .keys/dev_keys. Even though the file had been removed from the tip of the branch in a later commit, the object remained permanently accessible in the repository's git history — git never actually deletes committed objects.
kill-chain step 7: git --git-dir=/home/git/gitea-repositories/root/key_management.git show b01a6b7:.keys/dev_keys extracted the key; ssh-keygen confirmed fingerprint SHA256:[REDACTED: recovered credential]+Z6PAu/[REDACTED: recovered credential].
Exact commands 2
Clone the Key_Management repository with the admin credentials recovered from the notes app.
git clone http://<gitea-admin-user>:<gitea-admin-password>@$TARGET:3000/root/Key_Management.git /tmp/Key_Management
List all commits across all branches; locate the commit where the key was added then removed.
git -C /tmp/Key_Management log --all --oneline
5FootholdSSH authentication with stolen private key (T1078)
Authenticated as 'marcus' with the stolen SSH key and captured the user flag
The extracted private key authenticated directly to the SSH service on port 22 as 'marcus', giving an interactive shell and the user flag.
kill-chain step 7: ssh -i /tmp/marcus.key marcus@127.0.0.1 'id; hostname; cat /home/marcus/user.txt' returned uid=1000(marcus); user.txt read.
Exact commands 1
Authenticate with the recovered key; command yields the marcus user flag — represented here as <user.txt>.
ssh -i /tmp/marcus.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null marcus@$TARGET 'id; hostname; cat /home/marcus/user.txt'
6Lateral MovementCloud credential harvesting from application logs (T1552.005)
Harvested SecretsManager credentials for 'david' from LocalStack CloudWatch logs
From the marcus shell the attacker found a LocalStack AWS API emulator listening on 127.0.0.1:4566. Listing CloudWatch log groups and reading their event streams revealed that application log entries had captured AWS SecretsManager secret values in plaintext. The password embedded in those log events matched — and had been reused as — the OS-level password for the local account 'david', allowing a direct su to that account.
LocalStack endpoint at 127.0.0.1:4566 confirmed from marcus shell; CloudWatch log events contained SecretsManager secret values referencing david.
Exact commands 2
From the marcus SSH session — enumerate all CloudWatch log groups on the local LocalStack instance.
aws --endpoint-url=http://$LOOPBACK:4566 --no-sign-request logs describe-log-groups
Enumerate log streams inside each group to find those containing credential material.
aws --endpoint-url=http://$LOOPBACK:4566 --no-sign-request logs describe-log-streams --log-group-name <log-group-name>
7Privilege EscalationAbusing overpermissive KMS key policy to decrypt privileged material (T1552)
Used david's unrestricted KMS access to decrypt a deployment archive containing the root credential
As david, the attacker found an encrypted deployment bundle (servers.enc — a base64/gzip-packed archive) and identified two enabled KMS customer master keys in the Key_Management repository (IDs 804125db-bdf1-465a-a058-07fc87c0fad0 and 837a2f6e-e64c-45bc-a7aa-efa56a550401). David's IAM role on the LocalStack KMS service permitted unrestricted Decrypt calls. Calling kms:Decrypt with the correct key and the RSAES_OAEP_SHA_256 algorithm returned the plaintext archive; unpacking it yielded a servers.yml containing a root-level server credential.
KMS key IDs 804125db-bdf1-465a-a058-07fc87c0fad0 and 837a2f6e-e64c-45bc-a7aa-efa56a550401 identified in Key_Management repo; AWS key pair [REDACTED: recovered credential] referenced in david_kms_root.sh helper script; decrypted archive produced servers.yml with root credential.
Exact commands 2
From david's shell — list available KMS customer master keys.
aws --endpoint-url=http://$LOOPBACK:4566 --no-sign-request kms list-keys
Decrypt with the first enabled CMK and RSAES_OAEP_SHA_256 algorithm. If this key returns an error, retry with key ID 837a2f6e-e64c-45bc-a7aa-efa56a550401.
aws --endpoint-url=http://$LOOPBACK:4566 --no-sign-request kms decrypt --key-id 804125db-bdf1-465a-a058-07fc87c0fad0 --ciphertext-blob fileb://servers.enc --encryption-algorithm RSAES_OAEP_SHA_256 --output text --query Plaintext | base64 -d > /tmp/servers.dec
8Full CompromiseCredential reuse for root authentication (T1078.003)
Used the recovered server credential to obtain a root shell and capture the root flag
The credential recovered from the decrypted deployment archive was reused as the root account password on the host. The attacker authenticated as root and captured the root flag, completing full system compromise.
Root shell obtained via server credential from decrypted servers.yml; root.txt captured.
Exact commands 2
Enter the root server credential recovered from servers.yml when prompted.
su - root
Confirm uid=0(root) and capture the root flag — represented here as <root.txt>.
id && cat /root/root.txt

Attack patterns used

The transferable techniques behind the compromise.

CL-TE HTTP Request Smuggling — session captureExploitation

What it is

HAProxy 1.9.10 resolves request framing using Content-Length and silently ignores a Transfer-Encoding header that has been obfuscated with an embedded vertical-tab byte (0x0B). Gunicorn 20.0.0 honours the Transfer-Encoding header regardless, treating the request as chunked. By sending a single HTTP request that HAProxy forwarded in full by Content-Length while Gunicorn terminated early on the chunked zero-block, the attacker left a partial, attacker-controlled HTTP prefix queued on the backend connection. The next request to arrive on that connection — the administrator's periodic GET /notes/delete job — was prepended with that prefix, causing Gunicorn to echo the admin's full request (including Cookie header) back in the attacker-controlled response body. This yielded the administrator's live session cookie.

Why it works

Upgrade HAProxy to 2.2 or later, which rejects requests that contain both Content-Length and Transfer-Encoding and normalises framing before forwarding. Upgrade Gunicorn to 21.x or later. In the HAProxy configuration, add 'option http-server-close' to prevent connection reuse that carries a poisoned prefix, and add an ACL to reject any request presenting both headers: 'acl ambiguous_framing hdr_cnt(Transfer-Encoding) gt 0 hdr_cnt(Content-Length) gt 0' followed by 'http-request reject if ambiguous_framing'. Additionally, restrict the notes and comment endpoints to authenticated sessions only, limiting the pool of valid tokens available to submit a smuggling request.

Credential harvesting from compromised admin sessionCredential AccessT1552.001

What it is

With the captured admin session cookie the attacker authenticated to the port-5000 notes app as the administrator and retrieved private notes. One note stored Gitea login credentials in plaintext. These credentials granted authenticated access to the Gitea instance on port 3000.

Why it works

Immediately rotate the exposed Gitea credentials. Enforce a written policy prohibiting passwords, API keys, and tokens from being stored in notes, wikis, tickets, or any collaborative tool. Adopt a dedicated secrets manager (e.g., HashiCorp Vault or AWS Secrets Manager) for all shared service credentials; allow lookup by secret ID only. Consider adding a DLP rule or content scan on note creation that rejects content matching private-key or password patterns.

Exposed private key in version control historyCredential Theft from Version ControlT1552.004

What it is

Authenticated to Gitea with the recovered credentials, the attacker browsed the Key_Management repository and found that a developer had committed an RSA-3072 SSH private key for the local user 'marcus' in commit b01a6b7 at path .keys/dev_keys. Even though the file had been removed from the tip of the branch in a later commit, the object remained permanently accessible in the repository's git history — git never actually deletes committed objects.

Why it works

Immediately revoke and replace the SSH key for marcus. Rewrite the repository history to remove the secret object using git-filter-repo ('git filter-repo --path .keys/dev_keys --invert-paths'), force-push the rewritten history, and require all existing clones to be re-cloned from the cleaned remote. Install a pre-commit secret-scanning hook (Gitleaks or truffleHog) and add the same scanner to the CI pipeline so any future private-key or high-entropy-string commit is blocked before it reaches the remote.

Cloud credential harvesting from application logsLateral MovementT1552.005

What it is

From the marcus shell the attacker found a LocalStack AWS API emulator listening on 127.0.0.1:4566. Listing CloudWatch log groups and reading their event streams revealed that application log entries had captured AWS SecretsManager secret values in plaintext. The password embedded in those log events matched — and had been reused as — the OS-level password for the local account 'david', allowing a direct su to that account.

Why it works

Audit all logging code to ensure secret values, passwords, and tokens are never written to log streams: log only the secret's ARN or a redacted indicator. Enable CloudWatch Logs encryption with a dedicated KMS key and restrict GetLogEvents permissions to the minimum required roles using IAM least-privilege policies. Enforce a principle of unique credentials: AWS SecretsManager secrets must never be reused as local OS account passwords. Rotate the exposed david credential immediately.

Abusing overpermissive KMS key policy to decrypt privileged materialPrivilege EscalationT1552

What it is

As david, the attacker found an encrypted deployment bundle (servers.enc — a base64/gzip-packed archive) and identified two enabled KMS customer master keys in the Key_Management repository (IDs 804125db-bdf1-465a-a058-07fc87c0fad0 and 837a2f6e-e64c-45bc-a7aa-efa56a550401). David's IAM role on the LocalStack KMS service permitted unrestricted Decrypt calls. Calling kms:Decrypt with the correct key and the RSAES_OAEP_SHA_256 algorithm returned the plaintext archive; unpacking it yielded a servers.yml containing a root-level server credential.

Why it works

Immediately rotate the root server credential recovered from servers.yml. Update the KMS key policy to restrict kms:Decrypt to a specific, named service-account IAM principal (the deployment scheduled operations role) and explicitly deny it for all interactive user accounts. Review all key policies with 'aws kms get-key-policy --key-id <id> --policy-name default' and remove overly broad Allow statements. Store the root credential outside of any artifact whose decryption key is accessible to non-root principals — consider envelope encryption with a hardware-backed key whose policy requires MFA.

Findings

Eliminate the HAProxy→Gunicorn HTTP request-smuggling vulnerabilityCritical
HAProxy 1.9.10 and Gunicorn 20.0.0 disagree on which framing header takes precedence when a request carries both Content-Length and a Transfer-Encoding header. An attacker can exploit this disagreement to inject a hidden HTTP request that prepends itself to the next legitimate user's connection, stealing their session cookie without ever touching their browser.
Remove credentials from application notes and enforce a secrets management policyHigh
The site administrator stored Gitea login credentials as plaintext text in personal notes inside the web application. Once an attacker compromised the admin session — by any means — those credentials were recoverable with a single HTTP request.
Purge the committed SSH key from git history and prevent future secret commitsCritical
An SSH private key for the production account 'marcus' was pushed to the Key_Management repository in commit b01a6b7. Even after a subsequent commit removed the file from the working tree, the object remained permanently retrievable from git history — any clone of the repository could recover it with a single 'git show' command.
Sanitise CloudWatch log content and stop reusing AWS secrets as OS account passwordsHigh
Application code wrote AWS SecretsManager secret values — the actual plaintext passwords — into CloudWatch log event messages. Any user with access to the LocalStack endpoint could read those log streams and recover the credential. The same password was then reused as the OS-level password for the 'david' account, meaning a log-data disclosure became a full account takeover.
Restrict KMS key Decrypt permissions to authorised service roles only and rotate the exposed root credentialCritical
The KMS customer master keys protecting the deployment archive allowed the interactive user account 'david' to call kms:Decrypt without restriction. The decrypted archive contained the root server credential, so any account with Decrypt access could recover root privileges by design.

Exposed services

External surface