sql注入简单总结

最近两周刷了一下sqli-labs,对sql注入有了一个基本的认识。这里写个总结。

1.sql注入原理简单介绍
在一般的编程语言中,字符串是作为一种基本数据类型储存的,不会被编译器解析。但是有些函数会接受一个参数并将其解析为代码语句执行,如php中的eval()函数。有时我们需要接受用户的参数,并将其作为字符串的一部分传入类eval()函数(或类似的函数),若此时没有对用户参数进行严格过滤,就会产生各种安全漏洞,如js中的xss,php中eval()函数产生的webshell,以及数据库交互中产生的sql注入漏洞等。
很多编程语言都提供了与数据库进行交互的函数,如php中的mysql_query()/mysqli_query()函数,通过拼接用户传入的参数和特定语句构成sql查询语句。如

$un = $_POST['username'];
$pw = $_POST['password'];
$sql = "SELECT * FROM users WHERE name = '$un' AND password = '$pw'";
$result = mysql_query($sql);

以上语句即将用户传入的$id参数与select语句拼接构成sql语句后通过mysql_query()函数进行查询。
假设有一个恶意用户传入了参数username=admin&password=0' or 1=1#,而后端又没用对输入进行过滤/转义。拼接后的sql语句为"SELECT * FROM users WHERE name = 'admin' AND password = '0' or 1=1#'"'#'在sql中作为注释符使用。此时的sql语句中由于1=1恒为真,与前面语句进行or运算后仍恒为真,从而绕过了对password的校验实现了admin账号密码绕过。
在sql中除了#号外,-- (注意末尾有空格)也被视为注释符。有时页面以get方式提交数据,会直接写在url上。由于#在url中有特殊的含义(锚点),因此一般使用其url编码%23,后端接受到数据后会自动进行url解码。同样由于一般注释符都出现在变量的最后也就是url的最后,而url最末尾的空格在传输时会被略去,所以使用--+代替,+会被解码为空格。
另外在sql注入中,payload不一定是用户提交的参数,也可能在请求头中。需要根据具体的后端逻辑判断注入点。

2.注入类型判断

(1).注入类型

  • 数字型
    $sql = SELECT * FROM users WHERE id = $id

  • 单字符型
    $sql = SELECT * FROM users WHERE id = '$id'

  • 双字符型
    $sql = SELECT * FROM users WHERE id = "$id"

  • 加括号
    $sql = SELECT * FROM users WHERE id = ($id)

    $sql = SELECT * FROM users WHERE id = ('$id')

    $sql = SELECT * FROM users WHERE id = ("$id")

    $sql = SELECT * FROM users WHERE id = (($id))

    以上括号可以任意个

(2).类型判断

  • 三种基本类型判断
    一般通过直接在参数后接上引号来判断,如在单字符类型中,如果我们传入参数id=0',由于参数中的单引号闭合了变量前的引号,导致后面的引号未闭合,会产生查询报错。如果传入参数id=0",就不会产生报错。在页面查询错误和无查询结果回显不同时就可以通过这种方式来判断。
    若报错和无查询空白结果回显相同(如都返回空白页面),我们也可以通过在参数后加#号的方式来判断,如单字符中传入参数id=0' or 1=1#,参数中的引号会闭合变量前的引号,而变量后的引号由于被#号注释掉,不会产生查询报错,又由于or 1=1恒为真,因此会返回有效的查询结果。若不是单字符类型,0' or 1=1就全部被包含在字符串内不会报错,也不会查询到结果。但是也可能有注释符被过滤的情况。总是注入类型的判断不总是千篇一律,需要灵活应对。

  • 括号的判断
    判断括号前首先要判断出基本输入类型,这里假设为数值型。我们传入参数id=2 or 1=0。无括号时,sql语句为SELECT * FROM users WHERE id = 2 or 1=0会返回id=2的结果,有括号时,sql语句为SELECT * FROM users WHERE id = (2 or 1=0)这里由于括号内进行了或运算,2被作为布尔类型与1=0进行或运行会返回1,最后会返回id=1的查询结果。通过这种方式判断出有无括号后,再通过逐次添加括号的方式判断括号数量。
    这些只是注入类型最基础的判断方式,实际上我们可能遇到各种过滤/转义,需要具体分析,灵活应对。

