CTF实战:SSRF+CRLF+Python反序列化组合漏洞利用剖析
1. 题目场景与核心思路拆解
最近在复盘一道让我印象深刻的CTF题目,它把SSRF、CRLF和Python反序列化这三个看似独立的漏洞巧妙地串联在一起,形成了一个环环相扣的攻击链。这道题的精妙之处在于,它不仅仅考察了单个漏洞的利用,更考验选手对漏洞原理的深入理解以及如何将它们组合起来,实现从外网信息探测到内网服务攻击,最终完成代码执行的完整过程。对于想深入Web安全,特别是想搞懂漏洞组合拳的兄弟来说,这道题的价值远超普通签到题。
我们先来快速理解一下这三个漏洞在这道题里各自扮演什么角色。 SSRF ,即服务器端请求伪造,是我们的“敲门砖”。题目通常会给一个能对外发起请求的功能点,比如一个在线网页预览、一个转码服务或者一个获取远程图片的接口。我们的目标就是利用它,让服务器代替我们去访问它本来不该访问的内部网络资源,从而探测内网环境。 CRLF注入 ,全称是“回车换行注入”,在这里是我们的“变形器”。HTTP协议靠 \r\n 来分隔头部和正文,如果我们能在请求中注入这些控制字符,就能篡改HTTP请求,比如添加新的请求头。最后, Python反序列化 是我们的“终结技”。在内网某个服务中,我们找到了一个接收序列化数据的端点,通过构造恶意的序列化数据(Payload),触发其反序列化过程,最终实现远程代码执行,拿到我们梦寐以求的 flag 。
这道题的典型场景是:一个Web应用提供了一个URL访问功能(SSRF入口),但对外部请求做了一些过滤。我们需要绕过过滤,利用SSRF访问内网的某个服务(比如一个Redis或Memcached)。在构造SSRF请求时,通过CRLF注入,将一个精心构造的Python序列化数据作为请求的一部分(比如作为POST数据或特定的Header值),发送给内网服务。内网服务在反序列化这些数据时,就会执行我们预设的恶意代码。整个攻击链清晰体现了“突破边界 -> 内部探测 -> 协议利用 -> 代码执行”的渗透测试核心思路。
2. 漏洞原理深度剖析与利用条件
2.1 SSRF:从信息刺探到内网漫游
SSRF漏洞的本质是应用提供了从服务端发起网络请求的功能,但没有对用户输入的URL目标进行充分校验和限制。常见的触发点有:
curl_exec(),file_get_contents()等PHP函数。requests.get(),urllib.urlopen()等Python库的调用。- 一些在线功能如“转码”、“下载”、“预览”、“采集”。
在CTF中,SSRF的利用目标往往不是直接读文件,而是作为跳板。我们需要利用服务器作为代理,去扫描或攻击其内网中的其他服务。内网IP段(如 127.0.0.1 , 192.168.x.x , 10.x.x.x , 172.16.x.x-172.31.x.x )是首要目标。
绕过技巧 是SSRF题目的核心考点:
- URL解析差异 :利用
@符号。http://foo@127.0.0.1对于浏览器或某些库,foo会被解析为用户名,实际请求的是127.0.0.1。但一些简单的正则过滤可能只检查://之后的部分是否包含黑名单IP。 - IP地址变形 :
- 十进制IP:
http://2130706433等价于127.0.0.1。 - 八进制IP:
http://0177.0.0.1等价于127.0.0.1。 - 十六进制IP:
http://0x7f.0x0.0x0.0x1。 - IPv6缩写形式:
http://[::1]:80等价于127.0.0.1:80。
- 十进制IP:
- 域名重定向 :如果服务器会跟随302跳转,我们可以先请求一个自己控制的、返回302跳转到内网地址的页面。
- 协议利用 :尝试
file://,gopher://,dict://等协议。gopher协议尤其强大,它可以构造任意格式的TCP数据包,是攻击内网Redis、Memcached等服务的利器。dict协议可以快速探测端口是否开放。
注意 :在实际做题或测试时,一定要先
fuzz一下目标支持哪些协议,以及过滤规则到底是什么。盲目尝试效率很低。
2.2 CRLF注入:在HTTP流中“偷梁换柱”
CRLF代表回车(Carriage Return, \r , ASCII 0x0D)和换行(Line Feed, \n , ASCII 0x0A)。在HTTP协议中, \r\n\r\n 标志着请求头(或响应头)的结束和正文的开始。
漏洞产生的原因是,应用程序将用户输入的数据直接拼接到了HTTP请求头或响应头中,而没有对 \r 和 \n 进行转义或过滤。例如,一个设置 X-Forwarded-For 头的代码:
headers[‘X-Forwarded-For’] = user_input
如果 user_input 是我们可控的,并输入 127.0.0.1\r\nInjected-Header: evil ,那么最终发出的请求就会变成:
GET /target HTTP/1.1
Host: vulnerable.com
X-Forwarded-For: 127.0.0.1
Injected-Header: evil
...其他头部...
我们成功注入了一个新的头部 Injected-Header 。
在这道CTF题中,CRLF注入的妙用在于,它可能被用来在SSRF发出的请求中,“夹带”我们的攻击载荷。例如,题目可能将我们输入的URL的某个部分(如 Host 头或路径参数)直接拼接到发出的请求中。我们可以构造一个这样的URL: http://vuln-site/?url=http://127.0.0.1:6379/\r\nSET mykey myvalue\r\n 当服务器用类似 requests.get(url) 的方式请求时,如果处理不当,这个 \r\n 就会导致请求被分割,使得 SET mykey myvalue 被当作新的请求行或数据行发送给内网的Redis服务(端口6379)。这就实现了通过SSRF+CRLF向特定服务发送自定义指令。
2.3 Python反序列化:从数据到代码的“魔法”
Python的 pickle 模块用于序列化(将对象转化为字节流)和反序列化(将字节流还原为对象)。其危险性在于,反序列化过程会自动调用对象的 __reduce__ 方法。攻击者可以构造一个恶意的类,在其 __reduce__ 方法中返回一个可调用对象(如 os.system )和参数元组。当这个恶意对象被反序列化时, __reduce__ 返回的函数就会被执行。
一个最简单的攻击载荷生成代码如下:
import pickle
import os
class Evil:
def __reduce__(self):
# 反序列化时,会执行 os.system(‘whoami’)
return (os.system, (‘whoami’,))
evil = Evil()
payload = pickle.dumps(evil)
print(payload.hex()) # 输出十六进制形式的Payload
当存在漏洞的代码执行了 pickle.loads(payload) 时,就会执行 whoami 命令。
在CTF中,这个漏洞点可能隐藏得很深。它可能出现在:
- 一个接收
Cookie或POST数据并进行pickle.loads的接口。 - 一个使用
pickle作为缓存或会话存储的后端(如某些框架的配置)。 - 一个可以上传文件,并且后端会用
pickle.load读取文件内容的接口。
在这道组合题里,Python反序列化是最后一环。我们通过SSRF找到了内网的一个服务(比如一个用 Flask 写的管理接口),该接口接收某个参数并进行反序列化。然后,我们通过CRLF注入,在SSRF请求中,将我们生成的恶意 pickle 载荷作为那个参数的值传递过去,从而触发RCE。
3. 实战演练:一步步拆解与利用
假设我们拿到的题目是一个简单的Web界面,只有一个输入框,提示“输入URL以获取页面标题”。这很可能就是SSRF的入口。
3.1 第一步:信息收集与SSRF验证
首先,我们随便输入一个公网URL,如 http://httpbin.org/get ,查看返回。如果返回了该页面的内容或特定信息,说明存在服务器端请求。接着,尝试访问 http://127.0.0.1 或 http://localhost 。如果返回了错误信息(如连接被拒)或者是本机服务的默认页面(如nginx欢迎页),则证实了SSRF的存在,并且服务器可以访问回环地址。
我们需要探测内网有哪些服务。可以写一个简单的脚本,或者用Burp Suite的Intruder模块,对内网常见端口进行扫描。例如,扫描 127.0.0.1 的常见端口:
端口 6379 -> Redis
端口 27017 -> MongoDB
端口 11211 -> Memcached
端口 80/443 -> Web服务
端口 21 -> FTP
端口 22 -> SSH (通常只能探测是否开放)
假设我们发现 127.0.0.1:6379 是开放的,并且返回了 -ERR wrong number of arguments for ‘get‘ command 之类的错误,这强烈暗示着一个Redis服务。
3.2 第二步:绕过过滤与协议探测
题目不可能让我们直接访问 redis://127.0.0.1:6379 。我们需要尝试绕过。首先试试各种IP表示法。如果都被过滤了,考虑 URL编码 。例如,将 127.0.0.1 编码成 %31%32%37%2e%30%2e%30%2e%31 (每个字符的ASCII码十六进制)。有些过滤器可能只做一次解码,服务器端库会做第二次解码。
然后探测支持的协议。除了 http(s):// ,可以尝试 file:///etc/passwd 读文件, dict://127.0.0.1:6379/info 来探测Redis(如果支持dict协议,会返回Redis信息)。最重要的是 gopher:// 协议,它是一个万能协议,可以构造原始TCP数据包。攻击Redis的Payload通常通过Gopher协议发送。
3.3 第三步:构造CRLF注入点
现在我们需要找到在哪里注入CRLF。观察请求,服务器在发起请求时,可能会使用我们输入的URL来设置 Host 头,或者将URL的路径部分直接拼接到请求中。我们可以尝试输入: http://127.0.0.1:6379/%0d%0aINJECTED%0d%0a:6379/ 这里 %0d%0a 是 \r\n 的URL编码。发送后,我们查看服务器发出的实际请求(如果题目有回显,或者通过延时、报错来判断)。如果注入成功,我们可能看到Redis返回了关于 INJECTED 这个命令的错误。
我们的目标是向Redis发送能写入文件的命令。一个经典的利用方式是让Redis将数据写入Web目录,形成一个Webshell。但在这道题里,结合Python反序列化,可能有更直接的路径。也许内网有一个服务(端口5000)的某个接口(如 /admin/load )会 pickle.loads 接收到的 data 参数。
3.4 第四步:生成Python反序列化Payload并整合
我们先在本机生成一个能执行命令的 pickle 载荷。假设我们要读取 /flag 文件。
import pickle
import base64
import os
class Exploit:
def __reduce__(self):
# 注意:这里使用子进程模块更稳定,os.system可能受环境限制
import subprocess
return (subprocess.Popen, ((‘cat‘, ‘/flag‘),))
payload = pickle.dumps(Exploit())
# 通常需要base64或十六进制编码,以方便在HTTP请求中传输
b64_payload = base64.b64encode(payload).decode()
hex_payload = payload.hex()
print(“Base64:“, b64_payload)
print(“Hex:“, hex_payload)
现在,我们需要通过SSRF+CRLF,将一个HTTP POST请求发送到内网的 127.0.0.1:5000/admin/load ,并且POST数据中包含 data=<我们的payload> 。这需要精心构造Gopher协议的Payload,因为Gopher可以发送原始的HTTP报文。
构造一个Gopher的Payload相对复杂,但思路是:构建一个完整的HTTP POST请求字符串,将换行符 \r\n 替换成 %0d%0a ,然后作为Gopher URL的数据部分。
gopher://127.0.0.1:5000/_POST /admin/load HTTP/1.1%0d%0aHost: 127.0.0.1:5000%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length: <长度>%0d%0a%0d%0adata=<base64编码的pickle载荷>
我们需要计算 Content-Length ,即 data= 后面部分的长度。然后将整个字符串进行URL编码(主要是编码 % 本身,变成 %25 ),最终作为SSRF的输入URL。
3.5 第五步:发起攻击与获取Flag
将最终构造好的、包含Gopher Payload的URL,提交给题目最开始的SSRF输入点。服务器会代表我们,向 127.0.0.1:5000 发送一个HTTP POST请求,其中包含了恶意的 pickle 数据。内网的Python服务在 /admin/load 接口反序列化这个 data 参数时,就会触发我们的 Exploit 类的 __reduce__ 方法,从而执行 cat /flag 命令。
如果一切顺利,命令执行的结果可能会直接返回在HTTP响应中,也可能写入某个文件然后通过其他方式读取。我们需要根据题目的具体行为来判断。例如,如果 /admin/load 接口会将反序列化后对象的某个属性值返回,那么我们可能需要调整 __reduce__ ,让其执行命令并将结果存储到一个属性中。
4. 常见问题、调试技巧与防御建议
4.1 攻击过程中可能遇到的问题
-
SSRF过滤严格,所有内网IP和变形都被封 :
- 尝试DNS重绑定 :如果服务器在第一次DNS解析后不验证IP,可以设置一个DNS记录,TTL极短,第一次解析返回一个允许的外网IP,第二次解析返回
127.0.0.1。 - 利用非HTTP协议 :如
file,gopher,dict。有时过滤器只检查http/https。 - 利用跳转 :如果服务器跟随302/301跳转,可以准备一个先返回允许IP,再通过
Location头跳转到内网IP的页面。
- 尝试DNS重绑定 :如果服务器在第一次DNS解析后不验证IP,可以设置一个DNS记录,TTL极短,第一次解析返回一个允许的外网IP,第二次解析返回
-
CRLF注入被转义或过滤 :
- 尝试双重编码:
%250d%250a(%25是%的编码)。 - 尝试不同的注入位置:不只是路径,可能是
Host头、User-Agent头等。 - 观察服务器使用的是哪个HTTP库。
requests库和urllib在处理URL和头部时行为有细微差别。
- 尝试双重编码:
-
Python反序列化Payload不执行 :
- Python版本问题 :
pickle协议版本可能不同。生成Payload时指定协议版本:pickle.dumps(obj, protocol=0)(兼容性最好)或protocol=2(Python 2常用)。 - 导入路径问题 :
__reduce__中返回的函数必须是反序列化环境中可访问的。os.system是内置的,通常没问题。但像subprocess.Popen需要导入subprocess模块。稳妥起见,可以在__reduce__内部进行导入。 - 字符集与编码问题 :如果Payload通过HTTP传输,确保正确地进行URL编码或Base64编码,避免特殊字符被截断或误解。
- 目标环境限制 :可能存在
disable_functions或沙箱。尝试其他命令执行方式,如eval(‘__import__(“os”).system(“ls”)‘),但这需要目标能执行Python代码,通常通过构造更复杂的pickleopcode来实现。
- Python版本问题 :
4.2 调试与信息获取技巧
- 使用DNSLog或Burp Collaborator :在SSRF探测时,如果无法直接看到回显,可以尝试让服务器访问你的DNSLog域名(如
http://your-subdomain.dnslog.cn),通过查看DNS解析记录来判断请求是否成功发出。 - 利用时间盲注 :通过执行
sleep命令来判断漏洞是否存在。例如,在Redis中可以通过eval “os.execute(‘sleep 5‘)“ 0(如果Redis配置了Lua沙箱逃逸)或通过写入计划任务等方式引入延迟。 - 分步测试 :不要试图一步到位。先测试SSRF能否访问目标端口(通过返回错误信息)。再测试CRLF能否注入(通过注入一个简单的命令看是否有新错误)。最后再拼接完整的反序列化Payload。
4.3 从防御角度思考
理解攻击是为了更好的防御。针对这种组合漏洞:
- 防御SSRF :
- 白名单校验 :严格限制请求的目标URL,只允许访问预期的、已知安全的域名或IP。
- 禁用危险协议 :在代码层面或网络层面,禁止应用服务器发起
file://,gopher://,dict://,ftp://等非必要协议的请求。 - 使用内部解析器 :使用一个安全的、无法解析内网地址的URL解析库或服务。
- 网络隔离 :将可以发起外部请求的应用服务器部署在独立的DMZ区,严格限制其向内网发起连接的能力。
- 防御CRLF注入 :
- 严格输入验证 :在将用户输入拼接到HTTP头或任何使用CRLF作为分隔符的协议中之前,对
\r和\n进行过滤或转义。 - 使用安全的API :使用编程语言或框架提供的、安全的设置HTTP头的函数,这些函数通常会处理转义。
- 严格输入验证 :在将用户输入拼接到HTTP头或任何使用CRLF作为分隔符的协议中之前,对
- 防御不安全的反序列化 :
- 避免使用
pickle:对于不可信的数据源,坚决不使用pickle。可以使用JSON、XML等更安全的序列化格式。 - 使用白名单 :如果必须使用
pickle,可以考虑使用pickle.Unpickler并重写find_class方法,严格限制可以反序列化的类。 - 签名与加密 :对序列化后的数据进行签名或加密,确保其完整性和来源可信。
- 避免使用
这道题将三个漏洞串联,完美展示了攻击者如何利用应用逻辑的薄弱点,层层递进,最终突破系统边界。解决它需要耐心、对细节的把握和对每个漏洞原理的透彻理解。当你最终构造出那个完美的Payload并看到 flag 弹出来的时候,那种成就感,正是CTF比赛最吸引人的地方。多练习这种综合性题目,对于在实际渗透测试工作中形成完整的攻击链思维,有着不可替代的价值。
更多推荐
所有评论(0)