跳到主要内容 SQL 注入攻击原理、案例与防御方案 | 极客日志
SQL java
SQL 注入攻击原理、案例与防御方案 SQL 注入是 Web 安全中最常见的漏洞之一,通过拼接用户输入破坏 SQL 结构。本文阐述了其原理、危害及在 Java、Hibernate、MyBatis 中的代码示例。分析了 OFCMS 实际案例,并提供了参数化查询、全局过滤等修复方案,帮助开发者理解风险并实施防御。
极客零度 发布于 2025/2/7 更新于 2026/4/19 1 浏览1.1 SQL 注入攻击原理
SQL 注入漏洞可以说是在企业运营中会遇到的最具破坏性的漏洞之一,它也是目前被利用得最多的漏洞。要学会如何防御 SQL 注入,首先我们要学习它的原理。
针对 SQL 注入的攻击行为可描述为通过在用户可控参数中注入 SQL 语法,破坏原有 SQL 结构,达到编写程序时意料之外结果的攻击行为。其成因可以归结为以下两个原因叠加造成的:
程序编写者在处理应用程序和数据库交互时,使用字符串拼接的方式构造 SQL 语句。未对用户可控参数进行足够的过滤便将参数内容拼接进入到 SQL 语句中。注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件:用户能够控制输入。原本程序要执行的代码,拼接了用户输入的数据。
1.2 SQL 审计方法
手动找的话,可以直接找到 sqlmapper.xml 文件或者直接搜索 select、update、delete、insert "String sql="等关键词,定位 SQL xml 配置文件。
如果 sql 语句中有出现 $ 进行参数拼接,则存在 SQL 注入风险。
当找到某个变量关键词有 SQL 注入风险时,可以再根据调用链找到该存在注入风险的业务逻辑代码,查看参数来源是否安全、是否有配置 SQL 危险参数过滤的过滤器,最终确认是否存在 SQL 注入。以下给出可能造成 sql 注入攻击的关键字,审计时可根据实际情况进项查找
常见 SQL 语句关键词
SELECT
UPDATE
DELETE
INSERT
1.3 SQL 注入漏洞危害
1、攻击者可以做到
业务运营的所有数据被攻击
对当前数据库用户拥有的所有表数据进行增、删、改、查等操作
若当前数据库用户拥有 file_priv 权限,攻击者可通过植入木马的方式进一步控制 DB 所在服务器
若当前数据库用户为高权限用户,攻击者甚至可以直接执行服务器命令从而通过该漏洞直接威胁整个内网系统
2、可能对业务造成的影响
用户信息被篡改
攻击者偷取代码和用户数据恶意获取
线上代码被非法篡改,并造成为恶意攻击者输送流量或其他利益的影响
1.4 SQL 注入漏洞代码示例
Java 代码动态构建 SQL
Statement stmt = null ;
ResultSet rs = null ;
try {
String userName = ctx.getAuthenticatedUserName();
String sqlString = "SELECT * FROM t_item WHERE owner='" + userName + "' AND itemName='" + request.getParameter("itemName" ) + "'" ;
stmt = connection.createStatement();
rs = stmt.executeQuery(sqlString);
} (SQLException se) {
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
SQL 美化和格式化 在线格式化和美化您的 SQL 查询(它支持各种 SQL 方言)。 在线工具,SQL 美化和格式化在线工具,online
SQL转CSV/JSON/XML 解析 INSERT 等受限 SQL,导出为 CSV、JSON、XML、YAML、HTML 表格(见页内语法说明)。 在线工具,SQL转CSV/JSON/XML在线工具,online
catch
这里将查询字符串常量与用户输入进行拼接来动态构建 SQL 查询命令。仅当 itemName 不包含单引号时,这条查询语句的行为才会是正确的。如果一个攻击者以用户名 wiley 发起一个请求,并使用以下条目名称参数进行查询:
SELECT * FROM t_item WHERE owner = 'wiley' AND itemname = 'name' OR 'a' ='a'
此处,额外的 OR 'a'='a'条件导致整个 WHERE 子句的值总为真。那么,这个查询便等价于如下非常简单的查询:
这个简化的查询使得攻击者能够绕过原有的条件限制:这个查询会返回 items 表中所有储存的条目,而不管它们的所有者是谁,而原本应该只返回属于当前已认证用户的条目。
CallableStatement cs = null ;
ResultSet results = null ;
try {
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
cs = connection.prepareCall("{call sp_queryItem(?,?)}" );
cs.setString(1 , userName);
cs.setString(2 , itemName);
results = cs.executeQuery();
} catch (SQLException se) {
}
CREATE PROCEDURE sp_queryItem
@userName varchar (50 ),
@itemName varchar (50 )
AS
BEGIN
DECLARE @sql nvarchar(500 );
SET @sql = 'SELECT * FROM t_item WHERE owner = ''' + @userName + ''' AND itemName = ''' + @itemName + '''' ;
EXEC (@sql );
END
GO
在存储过程中,通过拼接参数值来构建查询字符串,和在应用程序代码中拼接参数一样,同样是有 SQL 注入风险的。
Hibernate 动态构建 SQL/HQL String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
Query sqlQuery = session.createSQLQuery("select * from t_item where owner = '" + userName + "' and itemName = '" + itemName + "'" );
List<Item> rs = (List<Item>) sqlQuery.list();
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
Query hqlQuery = session.createQuery("from Item as item where item.owner = '" + userName + "' and item.itemName = '" + itemName + "'" );
List<Item> hrs = (List<Item>) hqlQuery.list();
即使是使用 Hibernate,如果在动态构建 SQL/HQL 查询时包含了不可信输入,同样也会面临 SQL/HQL 注入的问题。
HQL 代码中,session.createQuery 使用 HQL 语句将查询到的数据存到 list 集合中,需要时在拿出来使用。而参数中 itemName 是通过 request.getParameter 直接获取。攻击者若在此处写入恶意语句,程序将恶意语句查询出来的数据存放在 list 集合中,再通过某处调用成功将数据显示在前台。
Mybatis 注入分析 Mybatis 框架下易产生 SQL 注入漏洞的情况主要分为以下三种:
例如对人员姓名检索进行模糊查询,如果考虑安全编码规范问题,其对应的 SQL 语句如下:
Select * from user where name like '%#{name}%'
但由于这样写程序会报错,研发人员将 SQL 查询语句修改如下:
Select * from user where name like '%${name}%'
在这种情况下我们发现程序不再报错,但是此时产生了 SQL 语句拼接问题,如果 java 代码层面没有对用户输入的内容做处理势必会产生 SQL 注入漏洞。
例如对人员姓名进行同条件多值检索的时候,如当用户输入 001,002,003…时,如果考虑安全编码规范问题,其对应的 SQL 语句如下:
Select * from name where id in (#{id})
但由于这样写程序会报错,研发人员将 SQL 查询语句修改如下:
Select * from name where id in (${id})
修改 SQL 语句之后,程序停止报错,但是却引入了 SQL 语句拼接的问题,如果没有对用户输入的内容做过滤,势必会产生 SQL 注入漏洞。
当根据姓名、id 序号等信息用户进行排序的时候,如果考虑安全编码规范问题,其对应的 SQL 语句如下:
Select * from user where name = 'qihoo' order by #{id} desc
但由于发布时间 id 不是用户输入的参数,无法使用预编译。研发人员将 SQL 查询语句修改如下:
Select * from user where name = 'qihoo' order by ${id} desc
修改之后,程序未通过预编译,但是产生了 SQL 语句拼接问题,极有可能引发 SQL 注入漏洞。
1.5 实战案例-OFCMS SQL 注入漏洞分析 本文中使用 ofcms 进行 SQL 注入漏洞讲解,此 CMS 算是对新手学习代码审计比较友好的 CMS。
后台页面:http://localhost:8080/ofcms-admin/admin/index.html
public void create () {
try {
String sql = getPara("sql" );
Db.update(sql);
rendSuccessJson();
} catch (Exception e) {
e.printStackTrace();
rendFailedJson(ErrorCode.get("9999" ), e.getMessage());
}
}
上述代码中使用 getpara 获取 sql 的参数值,并 update,跟进一下 getpara 和 update 方法。
跳转至 jfinal-3.2.jar/com/jfinal/core/controller.class
public String getPara (String name) {
return this .request.getParameter(name);
}
上述代码无特殊用意,就是获取参数值,继续跟进 Db.update 方法。
跳转至 jfinal-3.2.jar/com/jfinal/plugin/activerecord/Db.class
public static int update (String sql) {
return MAIN.update(sql);
}
跳转至 jfinal-3.2.jar/com/jfinal/plugin/activerecord/DbPro.class
public int update (String sql) {
return this .update(sql, DbKit.NULL_PARA_ARRAY);
}
public int update (String sql, Object... paras) {
Connection conn = null ;
int var4;
try {
conn = this .config.getConnection();
var4 = this .update(this .config, conn, sql, paras);
} catch (Exception var8) {
throw new ActiveRecordException (var8);
} finally {
this .config.close(conn);
}
return var4;
}
Object 是所有类的基类,而 Object... 是不确定方法参数情况下的一种多态表现形式 (可以传递多个参数)。
int update (Config config, Connection conn, String sql, Object... paras) throws SQLException {
PreparedStatement pst = conn.prepareStatement(sql);
config.dialect.fillStatement(pst, paras);
int result = pst.executeUpdate();
DbKit.close(pst);
return result;
}
至此,整个功能流程结束,在我们跟进的过程中,代码中无任何过滤语句,获取参数值,调用 update 方法更新,更新成功后返回结果。
漏洞验证 update of_cms_topic set topic_url= updatexml(1 ,concat(0x7e ,(user ())),0 ) where topic_id = 1
根据如上截图可看出我们传入的 SQL 语句是被完整的接收,并未做任何过滤直接带入数据库执行,所以此处直接写入漏洞代码爆出当前数据库账户为 root。
1.6 漏洞修复方法
添加全局过滤器,过滤特殊字符
PreparedStatement 参数化 如果使用参数化查询,则在 SQL 语句中使用占位符表示需在运行时确定的参数值。参数化查询使得 SQL 查询的语义逻辑被预先定义,而实际的查询参数值则等到程序运行时再确定。参数化查询使得数据库能够区分 SQL 语句中语义逻辑和数据参数,以确保用户输入无法改变预期的 SQL 查询语义逻辑。
在 Java 中,可以使用 java.sql.PreparedStatement 来对数据库发起参数化查询。在这个正确示例中,如果一个攻击者将 itemName 输入为 name' OR 'a' = 'a,这个参数化查询将免受攻击,而是会查找一个 itemName 匹配 name' OR 'a' = 'a 这个字符串的条目。
PreparedStatement stmt = null ;
ResultSet rs = null ;
try {
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
String sqlString = "SELECT * FROM t_item WHERE owner=? AND itemName=?" ;
stmt = connection.prepareStatement(sqlString);
stmt.setString(1 , userName);
stmt.setString(2 , itemName);
rs = stmt.executeQuery();
} catch (SQLException se) {
}
存储过程参数化 这个存储过程使用参数化查询,而未包含不安全的动态 SQL 构建。数据库编译此存储过程时,会生成一个 SELECT 查询的执行计划,只允许原始的 SQL 语义被执行。任何参数值,即使是被注入的 SQL 语句也不会被执行,因为它们不是执行计划的一部分。
CallableStatement cs = null ;
ResultSet results = null ;
try {
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
cs = connection.prepareCall("{call sp_queryItem(?,?)}" );
cs.setString(1 , userName);
cs.setString(2 , itemName);
results = cs.executeQuery();
} catch (SQLException se) {
}
Hibernate 参数化查询 Hibernate 支持 SQL/HQL 参数化查询。为了防止 SQL 注入以及改善性能,以上这些示例使用了参数化绑定 的方式来设置查询参数。
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
Query hqlQuery = session.createQuery("from Item as item where item.owner = ? and item.itemName = ?" );
hqlQuery.setString(1 , userName);
hqlQuery.setString(2 , itemName);
List<Item> rs = (List<Item>) hqlQuery.list();
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
Query hqlQuery = session.createQuery("from Item as item where item.owner = :owner and item.itemName = :itemName" );
hqlQuery.setString("owner" , userName);
hqlQuery.setString("itemName" , itemName);
List<Item> rs = (List<Item>) hqlQuery.list();
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName" );
Query sqlQuery = session.createSQLQuery("select * from t_item where owner = ? and itemName = ?" );
sqlQuery.setString(0 , owner);
sqlQuery.setString(1 , itemName);
List<Item> rs = (List<Item>) sqlQuery.list();
MyBatis 框架的修复方案 尽量使用 #描述参数,如果一定要使用 $,则需要自己过滤用户输入
按照新闻标题对新闻进行模糊查询,可将 SQL 查询语句设计如下:
select * from news where name like concat('%' ,#{name }, '%' )
采用预编译机制,避免了 SQL 语句拼接的问题,从根源上防止了 SQL 注入漏洞的产生。
在对新闻进行同条件多值查询的时候,可使用 Mybatis 自带循环指令解决 SQL 语句动态拼接的问题:
select * from news where id in < foreach collection= "ids" item= "item" open = "(" separator= "," close = ")"> #{item} < / foreach>
在 Java 层面做映射预编译机制只能处理查询参数,其他地方还需要研发人员根据具体情况来解决。如前面提到的排序情景:
Select * from news where title = '淘宝' order by #{time } asc ,
这里 time 不是查询参数,无法使用预编译机制,只能这样拼接:
Select * from news where title = '淘宝' order by ${time } asc
针对这种情况研发人员可以在 java 层面做映射来进行解决。如当存在发布时间 time 和点击量 click 两种排序选择时,我们可以限制用户只能输入 1 和 2。
当用户输入 1 时,我们在代码层面将其映射为 time,当用户输入 2 时,将其映射为 click。而当用户输入 1 和 2 之外的其他内容时,我们可以将其转换为默认排序选择 time(或者 click)。