Tokens - Pwn2Win

Description: Tokens was a python exploitation challenge during the 2016 Pwn2Win CTF competition held from 25-27 March. When solved the team was awarded 50 points. This challenge focused on being able to identify exploitable python code which would allow the competitor to take advantage of the running service. This challenge explored the vulnerability of misusing the python eval() method.


Introduction

Tokens was a fairly straight-forward challenge based around secure programming practices and involved a minimal amount of reversing. We were given a little bit of information about the program using a fixed value for the seed, and the source code.

Tokens

Points: 50

Category: Python Exploitation

Description:

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}.

Problem

Initially, our thought was to brute force the keyspace for the seed, because we knew it should be of a limited size, and had an idea of what characters it would contain based on the gentoken() function which produced the token for us. However, after several hours of trying to brute force the seed unsuccessfully, I revisted the source code and look for poor programming practices that didn’t initially jump out at me and then I found the eval() method. 1 So in order to test my solution locally I did two things:

  1. I created a test harness based on the original source code.
  2. Maintained a separate window with an interactive python session to test verify my inputs were valid python statements.

After having created these two testing windows I went to work trying to devise a way to take advantage of the eval() method. I found several resources online to include an blog by Ned Batchelder, Vipul Chaskar, and floyd’s IT Security. Which all of these blogs pointed out something very interesting. When eval() is used with the __import__2 python __builtin__ module it can become very dangerous. For example, You can do something like this which would provide you all the local variables in the current namespace:

eval("os.locals()")

Solution

In order to beat this challenge first you had to pass the validation() function which contained a blacklist of invalid characters based on your input. The list is contained the following ASCII characters which you had to avoid: " < > ! @ # $ % ^ & * - / 3 4 6 9 ? \ ` | ; { }. After passing the validation you had to pass the first if condition which ensured you at least provided the characters gen as your first argument. Last, the script allowed us to take advantage of the eval() method as described in the problem statement.

The input was crafted with gen first, then __import__ in order to dynamically import the os module, and last execute the system method so that we could get a bashshell. The entire command was constructed as such: gen __import__('os').system('bash'). Which allowed us to get our shell, cat the source code for tokens.py, and reveal the seed it was using. Score 50 points!

solution

Code Snippets

All code, solutions, and challenge materials are presented in this section in the case that anyone would like to try replicating our solution.

Tokens.py Source code

#!/usr/bin/python
# -*- coding: utf=8 -*-

from datetime import datetime

seed = "----------------------------------------"

def validation(input):
	err = int()
	input = str(input)
	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:
			err = 1
			break
		else:
			err = 0
	if not err: return 1
	else: return 0

def gentoken(dic, seed, date):
	pt1 = pow(dic.values()[0], 2)
	pt2 = int(date[2:4]) * int(date[4:]) + ~int(date[:2])
	pt3 = [ord(x) for x in seed if ord(x) % 2 == 0] * 4
	pt3 = pt3[1] * pt3[3] * pt3[3] * pt3[7] + pt3[-1] + pt3[-3] + pt3[-3] + pt3[-7] + sum(pt3[13:37])
	return abs(pt1-(pt2+pt3))


if __name__ == '__main__':
	print "\nUsage:"
	print "gen 'token serial number'\n"
	print "E.g.:"
	print "gen 2017\n"

	while True:
		try:
			var = raw_input(">>> ")
			if validation(var):
				tmp = var.split()
				cmd = tmp[0]
				serial = tmp[1]
				if cmd == "gen":
					tmp = eval("{" + "cmd" + ":" + serial + "}")
					if type(tmp.values()[0]) == int and len(str(tmp.values()[0])) == 4:
						now = datetime.now()
						date = "%02d%02d%02d" % (now.day, now.hour, now.minute)
						date = str(date)
						final = gentoken(tmp, seed, date)
						print "\nToken: %s" % final
						print "Valid until: %s:%s:59\n" % (now.hour, now.minute)
					else: continue

		except (KeyboardInterrupt, SystemExit): exit()
		except: continue

Local Testing Harness

seed = "123456789"

def validation(input):
    err = int()
    input = str(input)
    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:
        	err = 1
        	break
        else:
        	err = 0
        if not err:
            return 1
        else:
            return 0

if __name__ == '__main__':

    while True:
        try:
            var = raw_input(">>> ")
            if validation(var):
                tmp = var.split()
                print "Passed validation:\t" + str(tmp)
                cmd = tmp[0]
                serial = tmp[1]
                if cmd == "gen":
                    print "I think your command it: \t" + str(serial)
                    tmp = eval("{" + "cmd" + ":" + serial + "}")
                    print "Made it too far, but your eval input was: \t" + str(tmp)

                else: continue

        except (KeyboardInterrupt, SystemExit):
            exit()
        except:
            continue

Endnotes

  1. eval() is a __builtin__ module which allows a programmer to evaluate a python expression. It will also allow dynamic execution of arbitrary code objects such as compile(), locals(), and globals(). Read more here.

  2. import() is a __builtin__ module which according to the pydocs is not needed for everyday Python programming. It is invoked by the import statement and is usually only used in cases where you only know the name of a given module at runtime. What this also means is that we can potentially import any module we want from a CTF perspective - unless it is specifically left out or through sandboxing.