Scope
The text that came as description with the CTF was the following:
Have you heard of the next big evolution in the calculator space? No more need for batteries, no more need for solar panels. Pure performance and the full set of features you need right from the convience of your terminal. Try it out now! P.S. There might even be a flag hidden for you to find.
Non-sufficient input sanitizing?
The provided archive contained 3 files:
jailed
├─ Dockerfile
├─ flag.txt
├─ server.py
Walkthrough
The sever.py
file is pretty short:
from pwn import *
s = server(1337)
variables = {}
def performCalculation(calculation:str):
if 'system' in calculation:
return "What are you doing with my calculator!?"
try:
code = compile(calculation, '<string>', 'eval')
return eval(code, {"__builtins__": None}, variables)
except Exception as e:
return "It seems like your formula was incorrect."
while True:
r:remote = s.next_connection()
r.sendline('''
******************************************
* *
* Welcome to the secure calculator! *
* *
******************************************
Please enter the calculation you would like to perform.
You can use the following operators: +, -, *, /, %, **, and ().
Enter exit to quit.
''')
while True:
r.send(b">>> ")
try:
calculation = r.recvline().decode().strip()
except EOFError:
break
if 'exit' in calculation:
r.sendline(b"Goodbye!")
r.close()
break
result = performCalculation(calculation)
r.sendline(f"Result: {result}".encode())
It seems that the user input goes straight to performCalculation()
and the only
criteria for the user input to be valid is that it can’t contain ‘system’.
After that, the user input is treated as a Python statement with eval().
Unfortunately, the global variable __builtins__
is set to None, which means we can’t
use any of the (who would’ve thought) built-in methods. So there is no simple open()
available. This also restricts us from using __import__()
or import
.
So we need a way around this to be able to read the flag.txt
. We have no control
over the variables
variable outside the eval context where we could do something
like store a function pointer to a built-in method.
What we do have access to, however, are basic data types. Basic datatypes in Python
inherit directly from the object
class and thanks to dunder-methods we can get a
reference to this class and all classes that inherit from object
in the current scope.
More specifically, with:
().__class__.__bases__[0].__subclasses__()
What this does is:
- create an empty tuple object
- get the tuple class
- get parent class (
object
) - show all classes which directly inherit from
object
This returns a really long list but the only interesting thing (which I found) is the element at position 264:
subprocess.Popen
This class is able to execute shell commands. The most straight forward choice of
shell command is probably cat flag.txt
. Executing this command with the subprocess
module would look something like this:
subprocess.Popen(["cat", "flag.txt"], stdout=subprocess.PIPE, text=True).communicate()
This statement does the following:
- execute
cat flag.txt
- wait for it to finish
- capture and return shell output in a tuple
subprocess.PIPE
is just a constant attribute which holds the value -1, so we can
shorten this. Putting it all together the final user input for the ‘calculation’ will
look like this:
().__class__.__bases__[0].__subclasses__()[264](['cat', 'flag.txt'], stdout=-1,text=True).communicate()
Which will return the flag as first element in a tuple :)