前言
忆往昔,峥嵘岁月稠!大学已经到了大三了,打了很多比赛,回顾还是挺欣慰!此系列来由是想留一点东西,把所学知识整理一下,同时也是受github上Micro8分享的启发,故想做一些工作,以留后人参考,历时两个星期,第一系列SQL注入回顾篇出炉!内容分四节发布,其中SQL注入代码审计为两节,WAF绕过总结为1节,SQLMAP使用总结为1节!此为SQL注入代码审计第二部分。欢迎各位斧正,交流!
简介
SQL注入就是web应用程序对用户输入数据的合法性没有判断,前端传入后端的参数是攻击者可控的,并且参数带入数据库查询攻击者可以通过构造不同的sql语句来实现对数据库的任意操作。
Sql注入的产生需要满足以下两个条件:
- 参数用户可控:前端传给后端的参数内容是用户可以控制的。
- 参数带入数据库查询:传入的参数拼接到sql语句中,且带入数据库查询。
当传入的参数ID为1’时,数据库执行的代码如下所示。
Select * from users where id=1’
这个语句不符合数据库语法规范,所以会报错。当传入的参数为and 1=1时,执行的sql语句如下所示
Select * from users where id=1 and 1=1
因为1=1为真,且where语句中的id=1也为真,所以页面会返回与id=1相同的结果。当传入的id参数为and 1=2时,此时sql语句恒为假,所以服务器会返回与id=1不同的结果
在实际环境中,sql注入会导致数据库的数据泄露,在安全配置不当的情况下还可能会被攻击者拿到系统权限,进行文件的读写操作等。
普通的注入审计,可以通过$_GET
,$_POST
等传参追踪数据库操作,也可以通过select , delete , update,insert 数据库操作语句反追踪传参。
Mysql注入相关知识点
-
在Mysql 5.0版本之后,Mysql默认在数据库中存放一个”information_shcema”的数据库,在该库中,读者需要记住三个表名,分别是SCHEMATA,TABLES和COLUMNS。分存储该用户创建的所有数据库的库名,库名和表名,库名和表名,字段名。
-
Limit的用法:使用格式为limit m,n,其中m是指记录开始的位置,从0开始,表示第一条记录:n是指取n条记录。例如limit 0,1表示从第一条记录开始,取一条记录。
-
需要记住的几个函数
database()
:当前网站使用的数据库。version()
:当前MYSQL的版本。user()
:当前MySQL的用户。
-
注释符
在MYSQL中,常见的注释符的表达式为:#或–空格或/**/,
,
//,-- , --+,,%00,--a。
-
内联注释
内联注释的形式:/*!code */。内联注释可以用于整个SQL语句中用来执行我们的SQL语句,
举个栗子:
Index.php?id=-15 /*!UNION*/ /*!SELECT*/ 1,2,3
-
MYSQL对大小写不敏感,所以存在大小绕过。
时间注入
注入测试地址http://192.168.23.134/time/time.php?id=1
访问该网址,页面返回yes。,在网址的后面加上一个单引号,再次访问,页面返回no。这个结果与boolean注入非常相似,与之不同的是,时间注入是利用sleep()
或benchmark()
等函数让Mysql的执行时间更长。时间盲注多与IF(expr1,expr2,expr3)
结合使用,此if语句含义是:如果expr1是TRUE,则IF()的返回值为expr2
;否则值则为expr3
。所以判断数据库库名长度的语句应为:
if(length(database())>1,sleep(5),1)
这条语句的意思是,如果数据库名的长度大于1,则mysql查询休眠5秒,否则查询1。
而查询1的结果大约只有几十毫秒,所以可以根据Burp Suite页面的响应时间,判断条件时候正确。
页面的响应时间为6154毫秒,也就是6.154秒,表明页面成功执行了sleep(5)
,所以数据库名长度是大于1的,我们尝试将判断数据库库名长度语句中的长度改为10。
执行时间为1051毫秒,也就是1.051秒,也就是说服务器并没有执行sleep(5)
,而是执行了select 1,
所以返回yes。经过多次测试后就可以确定数据库库名的长度,接下来我们可以查询数据库库名的第一位字母,查询语句与Boolean盲注相似,使用substr
函数,这时的语句应为
if(substr(database(),1,1)=’d’,sleep(5),1)
程序延迟了6.024秒才返回,说明数据库库名的第一位字母是d,依此类推即可得出完整的数据库库名,表名,字段名和具体数据。此外,时间盲注受网速影响,所以有时候会出现偏差,建议选在网站网速较好的时候测试!
时间注入代码分析
在这个程序页面,程序获取GET参数ID,通过preg_match判断参数ID中是否存在Union危险字符,然后将参数ID拼接到SQL语句中。将数据库中查询SQL语句,如果有结果,则返回yes,否则返回no。当访问该页面时,代码根据数据库查询结果返回yes或no,而不返回数据库的任何数据在页面上,与Boolean注入不同的是,此处没有过滤sleep等字符,代码如下:
<?php
$con=mysqli_connect("localhost","root","root","dvwa");
// 检测连接
if (mysqli_connect_errno())
{
echo "连接失败: " . mysqli_connect_error();
}
$id = $_GET['id'];
if (preg_match("/union/i", $id)) {
exit("<htm><body>no</body></html>");
}
$result = mysqli_query($con,"select * from users where `user_id`='".$id."'");
$row = mysqli_fetch_array($result);
if ($row) {
exit("<htm><body>yes</body></html>");
}else{
exit("<htm><body>no</body></html>");
}
?>
堆叠查询注入
注入测试网址: http://192.168.23.134/dd/dd.php?id=1
堆叠查询可以执行多条语句,多语句之间以分号隔开。而堆叠查询注入就是利用这个特点,在第二个SQL语句中构造自己要执行的语句,首先访问id=1’,页面返回mysql错误,再访问1’%23
,页面返回正常结果。这里可以使用Boolean注入,时间注入,也可以使用另外一种方式—堆叠注入!
堆叠注入语句:
‘;select if(substr(user(),1,1)=’r’,sleep(3),1)%23
从堆叠注入语句可以看到,第二条SQL语句(substr(user(),1,1)=’r’,sleep(3),1)%23就是时间盲注的语句,执行结果如图所示
后面的注入与时间注入的一样,通过构造不同的时间注入语句,可以得到完整的数据库的库名,表名,字段名和具体的数据。以下语句是获取数据库的表名。
‘;select if(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1)=’u’,sleep(3),1)%23
堆叠注入代码分析
在堆叠注入界面中,程序获取GET参数ID,使用PDO的方式进行数据库查询,但仍然将参数ID拼接到查询语句,导致PDO没起到预编译的效果,程序仍然存在SQL注入漏洞,代码如下:
<?php
try {
$conn = new PDO("mysql:host=localhost;dbname=dvwa", "root", "root");
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $conn->query("SELECT * FROM users where `user_id` = '" . $_GET['id'] . "'");
$result = $stmt->setFetchMode(PDO::FETCH_ASSOC);
foreach($stmt->fetchAll() as $k=>$v) {
foreach ($v as $key => $value) {
//echo $value;
}
}
$dsn = null;
}
catch(PDOException $e)
{
echo "error";
}
$conn = null;
?>
使用PDO执行SQL语句时,可以执行多语句,不过这样通常不能直接得到注入结果,因为PDO只会返回第一条SQL语句执行结果所以在第二条语句中可以用update更新数据或者使用时间盲注获取数据。访问dd.php?id=1’;select if(ord(substr(user(),1,1))=114,sleep(3),1)%23
时,执行的sql语句为:
select * from users where ‘id’=’1’;select if(ord(substr(user(),1,1))=114,sleep(3),1)#
此时的sql语句为两条!
二次注入
注入测试地址:http://192.168.23.134/sqli-labs/Less-24/
二次注入的原理,在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 get_magic_quotes_gpc 对其中的特殊字符进行了转义,在写入数据库的时候还是保留了原来的数据,但是数据本身还是脏数据。
在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
这里以SQLIlab lesson-24为例,也是考察的二次注入的点。
这题正常的流程是首先注册一个账号,然后登陆进去会让你修改新的密码。
直接尝试在登陆处尝试SQL注入,payload: cck’#
发现失败
这里我们换一种思路,注册一个test’#的账户。
发现账户被成功注册,因此可以推测程序并未对脏数据对行后处理。经过转义的数据又被还原到数据库中。
这里我们有一个test的账户,密码为1234,如果我们以test’#账户登录,进行修改密码如下
此时不需要当前密码就可以修改密码
因此可以发现二次注入,注入的数据不会在刚开始就作用,而是在第二次引用的时候,由于脏数据被引用而导致的注入。
二次注入代码分析
我们查看login.php,审计一下登陆处的逻辑
登陆处的username
和password
都经过mysql_real_escape_string
函数的转义,直接执行SQL语句会转义’,所以该处无法造成SQL注入。
注册用户的时候用了mysql_escape_string
过滤参数
但是脏数据还是插入了数据库
username直接从数据库中取出,没有经过转义处理。在更新用户密码的时候其实执行了下面的命令:
“UPDATE users SET PASSWORD=’22′ where **username=’test’#**‘ and password=’$curr_pass’”;
因为我们将问题数据存储到了数据库,而程序再取数据库中的数据的时候没有进行二次判断便直接带入到代码中,从而造成了二次注入。
宽字符注入
注入测试网址:http://192.168.23.134/kzf/kuanzifu.php?id=1
访问id=1’,页面并没有报错,反而多了一个转义字符(反斜杠)。
从返回结果看,参数id=1在数据库查询时是被单引号包围的。当传入id=1’时,传入的单引号又被转义字符转义,导致参数id无法逃逸单引号字符的包围,所以在一般情况下,此时是不存在SQL注入漏洞的。不过,有一个特例,就是当数据库的编码为GBK时,可以使用宽字节注入,宽字节的格式是在地址后先加一个%df,繁体字’連’,所以此时单引号成功逃逸,报出MySQL数据库的错误,如图
由于输入的参数id=1’
,导致SQL语句多了一个单引号,所以需要使用注释符来注释程序自身的单引号,访问id=1%df’%23
,看页面返回结果。
此时不再报错。
使用and 1=1
和and 1=2
进一步判读注入,访问id=1%df’and 1=1%23
和id=1%df’and 1=2%23
,如图所示
当and 1=1
程序返回正常,and 1=2
程序返回错误,所以判断该参数id存在SQL注入漏洞,接着使用order by
查询数据库表的字段数量,得知字段数为8。
因为页面直接显示了数据库中地内容,所以可以使用Union查询。接下来就是union注入一样,先判断显示位。
接下来尝试在4的位置上查询当前数据库的库名。
查询数据库的表名时,一般使用以下语句。
select table_name from information_schema.tables where table_schema=’dvwa’ limit 0,1
但是此时,由于单引号被转义,会自动出现反斜杠,导致报错,所以此时可以利用另一种方法:嵌套查询。就是在一个查询语句中,再添加一个查询语句,如下
select table_name from information_schema.tables where table_schema=(select database()) limit 0,1
查询字段值,这里我们采用另外一种方式,16进制绕过,逃逸
union select 1, 2,3,(select column_name from information_schema.columns where table_schema=0x64767761 and table_name=0x7573657273 limit 0,1),5,6,7,8#
dvwa的16进制为0x64767761,users的16进制为0x7573657273
宽字符注入代码分析
在宽字节注入界面,程序获取GET
参数ID,并对参数ID使用addslashes()
转义,然后拼接SQL语句中,进行查询。
<?php
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_select_db('dvwa', $conn) OR emMsg("数据库连接失败");
mysql_query("SET NAMES 'gbk'",$conn);
$id = addslashes($_GET['id']);
$sql="SELECT * FROM users WHERE user_id='$id' LIMIT 0,1";
$result = mysql_query($sql, $conn) or die(mysql_error());
$row = mysql_fetch_array($result);
if($row)
{
echo $row['user_id'] .":". $row['user'];
}
else
{
print_r(mysql_error());
}
?>
</frot>
<?php
echo "<br>The Query string is:".$sql."<br>";
?>
当访问id=1’
时,执行的SQL语句为:
SELECT * from users where id=’1\’’
可以看到单引号被转义字符‘\’转义,所以在一般情况下,是无法注入的,当然也有绕过方法,但是由于在数据库查询前执行了 SET NAMES ‘GBK’
,将编码设置为宽字节GBK,所以此处存在宽字节注入漏洞。
在PHP中,通过iconv()
进行编码转换时,也可能存在宽字节注入漏洞!
Cookie注入
注入测试地址:http://192.168.23.134/cookie/cookie.php
页面没有GET参数,但是页面返回正常,使用Burp Suite抓取数据发现cookie中存在id=1的参数
修改cookie中的id=1为id=1’,然后再次访问url,发现页面返回错误。然后分别修改cookie中id=1为id=1 and 1=1和id=1 and 1=2,再次访问,访问结果如图
可以发现,此程序存在SQL注入。接下来就是常规操作了,不再赘述!
Cookie注入代码分析
程序通过 C O O K I E 获 取 浏 览 器 c o o k i e 中 的 数 据 , 在 c o o k i e 注 入 界 面 中 程 序 通 过 _COOKIE获取浏览器cookie中的数据,在cookie注入界面中程序通过 COOKIE获取浏览器cookie中的数据,在cookie注入界面中程序通过_COOKIE获取参数ID,然后直接将ID拼接到select语句中进行查询,如果有结果,输出到界面。
<?php
$id = $_COOKIE['id'];
$value = "1";
setcookie("id",$value);
$con=mysqli_connect("localhost","root","root","dvwa");
if (mysqli_connect_errno())
{
echo "连接失败: " . mysqli_connect_error();
}
$result = mysqli_query($con,"select * from users where `user_id`=".$id);
if (!$result) {
printf("Error: %s\n", mysqli_error($con));
exit();
}
$row = mysqli_fetch_array($result);
echo $row['user_id'] . " : " . $row['user'];
echo "<br>";
?>
可以看到,cookie中的参数ID并没有做任何过滤,就直接拼接到了SQL语句中,因此存在SQL注入漏洞!
Base64注入
注入测试地址:http://192.168.23.134/bs4/base64.php?id=MSc=
从URL可以看出,ID参数经过base64编码,’MSc=’是1’
的base64编码,当访问该网址时页面报错。
接下来验证注入,把参数为1 and 1=1
和 1 and 1=2
并进行base64编码如
测试1 and 1=1
如下
测试1 and 1=2
从返回结果可以看出,访问id =1 and 1=1时,页面返回与id=1相同的结果,而访问id=1 and 1=2时,页面返回与id=1不同的结果,所以存在SQL注入漏洞。接下来可以使用order by
查询字段,使用union完成此次注入
Base64注入代码分析
在base64注入界面,程序获取GET参数ID,用base64_decode()
进行解码,然后直接将解码后的$id
拼接到select语句进行查询,通过while循环,将查询结果输入到页面上。
<?php
$id = base64_decode($_GET['id']);
$conn = mysql_connect("localhost","root","root");
mysql_select_db("dvwa",$conn);
$sql = "select * from users where user_id=$id";
$result = mysql_query($sql);
while($row = mysql_fetch_array($result)){
echo "ID:".$row['id']."<br >";
echo "user:".$row['user_id']."<br >";
echo "pass:".$row['user']."<br >";
echo "<hr>";
}
mysql_close($conn);
echo "now use".$sql."<hr>";
?>
由于代码并未对解码后的参数$id进行过滤,就将参数拼接到了SQL查询语句中,所以存在sql注入漏洞!
这种攻击方式还有其它利用场景,例如,如果有WAF,则WAF会对传输的参数ID进行检查,但是由于传输中的参数ID经过base64编码,所以WAF很有可能检测不到危险代码,进而绕过了WAF!
XFF注入
注入测试地址:http://192.168.23.134/xff/xff.php
从bp抓取数据包可以看出HTTP请求的头部有一个X-Forwarded-for
。以下简称XFF头,它代表客户端真实ip,通过修改XFF头可以伪造客户端IP。
将XFF的值后面加个单引号,再次访问,报错
接下来,X-Forwarded分别设置为192.168.23.134’and 1=1#
和192.168.23.134’and 1=2#
,结果如图
通过页面的返回结果可知,该程序存在SQL注入,接下来就是常规注入了,不再赘述!
XFF注入代码分析
PHP中的getenv()
函数用于获取一个环境变量的值,类似于$_SERVER
或$_ENV
,返回环境变量对应的值,如果环境变量不存在则返回FALSE。
使用以下代码就可以获取客户端IP地址,程序先判断是否存在HTTP头部参数HTTP_CLIENT_IP
,如果存在,则赋给$ip
,如果不存在,则判断是否存在HTTP头部参数HTTP_X_FORWARED-FOR
,如果存在赋给$ip
,如果不存在,则将HTTP头部参数REMOTE_ADDR
赋给$ip
。
<?php
$con=mysqli_connect("localhost","root","root","dvwa");
if (mysqli_connect_errno())
{
echo "连接失败: " . mysqli_connect_error();
}
if(getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif(getenv('HTTP_X_FORWARDED_FOR')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
} elseif(getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
} else {
$ip = $HTTP_SERVER_VARS['REMOTE_ADDR'];
}
$result = mysqli_query($con,"select * from users where `ip`='$ip'");
if (!$result) {
printf("Error: %s\n", mysqli_error($con));
exit();
}
$row = mysqli_fetch_array($result);
echo $row['user_id'] . " : " . $row['user'];
echo "<br>";
?>
接下来,$ip
被拼接到了select语句,然后将查询结果输出到界面上。
由于HTTP头部参数是可以伪造的,所以可以添加CLIENT_IP
或XFF
头部,进行注入,且程序并未对得到的$IP
进行过滤,故存在SQL注入漏洞!
结语
到此SQL注入代码设计结束,下节内容为SQL注入的WAF绕过姿势,如有兴趣交流,请移步—>
传送门:
SQL注入回顾篇(一)
SQL注入回顾篇(三)
SQL注入回顾篇(四)