前言
1、这篇文章主要是为了加强对PHP代码的理解,为PHP代码审计做铺垫
2、主要目的是加深代码印象,其次是漏洞理解,没有具体解题方法,只有题目分析和思路
🍺目录🍺
- 一、Brute Force(蛮力)
- 二、Command Injection(命令注入)
- 三、CSRF(跨站请求伪造)
- 四、File Inclusion(文件包含)
- 五、File Upload(文件上传)
- 六、Insecure CAPTCHA(不安全的验证码)
- 七、SQL Injection(SQL 注入)
- 八、SQL Injection (Blind)(SQL 注入(盲注))
- 九、Weak Session IDs(弱会话 ID)
- 十、DOM Based Cross Site Scripting(基于 DOM 的跨站脚本(XSS))
- 十一、Reflected Cross Site Scripting (反射式跨站点脚本 (XSS))
- 十二、Stored Cross Site Scripting (存储跨站点脚本 (XSS))
一、Brute Force(蛮力)
描述
蛮力攻击可以通过许多不同的方式表现出来,但主要包括攻击者配置预定值,使用这些值向服务器发出请求,然后分析响应。为了提高效率,攻击者可以使用字典攻击(有或没有突变)或传统的暴力攻击(使用给定的字符类别,例如:字母数字、特殊、区分大小写(不))。考虑到给定的方法、尝试次数、进行攻击的系统的效率以及被攻击系统的估计效率,攻击者能够大致计算出提交所有选定的预定值需要多长时间
Low
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// 传入参数
$user = $_GET[ 'username' ];
$pass = $_GET[ 'password' ];
$pass = md5( $pass ); // 进行md5加密
// 进行数据库查询
$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>' );
// $result表示查询的结果
if( $result && mysqli_num_rows( $result ) == 1 ) {
// mysqli_num_rows( )函数判断返回信息是几行
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// mysqli_fetch_assoc()函数从结果集中取得一行作为关联数组
// 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方式
传入两个参数,没有对参数作过多处理,然后进行数据库查询
2、mysqli_query( )
函数执行某个针对数据库的查询,若查询成功即返回ture,若前面查询出错,die( )
函数返回错误提示,or
表示若前面是ture,则不执行后面函数
3、利用mysqli_num_rows( )
函数判断返回信息是几行,若数量为一即返回为ture,若返回错误信息,行数量不为一,与$result
的结果一起判断是否为ture,若为1,则表示查询成功,即登录成功
4、因为这里没有过滤限制,可以直接进行暴力破解,即一次次尝试,直到成功
Mediume
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// 抓紧时间用户名输入
$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)) ? "" : ""));
// mysqli_real_escape_string()函数转义在 SQL语句中使用的字符串中的特殊字符
// 抓紧时间密码输入
$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加密
// 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 ) {
// mysqli_num_rows( )函数判断返回信息是几行
// 获取用户详细信息
$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级别,对传入的两个参数进行了一定限制,增加了mysqli_real_escape_string()
函数,表示转义在 SQL 语句中使用的字符串中的特殊字符,如0x00、\n、\r、\、'、" 、0x1a(ctrl+z)
,可以一定性质的限制注入,但对暴露破解影响较小,直接爆破即可
High
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// 检查防范CSRF的令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 抓紧时间用户名输入
$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)) ? "" : ""));
// 抓紧时间密码输入
$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 ) {
// 获取用户详细信息
$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);
}
// 生成防范CSRF的令牌
generateSessionToken();
?>
相比较前面的Medium级别,这里增加了checkToken()
函数,即令牌验证,防范CSRF漏洞,然后对输入的参数进行stripslashes()
函数处理,去除字符串的反斜杠,也没有针对暴力破解进行防范
Impossible
<?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 ); //绑定一个参数到指定的变量名,即用$user参数替换":user"
$data->execute(); //执行一条预处理语句
$row = $data->fetch(); //从结果集中获取下一行
// 检查用户是否已被锁定
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
//rowCount()返回上一个SQL语句影响的行数,即判断用户名是否正确
//同时判断登录次数是否有超过限定次数
//用户锁定,注意,使用此方法将允许用户枚举!
//echo "<pre><br/>该帐号因多次错误登录而被锁定</pre>";
// 计算允许用户再次登录的时间
$last_login = strtotime( $row[ 'last_login' ] ); //获取某个时间的时间戳
$timeout = $last_login + ($lockout_time * 60); //应该被解锁时的的时间戳
$timenow = time(); //获取当前的时间戳
// 检查是否有足够的时间过去了,如果没有,则锁定帐户
if( $timenow < $timeout ) {
$account_locked = true;
// print "帐户被锁定<br/>";
}
}
// 检查数据库(如果用户名与密码匹配),进行预处理
$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' ];
// 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>";
}
// 重置错误登录计数
$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 ) );
// 给用户一些反馈
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();
?>
相比较与前面的类型,这里采用POST请求方式,同时在原有题目的基础上增加了PDO预处理方式
,对SQL注入有相当的防范,并且设置登录次数
,不允许进行爆破,一旦超过登录次数,直接就被账户锁定
,在一定的时间内禁止登录
,完全无法进行暴露,所以,这里称Impossible
二、Command Injection(命令注入)
描述
命令注入是一种攻击,其目标是通过易受攻击的应用程序在主机操作系统上执行任意命令。当应用程序将不安全的用户提供的数据(表单、cookie、HTTP 标头等)传递到系统 shell 时,命令注入攻击是可能的。在这种攻击中,攻击者提供的操作系统命令通常以易受攻击的应用程序的权限执行。由于输入验证不足,命令注入攻击很可能发生
Low
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// 输入
$target = $_REQUEST[ 'ip' ];
// 判断操作系统,执行ping命令
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
//php_uname( 's' )返回运行php的操作系统名称
//stristr() 函数搜索字符串在另一字符串中的第一次出现
// Windows
$cmd = shell_exec( 'ping ' . $target );
// shell_exec()将括号内的字符当作命令执行
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// 最终为用户提供反馈
echo "<pre>{$cmd}</pre>";
}
?>
1、首先,$_REQUEST方式传入参数,没有进行过滤
2、利用php_uname( )函数查看系统,然后stristr()函数确认系统是否是Windows系统
3、调用shell_exec()函数,执行系统命令,也因如此,容易造成执行恶意的命令
4、可以考虑拼接命令执行,我们可以用命令连接符(如&&)
进行多条命令执行
Medium
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// input
$target = $_REQUEST[ 'ip' ];
// 设置黑名单
$substitutions = array(
'&&' => '',
';' => '',
);
// 删除数组(黑名单)中的任何字符
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
//array_keys返回包含数组中所有键名的一个新数组
// 判断操作系统,执行ping命令
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target ); // -c 4 即为发送报文4次
}
// 最终为用户提供反馈
echo "<pre>{$cmd}</pre>";
}
?>
1、在前面的基础上,这里添加了字符过滤,设置黑名单$substitutions
,调用str_replace()
函数将输入参数中的'&&'、';'
两个符号替换为空,只要不使用这两个符号即可
2、可以使用"|"
,只执行该符号后面的命令,替换成黑名单没有的字符即可
High
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// input
$target = trim($_REQUEST[ 'ip' ]);
//trim()函数移除字符串两侧的空白字符或其他预定义字符,当第二个参数为空,默认移除("\0"-NULL)("\t"-制表符)("\n"-换行)("\x0B"-垂直制表符)("\r"-回车)(" "-空格)这六个
// 设置黑名单
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// 删除数组(黑名单)中的任何字符
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// 判断操作系统,执行ping命令
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
1、相比较前面,这里在传入参数时使用了trim( )函数对字符串进行过滤,移除字符串两侧的空白字符或其他预定义字符,限制了一下字符
2、同时在设置黑名单$substitutions
的力度上也加大,可以利用的地方是黑名单的"| "
字符,这里是多了一个空格的,我们可以不留空格,直接使用"|"
,即还是利用命令拼接符
Impossible
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// 检查防范CSRF的令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target ); //去除反斜杠
$octet = explode( ".", $target );
//explode()函数使用一个字符串分割另一个字符串,并返回由字符串组成的数组
// 将IP分割为4个八进制
// 检查每个八进制是否为整数
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
//is_numeric()函数用于判断括号内的值是否为数字或数字字符串,如果是返回true否则返回false
//同时sizeof()函数判断分割的数组是否为4
// 如果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 );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. 让用户知道有个错误
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
// 生成防范CSRF的令牌
generateSessionToken();
?>
1、这里增加了令牌检查,防范CSRF,同时对参数使用了stripslashes( )
函数去除了反斜杠
2、使用explode()
函数将IP根据"."
分块,然后is_numeric()
函数检查每一块是否为数字或数字字符串,限制了其他字符,同时还有sizeof()
函数确定是否分割的数组只有4块,导致注入的不可能性
三、CSRF(跨站请求伪造)
概述
跨站点请求伪造 (CSRF) 是一种攻击,它强制最终用户在当前已通过身份验证的 Web 应用程序上执行不需要的操作。借助社会工程学的一点帮助(例如通过电子邮件或聊天发送链接),攻击者可能会诱骗 Web 应用程序的用户执行攻击者选择的操作。如果受害者是普通用户,成功的 CSRF 攻击可以迫使用户执行状态更改请求,例如转移资金、更改电子邮件地址等。如果受害者是管理帐户,CSRF可能会破坏整个 Web 应用程序
Low
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// 密码是否匹配?
if( $pass_new == $pass_conf ) {
// They do!
$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 );
// mysqli_real_escape_string()函数,表示转义在 SQL 语句中使用的字符串中的特殊字符,如`0x00、\n、\r、\、'、" 、0x1a(ctrl+z)`
// 使用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>' );
// Feedback for the user
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);
}
?>
1、采用GET请求,先判断新密码是否匹配,然后使用mysqli_real_escape_string()
函数,表示转义在 SQL 语句中使用的字符串中的特殊字符,如0x00、\n、\r、\、'、" 、0x1a(ctrl+z)
,然后就直接更新数据库密码了,没有防范可言
2、这里更新密码后,重新登录该网站,登录密码就变为这里更改的密码了
Medium
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// 检查请求的来源
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
//$_SERVER['HTTP_REFERER']链接到当前页面的前一页面的 URL 地址
//$_SERVER['SERVER_NAME']服务器主机的名称
//stripos()检查后面字符串第二个参数(SERVER_NAME)在第一个字符串参数(HTTP_REFERER)出现的位置
// 输入
$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 );
// 更新数据库
$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>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
在前面的基础上,这里增加了stripos()
函数,调用了超全局变量$_SERVER
,加上了请求头的验证,对用户请求头中的Referer字段
进行验证,同时确认该字段中包含服务器的名字,所以就不能通过其他的网页链接打开修改密码
High
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// 检查防范CSRF的令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 输入
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// 密码是否匹配?
if( $pass_new == $pass_conf ) {
// They do!
$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 );
// 更新数据库
$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>' );
// Feedback for the user
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);
}
// 生成防范CSRF的令牌
generateSessionToken();
?>
1、这里增加了防范csrf
的token
,checkToken()
函数首先检查token,当访问改密页面的时候,服务器会发送一个随机的token参数
,向服务器发送改密请求时,需要带上随机token
,当服务器检查到正确的token时,才会处理请求,防止别人随意盗改密码
2、可以使用XSS获取到token
Impossible
<?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 ); //加密
// 检查当前密码是否正确,进行PDO预处理,判断当前的密码是否正确
$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 ); //绑定一个参数到指定的变量名,即用$pass_curr参数替换":password",变量类型是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 );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// 密码匹配问题
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// 生成防范CSRF的令牌
generateSessionToken();
?>
在检查防范CSRF的token
的基础上,不仅对参数进行限制,同时使用PDO技术,对当前的密码进行验证
,双重保证
的前提下,若当前密码正确,就进行更新数据库密码,也限制了更新的密码不能出现特殊字符,从而Impossible
四、File Inclusion(文件包含)
描述
文件包含漏洞:意思是文件包含,是指当服务器开启allow_url_include
选项时,就可以通过PHP的某些特性函数(include(),require()和include_once(),requir_once()
)利用URL去动态包含文件,此时如果没有对文件来源进行严格审查,就会导致任意文件读取或者任意命令执行。文件包含漏洞分为本地文件包含漏洞与远程文件包含漏洞,远程文件包含漏洞
是因为开启了PHP配置中的allow_url_fopen
选项,选项开启之后,服务器允许包含一个远程文件,服务器通过PHP特性(函数)去包含任意文件时,由于要包含的这个文件来源过滤不严,从而可以去包含一个恶意文件,而我们可以构造这个恶意文件来达到自己的目的
Low
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
//就直接获取page参数,包含相应文件
?>
显示三个文件,我们点击第一个,服务器直接包含相应文件,并将相应的结果返回
服务器包含文件时,不论文件的后缀是否.php
,都会先按PHP文件执行,若确实为PHP文件,则将执行结果返回,若不是,直接打印文件内容,可导致任意文件读取与任意命令执行
Medium
<?php
// 我们希望显示的页面
$file = $_GET[ 'page' ];
// 输入验证,就是进行一些过滤
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>
在输入参数后,将参数可能存在的"http://"
、"https://"
、"../"
、"..\""
全部过滤掉,同时可以考虑大小写,双写等进行绕过
High
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// 输入验证
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// fnmatch()函数根据指定的模式来匹配文件名或字符串
// 这里使用*通配符,即以file开头的文件名,后面可以是任意的字符,或者是include.php文件
// 这不是我们想要的页面!
echo "ERROR: File not found!";
exit;
}
?>
这里对进行包含的文件名进行限制,调用fnmatch()
函数,限制访问的参数名开头,这里也可以用伪协议:file://
(可以访问文件系统)
Impossible
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
这里直接限制
了上传的文件名,只能上传这四个文件,没得做了,果然Impossible
五、File Upload(文件上传)
描述
1、上传的文件对应用程序构成重大风险,多攻击的第一步是获取要攻击的系统的一些代码,然后攻击只需要找到一种方法来执行代码。使用文件上传有助于攻击者完成第一步
2、不受限制的文件上传的后果可能会有所不同,包括完全系统接管、文件系统或数据库过载、将攻击转发到后端系统、客户端攻击或简单的破坏。这取决于应用程序如何处理上传的文件,尤其是它的存储位置
3、这里确实有两类问题,第一个是文件元数据,如路径和文件名。这些通常由传输提供,例如 HTTP 多部分编码,此数据可能会诱使应用程序覆盖关键文件或将文件存储在错误位置。在使用元数据之前,您必须非常仔细地验证它
4、另一类问题是文件大小或内容,这里的问题范围完全取决于文件的用途,为了防止这种类型的攻击,您应该分析您的应用程序对文件所做的一切,并仔细考虑涉及哪些处理和解释器
Low
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// 我们要写到哪里去?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// basename()函数返回路径中的文件名部分
//$_FILES是一个预定义的数组变量,用来获取通过POST方式上传到服务器的文件数据
//如果为单个文件上传,那么$_FILES为二维数组,如果为多个文件上传,那么$_FILES为三维数组
// 我们能把文件移到上传文件夹吗?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// move_uploaded_file()函数将上传的文件移动到新位置
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
?>
POST方式文件上传,这里明显没有对上传的文件类型、内容等进行检查、过滤,同时,选择木马文件上传成功后,这里还显示上传的路径,可以根据路径执行文件,典型的文件上传漏洞
Medium
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// 我们要写到哪里去?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
// 它是一幅图像吗? 从文件类型和大小进行限制
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {
// 我们能把文件移到上传文件夹吗?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
对比与上一题,这里对上传的文件进行了限制,分别为文件类型、文件大小的限制,文件类型必须为png
或者jpeg
,同时上传的文件大小要小于等于100000
High
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// 我们要写到哪里去?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$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' ];
// strrpos()函数查找后面参数字符串在前面参数字符串中最后一次出现的位置,这里即“.”后面的文件后缀
// substr()函数返回字符串的一部分,规定在字符串的何处开始
// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// strtolower()函数把字符串转换为小写
// getimagesize()函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回FALSE
// 我们能把文件移到上传文件夹吗?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
源代码分析,strrpos()
函数直接找出上传文件后缀,然后strtolower()
函数将文件后缀全部小写,进行文件类型匹配限制,在限制类型和大小的基础上,getimagesize()
函数确保了上传的必须为图像,即可上传图片🐎进行绕过
Impossible
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// 检查防范CSRF的token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// File information
$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 = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
$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;
// uniqid()函数基于以微秒计的当前时间,生成一个唯一的ID
// $temp_file即临时文件
// ini_get()用来取php.ini文件里的环境变量的值
// sys_get_temp_dir()返回用于临时文件的目录路径
// DIRECTORY_SEPARATOR是一个显示系统分隔符的命令
// Is it an image? 从文件后缀、大小、类型限制
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);
// imagecreatefromjpeg()是一个内置函数,可从JPEG文件或URL新建一图像,此函数成功时返回图像资源标识符,失败时返回FALSE
// imagejpeg():以JPEG格式将图像输出到浏览器或文件
}
else {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_file, 9);
// imagepng():以PNG格式将图像输出到浏览器或文件
}
imagedestroy( $img );
// imagedestroy()是一个内置函数,用于摧毁图像并释放与图像相关的内存
// Can we move the file to the web root from the temp folder? 改变文件名
if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
// getchwd()函数返回当前工作目录
// rename()函数重命名文件或目录
// Yes!
echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
// 删除所有临时文件
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
相比较前面防范,这里明显加大了力度
1、在限制文件的后缀名、文件类型、文件大小和确保图像的基础上
2、以创建临时文件的方式,通过imagecreatefromjpeg()
这类函数对文件图像的内容进行过滤,确保没有图片🐎
3、并且重命名文件图片,上传成功后,只是将文件名显示出来,没有暴露文件路径
六、Insecure CAPTCHA(不安全的验证码)
Low
?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// 隐藏验证码表单
$hide_form = true;
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// 从第三方检查验证码
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']
)
// 验证码失败了吗?
if( !$resp ) {
// 当验证码输入错误时会发生什么?
$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;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// 检查两个密码是否匹配
if( $pass_new == $pass_conf ) {
// They do!
$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 );
// Update database
$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>' );
// Feedback for the end user
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);
}
?>
1、调用recaptcha_check_answer(privkey,remoteip,challenge,response)
函数,参数privkey是服务器申请的private key,remoteip是用户的ip,challenge 是 recaptcha_challenge_field 字段的值 ,来自前端页面 , response是 recaptcha_response_field字段的值
2、这里分为两步验证,第一步验证密码,第二步更新密码,这里可以直接跳过第一步,进行第二步更新密码
Medium
<?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// 隐藏验证码表单
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// 从第三方检查验证码
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// 验证码失败了吗?
if( !$resp ) {
// 当验证码输入错误时会发生什么
$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=\"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;
// Get input
$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 ) {
// They do!
$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 );
// Update database
$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);
}
?>
增加设置passed_captcha
参数,来确保是经过第一阶段,然后才进行第二阶段更新密码
High
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// 隐藏验证码表单
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// 从第三方检查验证码
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// $resp(这里是指谷歌返回的验证结果)的值我们控制不了,是由recaptcha_check_answer( )决定的
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 );
// Update database
$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>' );
// Feedback for user
echo "<pre>Password Changed.</pre>";
} else {
// 密码不匹配
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
} else {
// 当验证码输入错误时会发生什么
$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);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
1、两步验证换为一个验证
2、可以判断出当$resp == False
以及g-recaptcha-response != hidd3n_valu3
或者HTTP_USER_AGENT != reCAPTCHA
的时候,即无法通过验证,$resp
的值我们控制不了,是由谷歌返回的验证结果,所以我们可以从g-recaptcha-response
和HTTP_USER_AGENT
(http包头的User-Agent
参数)入手
Impossible
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// 检查token
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 ); //进行md5加密
// stripslashes函数删除反斜杠
// mysqli_real_escape_string()函数转义在 SQL 语句中使用的字符串中的特殊字符,前面有详细内容
$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 );
// 从第三方检查验证码
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// 验证码失败了吗
if( !$resp ) {
// 当验证码输入错误时会发生什么
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
}
else {
// 检查当前用户名和密码是否正确,进行PDO预处理
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' ); //预处理语句
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR ); //绑定一个参数到指定的变量名,即用dvwaCurrentUser()参数替换":user",类型是str
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute(); //执行一条预处理语句
// 新密码是否匹配,当前密码是否正确?
if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
// Update the database
$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();
// Feedback for the end user - success!
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;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到,Impossible
级别的代码增加了token
机制防御CSRF攻击,利用PDO
技术防护sql注入,验证过程也分成两部分了,验证码无法绕过,同时要求用户输入之前的密码,进一步加强了身份认证
七、SQL Injection(SQL 注入)
概述
SQL 注入攻击包括通过从客户端到应用程序的输入数据插入或注入SQL 查询,成功的 SQL 注入漏洞可以从数据库中读取敏感数据、修改数据库数据(插入/更新/删除)、对数据库执行管理操作(例如关闭 DBMS)、恢复 DBMS 文件中存在的给定文件的内容系统并在某些情况下向操作系统发出命令,SQL 注入攻击是一种注入攻击,其中 SQL 命令被注入到数据平面输入中,以影响预定义 SQL 命令的执行
Low
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// 输入
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$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>' );
// // mysqli_query()针对成功的SELECT、SHOW、DESCRIBE或 EXPLAIN查询,将返回一个mysqli_result对象,针对其他成功的查询,将返回TRUE,如果失败,则返回FALSE
// 得到结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// mysqli_fetch_assoc()函数从结果集中取得一行作为关联数组
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
分析源码,可以看到,在进行数据库查询的时候,$id是由引号包着的
,这里是字符注入
,同时,没有对输入的字符进行过滤
限制,所以存在SQL注入
Medium
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// 输入
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
// mysqli_real_escape_string()函数,表示转义在 SQL 语句中使用的字符串中的特殊字符,如0x00、\n、\r、\、'、" 、0x1a(ctrl+z)
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
// mysqli_query()针对成功的SELECT、SHOW、DESCRIBE或 EXPLAIN查询,将返回一个mysqli_result对象,针对其他成功的查询,将返回TRUE,如果失败,则返回FALSE
// 得到结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// mysqli_fetch_assoc()函数从结果集中取得一行作为关联数组
// 显示值
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
// 这将在稍后的index.php页面中使用
// 在这里设置它,这样我们就可以像在其他源脚本中一样在这里关闭数据库连接
$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_fetch_row()函数从结果集中取得一行,并作为枚举数组返回
mysqli_close($GLOBALS["___mysqli_ston"]);
// mysqli_close()函数关闭先前打开的数据库连接
?>
这里相比较与前面,主要增加了mysqli_real_escape_string()
函数,对输入的参数进行过滤,表示转义
在 SQL 语句中使用的字符串中的特殊字符,如0x00、\n、\r、\、'、" 、0x1a(ctrl+z)
,只要避开这些特殊字符就行
High
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// 输入
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );
// limit 1表示检索前面一个记录行,只能输出一个结果
// 得到结果
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
这里也只是在查询数据库的时候多了个limit 1
这样的一个限制,不过可以在注入的时候加个#
等符号注释掉即可
Impossible
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// 检查token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 输入
$id = $_GET[ 'id' ];
// 输入了数字吗?
if(is_numeric( $id )) {
// 检查数据库,采用预处理方式
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); //预处理语句
$data->bindParam( ':id', $id, PDO::PARAM_INT ); //绑定一个参数到指定的变量名,即用$id参数替换":id",int型
$data->execute(); //执行一条预处理语句
$row = $data->fetch(); //从结果集中获取下一行
// 确保只返回1个结果,即ture
if( $data->rowCount() == 1 ) {
// rowCount()返回受DELETE、INSERT、或UPDATE语句影响的行数,对SELECT 语句影响返回有所不同
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
采用预处理的方式是避免SQL注入的应对措施,让你的注入无从下手
八、SQL Injection (Blind)(SQL 注入(盲注))
描述
1、盲注 SQL(结构化查询语言)注入是一种SQL 注入攻击,它向数据库询问真假问题,并根据应用程序的响应确定答案。当 Web 应用程序被配置为显示一般错误消息但没有缓解易受 SQL 注入攻击的代码时,通常会使用这种攻击
2、当攻击者利用 SQL 注入时,有时 Web 应用程序会显示来自数据库的错误消息,抱怨 SQL 查询的语法不正确。SQL 盲注与普通SQL 注入几乎相同,唯一的区别是从数据库中检索数据的方式。当数据库不向网页输出数据时,攻击者被迫通过向数据库询问一系列真假问题来窃取数据。这使得利用 SQL 注入漏洞变得更加困难,但并非不可能
Low
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// 输入
$id = $_GET[ 'id' ];
// 检查数据库
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // 删除'or die'去避免提示mysql错误
// mysqli_query()针对成功的SELECT、SHOW、DESCRIBE或 EXPLAIN查询,将返回一个mysqli_result对象,针对其他成功的查询,将返回TRUE,如果失败,则返回FALSE
// 得到结果
$num = @mysqli_num_rows( $result );
// '@'字符抑制错误
// mysqli_num_rows()函数返回结果集中行的数量,如果查询失败,FALSE=0
if( $num > 0 ) {
// 最终对用户的反馈
echo '<pre>User ID exists in the database.</pre>';
}
else {
// 没有找到用户,所以页面没有!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
所谓盲注,就是让你看不到出现的错误,在执行查询的时候,删除了or die
,就是避免提示错误,同时,在调用mysqli_num_rows()
函数的时候也用的@
符号,就是防止出现错误信息
Medium
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// 输入
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// mysqli_real_escape_string()函数转义在 SQL语句中使用的字符串中的特殊字符
// trigger_error()函数创建用户自定义的错误消息
// 检查数据库
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid );
// 删除'or die'抑制mysql错误
// 结果
$num = @mysqli_num_rows( $result ); // '@'字符抑制错误
if( $num > 0 ) {
// 查询成功
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
这里是数字型盲注,相比较前面,mysqli_real_escape_string()
函数,对输入的参数进行了特殊字符的过滤
High
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// 输入
$id = $_COOKIE[ 'id' ];
// 检查数据库
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid );
// 删除'or die'抑制 mysql错误
// 结果
$num = @mysqli_num_rows( $result );
// '@'字符抑制错误
if( $num > 0 ) {
// 成功查询
echo '<pre>User ID exists in the database.</pre>';
}
else {
// 睡眠时间可能是随机的
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// 没有找到用户,所以页面没有!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
echo '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
对比前面,在进行查询的时候加了点限制,Limit 1
,只能输入一行,可以使用注释符去掉,同时在防错误上增加了睡眠时间,防止爆破
Impossible
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// 检查token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 输入
$id = $_GET[ 'id' ];
// 输入数字了吗
if(is_numeric( $id )) {
// 检查数据库,进行预处理
$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(); //执行一条预处理语句
// 得到结果
if( $data->rowCount() == 1 ) {
// rowCount()返回受DELETE、INSERT、或UPDATE语句影响的行数,对SELECT 语句影响返回有所不同,这里为ture,即等于1
// 成功查询
echo '<pre>User ID exists in the database.</pre>';
}
else {
// 没有找到用户,所以页面没有!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// 生成token
generateSessionToken();
?>
在检查token的基础上,进行了查询预处理,加强了防范
九、Weak Session IDs(弱会话 ID)
描述
由于Session ID是用户登录之后才持有的唯一认证凭证,如果SessionID的生成规律过于简单(即weak),则会被黑客窃取,此时在一定程度上就等同于账户失窃,黑客就不需要再攻击登陆过程即可获取访问权限,无需登录密码直接进入特定用户界面
Low
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
// $_SERVER['REQUEST_METHOD'] 访问页面时的请求方法
if (!isset ($_SESSION['last_session_id'])) {
$_SESSION['last_session_id'] = 0;
}
// 这里重置seesion值
$_SESSION['last_session_id']++;
$cookie_value = $_SESSION['last_session_id'];
setcookie("dvwaSession", $cookie_value);
// 设置一个新cookie
// setcookie() 函数向客户端发送一个 HTTP cookie
// setcookie(name,value),name确定cookie的名称,value确定cookie的值
}
?>
弱会话,点击一次就换一次cookie
,cookie值加一
Medium
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
// Time函数从服务器上获取时间,进行格式化,来作为新的cookie值
setcookie("dvwaSession", $cookie_value);
// setcookie(name,value),name确定cookie的名称,value确定cookie的值
}
?>
这里的cookie变换
是随时间变化而变化的
High
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
// $_SERVER['REQUEST_METHOD'] 访问页面时的请求方法
if (!isset ($_SESSION['last_session_id_high'])) {
$_SESSION['last_session_id_high'] = 0;
}
// 重置cookie值
$_SESSION['last_session_id_high']++;
$cookie_value = md5($_SESSION['last_session_id_high']);
// 在令cookie值加一的基础上,将cookie值进行md5加密
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
// setcookie(name,value,expire,path,domain,secure)
// 1、name必需,确定cookie的名称 2、value必需,确定cookie的值 3、expire可选,确定cookie的有效期 4、path可选,规定cookie的服务器路径
// 5、domain可选,规定cookie的域名 6、secure可选,规定是否通过安全的HTTPS连接来传输cookie
// $_SERVER['HTTP_HOST']表示当前请求的Host:头部的内容
}
?>
对cookie值进行md5加密
后,对cookie值
存在的时间生产一个有效期
,同时规定cookie的服务器路径
,确定了cookie的域名
,弱会话的实现难度加大
Impossible
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
// sha1()函数计算字符串的 SHA-1散列
// mt_rand()函数可用于生成伪随机数
// time()格式化当前时间来利用
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true);
// setcookie(name,value,expire,path,domain,secure)
// 1、name必需,确定cookie的名称 2、value必需,确定cookie的值 3、expire可选,确定cookie的有效期 4、path可选,规定cookie的服务器路径
// 5、domain可选,规定cookie的域名 6、secure可选,规定是否通过安全的HTTPS连接来传输cookie
// $_SERVER['HTTP_HOST']表示当前请求的Host:头部的内容
}
?>
这里对cookie值
进行随机的选取,mt_rand()
和time()
函数随机cookie值,然后求改值的散列值
,进一步复杂,在setcookie()
函数的配置上,参数也更复杂,采用安全的HTTPS连接来传输cookie
十、DOM Based Cross Site Scripting(基于 DOM 的跨站脚本(XSS))
描述
基于 DOM 的XSS(或在某些文本中称为“type-0 XSS”)是一种 XSS 攻击,其中攻击载荷是通过修改原始客户端使用的受害者浏览器中的 DOM“环境”而执行的脚本,以便客户端代码以“意外”方式运行。也就是说,页面本身(即 HTTP 响应)没有改变,但页面中包含的客户端代码由于 DOM 环境中发生的恶意修改而执行不同,这与其他 XSS 攻击(存储或反射)形成对比,其中攻击有效载荷放置在响应页面中(由于服务器端缺陷)
Low
<?php
# No protections, anything goes
// 没有保护措施,什么都有
?>
直接在get请求后面接上XSS即可
Medium
<?php
// 有什么投入吗
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
// array_key_exists()函数检查某个数组中是否存在指定的键名,如果键名存在则返回true,如果键名不存在则返回false
// is_null — 检测变量是否为NULL
$default = $_GET['default'];
// 不允许脚本标记
if (stripos ($default, "<script") !== false) {
// stripos()函数查找字符串在另一字符串中第一次出现的位置(不区分大小写)
header ("location: ?default=English");
// header()函数向客户端发送原始的 HTTP 报头
exit;
}
}
?>
这里对输入参数多了一些判断,检查参数名和参数是否为NULL,调用stripos()
函数过滤"<script"
这个特殊字符
High
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
// array_key_exists()函数检查某个数组中是否存在指定的键名,如果键名存在则返回true,如果键名不存在则返回false
// 白名单允许的语言,switch选择
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
// ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
在前面的基础上,设置了白名单
,只允许白名单上的四个语言
Impossible
<?php
// Don't need to do anything, protction handled on the client side
// 不需要做任何事情,保护在客户端处理
?>
就是在客户端设置了拦截,无需服务端做什么
十一、Reflected Cross Site Scripting (反射式跨站点脚本 (XSS))
概述
反射式攻击是指注入的脚本从 Web 服务器反射出来的攻击,例如在错误消息、搜索结果或任何其他响应中,其中包括作为请求的一部分发送到服务器的部分或全部输入。反射攻击通过其他途径传递给受害者,例如在电子邮件消息中或在其他网站上。当用户被诱骗点击恶意链接、提交特制表单,甚至只是浏览到恶意网站时,注入的代码就会传播到易受攻击的网站,从而将攻击反射回用户的浏览器。然后浏览器执行代码,因为它来自“受信任的”服务器。反射型 XSS 有时也称为非持久性或 II 型XSS
Low
<?php
header ("X-XSS-Protection: 0");
// 输入
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// array_key_exists()函数检查某个数组中是否存在指定的键名,如果键名存在则返回true,如果键名不存在则返回false
// 输出
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
很典型的输入什么,就输出什么,简称反射
Medium
<?php
header ("X-XSS-Protection: 0");
// 输入
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// array_key_exists()函数检查某个数组中是否存在指定的键名,如果键名存在则返回true,如果键名不存在则返回false
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// str_replace()函数以其他字符替换字符串中的一些字符(区分大小写),这里将<script>字符转换为空
// 输出
echo "<pre>Hello ${name}</pre>";
}
?>
这里多了对输入字符的过滤,将含有<script>
的字符替换掉
High
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
// preg_replace函数执行一个正则表达式的搜索和替换,i表示不区分大小写
echo "<pre>Hello ${name}</pre>";
}
?>
这里的过滤限制严格一些,采用正则替换,无论大小写,双写等绕过,<script
字符都没办法绕过的
Impossible
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// 检查防范CSRF的令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// 输入
$name = htmlspecialchars( $_GET[ 'name' ] );
// htmlspecialchars()函数把一些预定义的字符转换为 HTML实体
// & (和号)成为 &
// " (双引号)成为 "
// ' (单引号)成为 '
// < (小于)成为 <
// > (大于)成为 >
// 输出
echo "<pre>Hello ${name}</pre>";
}
// 生成防范CSRF的令牌
generateSessionToken();
?>
htmlspecialchars()
函数将一些字符转为HTML实体,实则也是一种特殊字符过滤
十二、Stored Cross Site Scripting (存储跨站点脚本 (XSS))
描述
存储攻击是将注入的脚本永久存储在目标服务器上的攻击,例如数据库、消息论坛、访问者日志、评论字段等。然后受害者在请求存储时从服务器检索恶意脚本信息,存储型 XSS有时也称为 Persistent或 Type-I XSS
Low
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// 输入
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// trim()函数从字符串的两端删除空白字符和其他预定义字符,当第二个参数为空,默认移除("\0"-NULL)("\t"-制表符)("\n"-换行)("\x0B"-垂直制表符)("\r"-回车)(" "-空格)这六个
// 给输入的信息消毒
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// stripslashes()删除反斜杠
// mysqli_real_escape_string()函数,表示转义在SQL语句中使用的字符串中的特殊字符,如0x00、\n、\r、\、'、" 、0x1a(ctrl+z)
// 给输入的名字消毒
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// 更新数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$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>' );
//mysql_close();
}
?>
将输入的信息和名字两个参数去除反斜杠,同时调用mysqli_real_escape_string()
函数,将一些特殊字符进行转义
Medium
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// trim()函数从字符串的两端删除空白字符和其他预定义字符,当第二个参数为空,默认移除("\0"-NULL)("\t"-制表符)("\n"-换行)("\x0B"-垂直制表符)("\r"-回车)(" "-空格)这六个
// 进行过滤
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// addslashes()函数返回在预定义的字符前添加反斜杠的字符串,预定义字符是:单引号(')、双引号(")、反斜、(\)、NULL
// strip_tags()函数剥去字符串中的HTML、XML以及 PHP的标签
// htmlspecialchars()函数把一些预定义的字符转换为HTML实体,前面题目有提到
// 进行过滤
$name = str_replace( '<script>', '', $name ); //去掉<script>符号
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// 更新数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$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>' );
//mysql_close();
}
?>
对输入的两个参数都进行了分别进行了过滤,其他照常
High
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// 进行过滤
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// 过滤
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// preg_replace()函数进行了正则过滤
// 更新数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$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>' );
//mysql_close();
}
?>
对比前面一难度,这里比前面多了正则过滤,过滤掉大小写,双写等等的<script
字符
Impossible
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// 检查token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// trim()函数从字符串的两端删除空白字符和其他预定义字符,当第二个参数为空,默认移除("\0"-NULL)("\t"-制表符)("\n"-换行)("\x0B"-垂直制表符)("\r"-回车)(" "-空格)这六个
// 过滤
$message = stripslashes( $message ); // stripslashes()删除反斜杠
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// htmlspecialchars()函数把一些预定义的字符转换为HTML实体,前面题目有提到
// 过滤
$name = stripslashes( $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$name = htmlspecialchars( $name );
// 更新数据库,同时进行预处理
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' ); // 预处理语句
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR ); // 绑定一个参数到指定的变量名,即用$name参数替换":name",str型
$data->execute(); // 执行一条预处理语句
}
// 生成token
generateSessionToken();
?>
Impossible
这里比前面多了在数据库进行更新的时候,要进行预处理