Boston Key Party (BkP) CTF is a challenging annual CTF organized by several Boston area university alums. It's a challenging CTF that has focused on exploitation, reversing, and cryptography in the past.

I have friends organizing it, so I gave their challenges a try and managed to solve a few.

Writeups

Simple Calc

(pwn, solved by 186)

what a nice little calculator! https://s3.amazonaws.com/bostonkeyparty/2016/b28b103ea5f1171553554f0127696a18c6d2dcf7
simplecalc.bostonkey.party 5400

I teamed up with @kierk for this challenge.

We're given an ELF64 binary. Opening it up in IDA and running the decompiler reveals to us:

int __cdecl main(int argc, const char **argv, const char **envp)  
{
  void *v3; // [email protected]
  int result; // [email protected]
  const char *v5; // [email protected]
  __int64 v6; // [email protected]
  char v7; // [sp+10h] [bp-40h]@14
  int v8; // [sp+38h] [bp-18h]@5
  int v9; // [sp+3Ch] [bp-14h]@1
  __int64 v10; // [sp+40h] [bp-10h]@4
  int i; // [sp+4Ch] [bp-4h]@4

  v9 = 0;
  setvbuf(stdin, 0LL, 2LL, 0LL);
  v3 = stdout;
  setvbuf(stdout, 0LL, 2LL, 0LL);
  print_motd(v3);
  printf((unsigned __int64)"Expected number of calculations: ");
  _isoc99_scanf((unsigned __int64)"%d");
  handle_newline("%d", &v9);
  if ( v9 <= 255 && v9 > 3 )
  {
    v5 = (const char *)(4 * v9);
    LODWORD(v6) = malloc(v5);
    v10 = v6;
    for ( i = 0; i < v9; ++i )
    {
      print_menu(v5);
      v5 = "%d";
      _isoc99_scanf((unsigned __int64)"%d");
      handle_newline("%d", &v8);
      switch ( v8 )
      {
        case 1:
          adds("%d");
          *(_DWORD *)(v10 + 4LL * i) = dword_6C4A88;
          break;
        case 2:
          subs("%d");
          *(_DWORD *)(v10 + 4LL * i) = dword_6C4AB8;
          break;
        case 3:
          muls("%d");
          *(_DWORD *)(v10 + 4LL * i) = dword_6C4AA8;
          break;
        case 4:
          divs("%d");
          *(_DWORD *)(v10 + 4LL * i) = dword_6C4A98;
          break;
        default:
          if ( v8 == 5 )
          {
            memcpy(&v7, v10, 4 * v9);
            free(v10);
            return 0;
          }
          v5 = "Invalid option.\n";
          puts("Invalid option.\n");
          break;
      }
    }
    free(v10);
    result = 0;
  }
  else
  {
    puts("Invalid number.");
    result = 0;
  }
  return result;
}

It seems the program is infact, a simple calc. The program can add, subtract, multiply, or divide for us. Looking deeper into the program's functions reveals that there's nothing really too interesting, summarized here:

  • print_motd - a simple 1 line printf, nothing exploitable
  • handle_newline - does getchar until a \0 or a \n, perhaps buggy, but doesn't seem exploitable
  • print_menu - simple printfs, nothing exploitable
  • adds, subs, muls, divs - do basic arithmetic of two arguments, the arguments are limited to > 39, for some reason. It seems you can cause integer overflows here perhaps. The interesting thing in these is that result of the functions seems to get put into the global data section, .bss.

The main function allows us 5 options, the 4 basic math operators, and a save and exit. The save and exit is particularly interesting. The save and exit option does a memcpy of global memory onto the stack. However, there was only 11 dwords allocated for the copy operation by the stack. If we ask main for more than 11 operations, prompted here: printf((unsigned __int64)"Expected number of calculations: ");, then we'll have a simple stack buffer overflow. With that, we can overwrite the return address, and initiate a ropchain.

