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题目的核心考点:

  1. URL解析差异 :利用 @ 符号。 http://foo@127.0.0.1 对于浏览器或某些库, foo 会被解析为用户名,实际请求的是 127.0.0.1 。但一些简单的正则过滤可能只检查 :// 之后的部分是否包含黑名单IP。
  2. 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
  3. 域名重定向 :如果服务器会跟随302跳转,我们可以先请求一个自己控制的、返回302跳转到内网地址的页面。
  4. 协议利用 :尝试 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 攻击过程中可能遇到的问题

  1. SSRF过滤严格,所有内网IP和变形都被封

    • 尝试DNS重绑定 :如果服务器在第一次DNS解析后不验证IP,可以设置一个DNS记录,TTL极短,第一次解析返回一个允许的外网IP,第二次解析返回 127.0.0.1
    • 利用非HTTP协议 :如 file , gopher , dict 。有时过滤器只检查 http/https
    • 利用跳转 :如果服务器跟随302/301跳转,可以准备一个先返回允许IP,再通过 Location 头跳转到内网IP的页面。
  2. CRLF注入被转义或过滤

    • 尝试双重编码: %250d%250a %25 % 的编码)。
    • 尝试不同的注入位置:不只是路径,可能是 Host 头、 User-Agent 头等。
    • 观察服务器使用的是哪个HTTP库。 requests 库和 urllib 在处理URL和头部时行为有细微差别。
  3. 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代码,通常通过构造更复杂的 pickle opcode来实现。

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头的函数,这些函数通常会处理转义。
  • 防御不安全的反序列化
    • 避免使用 pickle :对于不可信的数据源,坚决不使用 pickle 。可以使用JSON、XML等更安全的序列化格式。
    • 使用白名单 :如果必须使用 pickle ,可以考虑使用 pickle.Unpickler 并重写 find_class 方法,严格限制可以反序列化的类。
    • 签名与加密 :对序列化后的数据进行签名或加密,确保其完整性和来源可信。

这道题将三个漏洞串联,完美展示了攻击者如何利用应用逻辑的薄弱点,层层递进,最终突破系统边界。解决它需要耐心、对细节的把握和对每个漏洞原理的透彻理解。当你最终构造出那个完美的Payload并看到 flag 弹出来的时候,那种成就感,正是CTF比赛最吸引人的地方。多练习这种综合性题目,对于在实际渗透测试工作中形成完整的攻击链思维,有着不可替代的价值。

更多推荐