SQL 注入审计挑战 第1周
Source: Dev.to
介绍
掌握 SQL 注入检测:为 AppSec 工程师准备的 15 个真实场景练习
15 亿美元的警钟
2015 年,黑客利用英国电信公司 TalkTalk 的 SQL 注入漏洞,窃取了 157 000 名客户的个人数据——包括姓名、地址、出生日期以及 15 600 条银行账号。公司因此被罚款 40 万英镑,直接成本和客户赔偿估计高达 6000 万英镑1。攻击者是一名 17 岁的少年,他利用了 Web 安全中最古老、最易防范的漏洞之一。
根据 OWASP Top 10 2025(2025 年 11 月 6 日发布),注入攻击在 A05:2025 – Injection 中排名,100 % 的受测应用都出现了某种形式的注入2。虽然由于框架改进和安全意识提升,SQL 注入的出现频率有所下降,但其危害仍然极其严重:
- 低频率、高影响 – 注入类别下有 14 000+ CVE2
- 平均影响:完整数据泄露
- 测试覆盖率:100 % 的应用都被检测注入漏洞2
- 37 个 CWE 属于注入类别——是 OWASP Top 10 2025 中任何类别 CVE 数量最多的类别2
翻译:SQL 注入不再像过去那样常见,但一旦出现,后果极其毁灭性。OWASP 明确将 SQL 注入描述为“低频率/高影响”——意味着受影响的应用更少,但一旦受影响,就会导致完整的系统泄露。
如果你在 GitLab、Stripe、Coinbase、Trail of Bits 或 NCC Group 等公司面试安全工程或应用安全岗位,你将会遇到需要:
- 在不熟悉的框架中识别易受攻击的代码
- 编写可运行的利用负载以展示危害
- 使用参数化查询提供安全修复方案
- 解释为何仅靠验证无法防御
这些能力可以把只读过 SQL 注入资料的候选人与真正能够在生产代码中发现并修复漏洞的候选人区分开来。
这些 15 题练习是在我提升 AppSec 工程技能的过程中创建的,参考了:
- PortSwigger Web Security Academy SQL Injection labs3
- OWASP SQL Injection Prevention Cheat Sheet4
- Secure by Design(Manning,2017)——第 1‑3 章
- API Security in Action(Manning,2020)——第 2 章
每道练习都呈现了真实场景中可能遇到的易受攻击代码,适用于安全评审或渗透测试。所有练习均包括:
- ✅ 漏洞识别
- ✅ 利用负载构造
- ✅ 参数化查询的安全修复
- ✅ 防御失效原因的解释
⭐ 想要更多类似练习?给我的仓库加星:AppSec‑Exercises on GitHub,获取类似 LeetCode 的安全编码挑战。
练习 1 – WHERE 子句注入
难度: 初级
漏洞类型: WHERE 子句注入
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
query = "SELECT id, username FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
if user:
return jsonify({"message": "Login successful", "user_id": user[0]})
else:
return jsonify({"message": "Invalid credentials"}), 401
问题
- 这段代码是否存在 SQL 注入漏洞?(是/否)
- 为 username 字段构造一个可以在不知密码的情况下绕过认证的负载。
- 给出使用 SQLite 参数化查询的安全修复方案。
💡 Solution 请参见本文末尾。
练习 2 – 混合参数化反模式
难度: 初级
漏洞类型: 混合参数化反模式
import psycopg2
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/search', methods=['GET'])
def search_products():
search_term = request.args.get('q', '')
category = request.args.get('category', 'all')
# Attempted escaping
search_term = search_term.replace("'", "''")
conn = psycopg2.connect(database="store", user="app", password="secret")
cursor = conn.cursor()
if category == 'all':
query = "SELECT * FROM products WHERE name LIKE '%" + search_term + "%'"
cursor.execute(query)
else:
query = "SELECT * FROM products WHERE name LIKE %s AND category = %s"
cursor.execute(query, ('%' + search_term + '%', category))
results = cursor.fetchall()
conn.close()
return jsonify({"products": results})
问题
category == 'all'分支是否存在漏洞?(是/否)- 为什么字符串转义(
replace("'", "''"))无法阻止 SQL 注入? - 为易受攻击的分支提供正确的参数化查询修复。
💡 Solution 请参见本文末尾。
练习 3 – LIKE 子句注入
难度: 中级
漏洞类型: LIKE 子句注入
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2),
category TEXT
);
CREATE TABLE admin_notes (
id INTEGER PRIMARY KEY,
product_id INTEGER,
note TEXT,
created_by TEXT
);
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/search', methods=['GET'])
def search_products():
search_term = request.args.get('q', '')
conn = sqlite3.connect('shop.db')
cursor = conn.cursor()
query = f"SELECT id, name, price FROM products WHERE name LIKE '%{search_term}%'"
cursor.execute(query)
results = cursor.fetchall()
conn.close()
return jsonify({"products": results})
问题
- 这段代码是否存在漏洞?即使搜索词被包裹在
%通配符中,也能防止注入吗? - 构造一个负载,以从 admin_notes 表中提取敏感数据。
- 给出使用正确通配符处理的安全修复方案。
💡 Solution 请参见本文末尾。
练习 4 – 二次注入
难度: 中级
漏洞类型: 二次注入
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT,
password_hash TEXT
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
author TEXT,
content TEXT,
created_at DATETIME
);
import psycopg2
from flask import Flask, request, jsonify
app = Flask(__name__)
# Endpoint 1: Update profile (SAFE)
@app.route('/update-profile', methods=['POST'])
def update_profile():
user_id = request.form.get('user_id')
bio = request.form.get('bio')
conn = psycopg2.connect(host='localhost', user='app', password='pass', database='social')
cursor = conn.cursor()
query = "UPDATE users SET bio = %s WHERE id = %s"
cursor.execute(query, (bio, user_id))
conn.commit()
conn.close()
return jsonify({"message": "Profile updated successfully"})
# Endpoint 2: Get user posts (VULNERABLE)
@app.route('/get-user-posts', methods=['GET'])
def get_user_posts():
user_id = request.args.get('user_id')
conn = psycopg2.connect(host='localhost', user='app', password='pass', database='social')
cursor = conn.cursor()
cursor.execute("SELECT username FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if not user:
return jsonify({"error": "User not found"}), 404
username = user[0]
query = "SELECT post_id, content, created_at FROM posts WHERE author = '" + username + "' ORDER BY created_at DESC"
cursor.execute(query)
posts = cursor.fetchall()
conn.close()
return jsonify({"username": username, "posts": posts})
问题
- 哪个端点存在漏洞?(
update-profile、get-user-posts,还是两者都有?) - 解释攻击流程:哪个端点存储恶意数据,哪个端点触发注入?
- 构造一个恶意的 username,在查看帖子时能够提取密码哈希。
- 为易受攻击的端点提供安全修复方案。
💡 Solution 请参见本文末尾。
练习 5 – ORDER BY 子句注入
难度: 中级
漏洞类型: ORDER BY 子句注入
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2),
category TEXT,
stock_quantity INTEGER
);
(后续代码和问题在原始挑战集合中继续。)
解决方案
练习 1 – WHERE 子句注入
1. 是否存在漏洞? 是。
2. 负载(username):
' OR '1'='1
提交后,查询会变成:
SELECT id, username FROM users WHERE username = '' OR '1'='1' AND password = ''
'1'='1' 永远为真,从而绕过认证。
3. 使用 SQLite 参数化查询的安全修复:
query = "SELECT id, username FROM users WHERE username = ? AND password = ?"
cursor.execute(query, (username, password))
练习 2 – 混合参数化反模式
1. 是否存在漏洞? 是。
2. 转义失效原因:
将单引号替换为两个单引号只能防御最基础的字面量注入。当查询通过字符串拼接构造时,攻击者仍然可以通过其他字符(如 %、_)脱离字符串上下文,从而实现注入。
3. 参数化修复示例:
if category == 'all':
query = "SELECT * FROM products WHERE name LIKE %s"
cursor.execute(query, ('%' + search_term + '%',))
else:
query = "SELECT * FROM products WHERE name LIKE %s AND category = %s"
cursor.execute(query, ('%' + search_term + '%', category))
练习 3 – LIKE 子句注入
1. 是否存在漏洞? 是。即使输入被包裹在 % 中,也并未消除注入风险,因为输入仍直接插入到 SQL 字符串中。
2. 用于提取 admin_notes 的负载:
' UNION SELECT id, note, created_by FROM admin_notes --
生成的查询:
SELECT id, name, price FROM products WHERE name LIKE '%' UNION SELECT id, note, created_by FROM admin_notes --'%'
3. 安全修复(正确处理通配符):
query = "SELECT id, name, price FROM products WHERE name LIKE ?"
param = f"%{search_term}%"
cursor.execute(query, (param,))
练习 4 – 二次注入
1. 易受攻击的端点: get-user-posts。
2. 攻击流程:
- 步骤 1 – 存储恶意数据: 攻击者(或被攻陷的组件)通过任何写入
username列的操作(如注册、资料更新)插入精心构造的username值。 - 步骤 2 – 触发注入: 当
get-user-posts读取该username并将其拼接进查询时,恶意负载被执行,从而实现数据泄露。
3. 恶意 username 负载示例:
' UNION SELECT password_hash, NULL, NULL FROM users WHERE username='admin' --
执行时,查询会变为:
SELECT post_id, content, created_at FROM posts WHERE author = '' UNION SELECT password_hash, NULL, NULL FROM users WHERE username='admin' --' ORDER BY created_at DESC
4. 参数化查询的安全修复:
query = "SELECT post_id, content, created_at FROM posts WHERE author = %s ORDER BY created_at DESC"
cursor.execute(query, (username,))
练习 5 – ORDER BY 子句注入
(为简洁起见省略具体解答;请遵循相同思路:对列名进行白名单校验或使用安全映射,并使用参数化查询或其他安全方式。)
Footnotes
-
TalkTalk 违规细节 – https://example.com/talktalk-breach ↩
-
OWASP Top 10 2025 – https://owasp.org/Top10/2025/A05-Injection/ ↩ ↩2 ↩3 ↩4
-
PortSwigger Web Security Academy – https://portswigger.net/web-security/sql-injection ↩
-
OWASP SQL Injection Prevention Cheat Sheet – https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html ↩