There is one other interesting point here, and that's the call to free(). Initially we ran into the problem of free() failing, and causing a segmentation fault when we were playing with the program with over 11 operations. That's because we kept spamming the global memory with the values "40..41..42..43..44", and free() would try to free the memory located at 0x0000000000000059. We were stuck, but we guessed perhaps free() doesn't fail for 0 or null, and that turned out to be true, as documented here:

The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation. If ptr is a null pointer, no action occurs.

So we can simply make sure the 19th value copied onto the stack is 0x0000000000000000.

So let's create this ropchain, luckily the binary is gigantic, and has everything we need. Running ropgadget (also included in my sec-tools) on the binary produces for us an already complete ropchain:

ropgadget --binary b28b103ea5f1171553554f0127696a18c6d2dcf7 --ropchain  
> https://gist.github.com/eugenekolo/d07741d6e7b261b3c339#file-ropchain-py

Now, we have to get that chain onto the stack, and we're set. We have control of a global array in memory that gets copied onto the stack. Getting the exact bytes we want into that global array is a bit tricky. We have to use one of the operations of the calculator (sub is my favourite), with specially crafted operands. The result of that calculation will be put into the global array. This part is a bit tricky in that it takes 2 operations of the calculator to fill 64-bit blocks in memory that the program uses (due to it being a 64-bit elf) - the calculations return 32-bit numbers. So, we end up with a chain of operations that looks like this:

2          # Subtract operation  
4602868  
100        # Result = 4602768

2          # Subtract operation  
100  
100        # Result = 0

2          # Subtract operation  
4602868  
100        # Result = 4602768

2          # Subtract operation  
100  
100        # Result = 0

...

5 # Save and exit operation  

That will result in 0x0000000000463B90 and 0x0000000000463B90 quad-words in the global memory.

The global array is then copied onto the stack with a save and exit. The copying overwrites the return address on the stack, and begins our ropchain.

Now we have to somehow get the entire ropchain into memory with some calculator operations. I went with a low-tech pwning script for this exploitation. I wrote a pwn_script that outputted the above chain of operations, newlines included. When the pwn_header (to initiate 254 operations, and make sure free() frees 0x0) and the output of the pwn_script is copied into the remote session of the challenge, nc simplecalc.bostonkey.party 5400, a series of subtraction operations with different operands will occur. Finally a 5 will be passed to the remote session, and cause a save and exit. That will kick off the ropchain now in the .bss section, revealing to us this tasty shell:

$ ls
key  
run.sh  
simpleCalc  
simpleCalc_v2  
socat_1.7.2.3-1_amd64.deb  
$ cat key
BKPCTF{what_is_2015_minus_7547}  

OptiProxy

(web, solved by 115)

inlining proxy here http://optiproxy.bostonkey.party:5300

This was a fun challenge about a vulnerable Ruby program. Navigating to the webpage given brings us to a simple website that prompts us:

welcome to the automatic resource inliner, we inline all images go to /example.com to get an inlined version of example.com flag is in /flag source is in /source  

Unfortunately the flag is not as easy as just navigating to http://optiproxy.bostonkey.party:5300/flag.

Navigating to /source gives us the source code:

require 'nokogiri'  
require 'open-uri'  
require 'sinatra'  
require 'shellwords'  
require 'base64'  
require 'fileutils' 

set :bind, "0.0.0.0"  
set :port, 5300  
cdir = Dir.pwd 

get '/' do str = "welcome to the automatic resource inliner, we inline all images" str << " go to /example.com to get an inlined version of example.com" str << " flag  
is in /flag" str << " source is in /source" str end 

get '/source' do IO.read "/home/optiproxy/optiproxy.rb" end 

get '/flag' do str = "I mean, /flag on the file system... If you're looking here, I question" str << " your skills" str end 

