前言:
知识面决定着攻击面,所以只要知识面广就不怕挖不到洞,而往往在SRC挖掘中白盒审计也是非常重要的一部分,尤其是CNVD啊EDU这种通用漏洞需求较广的平台中,代码审计这项技能就是不可或缺的了,今天花某分享的是DVWA中Sql注入4个等级的逐行代码审计实例,后续还会带来实战代码审计啊等等内容。
Low等级:
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
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
一步步来分析:
第1行和第二行:
if( isset( $_REQUEST[ 'Submit' ]
) ) {
$id = $_REQUEST[ 'id' ];
首先判断有没有Submit参数的存在,检查有没有提交表单,然后在从请求 中获取“id”的参数赋给变量“$id”存储id
到了后面的
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
很明显的Sql查询语句,也是通常sql注入漏洞存在的地方之一,在上面的实例中想在“users”的表中根据提供的$id的值选取“first_name”和“last_name”的字段。
然后执行到后面的
$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>' );
这句执行了Sql查询语句,将结果存储在又一个变量“$result”中,查询失败就会回显错误信息。
分析到这里其实漏洞面已经明了了,但是这篇文章主要目的是代码审计而不是漏洞通关所以还是继续审计下去。
while( $row = mysqli_fetch_assoc( $result ) ) {
$first = $row["first_name"];
$last = $row["last_name"];
这段当中第一行使用了“mysqli_fetch_assoc”函数作用是从结果集上获取数据,将逐行的值存储在“$row”数组值,循环遍历每行结果,第二行中是从“row”数组中获取“first_name”的值,并存储在变量$first中,第三行也是同理获取“last_name”的值,并存储在变量$last中。
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
mysqli_close($GLOBALS["___mysqli_ston"]);
最后的两行中第一行的作用是把获取的$id,$first,$last的值通过html格式化,附加到$html中,随后一行的作用就是关闭数据连接!
未进行过滤且漏洞百出,直接在1后添加'闭合成功报错!
Medium等级:
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
$html .= "<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"]);
还是逐行分析
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
首先前两行和Low等级就有所不同了,这句中是监测是否存在“Submit”的POST参数,判断表单是否提交,而区别就是POST请求,第二行也是一样的从POST请求id的参数,赋值给变量$id,存储用户所输入的id值。
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
这句使用了“mysqli_real_escape_string”函数对变量$id进行转义,防止Sql注入攻击
$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>' );
这两行和low等级中的是大同小异的,线是构建sql语句,选择名为“user”表中的user_id等于变量$id的行,然后选择里面的first和last_name的字段也就是我们输入1后回显admin的原因
后面的一句就是执行Sql语句,将结果存在变量$result中,查询失败就输入错误。
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
这句大家刚刚看完了low等级的审计相信大家已经很熟悉了,可以先不看花某下面的审计点开图片自己审计试试!
ok!这段开通的作用就是使用了mysqli_fetch函数从结果集中逐行获取数据,然后把每一行值存在$row数组中,循环遍历结果集合中的每一行,后面是从$row数组中获取“first_name”字段值,存储在$first变量中,后面的也是同理从$row数组中获取“last_name”字段值,存储在$last变量中,接下来的
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
就是将获取的id,first_name,last_name的值通过html格式化,然后加到$html变量中。
$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"]);
接下来的一段中第一行就是构建sql语句计算users表中有几行,第二行构建sql查询把结果存在$result变量中,查询失败就输入错误,第三行使用mysqli_fetch_row函数从结果中获取行数据,然后有个索引将它访问到元素,存储在$number_or变量中,最后一行就是关闭数据库连接。
但其实这一等级还是没有添加过滤,也是可以直接用延时注入绕过
成功延时!
High等级:
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
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
在逐行开始之前看看靶场实例
需要先点击创建表单,了解了这一点我们在开始逐行分析:
if( isset( $_SESSION [ 'id' ] ) ) {
首先判断是否存在叫“id”的$_SESSION参数,检查了会话中有没有存储id信息
$id = $_SESSION[ 'id' ];
获取SESSION中的id参数,然后把它赋值给变量$id,作用就是存储用户的id
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
构建了Sql查询语句,选择了user_id等于$id的记录且只返回一条结果
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or ( '<pre>Something went wrong.</pre>' );
执行Sql语句把结果存储在$result变量里,查询失败就返回错误
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
还是那几句很熟悉的代码第一行使用mysqli_fetch_assoc函数从结果集合中逐行获取数据,然后把每行的值存储在$row数组中循环遍历每一行结果,第二行从数组$row中获取first_name字段值并存储在first变量中,第三行从数组$row中获取last_name的值存储在$last的变量中。
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
将获取的id,first_name,last_name的值用html格式化然后加到变量$html中。
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
最后一句关闭数据库连接,和前两次不同的就是它最后把值赋给变量$_mysqli_res,关闭成功返回true,失败的话就返回false.
这一等级就是典型的防止注入工具的写法了,但还是没有采用过滤依旧可以延时注入。
成功延时!
Impossible等级:
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
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
逐行开始:
if( isset( $_GET[ 'Submit' ] ) ) {
判断是否存在Submit的GET参数,来判断表单是否提交
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
调用check Token函数,用于延时反CSRF的令牌,把用户提交的user_token参数和会话之中的session_token参数传给check Token函数
$id = $_GET[ 'id' ];
这句就是非常简单的一句php代码了,从GET请求中获取id的参数,然后把它赋值给变量$id,作用就是存储用户输入的id
if(is_numeric( $id )) {
判断变量$id是不是数字
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
看似是数据库查询语句但这段其实是准备数据库查询语句,选择表中的user等于id的记录,然后限制返回一条结果这条查询使用参数的预处理(参数为绑定参数)
$data->bindParam( ':id', $id, PDO::PARAM_INT );
绑定参数,把变量$id绑定到查询的:id的占位符中,还指定了::PARAM_INT作为参数的类型,这句和上面这小段审计起来还是有难度的
$data->execute();
刚刚说上上段看似的数据查询语句但其实只是准备查询到了这句是才是真正的执行数据查询
$row = $data->fetch();
获取结果集中的一行数据,把它存在$row变量中
if( $data->rowCount() == 1 ) {
检查结果集中返回的行数是否等于1
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
这两句又见到了,从$row数组中获取first_name的字段值,并且将它存在变量$first中,第二句从$row数组中获取last_name的字段值,并将它存在变量$last中
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
将获取的id,first_name,last_neamr的值通过html格式化,追加到变量$html中,并反馈出来
generateSessionToken();
调用了generateSession Token函数,生成反CSRF令牌然后存储在会话里。
这段代码就是用的预处理去防止的Sql注入漏洞,就是在代码和数据之前像是砌了一堵墙,谁也不耽误谁防御了Sql注入而后面也只是会把execute的参数当做纯参数赋值给语句
总结:逐行分析这种方法在实战中花某是不推荐的因为太影响效率,今天花某逐行只是为了帮助大家理解学习审计,大家在实战中还是要着重 参数,可控变量,函数,过滤等等的查看查询!
原文始发于微信公众号(flower安全混子):代码分析之Sql注入漏洞的审计DVWA逐行分析4个等级
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论