The Challenge


#!/usr/bin/env python3.10

import ast
import math
from typing import Union


def is_expression_safe(node: Union[ast.Expression, ast.AST]) -> bool:
    match type(node):
        case ast.Constant:
            return True
        case ast.List | ast.Tuple | ast.Set:
            return is_sequence_safe(node)
        case ast.Dict:
            return is_dict_safe(node)
        case ast.Name:
            return node.id == "math" and isinstance(node.ctx, ast.Load)
        case ast.UnaryOp:
            return is_expression_safe(node.operand)
        case ast.BinOp:
            return is_expression_safe(node.left) and is_expression_safe(node.right)
        case ast.Call:
            return is_call_safe(node)
        case ast.Attribute:
            return is_expression_safe(node.value)
        case _:
            return False


def is_sequence_safe(node: Union[ast.List, ast.Tuple, ast.Set]):
    return all(map(is_expression_safe, node.elts))


def is_dict_safe(node: ast.Dict) -> bool:
    for k, v in zip(node.keys, node.values):
        if not is_expression_safe(k) and is_expression_safe(v):
            return False
    return True


def is_call_safe(node: ast.Call) -> bool:
    if not is_expression_safe(node.func):
        return False
    if not all(map(is_expression_safe, node.args)):
        return False
    if node.keywords:
        return False
    return True


def is_safe(expr: str) -> bool:
    for bad in ['_']:
        if bad in expr:
            # Just in case!
            return False
    return is_expression_safe(ast.parse(expr, mode='eval').body)


if __name__ == "__main__":
    print("Welcome to SafetyCalc (tm)!\n"
          "Note: SafetyCorp are not liable for any accidents that may occur while using SafetyCalc")
    while True:
        ex = input("> ")
        if is_safe(ex):
            try:
                print(eval(ex))
            except Exception as e:
                print(f"Something bad happened! {e}")
        else:
            print("Unsafe command detected! The snake approaches...")
            exit(-1)

This Python script is gonna interpret python code with the eval function after doing some parsing with the AST module. The parsing is gonna make sure that every component of the evaluated expression is safe. Function calls are only considered safe if they are part of the math module. After reading the math documentation, we realized that there was no way of doing nasty things with any function so we decided to try to bypass the AST checks.

The Resolution

vuln_func_code

In this function, the if statement line 37 doesn’t take into account operator precedence in Python, since not has a higher precedence than and, not only applies to the key verification and not to the value verification. If it returns True, then the value of the dictionary is not gonna be checked. Thanks to that vulnerability, we can embed the open symbol in the dictionary.

vuln_func_code

Function calls are sanitized with a check on ast.Name values which checks any normal function call but not class methods that are considered by the AST module as ast.Attribute. Dereferencing a dictionary by calling directly the value is an ast.Subscript type of operation which returns False. We then have to call dictionary.get to get open and then call it on flag.txt. Once the file is opened, we simply need to add the read method to get the content of it.

vuln_func_code