目录
一、查看源码
二、功能分析
三、CSRF防范分析
1、CSRF令牌验证机制
(1)核心原理
(2)防范机制
2、旧密码确认防御实现
(1)核心原理
(2)为什么旧密码确认能有效防范CSRF?
本系列为通过《DVWA靶场通关笔记》的CSRF关卡(low,medium,high,impossible共4关)渗透集合,通过对相应关卡源码的代码审计找到讲解渗透原理并进行渗透实践,本文为CSRF impossible关卡的原理分析部分,讲解相对于low、medium和high级别,为何对其进行渗透测试是Impossible的。
一、查看源码
进入DVWA靶场CSRF的源码目录,找到impossible.php源码,分析其为何能让这一关卡名为不可能实现命令执行渗透。
打开impossible.php文件,对其进行代码审计,这段PHP代码实现了一个密码修改功能,通过CSRF令牌验证防止跨站请求伪造,使用预处理语句防范SQL注入,并对用户输入的当前密码进行MD5加密验证,确保新旧密码匹配后才更新数据库。
-
密码修改验证:处理用户发起的密码更改请求
-
CSRF防护:通过token验证防止跨站请求伪造攻击
-
密码验证:确认当前密码正确且新密码匹配
详细注释后的impossible.php源码如下所示,主要实现密码修改功能,修复了CSRF安全风险。
<?php
// 检查是否通过 GET 方法提交了 'Change' 参数(触发密码修改操作)
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' ];// 处理当前密码:// 1. 移除可能的转义字符// 2. 使用 mysqli_real_escape_string 进行 SQL 转义(尽管后续使用了预处理语句)// 3. 计算 MD5 哈希值(用于与数据库中存储的密码比较)$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 );// 查询数据库,验证当前密码是否正确// 使用 PDO 预处理语句防止 SQL 注入$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();// 验证条件:// 1. 新密码和确认密码是否一致// 2. 当前密码是否与数据库中存储的密码匹配if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {// 条件满足,处理新密码:// 1. 移除可能的转义字符// 2. 使用 mysqli_real_escape_string 进行 SQL 转义(尽管后续使用了预处理语句)// 3. 计算 MD5 哈希值(用于存储到数据库)$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 );// 更新数据库中的密码// 使用 PDO 预处理语句确保安全性$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();// 显示成功信息$html .= "<pre>Password Changed.</pre>";}else {// 显示失败信息(密码不匹配或当前密码错误)$html .= "<pre>Passwords did not match or current password incorrect.</pre>";}
}// 生成新的 CSRF 令牌,用于后续请求的验证
generateSessionToken();
?>
二、功能分析
这段 PHP 代码实现了一个密码修改功能,用户提交当前密码、新密码和确认密码后,系统先验证Anti-CSRF令牌防止跨站请求伪造,然后检查当前密码是否正确(通过数据库查询)以及新密码是否匹配。验证通过后,对新密码进行MD5哈希处理并更新到数据库。代码使用PDO预处理语句防止SQL注入,同时兼容性地使用mysqli_real_escape_string进行额外防护。
- Anti-CSRF 防护:
- 使用
checkToken()
验证用户提交的 Token,防止 CSRF 攻击。 - 每次页面加载时生成新的 Token(
generateSessionToken()
)。
- 使用
- 密码修改逻辑:
- 用户提交
password_current
(当前密码)、password_new
(新密码)、password_conf
(确认密码)。 - 检查
password_new
和password_conf
是否一致。 - 检查
password_current
是否正确(查询数据库)。 - 如果验证通过,更新数据库中的密码。
- 用户提交
- 输入清理与哈希:
- 使用
stripslashes()
去除可能的反斜杠(防止魔术引号影响)。 - 使用
mysqli_real_escape_string
防止 SQL 注入(兼容旧代码)。 - 使用
md5()
对密码进行哈希。
- 使用
- 数据库操作:
- 使用 PDO 预处理语句(
prepare
+bindParam
)防止 SQL 注入。 - 查询当前密码是否正确(
SELECT ... LIMIT 1
)。 - 更新密码(
UPDATE users SET password = ...
)。
- 使用 PDO 预处理语句(
- 反馈信息:
- 成功时返回
Password Changed.
。 - 失败时返回
Passwords did not match or current password incorrect.
- 成功时返回
三、CSRF防范分析
代码中主要使用两种方法进行防御,一个恶意请求必须同时满足以下两个条件才能成功:
-
携带正确且最新的CSRF Token。(第一道关卡)
-
携带正确的当前用户密码。(第二道关卡)
缺少任何一个,攻击都会失败。具体如下所示。
防御层 | 机制 | 优点 | 局限性 |
---|---|---|---|
第一层:CSRF Token | 验证请求是否来源于真正的网站表单。 | 直接针对CSRF攻击原理,防范绝大多数自动化攻击。 | 如果Token生成、存储或验证逻辑存在缺陷,可能被绕过。 |
第二层:旧密码确认 | 验证执行敏感操作的用户是否知晓某个机密。 | 极其强大。即使CSRF Token因某种原因失效,此层依然坚挺。攻击者几乎无法绕过。 | 对用户体验有轻微影响(需要多输入一个字段)。 |
1、CSRF令牌验证机制
(1)核心原理
核心原因在于,它不仅仅验证用户的登录凭证(Session Cookie),还要求每个敏感请求(如修改密码)都必须携带一个一次性、随机的、与会话绑定的令牌。
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
(2)防范机制
同步令牌模式:服务器为每个会话生成一个随机、不可预测的令牌(Token),并将其同时存储在服务器端(Session)和客户端(表单隐藏域)。当客户端提交请求时,必须带回此令牌。服务器通过比对两者是否一致来验证请求是否来源于真实的网站页面,从而拒绝伪造的跨站请求。
-
生成令牌 (Generate):在用户访问包含表单的页面时(如第一次访问密码修改页面),服务器端会调用
generateSessionToken()
函数生成一个随机令牌,并将其存储在用户的会话($_SESSION
)中。 -
分发令牌 (Distribute):服务器将这个令牌值嵌入到返回给用户的HTML表单中,作为一个隐藏字段(hidden field)。
-
提交令牌 (Submit):当用户提交表单时,浏览器会自动将这个隐藏字段的值连同其他表单数据一并提交到服务器。
-
验证令牌 (Verify):服务器接收到请求后,首先调用
checkToken()
函数,比对用户提交的令牌($_REQUEST['user_token']
)和存储在服务器端会话中的令牌($_SESSION['session_token']
)是否完全一致。-
一致:请求被认为是合法的,继续执行密码修改逻辑。
-
不一致或缺失:请求被认为是伪造的,立即终止,并将用户重定向到首页(
'index.php'
)
-
2、旧密码确认防御实现
(1)核心原理
权限复核(Re-authentication):要求用户在执行极高权限操作(如修改密码、邮箱)时,再次提供一项只有本人才知道的机密信息(当前密码)。这确保了即使是已经通过Cookie认证的用户会话,也必须证明操作者确实知道密码,而不仅仅是持有会话ID。这有效防御了CSRF、会话劫持以及在已登录设备上离开后的未授权操作。
(2)为什么旧密码确认能有效防范CSRF?
CSRF攻击的核心是 “借刀杀人” :利用用户浏览器中已有的身份凭证(如Session Cookie)来执行非用户本意的操作。这里特别强调非本意,即用户根部不知道要点击这个链接会直接导致什么后果。
旧密码确认能防御CSRF,并不是因为它是一个“明显的确认步骤”来提醒用户(虽然这算是一个次要的副作用),而是因为它在服务器端的技术层面,为执行敏感操作增加了一个攻击者无法提供的、必须验证通过的必要参数。
它将请求从 “有Cookie就能执行” 变成了 “有Cookie且知道密码才能执行” ,从而从根本上破坏了CSRF攻击成立的条件。这是一种非常强大且根本的防御措施。
-
未知的“秘密”:用户的当前密码是一个只有用户自己知道的秘密。攻击者可以伪造请求的URL和参数,但他们无法伪造用户当前的密码。
-
攻击请求必然失败:攻击者构造的CSRF恶意链接中,
password_current
参数只能是一个猜测值(如123456
、password
等常见密码)或空值。