Challenge files HERE
Details
This is a challenge I created for TFCCTF 2022 and my intended solution.
Description
We managed to get access to the source code but we got disconnected before being able to download all of it. There has to be a way to get it…
Solution
If you play around with the application you’ll get a few prompts
Enter your name: Bob
What would you like to do?
1. Check balance
2. Work
3. Buy hint ($30)
4. Buy flag ($1337)
5. Exit
>>> [INPUT]
- Shows your current balance
- Work and get money. You can only work 6 times and each time you get between $5 and $10
- Prints a useless string for $30
- Prints the flag if you have $1337
- Exits
After looking at main.py
, one can see the lines
if choice == "42":
secret_debugger()
That use the following code
def safe_eval(code):
tree = compile(code, "<string>", 'exec', flags=ast.PyCF_ONLY_AST)
for x in ast.walk(tree):
if type(x) not in (ast.Module, ast.Expr, ast.Attribute, ast.Name, ast.Load):
return "Invalid operation"
return eval(code)
def secret_debugger():
while True:
try:
code = input("DEBUG>>> ")
print(safe_eval(code))
except Exception as x:
print(x)
break
safe_eval
is used to allow only reading variables, with a few more limitations.
Being able to read variables is enough to recreate the code.
First, get some information on controller
. By reading controller.__dict__
you get the class variables
{
"name": "Bob",
"money": 0,
"works": 0,
"encrypted_flag": "ENCRYPTED_FLAG_HERE"
}
This can be used to reconstruct the class locally
class Controller:
def __init__(self):
self.name = "ok"
self.money = 9999
self.works = 1
self.encrypted_flag = 'ENCRYPTED_FLAG_HERE'
controller = Controller()
As you can see, the flag is encrypted and there has to be a way to get it. If you check Controller.__dict__
(the class this time), you can see there is no decryption method, just the ones used in main.py
Making an educated guess, you should run the buy_flag
method.
Python code can be recreated using data found in the function.__code__
. You need to create a CodeType object with the data found. You can find the constructor parameters by running function.__code__.__doc__
locally (Note: in Python 3.10 this will not give the constructor)
code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,
flags, codestring, constants, names, varnames, filename, name,
firstlineno, lnotab[, freevars[, cellvars]])
Create a code object. Not for the faint of heart.
Another way to find the argument order is to check the python repository and find references like this test.
The data needed can be found using controller.buy_flag.__code__.co_ARGNAME
, where ARGNAME is the name of the argument. Exception is codestring
, that one being co_code
After creating the CodeType, you can to create a function from it using FunctionType(code_object, globals)
And finally run the function with the controller created earlier as parameter (as controller.buy_flag()
works like Controller.buy_flag(controller)
). This should return a string that can be printed.
Notes
If the code doesn’t work as expected locally (eg. gives the same string with any amount of money) use the same python version as the challenge. The Dockerfile is included as a hint to use Python 3.10 (or what is the latest version if run in the future)
I saw a more elegant solution by Tzlils (Solution) that recreated the Controller class, skipping the assumption that the constructor is trivial.