[write-up] HITCON CTF 2015 - Simple (Crypto 100)

From Oct 17 10:00 AM til Oct 18 10:00 PM the HITCON CTF Qualification round took place and I decided to have a look at some of the tasks together with my brother. We were not actively trying to solve problems for the whole time due to the fact that we were both kind of ill and needed some more rest than usual.
Nevertheless the contest was great fun and we really enjoyed the tasks we attempted to solve.
The tasks were from the categories misc, web, pwn, crypto, reverse, stego and forensics and the points awarded ranged from 1 to 500.

Simple - Crypto 100

This was the task description:

Become admin!


We were provided with a link to a web application as well its source code.

#!/usr/bin/env ruby

require 'sinatra/base'
require 'sinatra/cookies'
require 'openssl'
require 'json'

KEY = IO.binread('super-secret-key')
FLAG = IO.read('/home/simple/flag').strip

class SimpleApp < Sinatra::Base
  helpers Sinatra::Cookies

get '/' do
    auth = cookies[:auth]
    if auth
        c = OpenSSL::Cipher.new('AES-128-CFB')
        c.key = KEY
        c.iv = auth[0...16]
        json = c.update(auth[16..-1]) + c.final
        r = JSON.parse(json)
        if r['admin'] == true
          "You're admin! The flag is #{FLAG}"
          "Hi #{r['username']}, try to get admin?"
      rescue StandardError
        'Something wrong QQ'
<html><body><form action='/' method='POST'>
<input type='text' name='username'/>
<input type='password' name='password'/>
<button type='submit'>register!</button>

  post '/' do
    username = params['username']
    password = params['password']
    if username && password
      data = {
        username: username,
        password: password,
        db: 'hitcon-ctf'
      c = OpenSSL::Cipher.new('AES-128-CFB')
      c.key = KEY
      iv = c.random_iv
      json = JSON.dump(data)
      enc = c.update(json) + c.final
      cookies[:auth] = iv + enc
      redirect to('/')
      'Invalid input!'

Looking at the first part we can see that upon visiting the website the value from the cookie named "auth" is loaded and the application tries to decrypt it. The encryption method that was used is "AES-128-CFB" and the format should be JSON after the decryption.
The application then parses the JSON string and looks at the value for the key "admin".
If that is "true" the flag will be printed.
If anything goes wrong no flag will be presented to us.

The second part describes what happens when a POST request is issued to the application.
First the value of the "username" and "password" parameter are filled into a data structure together with the key "db" and the value "hitconf-ctf".
This data is then converted to valid JSON and encrypted with e secret key and 128bit AES CFB.
A random IV is used as well but it is prepended to the encrypted string and can be read from there later.

Finding the problem

We first had a look at the Wikipedia page for AES CFB.
There we can see the following image:
Source: https://upload.wikimedia.org/wikipedia/commons/f/fd/Cfb_encryption.png

Something interesting catched my eye right away.
Looking at the first ciphertext block I realized that this block is created by XORing the output of AES encryption (fueled by the IV and the secret key) with the plaintext.
That is like: (KEY°IV) XOR plain
If we XOR that with the first plaintext block again we receive:
(Key°IV) XOR plain XOR plain = (Key°IV)

We could now XOR our modified plaintext with the value we received in the step before to create our own valid ciphertext.
The only thing we had to make sure was that our plaintext was valid JSON.

Solving the task

We assumed that up until now the JSON part of the first block was:
{"username":"tes (we chose the username test)
So what we wanted was a string like:
{"admin" : true}
That is exactly 16 bytes and will therefor fit inside the first block just right.

Looking back at the first part of the source code we can see that we don't need the old JSON with username and password as the script does not check for either of them. So we could just replace the old block with our new one and skip the rest of the blocks.

So we took the second 16 byte block from our encrypted cookie value (the first one is the IV):
Took the hex of the original plaintext:
XORed them both
0x33a3d16290f81267a1c8b422b470d5aa ^ 0x7b22757365726e616d65223a22746573 = 0x4881a411f58a7c06ccad96189604b0d9
And then took the hex representation of our modified plaintext and XORed that with the value before:
0x4881a411f58a7c06ccad96189604b0d9 ^ 0x7b2261646d696e22203a20747275657d = 0x33a3c57598e31224ec97b66ce471d5a4

Together with the IV (the first 16 bytes from the cookie) we formed a new cookie value, now 32 byte long and sent a request to the application.
That worked just fine and we received a flag for this crypto challenge worth 100 points:


Denis Werner