截至2022.11.24,DVWA共有十四个模块,分别是:
Brute Force(暴力破解)
Command Injection(命令行注入)
CSRF(跨站请求伪造)
File Inclusion(文件包含)
File Upload(文件上传)
Insecure CAPTCHA (不安全的验证码)
SQL Injection(SQL注入)
SQL Injection(Blind)(SQL盲注)
Weak Session IDs (弱会话ID)
XSS (DOM) (DOM型跨站脚本)
XSS(Reflected)(反射型跨站脚本)
XSS(Stored)(存储型跨站脚本)
CSP Bypass (CSP绕过)
JavaScript
按我的思路就是,Low、Media、High是拿来攻击的,Impossible是来教你防御的
然后就是缩减了一下目录,本来攻击方式、源码审计、漏洞原理都加了标题,但是那样目录就太长太丑了,想想还是删了算了
直接开冲!
暴力破解,又叫撞库、穷举,使用大量的字典逐个在认证接口尝试登录,理论上,只要字典足够强大,破解总是会成功的。
阻止暴力破解的最有效方式是设置复杂度高的密码(英文字母大小写、数字、符号混合)。
而如果你的字典是从某网站泄露出来的,你使用它试图登陆其他网站,就便是撞库。撞库攻击的成功率高于暴力破解,因为你在A网站的用户名、密码通常和B网站的用户名、密码一致。
DVWA Security界面将难度设置为Low
先看登录界面,填写账号密码后抓包
bp抓包后转到intruder模块,给username和password加上tag§
type选择Cluster bomb
对应位置插入字典后,点击右边的attack开始攻击
也可以更改Options
的Number of threads
修改进程数,增加爆破效率
根据响应包的大小以及响应体的内容判断是否成功,可以看到成功爆出一个用户admin/password
,也是平台登录的默认账号
马上停止爆破,点到为止,一千四百万条顶不住啊。
源码审计
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "Welcome to the password protected area {$user}
"; $html .= "{$avatar}\" />"; } else { // Login failed $html .= ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
Username and/or password incorrect.
username
未过滤password
只有一个md5加密,但是对实际的爆破过程毫无影响
首先很直观的就是一个万能密码(
由于限制了结果只能有一条,要加上limit 1
admin' or 1=1 limit 1#
爆破过程同上,但是可以明显感觉到,爆破的速度慢了很多
为了方便演示,直接把admin/password放在前几条了,反正多跑无益,还浪费时间
源码审计
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "Welcome to the password protected area {$user}
"; $html .= "{$avatar}\" />"; } else { // Login failed sleep( 2 ); $html .= ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
Username and/or password incorrect.
用了mysqli_real_escape_string()
函数转义输入的用户名密码,但是没有设置编码,在gbk编码的数据库上可以进行sql注入
登录失败后sleep(2)
延迟两秒
抓包发现url栏多了一项user_token
bp抓包后发现,只有在token正确的情况下才能尝试登录,其他情况均为302重定向
bp也是有带token爆破的功能的,首先转到intruder模块,选择Pitchfork模式
payload1是密码位,正常插入密码字典
payload2是token位,选择Recursive grep
设置Start和End,将数据留在中间,也可以直接用鼠标在下方拖动选取文字
PS:设置grep时建议刷新一次浏览器,填入最新的token,避免访问失效,下面框里没东西
设置完成后回到payload界面选中
因为token不能复用,将option的线程设置为一,重定向选择always
开始爆破后即可根据响应包大小判断是否成功
源码审计
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
$html .= "Welcome to the password protected area {$user}
"; $html .= "{$avatar}\" />"; } else { // Login failed sleep( rand( 0, 3 ) ); $html .= ""; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } // Generate Anti-CSRF token generateSessionToken(); ?>
Username and/or password incorrect.
开始后先校验tokencheckToken()
,若token不匹配则重定向到登陆界面,不再进行判断
结束前生成新的tokengenerateSessionToken()
账号密码多用了一个stripslashes()
函数删除反斜杠
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 .= "
This account has been locked due to too many incorrect logins.
";
// 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) . "
";
print "The timenow is: " . date ("h:i:s", $timenow) . "
";
print "The timeout is: " . date ("h:i:s", $timeout) . "
";
*/
// 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
";
}
}
// 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 .= "Welcome to the password protected area {$user}
";
$html .= "{$avatar}\" />";
// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
$html .= "Warning: Someone might of been brute forcing your account.
";
$html .= "Number of login attempts: {$failed_login}.
Last login attempt was at: ${last_login}.
";
}
// 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 .= "
Username and/or password incorrect.
Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in {$lockout_time} minutes.
";
// 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();
?>
代码行数直接比High多了一倍多,虽然注释挺多的
提交方式由GET改为POST
失败次数是写入数据库的,sql语句也改为参数化查询
不好说了,立体机动防御了
命令注入,是指在某些需要输入数据的位置,构造恶意代码破环原有的语句结构,而系统缺少有效的过滤,许多内容管理cms存在命令注入漏洞。
简洁的界面
象征性的ping一下,编码问题就不管了,影响不大
先来一个经典的ping
命令执行常用的方式为||
和&&
管道符
更多的可以看看这个命令执行
还有一些过滤技巧CTF—命令执行总结
||
当前项执行失败后执行后项
&&
前项成功后执行后项
源码审计
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 .= "{$cmd}
";
}
?>
没有过滤,简单判断一下操作系统就拼接执行了
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 );
// 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 .= "{$cmd}
";
}
?>
还多过滤了一个分号,其他没啥了
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);
// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the characters in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// 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 .= "{$cmd}
";
}
?>
还真就剩了个|
,不知道为什么过滤'| ' => '','||' => '',
l后面是空格说明仅仅过滤了l+空格 没过滤单独的l,他好温柔,我哭死
打都不打,直接看源码
源码审计
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 .= "{$cmd}
";
}
else {
// Ops. Let the user name theres a mistake
$html .= 'ERROR: You have entered an invalid IP.
';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
还是先checkToken()
然后$octet = explode( ".", $target );
接is_numeric()
硬性规定只能输入ip了
一种可以被攻击者用来通过用户浏览器冒充用户身份向服务器发送伪造请求并被目标服务器成功执行的漏洞被称之为CSRF漏洞。
特点:
用户浏览器:表示的受信任的用户
冒充身份:恶意程序冒充受信任用户(浏览器)身份
伪造请求:借助于受信任用户浏览器发起的访问
由于CSRF攻击的特殊性,还是以钓鱼和源码解析为主好了
源码审计
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the user
$html .= "Password Changed."; } else { // Issue with passwords matching $html .= "
Passwords did not match."; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
没有安全防护措施,简单判断一下输入的新旧密码是否相同,然后修改当前用户的密码
渗透测试
先看一眼界面,下面是平平无奇的更改密码用的表格,上面是一个测试登陆用的弹窗,直接改密码!
可以看到是GET方法修改密码
点击Test,登录成功
此时就可以bp抓包生成CSRF Poc了
保存到本地文件然后将密码改为111111,点击发送
密码修改成功
源码审计
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the user
$html .= "Password Changed."; } else { // Issue with passwords matching $html .= "
Passwords did not match."; } } else { // Didn't come from a trusted source $html .= "
That request didn't look correct."; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
增加了$_SERVER[ 'HTTP_REFERER' ]
头验证
渗透测试
题目的过滤方式为stripos()
,可以利用命名文件夹的方式绕过Referer
头的检测
源码审计
$change = false;
$request_type = "html";
$return_message = "Request Failed";
if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
$data = json_decode(file_get_contents('php://input'), true);
$request_type = "json";
if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
array_key_exists("password_new", $data) &&
array_key_exists("password_conf", $data) &&
array_key_exists("Change", $data)) {
$token = $_SERVER['HTTP_USER_TOKEN'];
$pass_new = $data["password_new"];
$pass_conf = $data["password_conf"];
$change = true;
}
} else {
if (array_key_exists("user_token", $_REQUEST) &&
array_key_exists("password_new", $_REQUEST) &&
array_key_exists("password_conf", $_REQUEST) &&
array_key_exists("Change", $_REQUEST)) {
$token = $_REQUEST["user_token"];
$pass_new = $_REQUEST["password_new"];
$pass_conf = $_REQUEST["password_conf"];
$change = true;
}
}
if ($change) {
// Check Anti-CSRF token
checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );
// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
$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 );
// Feedback for the user
$return_message = "Password Changed.";
}
else {
// Issue with passwords matching
$return_message = "Passwords did not match.";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
if ($request_type == "json") {
generateSessionToken();
header ("Content-Type: application/json");
print json_encode (array("Message" =>$return_message));
exit;
} else {
$html .= ""
. $return_message . "
";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
先验证token是否存在,再验证token值是否正确。要绕过High级别的反CSRF机制,关键是要获取用户当前token。
渗透测试
还是先抓个包
由于加了token,这里的token就要通过其他方法获取了,比如先弹一个xss什么的(
标准答案来咯
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
$html .= "Password Changed.
";
}
else {
// Issue with passwords matching
$html .= "Passwords did not match or current password incorrect.
";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
程序开发人员通常会把可重复使用的函数写到单个文件中,在使用某个函数的时候,直接调用此文件,无需再次编写,这种调用文件的过程通常称为包含。
文件包含函数加载的参数没有经过过滤或严格定义,可以被用户控制,包含其他非预期文件,导致了文件信息泄露或执行非预期代码。
点开界面是三个文件包含
点击后是三个文件包含,URL为?page=file3.php
甚至XSS
回到正题,有1、2、3那可以试试4
目录穿越,绝对路径,远程文件包含都是没问题的
源码审计
还真是,简洁明了啊
// The page we wish to display
$file = $_GET[ 'page' ];
?>
来者不拒,下一位!
先包含file4.php,没有问题
然后测试我们的phpinfo,可以发现目录穿越和远程包含失效了,准确的说../
和http协议被替换为空了
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\\" ), "", $file );
?>
如图所示,润
不过可以用file协议绕过?page=file:///D:/phpstudy_pro/rua/DVWA-master/phpinfo.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;
}
?>
匹配$file必须为file
开头的字符串或include.php
因为只匹配了开头,那么file协议刚刚好满足此条件
linux系统就可以尝试去包含一些日志啊、/etc/shadow、/etc/passwd之类的了
// 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,即文件上传漏洞,指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限。
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
$html .= 'Your image was not uploaded.
';
}
else {
// Yes!
$html .= "{$target_path} succesfully uploaded!
";
}
}
?>
无检测,无黑白名单过滤
接着上传php,提示只能上传jpeg或png
打开bp,抓包修改Content-Type,上传成功
源码审计
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
$html .= 'Your image was not uploaded.
';
}
else {
// Yes!
$html .= "{$target_path} succesfully uploaded!
";
}
}
else {
// Invalid file
$html .= 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
?>
判断了type和文件大小
无伤大雅
传来传去传不上去了,生成个图片去文件包含好了(
先来个图片
copy 1.png/b+1.php/a shell.png
上传的时候注意图片大小,或者直接用1像素图片()
本地测试成功
使用工具时记得带上cookie,到时候返回数据为空找谁都不好使
源码审计
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
$html .= 'Your image was not uploaded.
';
}
else {
// Yes!
$html .= "{$target_path} succesfully uploaded!
";
}
}
else {
// Invalid file
$html .= 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
?>
要求后缀名是图片,getimagesize()检测结果不能为false
只能传图片然后文件包含了
源码来咯
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!
$html .= "${target_file} succesfully uploaded!
";
}
else {
// No
$html .= 'Your image was not uploaded.
';
}
// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
$html .= 'Your image was not uploaded. We can only accept JPEG or PNG images.
';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
checkToken() 判断token是否正确
md5( uniqid() . $uploaded_name ) 对上传的文件进行重命名
strtolower( $uploaded_ext ) 判断后缀名
$uploaded_type 判断文件mimetype
getimagesize() 判断文件起始字符是否符合
imagecreatefromjpeg() 图片二次渲染(可以绕过)
rename() 判断文件是否能移动到web目录下
心血来潮想研究下getimagesize(),发现好像仅仅只对文件开始的几个字符进行了判断,例如PNG的
// Retrieve PNG width and height without downloading/reading entire image.
function getpngsize( $img_loc ) {
$handle = fopen( $img_loc, "rb" ) or die( "Invalid file stream." );
if ( ! feof( $handle ) ) {
$new_block = fread( $handle, 24 );
if ( $new_block[0] == "\x89" &&
$new_block[1] == "\x50" &&
$new_block[2] == "\x4E" &&
$new_block[3] == "\x47" &&
$new_block[4] == "\x0D" &&
$new_block[5] == "\x0A" &&
$new_block[6] == "\x1A" &&
$new_block[7] == "\x0A" ) {
if ( $new_block[12] . $new_block[13] . $new_block[14] . $new_block[15] === "\x49\x48\x44\x52" ) {
$width = unpack( 'H*', $new_block[16] . $new_block[17] . $new_block[18] . $new_block[19] );
$width = hexdec( $width[1] );
$height = unpack( 'H*', $new_block[20] . $new_block[21] . $new_block[22] . $new_block[23] );
$height = hexdec( $height[1] );
return array( $width, $height );
}
}
}
return false;
}
?>
PHP手册里只有jpg和png的
至于gif的话,我感觉Gif89a
应该是可以直接绕过判断的
if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' || strtolower( $uploaded_ext ) == 'gif' ) &&
( $uploaded_size < 100000 ) &&
( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' || $uploaded_type == 'image/gif' ) &&
getimagesize( $uploaded_tmp ) ) {
然而在判断里加入gif后,经过实际测试发现,只需要GIF
三个字符即可绕过getimagesize()
GI时就失败了,报错信息我没改,小写的Gif也是没有用的,小tips,可以记着
Insecure CAPTCHA,不安全的验证码
验证码:6
感觉……不如Unsafe verification process(不安全的验证过程),主要是验证流程出现了逻辑漏洞。
没有密钥的可以根据提示链接,科学上网,创建一个key,类型没深究,选了一个v2隐形,然后将密钥复制到本地config.inc.php
文件即可
网址乱填的,如有雷同,不胜荣幸,域名改为127.0.0.1即可,此时本地访问就不要用localhost了
我发现这个隐形嘎嘎简单,单击点一下就验证成功了,啥也不用干
开始正题
本来挺快的
bp代理一开就卡死在google了,可能代理不走梯子?
本来想摆烂直接审源码的,后面一想,反正目前任务是绕过验证码,直接默认不通过就好了(
直接注释有关验证码的函数
直接改为0,默认不通过
搞定
好耶,码没了(
提交修改,bp抓包能看到step=1,并且响应包提示验证码错误
看到1就应该改2试试,更改成功,绕过了验证
源码审计
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']
// );
$resp=0;
// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "
The CAPTCHA was incorrect. Please try again.
";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
$html .= "
You passed the CAPTCHA! Click the button to confirm your changes.
\" />
{$pass_conf}\" />
";
}
else {
// Both new passwords do not match.
$html .= "Both passwords must match.
";
$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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for the end user
$html .= "Password Changed."; } else { // Issue with the passwords matching $html .= "
Passwords did not match."; $hide_form = false; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } ?>
第一次看见Low这么长的,长归长,其实就两部分,分为step1和step2
step1的验证码通过两次密码相同后会出现一个全是type="hidden"
的表单和一个按钮其中有一条,进入到step2
step2就更加简单了,比较密码是否相同后直接修改
step1不变,step2多了一条You have not passed the CAPTCHA.
偷偷将$resp
改为1,发送抓包,看到响应包里多了一条passed_captcha=true
由于没有具体的数值,很明显,这也是可以伪造的,将$resp
改回0,重新发包,修改step=2和添加passed_captcha=true。
结果的话还是和上图一样
源码审计
step1的表单多了一行
<input type="hidden" name="passed_captcha" value="true" />
step2多了一个对该行的判断
// Check to see if they did stage 1
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "
You have not passed the CAPTCHA.
";
$hide_form = false;
return;
}
过
不看源码都知道加了token
下一步只能根据源码了
修改UA头User-Agent: reCAPTCHA
添加POST参数g-recaptcha-response=hidd3n_valu3
token检测没开,无所谓了
源码审计
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']
// );
$resp=0;
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( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Feedback for user
$html .= "Password Changed."; } else { // Ops. Password mismatch $html .= "
Both passwords must match."; $hide_form = false; } } else { // What happens when the CAPTCHA was entered incorrectly $html .= "
"; $hide_form = false; return; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); } // Generate Anti-CSRF token generateSessionToken(); ?>
The CAPTCHA was incorrect. Please try again.
验证就看这一句
if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
)
token少加了一行验证
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
验证码通过或者UA头和POST参数g-recaptcha-response符合要求
说白了还是没用上验证码的验证结果
源码审计
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
$html .= "
The CAPTCHA was incorrect. Please try again.
";
$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!
$html .= "Password Changed.
";
}
else {
// Feedback for the end user - failed!
$html .= "Either your current password is incorrect or the new passwords did not match.
Please try again.
";
$hide_form = false;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
总结加固方式
验证token
过滤密码
确认验证结果if( !$resp ) (划重点!!!)
参数化处理
验证就要好好单步验证,别整那些花里胡哨的,又是step2
又是passed_captcha
又是||
的
SQL Injection(SQL注入),是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的。
哟,老熟人
测试得单引号注入
order by 判断列数,2可以3报错得2列
点到为止,懒得注了
源码审计
删减版,另一个是SQLITE
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( ''
. ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '
' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "ID: {$id}"; } mysqli_close($GLOBALS["___mysqli_ston"]); break; } } ?>
First name: {$first}
Surname: {$last}
判断数据库,拼接执行,毫无过滤
F12瞅一眼流量,POST注入,复制请求体到hackbar
测试注入类型时发现单引号和双引号都被添加反斜杠过滤,我又是utf8的数据库,宽字节用不了了
结果反手就是一个数字型注入,瞬间失去了转义的意义
源码审计
接着删
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( ''
. mysqli_error($GLOBALS["___mysqli_ston"]) . '
' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "ID: {$id}"; } break; } } ?>
First name: {$first}
Surname: {$last}
mysqli_real_escape_string(),但是数字型
不让直接注了,改弹窗了
但是,没区别啊
源码审计
high.php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( 'Something went wrong.
' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "ID: {$id}
First name: {$first}
Surname: {$last}
";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
}
}
?>
session-input.php
define( 'DVWA_WEB_PAGE_TO_ROOT', '../../' );
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';
dvwaPageStartup( array( 'authenticated', 'phpids' ) );
$page = dvwaPageNewGrab();
$page[ 'title' ] = 'SQL Injection Session Input' . $page[ 'title_separator' ].$page[ 'title' ];
if( isset( $_POST[ 'id' ] ) ) {
$_SESSION[ 'id' ] = $_POST[ 'id' ];
//$page[ 'body' ] .= "Session ID set!
";
$page[ 'body' ] .= "Session ID: {$_SESSION[ 'id' ]}
";
$page[ 'body' ] .= "";
}
$page[ 'body' ] .= "
";
dvwaSourceHtmlEcho( $page );
?>
弹窗设置$_SESSION[ ‘id’ ],直接利用$_SESSION[ ‘id’ ]注入,还是无过滤,还以为会有点黑白名单什么的
源码审计
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 )) {
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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
$html .= "ID: {$id}
First name: {$first}
Surname: {$last}
";
}
break;
case SQLITE:
global $sqlite_db_connection;
$stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
$result = $stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.
$num_columns = $result->numColumns();
if ($num_columns == 2) {
$row = $result->fetchArray();
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
$html .= "ID: {$id}
First name: {$first}
Surname: {$last}
";
}
}
break;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
intval()过滤,绝杀!无解!
参数化查询锦上添花
单引号盲注
?id=1' and 1=1#&Submit=Submit#
?id=1' and 0=1#&Submit=Submit#
源码审计
惯例删除SQLITE
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
$exists = false;
if ($result !== false) {
try {
$exists = (mysqli_num_rows( $result ) > 0);
} catch(Exception $e) {
$exists = false;
}
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
}
if ($exists) {
// Feedback for end user
$html .= 'User ID exists in the database.
';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= 'User ID is MISSING from the database.
';
}
}
?>
判断查询结果有无数据
有就exists
没有就MISSING
POST,数字型
id=1 and 0=1#&Submit=Submit
id=1 and 1=1#&Submit=Submit
源码审计
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$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
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
$exists = false;
if ($result !== false) {
try {
$exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
}
?>
同上
mysqli_real_escape_string(),但是数字型
改成cookie注入了,大差不差
id=1' and 1=1#&Submit=Submit
id=1' and 0=1#&Submit=Submit
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
$exists = false;
if ($result !== false) {
// Get results
try {
$exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
break;
}
if ($exists) {
// Feedback for end user
$html .= 'User ID exists in the database.
';
}
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
$html .= 'User ID is MISSING from the database.
';
}
}
?>
cookie-input.php
define( 'DVWA_WEB_PAGE_TO_ROOT', '../../' );
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';
dvwaPageStartup( array( 'authenticated', 'phpids' ) );
$page = dvwaPageNewGrab();
$page[ 'title' ] = 'Blind SQL Injection Cookie Input' . $page[ 'title_separator' ].$page[ 'title' ];
if( isset( $_POST[ 'id' ] ) ) {
setcookie( 'id', $_POST[ 'id' ]);
$page[ 'body' ] .= "Cookie ID set!
";
$page[ 'body' ] .= "";
}
$page[ 'body' ] .= "
";
dvwaSourceHtmlEcho( $page );
?>
没过滤,直接取cookie拼接查询,概率sleep()
源码审计
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$exists = false;
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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();
$exists = $data->rowCount();
break;
case SQLITE:
global $sqlite_db_connection;
$stmt = $sqlite_db_connection->prepare('SELECT COUNT(first_name) AS numrows FROM users WHERE user_id = :id LIMIT 1;' );
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
$result = $stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.
$num_columns = $result->numColumns();
if ($num_columns == 1) {
$row = $result->fetchArray();
$numrows = $row[ 'numrows' ];
$exists = ($numrows == 1);
}
}
break;
}
}
// Get results
if ($exists) {
// Feedback for end user
$html .= 'User ID exists in the database.
';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= 'User ID is MISSING from the database.
';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
token,intval(),参数化
御三家
当用户登录后,在服务器就会创建一个会话(session),叫做会话控制,接着访问页面的时候就不用登录,只需要携带对应的cookie去访问。
sessionID作为特定用户访问站点所需要的唯一内容。如果能够计算或轻易猜到该sessionID,则攻击者将可以轻易获取访问权限,无需录直接进入特定用户认证界面,进而进行其他操作。
之后只要cookies随着http请求发送服务器,服务器就知道你是谁了。SessionID一旦在生命周期内被窃取,就等同于账户失窃(也就是熟悉的越权)。
先看
再点,可以发现每次点击dvwaSession都会加一
那么理论上就可以伪造成下一位或者上一位的id,来达到越权访问的效果
之前有次渗透就是,后台登录完cookie就是用户id,那时候我是4200,下意识把cookie改为1,成功访问到其他用户个人信息,嘎嘎越权。
dvwaSession咋改都没什么区别,还是建议加一个界面,比如
if ($_COOKIE['dvwaSession'] == 1){
$html= 'welcome admin
';
}
假设admin登录时$_SESSION[‘last_session_id’]为1,他的$_COOKIE[‘dvwaSession’]为1
此时我们将本地的$_COOKIE[‘dvwaSession’]改为1即可越权为admin登录
源码审计
$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);
}
if ($_COOKIE['dvwaSession'] == 1){
$html= 'welcome admin
';
}
?>
每次生成的id依次递增,遍历爆破一下即可越权。
先生成dvwaSession找找规律
1669944854
1669944903
1669944911
1669944955
干这行的多少还是有些敏感的,session就是时间戳,越权的话,还是从现在往前爆破就行
源码审计
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>
time()
先找规律
很怪,我的setcookie不生效了
不过,看还是能看的
c51ce410c124a10e0db5e4b97fc2af39
aab3238922bcc25a6f606eb525ffdc56
9bf31c7ff062936a96d3c8bd1f8f2ff3
6f4922f45568161a8cdf4ad2299f6d23
源码审计
$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
源码审计
$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
再sha1
这谁顶得住啊
DOM型XSS漏洞通常出现在以下情况: JavaScript从攻击者可控的源(如URL)获取数据,并将其传递给支持动态代码执行的接收器(如eval()或innerHTML)。这使得攻击者能够执行恶意的JavaScript,这通常允许他们劫持其他用户的帐户。
要实现基于dom的XSS攻击,需要将数据放置到源中,以便将其传播到接收器并导致执行任意JavaScript。
一眼,无事发生,
在多一眼看一眼就会被爆炸
点开下拉列表可以发现这个English是独立于选项之外的,也就是url传参
经典永流传
源码审计
Low.php
# No protections, anything goes
?>
彻底开摆了
index.php
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write(" + $decodeURI(lang) + "");
document.write("");
}
document.write("");
document.write("");
document.write("");
document.write("");
script>
无视风险,直接拼接
继续
但是直接重定向?default=English
了
直接尝试img标签发现直接拼接到value里了
闭合一下
English>/option>
源码审计
// 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, "
源码审计
// 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;
}
}
?>
php页面只允许表单内容提交,没有问题。
但是有内鬼,终止执行
源码审计
# Don't need to do anything, protection handled on the client side
?>
乐
转index.php
# For the impossible level, don't decode the querystring
$decodeURI = "decodeURI";
if ($vulnerabilityFile == 'impossible.php') {
$decodeURI = "";
}
document.write(" + $decodeURI(lang) + "");
反射型XXS是一种非持久性的攻击,它指的是恶意攻击者往Web页面里插入恶意代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的目的。
源码审计
header ("X-XSS-Protection: 0");
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
$html .= 'Hello '
. $_GET[ 'name' ] . '
';
}
?>
无过滤,直接拼
标签没了
上图片
反正图片噶了还有七七八八的标签和方法
源码审计
header ("X-XSS-Protection: 0"); // Is there any input? if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { // Get input $name = str_replace( '
噶了
比起能弹窗,我更想知道我的img哪去了
POST请求txtName= mtxMessage=
div
我的img捏
源码审计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( ' "; } $page[ 'body' ] .= ' ';
他好温柔,怕你创建不了特地准备了两个测试用例
但是我都执行不了,大抵是出不了网罢,算了,我不知道Medium
script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';
unsafe-inline:允许使用内联资源,如内联
源码审计$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"); # ?> <?php if (isset ($_POST['include'])) { $page[ 'body' ] .= " " . $_POST['include'] . " "; } $page[ 'body' ] .= ' ';
还是贴心的给出了教程
High
化繁为简了
Content-Security-Policy:script-src 'self';
粗看看不出什么,只能看到有两个solveSum是相同的
尝试bp抓包,将solveSum改为xss语句,执行成功
具体还得看网页引用的high.jsfunction clickButton() { var s = document.createElement("script"); s.src = "source/jsonp.php?callback=solveSum"; document.body.appendChild(s); } //单击按钮后生成一个js的外链(好像是这么叫,反正就是外部引入 function solveSum(obj) { if ("answer" in obj) { document.getElementById("answer").innerHTML = obj['answer']; } } //自定义函数solveSum(),修改answer的值 //solveSum({"answer":"15"}) var solve_button = document.getElementById ("solve"); if (solve_button) { solve_button.addEventListener("click", function() { clickButton(); }); } //给按钮上监听单机事件
所以引入XSS时便会执行
alert(1)({"answer":"15"})
弹窗源码审计
high.php<?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
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 = $_GET['callback']; echo $callback . "(".json_encode($outp).")";
jsonp.php?callback=solveSum
callback的值会原样输出回html页面造成XSSImpossible
源码审计
impossible.php<?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>Unlike the high level, this does a JSONP call but does not use a callback, instead it hardcodes the function to call.</p><p>The CSP settings only allow external JavaScript on the local server and no inline 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/impossible.js"></script> ';
jsonp_impossible.php
<?php header("Content-Type: application/json; charset=UTF-8"); $outp = array ("answer" => "15"); echo "solveSum (".json_encode($outp).")"; ?>
CSP限制为自身,js回调函数写死,摆烂咯~
JavaScript
应该是js代码有问题,导致一些不允许的操作,比如说更改样式(color)、删除限制(maxlength=“10”)、关闭验证(删除function函数)、修改条件(控制台修改指定值属性false为true)等
Low
那就试success
token错误
f12查看源码可以发现token都为js生成,且固定(默认ChangeMe
的md5(rot13())
)
再根据token的生成原理以及错误信息可以大胆猜测提交的token要改为success
的md5(rot13())
最简单的还是控制台,因为已经在js里看到数据的加密了
修改,提交,验证通过
源码审计
这回的分类在index.php里,为了防止剧透删除了switch的分支if ($_SERVER['REQUEST_METHOD'] == "POST") { if (array_key_exists ("phrase", $_POST) && array_key_exists ("token", $_POST)) { $phrase = $_POST['phrase']; $token = $_POST['token']; if ($phrase == "success") { switch( $_COOKIE[ 'security' ] ) { case 'low': if ($token == md5(str_rot13("success"))) { $message = "
Well done!
"; } else { $message = "Invalid token.
"; } break; } } else { $message = "You got the phrase wrong.
"; } } else { $message = "Missing phrase or token.
"; } }
if ($token == md5(str_rot13("success")))
猜测之中,简单,过Medium
此时token变成了
XXeMegnahCXX
,逆向首尾加俩X
关于token,比较复杂的方法就是去逆向他的加密逻辑,来达到伪造数据,懒狗一点就是直接修改数据,调用函数
先更改id='phrase'
的value值
控制台直接调用medium.js
再回看token,成功改变
提交成功
源码审计case 'medium': if ($token == strrev("XXsuccessXX")) { $message = "
Well done!
"; } else { $message = "Invalid token.
"; } break;没啥好审的
High
这回token有点长
一看high.js,看不懂,找一下js混淆
可以得到是javascript-obfuscator混淆
在线反混淆一下
只看独立的,不看被调用的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_part_1("ABCD", 44)直接执行 token_part_2("XX")延时300ms token_part_3()绑定在按钮上
那么我们只要输入phrase的值为success再执行part1和part2,最后点击按钮就能成功验证token
token_part_1(“ABCD”, 44);
token_part_2(“XX”);
提交触发token_part_3
成功通过验证源码审计
case 'high': if ($token == hash("sha256", hash("sha256", "XX" . strrev("success")) . "ZZ")) { $message = "
Well done!
"; } else { $message = "Invalid token.
"; } break;跟js的加密过程一样就是了,逆向,拼接,sha256
Impossible
挡不住啊,前端js再牛,终归也是开源的,谁都能看见,稍微有效点的方法可能也就是冷门的混淆了,不知道混淆套娃后的js还能不能用,总之,能用后台生成token就尽量用后台,前台看看数据就好哩。完结撒花
前前后后水了一周,中间还查了好多怪东西。
总之还是很强的,漏洞的利用和修复都涵盖到了。