3.数据库名表名列名获取

  • 系统库
    一般来说数据库软件中会有一个系统库储存了所有数据库信息。通过对这个库进行注入可获取这个软件里全部的数据库信息。这里我们以mysql为例。
    mysql中的information_schema库储存了所有数据库信息。其中的scehmata表的schema_name列储存了所有数据库名;tables表的table_name储存了所有列名,table_schema储存了其对应的库名;columns表的column_name储存所有列名,table_name对应其表名,table_schema对应其库名。
    关于这部分,攻防世界中有个题叫NewsCenter,就是通过对information_schema库的查询进而获取表名列名最终获取flag。这题中是通过联合查询的方式获取信息的,联合查询union语句是最简单的sql注入方式,可以同时查询多个表中的信息,具体会在下面介绍。

4.简单的注入方式介绍

  • 联合查询注入
    联合查询利用的是union语句,union语句可以同时查询多个表但是只会返回第一个查询结果。所以我们需要令第一个查询结果为空,让其返回我们构造的union语句查询到的结果。同时union查询语句查询的列数要和第一个查询语句相同,所以我们要首先判断其查询列数。
    判断查询列数一般用order by语句。order by用于对查询结果进行排序,如order by id就是根据id进行排序。但order by后面不仅可以跟列名/别名也可以跟数字,代表第几列。在SELECT id,name,pass FROM users ORDER BY 2中查询结果会根据name值进行排序。如果这里换成order by 4就会产生报错因为查询结果并没有第4列。通过这种方式就可以判断出第一个查询语句的查询列数。有时候order被过滤/转义也可以通过union select 1,2,3这种方式一直到不报错未知就可以知道查询列数了。这种方式还有一个作用就是判断回显的数据是查询结果的哪部分,因为select 1,2,3的返会结果始终是1,2,3,所以通过显示了哪几个数字就可以知道查询的哪几列被显示出来了。
    有的时候查询语句中会通过limit语句限制返回第一条结果(或者通过php语句限制只返回回显第一个结果),我们可以使用group_concat()函数将查询结果合并为一条。group_concat()函数用于合并group by分组后的同组内的值,如果我们没用指定group by的依据,就会合并所有的值。

  • 报错注入
    报错注入是将我们需要的信息包含在报错信息中回显出来,有的时候原sql语句可能不是select查询语句,而是update/insert等语句,这样我们就无法构造联合查询,这时我们就可以利用报错注入。在有报错回显时才可以利用报错注入,并且由于存在报错语句字符数量限制,所以不能一次获得大量数据。报错注入大致分两种,一种是双查询注入,一种是updatexml()/extractvalue()注入。

    • 双查询注入
      在双查询注入中需要用到几个函数,分别是rand(),floor(),count(),另外还需要用到group by语句。rand()函数产生一个[0,1)的随机数;floor()函数向下取整;count()函数用于对查询结果进行计数,如果使用了group by语句,就会对分组后的结果分别计数。
      对于SELECT * FROM users WHERE id = '$id'这个语句,如果提交参数id=0' union select count(*),concat((select id from users limit 0,1),floor(rand()*2)) as a from information_schema.schemata group by a#最终拼接而成的sql语句为SELECT * FROM users WHERE id = '0' union select count(*),concat((select id from users limit 0,1),floor(rand()*2)) as a from information_schema.schemata group by a#'
      这个语句由几个部分组成,首先是一个普通的select查询语句,然后是一个union select联合查询语句。联合查询的内容是count()和concat()两个函数,后面的as语句用于给concat()函数的查询结果取一个别名,这里的别名是a用于后面group by语句分组。count(*)用来对group by语句分组后对每个组进行计数;concat()函数用来拼接它的两个参数分别是一个select语句和一个floor()函数,这里的select语句即为双查询,即在select语句中包含一个select语句,floor(rand()*2)会产生一个随机数0或者1。最终concat()函数会将参数里的select语句查询结果和floor()函数产生的随机数进行拼接,由group by语句对其进行分组再由count()函数对其进行计数。
      在使用group by语句和count()函数时,mysql会建立一个临时的虚拟表用来对查询结果进行分组计数。若查询到的键(查询结果)不在表中时,mysql会将其插入作为一个新的键;若该键已经存在,则令其计数器+1。假设第一次floor(rand()*2)的结果为0,concat()函数中select语句查询结果为value,那么拼接后的键值为value0,虚拟表中不存在这个值所以将concat((select id from users limit 0,1),floor(rand()*2))的值插入,注意这里插入时floor(rand()*2)又被执行了一次,所以可能插入value0也可能插入value1。我们假设插入了value1,那么再判断第二条结果,如果这次floor(rand()*2)结果为0,那么合并的值为value0,因为虚拟表中不存在这个键值,所以插入,插入时又执行了floor(rand()*2)函数,如果插入时其产生的结果为1,那么键值为value1,虚拟表中已经存在这个键值,就会出现键值冲突从而引发mysql报错Duplicate entry 'value1' for group_key。从这个报错语句中我们就获得了concat()函数里select语句查询的结果value。
      双查询报错使用时存在一些限制,比如因为第一次插入前虚拟表中不存在键值,所以第一次插入一定不会产生报错,因此联合查询的结果数量必须两个以上。这里我们使用information_schema库中的schemata表因为这里储存了数据库信息,一般来说数据库软件会有几个系统库,可以满足查询结果多于两条的要求,当然也可以选择tables表或者其他表。另外由于插入是否报错是由rand()函数的结果决定的,而rand()函数产生的是一个随机值,因此可能不会每次都能成功爆出信息,需要多刷新几次。
    • updatexml()/extratvalue()注入
      updatexml()函数有三个参数,第一个和第三个参数是普通的字符串类型,而第二个参数为xpath格式的字符串,第一个字符串为xml文档名,updatexml()函数的作用是在第一个参数指定的xml文档中通过第二个参数匹配节点,然后用第三个参数替换匹配到的节点值。
      extractvalue()有连个参数,第一个参数为普通字符串,第二个参数为xpath格式字符串。这两个参数意义同updatexml()函数。extractvalue()函数会从第一个参数指定的xml文档中通过第二个参数匹配并返回匹配结果。
      虽然他们的作用不同,但他们在sql注入的用法完全相同,都是利用拼接不符合xpath格式的字符串作为参数从而导致mysql查询报错,从报错中获取需要的信息。例如我们构造参数id=0' or updatexml(1,concat('$',(select database())),1)#,就会返回查询报错UnKnown XPATH variable at: '$databasename'从而获取了数据库名databasename。
      这两个函数与双查询注入相比,有几个优点,一是不存在随机数的问题,一次就可以判断有没有注入成功;二是payload更短,写起来更方便;三是在函数中可以更方便的用括号代替空格,当空格被过滤的时候这就是一种有效的绕过手段。
      但是这两个函数的漏洞在最新的mysql版本中已经被修复。并且要注意xpath参数有最大32位的长度限制。一般来说我用updatexml()函数更多(因为不会拼extractvalue。
  • outfile文件写入
    文件写入是利用sql中的into outfile语句将特定语句写入外部文件,通过这种方式我们可以构造webshell等。但是由于mysql对于文件的权限限制很严格,所以这种方法很难利用。

  • 堆叠注入
    sql中通过分号分隔多个语句,因此产生堆叠注入这种思路。我们在提交的参数中加入分号结束前面的语句,就可以在分号后执行任何我们需要的语句。但是php中的mysql_query()函数并不能执行多条sql语句,必须为mysql_multi_query()函数,因此这种方法使用也很有限(但是ctf里好像很常见?
    但是由于mysql_multi_query()函数只会返回第一个语句的结果,所以一般后面构造的payload都用来执行insert或update等语句。关于堆叠注入,buuctf里面有一个强网杯2019随便注,姿势特别骚,可以去看看。

  • 二次注入
    二次注入的概念类似于存储型xss,是将payload写入数据库中,然后在程序取出数据库内数据并且拼接为新的sql语句时产生注入。这部分可以参考sqli-labs Less-24,这里提供了一个登录页面,并且开放了注册接口。我们可以注册一个名为admin' #的用户并登录,然后修改该账号的密码。修改完成后用修改后的密码登录admin账号发现登录成功,即注入成功。

  • 盲注
    在页面即不存在报错回显,又不回显查询到的数据时,大部分注入方式都无法利用,这时可以考虑盲注。盲注大致分为两种分别是布尔盲注和时间盲注。由于盲注一次只能获取一比特的信息,所以一般需要很多次判断才能获取部分有效的信息,这部分可以使用脚本来完成。并且有些站点可能存在扫描限制不允许同一ip短时间内大量访问,脚本可能会收到大量的502响应。因此一般在上面的方法都无法注入的时候才会考虑盲注。

    • 布尔盲注
      在查询成功和查询失败回显不同时可以考虑布尔注入,我们可以使用length()函数判断出查询结果的长度,再通过substr()函数和二分法一次找出查询结果每一位的值,最终可以得到查询的结果。例如我们提交参数id=1' and length((select database()))=8--+,如果length()=8为真,页面会返回id=1的查询结果,否则返回未查询到结果,这样我们就判断出来数据库名的长度为8位。再通过id=1' and substr((select database()),1,1)>'A'--+判断是否数据库名第一位大于'A'。这里要注意一点就是sql中substr()函数截取字符串索引是从1开始而不是0开始的。
    • 时间盲注
      在页面返回结果始终相同时,就不能通过页面返回结果来判断注入结果了,这时候可以尝试时间盲注。我们可以使用sql中的if语句来执行sleep()函数从而判断我们设定的条件是否正确。例如我们构造urlid=1' and if((length((select database()))=8),sleep(1),1)--+,通过页面回显是否延迟判断if条件是否正确。除了if语句,我们也可以通过and和or来实现时间盲注,因为如果前一个语句为真,就不会判断or后的语句;同理如果前一个结果为假,就不会判断and后的语句。通过这个思路可以提交参数id=1' and length((select database()))=8 and sleep(1)--+,通过页面回显是否延迟判断条件真假。

5.基本绕过姿势

一般正常的题都不会这么直接给注的,都会有一些过滤,这时候要考虑怎么绕过这些过滤。对于检测到的关键字也可能有不同的处理,有的会直接过滤而有些会进行转义。对于不同的情况,可以尝试不同的绕过方法。

  • 过滤绕过
    • 过滤注释符
      单字符或双字符注入时,因为我们在参数中加入了引号闭合原sql语句中得到引号导致后面的引号未闭合,一般可以通过注释符将其注释掉。但是在注释符被注释掉的情况下,无法通过这种方式闭合引号,我们就可以考虑通过order by等语句来闭合引号,例如payload?id=0' union select 1,group_concat(username),group_concat(password) from users order by '1
      这里通过最后order by语句中的引号闭合了原sql语句中的引号。也可以通过group by等语句。
    • 过滤关键字
      这种是通过黑名单检测sql关键字比如select,union等。但是关键字过滤存在过滤次数的问题,例如对字符串"qorororore"过滤'or',过滤完变成"qe",但是如果是字符串"oorr",过滤完可能就是'or',因为正则只能匹配到中间的'or',如果要过滤两边的or,必须要进行两遍过滤。一般来说不会进行很多次过滤因为正常的参数里也可能包含'or'等。
      除了一般的绕过方法,一些特别的关键字也可以使用符号替代,比如||替代'or',&&替代'and',空格可以使用
      • %09 TAB 键(水平)
      • %0a 新建一行
      • %0b TAB 键(垂直)
      • %0c 新的一页
      • %0d return 功能
      • %a0 空格
        等替代。
  • 转义绕过
    转义一般是在特殊符号前加上\消去其特殊含义,例如addslashes()/mysql_real_escape_string()函数。这时可以考虑宽字节注入,当数据库设置了宽字节编码时(如gbk),会将大于127的字节和其后一个字节当作一个汉字。因此我们可以在特殊符号前加上%ed,转义后%ed和\被编码为一个汉字从而失去了转义的作用。

你可能感兴趣的:(sql注入简单总结)