get '/:url'  
do  
    url = params[:url] 
    main_dir = Dir.pwd 
    temp_dir = "" 
    dir = Dir.mktmpdir "inliner" Dir.chdir dir 
    temp_dir = dir 
    exec = "timeout 2 wget -T 2 --page-requisites #{Shellwords.shellescape url}" `#{exec}` 
    my_dir = Dir.glob ("**/") Dir.chdir my_dir[0] 
    index_file = "index.html" 
    html_file = IO.read index_file 
    doc = Nokogiri::HTML(open(index_file)) 
    doc.xpath('//img').each 
    do 
        |img| header = img.xpath('preceding::h2[1]').text 
        image = img['src'] 
        img_data = "" 
        uri_scheme = URI(image).scheme 
            begin if (uri_scheme == "http" or uri_scheme == "https") 
                url = image 
            else 
                url = "http://#{url}/#{image}" 
            end 
        img_data = open(url).read 
        b64d = "data:image/png;base64," + Base64.strict_encode64(img_data) 
        img['src'] = b64d rescue # gotta catch 'em all puts "lole" next end 
    end 
    puts dir FileUtils.rm_rf dir Dir.chdir main_dir doc.to_html 
end  

At first glance, you might think to try exploiting exec = "timeout 2 wget -T 2 --page-requisites #{Shellwords.shellescape url}, but I was unable to find anything online that is able to escape Shellwords.shellescape. I believe it's safe to assume, that's a well written module, with no known exploits, and finding a 0day in it, while possible, is probably outside the scope of the challenge.

Moving on to understanding what exactly the code is doing, it seems to follow these steps:

  1. Parse out the parameter given after /, e.g. example.com if navigated to http://optiproxy.bostonkey.party:5300/example.com
  2. Create a folder named inliner and change directory into it.
  3. Run wget to create a mirror of the website provided in (1)
  4. Parse index.html from the mirrored website, and for each img tag:
    1. Grab the src parameter of the img tag
      • If the src parameter's URI scheme is http or https, it's good to go
      • Else if the URI scheme isn't http or https, then form a string that does have http uri scheme
    2. Open the url string created in (4a), and read the data from it
    3. Base64 the image data read, and change the src parameter of the img tag to the base64 of the image data
  5. Return the inliner dir to the user of the web app, and clean up

Okay, so where's the exploit? First things first, I tried passing something more interesting than just a simple webpage as the url to the web app to inline, i.e. file://../../../../flag, meaning I navigated to http://optiproxy.bostonkey.party:5300/file://../../../../flag, but that just returns to us the error message saying flag isn't there. I then thought that what I actually need is a website with an img tag whose src parameter is file://../../../../flag, so I whipped that up quickly:

<html><body>  
<img src="file://../../../../flag" />  
</body></html>  

I served the webpage off of my remote server with python3 -m http.server for a quick HTTP server. Navigating over to http://optiproxy.bostonkey.party:5300/mywebsite.com:8000 gives me a blank screen. Checking the console and source code reveals the issue: Not allowed to load local resource: file://../flag

It looks like the web app did mirror my website, it just didn't actually open the file, because of the uri_scheme check, file != http. Okay, so we really do need something that'll pass the URI scheme check, back to Google to learn more about Ruby's uri_scheme leads me to http://sakurity.com/blog/2015/02/28/openuri.html, jackpot. Interesting, it seems that a URL like http:/foobar will pass the uri_scheme check, and if we can somehow have a folder named http: (note the :) then we have a remote file read (into /flag!). Now... how do we possibly make the folder named http:.

After some thinking, I realized the solution, just make our fake website have a subdirectory named http: that'll get included by the index.html, because the wget call in the webapp is 2 levels deep, this will work!

<html><body>  
<img src="./http:/hi" />  
<img src="http:/../../../../../../../../flag" />  
</body></html>  

Note that the 2nd src tag above, is to http:/, and not http://. Getting that website inlined, will cause the Ruby web application to create a directory structure with a folder named http: in it, and then open a file in reference to the folder http:, and return to us:

<html><body>  
<img src="data:image/png;base64,aGloaQo=">  
<img src="data:image/png;base64,QktQQ1RGe21heWJlIGRvbnQgaW5saW5lIGV2ZXJ5dGhpbmcufQo=">  
</body></html>  

Decode the 2nd base64:
BKPCTF{maybe dont inline everything.}