paulscallanjr / 247CTF-TryAndCatch

A writeup for 247CTF's "Try and Catch" challenge.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

247CTF "Try And Catch"

A writeup for 247CTF's "Try and Catch" challenge.

This challenge is a great showcase of what can happen when a developer does not disable/remove features that were never intended to be included with production build software.

Tools used:

  • Debian 11
  • Firefox ESR (for interface with the webapp)
  • Atom (for easy viewing/editing of Python scripts)
  • Terminator (for installing and navigating through Python modules)

The challenge:

challenge

We start off in a plaintext file that is the source code for the app we will be exploiting in this challenge.

target_app

A quick glance over the source code gleans that:

  • The app is written in Python.
  • The app is a Flask WSGI web app:
from flask import Flask, request
  • Appending /calculator to the URL will run the app instead of showing it's source:
@app.route('/calculator')
  • The app is using the Werkzeug WSGI library and the debugger for that library is turned on:
from werkzeug.debug import DebuggedApplication

...

app.wsgi_app = DebuggedApplication(app.wsgi_app, True)

...

if __name__ == "__main__":
    app.run(debug=True)

We could try to mess with the input of the calculator function to get an exception, but there is error handling present and we would have to trigger an exception other than a Value or Type error to get an unmanaged exception instead of the safe_cast() function just returning a None type object:

def safe_cast(val, to_type):
    try:
        return to_type(val)
    except (ValueError, TypeError):
        return None

If we can't get arround safe_cast()'s error handling, the None type object returned from safe_cast() will signal the flag() function to simply give us a message about the exception through flag()'s return and we won't ever get the exception from the debugger:

if None in (number_1, number_2, operation) or not operation in calculate:
        return "Invalid calculator parameters"

Instead of having to fight with safe_cast()'s sanitized input and error handling, we can just try to get access to the debugger's console directly and use it to help us produce an exception.

We noted earlier the debugger was left enabled and a quick look at this page from Werkzeug's documentation provides us with a good indicator that we are on the right track:

danger

What this tells us is if we get access to the debugger console we can execute arbitrary code on the server, so we might not have to trigger an exception to get the flag, and can instead look for it on the server's file system (more on this below)

We want to look over Werkzeug's code to find out more about how the debugger works as the documentation only shows ways of interaction through the source code. We can install Werkzeug using pip:

pip

To find out where pip has stored Werkzeug's module files we can use the Python interpreter to print out the contents of sys.path, which is the object Python uses to store various paths that are important to the Python installation:

pip_werkzeug

We are interested in the __init__.py inside the debug directory:

werkzeug_files

We can see in a comment at the top of the DebuggedApplication() class that there is a variable for a default URL path to the console:

class DebuggedApplication:
    """Enables debugging support for a given application::"""

...

""":param console_path: the URL for a general purpose console."""

If we scroll further down to where the variables are defined in the class we see console_path is by default set to /console:

def __init__(
        self,
        app: "WSGIApplication",
        evalex: bool = False,
        request_key: str = "werkzeug.request",
        console_path: str = "/console",
        console_init_func: t.Optional[t.Callable[[], t.Dict[str, t.Any]]] = None,
        show_hidden_frames: bool = False,
        pin_security: bool = True,
        pin_logging: bool = True,

Appending /console to the end of the site's URL, as if it were another application route like the /calculator, provides a Python interpreter prompt!

console

We can use this prompt to execute Python code on the machine hosting the app. Let's figure out what files are in our current working directory. We can run the ls command using the subprocess module, and redirect it's output to a print() function:

import subprocess;
command = subprocess.Popen(['ls'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT);
stdout,stderr = command.communicate();
print(stdout)

Note: Python code used in this write up is formatted as multi-line for easier reading, but was input to the interpreter as single line.

ls shows us there is a flag.txt file in our directory:

command1

We can now open the file as text and display it using another print() function:

flag = open("./flag.txt", "rt");
print(flag.read())

🥳 Flag obtained! 🎉

command2

Summary

The vulnerability we exploited in this challenge was Arbitrary Code Execution. This particular CTF was not difficult for me because I'm familiar with Python, but I decided to make a write up for it anyways because it was a fun challenge and it shows how security in place (the safe_cast() function being the security as it provides a sanitized input and would have prevented us from accessing the debugger by handling exceptions we could have thrown with some malformed input) was esentially rendered useless by allowing us access to the debugger console.

To mitigate this type of attack the developer could have disabled the debugger console for production builds. Furthermore, the Werkzeug documentation highly recommends developers set a pin to authenticate with before being allowed access to a debug console to help prevent this type of scenario from happening.

About

A writeup for 247CTF's "Try and Catch" challenge.

License:Do What The F*ck You Want To Public License