DVWA靶场通关实战

DVWA

  • Brute Force(暴力破解)
    • Low
    • Medium
    • High
    • Impossible
  • Command Injection(命令行注入)
    • Low
    • Medium
    • High
    • Impossible
  • CSRF(跨站请求伪造)
    • Low
    • Medium
    • High
    • Impossible
  • File Inclusion(文件包含)
    • Low
    • Medium
    • High
    • Impossible
  • File Upload(文件上传)
    • Low
    • Medium
    • High
    • Impossible
  • Insecure CAPTCHA (不安全的验证码)
    • Low
    • Medium
    • High
    • Impossible
  • SQL Injection(SQL注入)
    • Low
    • Medium
    • High
    • Impossible
  • SQL Injection(Blind)(SQL盲注)
    • Low
    • Medium
    • High
    • Impossible
  • Weak Session IDs (弱会话ID)
    • Low
    • Medium
    • High
    • Impossible
  • XSS (DOM) (DOM型跨站脚本)
    • Low
    • Medium
    • High
    • Impossible
  • XSS(Reflected)(反射型跨站脚本)
    • Low
    • Medium
    • High
    • Impossible
  • XSS(Stored)(存储型跨站脚本)
    • Low
    • Medium
    • High
    • Impossible
  • CSP Bypass (CSP绕过)
    • Low
    • Medium
    • High
    • Impossible
  • JavaScript
    • Low
    • Medium
    • High
    • Impossible
  • 完结撒花

截至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是来教你防御的
然后就是缩减了一下目录,本来攻击方式、源码审计、漏洞原理都加了标题,但是那样目录就太长太丑了,想想还是删了算了
直接开冲!

Brute Force(暴力破解)

暴力破解,又叫撞库、穷举,使用大量的字典逐个在认证接口尝试登录,理论上,只要字典足够强大,破解总是会成功的。
阻止暴力破解的最有效方式是设置复杂度高的密码(英文字母大小写、数字、符号混合)。

而如果你的字典是从某网站泄露出来的,你使用它试图登陆其他网站,就便是撞库。撞库攻击的成功率高于暴力破解,因为你在A网站的用户名、密码通常和B网站的用户名、密码一致。

Low

DVWA Security界面将难度设置为Low

先看登录界面,填写账号密码后抓包

bp抓包后转到intruder模块,给username和password加上tag§
type选择Cluster bomb

对应位置插入字典后,点击右边的attack开始攻击

也可以更改OptionsNumber of threads修改进程数,增加爆破效率

根据响应包的大小以及响应体的内容判断是否成功,可以看到成功爆出一个用户admin/password,也是平台登录的默认账号

马上停止爆破,点到为止,一千四百万条顶不住啊。

源码审计

<?php
if( isset( $_GET[ 'Login' ] ) ) {// Get username$user = $_GET[ 'username' ];// Get password$pass = $_GET[ 'password' ];$pass = md5( $pass );// Check the database$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );if( $result && mysqli_num_rows( $result ) == 1 ) {// Get users details$row    = mysqli_fetch_assoc( $result );$avatar = $row["avatar"];// Login successful$html .= "<p>Welcome to the password protected area {$user}</p>";$html .= "<img src=\"{$avatar}\" />";}else {// Login failed$html .= "<pre><br />Username and/or password incorrect.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

username未过滤password只有一个md5加密,但是对实际的爆破过程毫无影响
首先很直观的就是一个万能密码(
由于限制了结果只能有一条,要加上limit 1
admin' or 1=1 limit 1#

Medium

爆破过程同上,但是可以明显感觉到,爆破的速度慢了很多
为了方便演示,直接把admin/password放在前几条了,反正多跑无益,还浪费时间

源码审计

<?phpif( isset( $_GET[ 'Login' ] ) ) {// Sanitise username input$user = $_GET[ 'username' ];$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_GET[ 'password' ];$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass = md5( $pass );// Check the database$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );if( $result && mysqli_num_rows( $result ) == 1 ) {// Get users details$row    = mysqli_fetch_assoc( $result );$avatar = $row["avatar"];// Login successful$html .= "<p>Welcome to the password protected area {$user}</p>";$html .= "<img src=\"{$avatar}\" />";}else {// Login failedsleep( 2 );$html .= "<pre><br />Username and/or password incorrect.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>

用了mysqli_real_escape_string()函数转义输入的用户名密码,但是没有设置编码,在gbk编码的数据库上可以进行sql注入
登录失败后sleep(2)延迟两秒

High

抓包发现url栏多了一项user_token
bp抓包后发现,只有在token正确的情况下才能尝试登录,其他情况均为302重定向

bp也是有带token爆破的功能的,首先转到intruder模块,选择Pitchfork模式

payload1是密码位,正常插入密码字典
payload2是token位,选择Recursive grep

然后去option里设置Grep-Extract

设置Start和End,将数据留在中间,也可以直接用鼠标在下方拖动选取文字
PS:设置grep时建议刷新一次浏览器,填入最新的token,避免访问失效,下面框里没东西
设置完成后回到payload界面选中

因为token不能复用,将option的线程设置为一,重定向选择always


开始爆破后即可根据响应包大小判断是否成功

源码审计

<?phpif( isset( $_GET[ 'Login' ] ) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Sanitise username input$user = $_GET[ 'username' ];$user = stripslashes( $user );$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_GET[ 'password' ];$pass = stripslashes( $pass );$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass = md5( $pass );// Check database$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );if( $result && mysqli_num_rows( $result ) == 1 ) {// Get users details$row    = mysqli_fetch_assoc( $result );$avatar = $row["avatar"];// Login successful$html .= "<p>Welcome to the password protected area {$user}</p>";$html .= "<img src=\"{$avatar}\" />";}else {// Login failedsleep( rand( 0, 3 ) );$html .= "<pre><br />Username and/or password incorrect.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}// Generate Anti-CSRF token
generateSessionToken();?>

开始后先校验tokencheckToken(),若token不匹配则重定向到登陆界面,不再进行判断
结束前生成新的tokengenerateSessionToken()
账号密码多用了一个stripslashes()函数删除反斜杠

Impossible

失败三次,锁定15分钟,没辙了,摆烂了

源码审计

<?phpif( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Sanitise username input$user = $_POST[ 'username' ];$user = stripslashes( $user );$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));// Sanitise password input$pass = $_POST[ 'password' ];$pass = stripslashes( $pass );$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));$pass = md5( $pass );// Default values$total_failed_login = 3;$lockout_time       = 15;$account_locked     = false;// Check the database (Check user information)$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();$row = $data->fetch();// Check to see if the user has been locked out.if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) )  {// User locked out.  Note, using this method would allow for user enumeration!//$html .= "<pre><br />This account has been locked due to too many incorrect logins.</pre>";// Calculate when the user would be allowed to login again$last_login = strtotime( $row[ 'last_login' ] );$timeout    = $last_login + ($lockout_time * 60);$timenow    = time();/*print "The last login was: " . date ("h:i:s", $last_login) . "<br />";print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";*/// Check to see if enough time has passed, if it hasn't locked the accountif( $timenow < $timeout ) {$account_locked = true;// print "The account is locked<br />";}}// Check the database (if username matches the password)$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR);$data->bindParam( ':password', $pass, PDO::PARAM_STR );$data->execute();$row = $data->fetch();// If its a valid login...if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {// Get users details$avatar       = $row[ 'avatar' ];$failed_login = $row[ 'failed_login' ];$last_login   = $row[ 'last_login' ];// Login successful$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";$html .= "<img src=\"{$avatar}\" />";// Had the account been locked out since last login?if( $failed_login >= $total_failed_login ) {$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";$html .= "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";}// Reset bad login count$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();} else {// Login failedsleep( rand( 2, 4 ) );// Give the user some feedback$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";// Update bad login count$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();}// Set the last login time$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();
}// Generate Anti-CSRF token
generateSessionToken();?>

代码行数直接比High多了一倍多,虽然注释挺多的
提交方式由GET改为POST
失败次数是写入数据库的,sql语句也改为参数化查询
不好说了,立体机动防御了

Command Injection(命令行注入)

命令注入,是指在某些需要输入数据的位置,构造恶意代码破环原有的语句结构,而系统缺少有效的过滤,许多内容管理cms存在命令注入漏洞。

Low

简洁的界面

象征性的ping一下,编码问题就不管了,影响不大
先来一个经典的ping
命令执行常用的方式为||&&管道符
更多的可以看看这个命令执行
还有一些过滤技巧CTF—命令执行总结
||当前项执行失败后执行后项
&&前项成功后执行后项
源码审计

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Get input$target = $_REQUEST[ 'ip' ];// Determine OS and execute the ping command.if( stristr( php_uname( 's' ), 'Windows NT' ) ) {// Windows$cmd = shell_exec( 'ping  ' . $target );}else {// *nix$cmd = shell_exec( 'ping  -c 4 ' . $target );}// Feedback for the end user$html .= "<pre>{$cmd}</pre>";
}?>

没有过滤,简单判断一下操作系统就拼接执行了

Medium

再次尝试时发现&&被过滤了
||可以正常执行

源码审计

<?phpif( 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 .= "<pre>{$cmd}</pre>";
}?>

还多过滤了一个分号,其他没啥了

High

过滤了&||,但是剩了个|还在
源码审计

<?phpif( 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 .= "<pre>{$cmd}</pre>";
}?>

还真就剩了个|,不知道为什么过滤'| ' => '','||' => '',
l后面是空格说明仅仅过滤了l+空格 没过滤单独的l,他好温柔,我哭死

Impossible

打都不打,直接看源码
源码审计

<?phpif( isset( $_POST[ 'Submit' ]  ) ) {// Check Anti-CSRF tokencheckToken( $_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 integerif( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {// If all 4 octets are int's put the IP back together.$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];// Determine OS and execute the ping command.if( stristr( php_uname( 's' ), 'Windows NT' ) ) {// Windows$cmd = shell_exec( 'ping  ' . $target );}else {// *nix$cmd = shell_exec( 'ping  -c 4 ' . $target );}// Feedback for the end user$html .= "<pre>{$cmd}</pre>";}else {// Ops. Let the user name theres a mistake$html .= '<pre>ERROR: You have entered an invalid IP.</pre>';}
}// Generate Anti-CSRF token
generateSessionToken();?>

还是先checkToken()
然后$octet = explode( ".", $target );is_numeric()
硬性规定只能输入ip了

CSRF(跨站请求伪造)

一种可以被攻击者用来通过用户浏览器冒充用户身份向服务器发送伪造请求并被目标服务器成功执行的漏洞被称之为CSRF漏洞。
特点:

用户浏览器:表示的受信任的用户
冒充身份:恶意程序冒充受信任用户(浏览器)身份
伪造请求:借助于受信任用户浏览器发起的访问

由于CSRF攻击的特殊性,还是以钓鱼和源码解析为主好了

Low

源码审计

<?phpif( 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$html .= "<pre>Password Changed.</pre>";}else {// Issue with passwords matching$html .= "<pre>Passwords did not match.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

没有安全防护措施,简单判断一下输入的新旧密码是否相同,然后修改当前用户的密码

渗透测试
先看一眼界面,下面是平平无奇的更改密码用的表格,上面是一个测试登陆用的弹窗,直接改密码!

可以看到是GET方法修改密码

点击Test,登录成功

此时就可以bp抓包生成CSRF Poc了

保存到本地文件然后将密码改为111111,点击发送

密码修改成功

Medium

源码审计

<?phpif( isset( $_GET[ 'Change' ] ) ) {// Checks to see where the request came fromif( 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$html .= "<pre>Password Changed.</pre>";}else {// Issue with passwords matching$html .= "<pre>Passwords did not match.</pre>";}}else {// Didn't come from a trusted source$html .= "<pre>That request didn't look correct.</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

增加了$_SERVER[ 'HTTP_REFERER' ]头验证

渗透测试
题目的过滤方式为stripos(),可以利用命名文件夹的方式绕过Referer头的检测

High

源码审计

<?php
$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 tokencheckToken( $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 .= "<pre>" . $return_message . "</pre>";}
}// Generate Anti-CSRF token
generateSessionToken();?>

先验证token是否存在,再验证token值是否正确。要绕过High级别的反CSRF机制,关键是要获取用户当前token。

渗透测试
还是先抓个包
由于加了token,这里的token就要通过其他方法获取了,比如先弹一个xss什么的(

Impossible

标准答案来咯

<?phpif( isset( $_GET[ 'Change' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 .= "<pre>Password Changed.</pre>";}else {// Issue with passwords matching$html .= "<pre>Passwords did not match or current password incorrect.</pre>";}
}// Generate Anti-CSRF token
generateSessionToken();?>


旧密码+新密码+token+数据库参数化查询,强无敌!

File Inclusion(文件包含)

程序开发人员通常会把可重复使用的函数写到单个文件中,在使用某个函数的时候,直接调用此文件,无需再次编写,这种调用文件的过程通常称为包含。
文件包含函数加载的参数没有经过过滤或严格定义,可以被用户控制,包含其他非预期文件,导致了文件信息泄露或执行非预期代码。

Low

点开界面是三个文件包含

点击后是三个文件包含,URL为?page=file3.php

甚至XSS

回到正题,有1、2、3那可以试试4

目录穿越,绝对路径,远程文件包含都是没问题的


源码审计
还真是,简洁明了啊

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

来者不拒,下一位!

Medium

先包含file4.php,没有问题
然后测试我们的phpinfo,可以发现目录穿越和远程包含失效了,准确的说../和http协议被替换为空了




源码审计

<?php// The page we wish to display
$file = $_GET[ 'page' ];// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\\" ), "", $file );?>

如图所示,润

High

file4依旧没问题

剩下的都是问题

不过可以用file协议绕过?page=file:///D:/phpstudy_pro/rua/DVWA-master/phpinfo.php

看下源码好了
代码审计

<?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之类的了

Impossible

<?php// The page we wish to display
$file = $_GET[ 'page' ];// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {// This isn't the page we want!echo "ERROR: File not found!";exit;
}?>

写死了,摆烂了,没救了

File Upload(文件上传)

File Upload,即文件上传漏洞,指用户上传了一个可执行的脚本文件,并通过此脚本文件获得了执行服务器端命令的能力。通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限。

Low

没过滤,传个🐎直接getshell

源码审计

<?phpif( 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 .= '<pre>Your image was not uploaded.</pre>';}else {// Yes!$html .= "<pre>{$target_path} succesfully uploaded!</pre>";}
}?>

无检测,无黑白名单过滤

Medium

接着上传php,提示只能上传jpeg或png

打开bp,抓包修改Content-Type,上传成功

源码审计

<?phpif( 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 .= '<pre>Your image was not uploaded.</pre>';}else {// Yes!$html .= "<pre>{$target_path} succesfully uploaded!</pre>";}}else {// Invalid file$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}?>

判断了type和文件大小
无伤大雅

High

传来传去传不上去了,生成个图片🐎去文件包含好了(
先来个图片🐎
copy 1.png/b+1.php/a shell.png

上传的时候注意图片大小,或者直接用1像素图片()


本地测试成功

使用工具时记得带上cookie,到时候返回数据为空找谁都不好使

源码审计

<?phpif( 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 .= '<pre>Your image was not uploaded.</pre>';}else {// Yes!$html .= "<pre>{$target_path} succesfully uploaded!</pre>";}}else {// Invalid file$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}?>

要求后缀名是图片,getimagesize()检测结果不能为false
只能传图片🐎然后文件包含了

Impossible

源码来咯

<?phpif( isset( $_POST[ 'Upload' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";}else {// No$html .= '<pre>Your image was not uploaded.</pre>';}// Delete any temp filesif( file_exists( $temp_file ) )unlink( $temp_file );}else {// Invalid file$html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';}
}// Generate Anti-CSRF token
generateSessionToken();?>
checkToken()						判断token是否正确
md5( uniqid() . $uploaded_name )	对上传的文件进行重命名
strtolower( $uploaded_ext )			判断后缀名
$uploaded_type 						判断文件mimetype
getimagesize()						判断文件起始字符是否符合
imagecreatefromjpeg()				图片二次渲染(可以绕过)
rename()							判断文件是否能移动到web目录下

心血来潮想研究下getimagesize(),发现好像仅仅只对文件开始的几个字符进行了判断,例如PNG的

<?php
// 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 (不安全的验证码)

Insecure CAPTCHA,不安全的验证码
验证码:6
感觉……不如Unsafe verification process(不安全的验证过程),主要是验证流程出现了逻辑漏洞。

没有密钥的可以根据提示链接,科学上网,创建一个key,类型没深究,选了一个v2隐形,然后将密钥复制到本地config.inc.php文件即可
网址乱填的,如有雷同,不胜荣幸,域名改为127.0.0.1即可,此时本地访问就不要用localhost了


我发现这个隐形嘎嘎简单,单击点一下就验证成功了,啥也不用干

开始正题

Low

本来挺快的
bp代理一开就卡死在google了,可能代理不走梯子?

本来想摆烂直接审源码的,后面一想,反正目前任务是绕过验证码,直接默认不通过就好了(
直接注释有关验证码的函数

直接改为0,默认不通过

搞定

好耶,码没了(
提交修改,bp抓包能看到step=1,并且响应包提示验证码错误

看到1就应该改2试试,更改成功,绕过了验证

源码审计

<?phpif( 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     .= "<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$html .= "<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 matchif( $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$html .= "<pre>Password Changed.</pre>";}else {// Issue with the passwords matching$html .= "<pre>Passwords did not match.</pre>";$hide_form = false;}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>

第一次看见Low这么长的,长归长,其实就两部分,分为step1和step2
step1的验证码通过两次密码相同后会出现一个全是type="hidden"的表单和一个按钮其中有一条<input type=\"hidden\" name=\"step\" value=\"2\" />,进入到step2
step2就更加简单了,比较密码是否相同后直接修改

Medium

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 1if( !$_POST[ 'passed_captcha' ] ) {$html     .= "<pre><br />You have not passed the CAPTCHA.</pre>";$hide_form = false;return;}

High

不看源码都知道加了token

下一步只能根据源码了
修改UA头User-Agent: reCAPTCHA
添加POST参数g-recaptcha-response=hidd3n_valu3
token检测没开,无所谓了

源码审计

<?phpif( 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( '<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'))

token少加了一行验证

checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

验证码通过或者UA头和POST参数g-recaptcha-response符合要求
说白了还是没用上验证码的验证结果

Impossible

源码审计

<?phpif( isset( $_POST[ 'Change' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";$hide_form = false;}else {// Check that the current password is correct$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );$data->execute();// Do both new password match and was the current password correct?if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {// Update the database$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );$data->execute();// Feedback for the end user - success!$html .= "<pre>Password Changed.</pre>";}else {// Feedback for the end user - failed!$html .= "<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();?>

总结加固方式

验证token
过滤密码
确认验证结果if( !$resp ) (划重点!!!)
参数化处理

验证就要好好单步验证,别整那些花里胡哨的,又是step2又是passed_captcha又是||

SQL Injection(SQL注入)

SQL Injection(SQL注入),是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的。

Low

哟,老熟人

测试得单引号注入
order by 判断列数,2可以3报错得2列

点到为止,懒得注了

源码审计
删减版,另一个是SQLITE

<?phpif( 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( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Get values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end user$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}mysqli_close($GLOBALS["___mysqli_ston"]);break;} 
}
?>

判断数据库,拼接执行,毫无过滤

Medium

F12瞅一眼流量,POST注入,复制请求体到hackbar

测试注入类型时发现单引号和双引号都被添加反斜杠过滤,我又是utf8的数据库,宽字节用不了了
结果反手就是一个数字型注入,瞬间失去了转义的意义

源码审计
接着删

<?phpif( 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( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Display values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end user$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}break;}
}
?>

mysqli_real_escape_string(),但是数字型

High

不让直接注了,改弹窗了
但是,没区别啊

源码审计
high.php

<?phpif( 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( '<pre>Something went wrong.</pre>' );// Get resultswhile( $row = mysqli_fetch_assoc( $result ) ) {// Get values$first = $row["first_name"];$last  = $row["last_name"];// Feedback for end user$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);		break;}
}?>

session-input.php

<?phpdefine( '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!<br /><br /><br />";$page[ 'body' ] .= "Session ID: {$_SESSION[ 'id' ]}<br /><br /><br />";$page[ 'body' ] .= "<script>window.opener.location.reload(true);</script>";
}$page[ 'body' ] .= "
<form action=\"#\" method=\"POST\"><input type=\"text\" size=\"15\" name=\"id\"><input type=\"submit\" name=\"Submit\" value=\"Submit\">
</form>
<hr />
<br /><button οnclick=\"self.close();\">Close</button>";dvwaSourceHtmlEcho( $page );?>

弹窗设置$_SESSION[ ‘id’ ],直接利用$_SESSION[ ‘id’ ]注入,还是无过滤,还以为会有点黑白名单什么的

Impossible

源码审计

<?phpif( isset( $_GET[ 'Submit' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 returnedif( $data->rowCount() == 1 ) {// Get values$first = $row[ 'first_name' ];$last  = $row[ 'last_name' ];// Feedback for end user$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}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 .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}}break;}}
}// Generate Anti-CSRF token
generateSessionToken();?>

intval()过滤,绝杀!无解!
参数化查询锦上添花

SQL Injection(Blind)(SQL盲注)

Low

单引号盲注

?id=1' and 1=1#&Submit=Submit#
?id=1' and 0=1#&Submit=Submit#


过程,前几天写的掌控安全的靶场里有

源码审计
惯例删除SQLITE

<?phpif( 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 .= '<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$html .= '<pre>User ID is MISSING from the database.</pre>';}}?>

判断查询结果有无数据
有就exists没有就MISSING

Medium

POST,数字型

id=1 and 0=1#&Submit=Submit
id=1 and 1=1#&Submit=Submit

源码审计

<?phpif( 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(),但是数字型

High

改成cookie注入了,大差不差

id=1' and 1=1#&Submit=Submit
id=1' and 0=1#&Submit=Submit


源码审计
high.php

<?phpif( 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 resultstry {$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 .= '<pre>User ID exists in the database.</pre>';}else {// Might sleep a random amountif( 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 .= '<pre>User ID is MISSING from the database.</pre>';}
}?>

cookie-input.php

<?phpdefine( '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!<br /><br /><br />";$page[ 'body' ] .= "<script>window.opener.location.reload(true);</script>";
}$page[ 'body' ] .= "
<form action=\"#\" method=\"POST\"><input type=\"text\" size=\"15\" name=\"id\"><input type=\"submit\" name=\"Submit\" value=\"Submit\">
</form>
<hr />
<br /><button οnclick=\"self.close();\">Close</button>";dvwaSourceHtmlEcho( $page );?>

没过滤,直接取cookie拼接查询,概率sleep()

Impossible

源码审计

<?phpif( isset( $_GET[ 'Submit' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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 resultsif ($exists) {// Feedback for end user$html .= '<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$html .= '<pre>User ID is MISSING from the database.</pre>';}
}// Generate Anti-CSRF token
generateSessionToken();?>

token,intval(),参数化
御三家

Weak Session IDs (弱会话ID)

当用户登录后,在服务器就会创建一个会话(session),叫做会话控制,接着访问页面的时候就不用登录,只需要携带对应的cookie去访问。

sessionID作为特定用户访问站点所需要的唯一内容。如果能够计算或轻易猜到该sessionID,则攻击者将可以轻易获取访问权限,无需录直接进入特定用户认证界面,进而进行其他操作。

之后只要cookies随着http请求发送服务器,服务器就知道你是谁了。SessionID一旦在生命周期内被窃取,就等同于账户失窃(也就是熟悉的越权)。

Low

先看

再点,可以发现每次点击dvwaSession都会加一

那么理论上就可以伪造成下一位或者上一位的id,来达到越权访问的效果
之前有次渗透就是,后台登录完cookie就是用户id,那时候我是4200,下意识把cookie改为1,成功访问到其他用户个人信息,嘎嘎越权。

dvwaSession咋改都没什么区别,还是建议加一个界面,比如

if ($_COOKIE['dvwaSession'] == 1){$html= '<pre>welcome admin</pre>';
}

假设admin登录时$_SESSION[‘last_session_id’]为1,他的$_COOKIE[‘dvwaSession’]为1
此时我们将本地的$_COOKIE[‘dvwaSession’]改为1即可越权为admin登录

源码审计

<?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);
}if ($_COOKIE['dvwaSession'] == 1){$html= '<pre>welcome admin</pre>';
}
?>

每次生成的id依次递增,遍历爆破一下即可越权。

Medium

先生成dvwaSession找找规律

1669944854
1669944903
1669944911
1669944955

干这行的多少还是有些敏感的,session就是时间戳,越权的话,还是从现在往前爆破就行

源码审计

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

time()

High

先找规律
很怪,我的setcookie不生效了
不过,看还是能看的

c51ce410c124a10e0db5e4b97fc2af39
aab3238922bcc25a6f606eb525ffdc56
9bf31c7ff062936a96d3c8bd1f8f2ff3
6f4922f45568161a8cdf4ad2299f6d23

不出意外是md5了

不出意外也是0开始递增了

源码审计

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

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
再sha1
这谁顶得住啊

XSS (DOM) (DOM型跨站脚本)

DOM型XSS漏洞通常出现在以下情况: JavaScript从攻击者可控的源(如URL)获取数据,并将其传递给支持动态代码执行的接收器(如eval()或innerHTML)。这使得攻击者能够执行恶意的JavaScript,这通常允许他们劫持其他用户的帐户。
要实现基于dom的XSS攻击,需要将数据放置到源中,以便将其传播到接收器并导致执行任意JavaScript。

Low

一眼,无事发生,
在多一眼看一眼就会被爆炸
点开下拉列表可以发现这个English是独立于选项之外的,也就是url传参

经典永流传<script>alert(1)</script>

源码审计
Low.php

<?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("<option value='" + lang + "'>" + $decodeURI(lang) + "</option>");document.write("<option value='' disabled='disabled'>----</option>");}document.write("<option value='English'>English</option>");document.write("<option value='French'>French</option>");document.write("<option value='Spanish'>Spanish</option>");document.write("<option value='German'>German</option>");
</script>

无视风险,直接拼接

Medium

继续<script>alert(1)</script>
但是直接重定向?default=English
直接尝试img标签发现直接拼接到value里了

闭合一下
English>/option></select><img src='x' onerror='alert(1)'>

源码审计

<?php// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {$default = $_GET['default'];# Do not allow script tagsif (stripos ($default, "<script") !== false) {header ("location: ?default=English");exit;}
}?>

stripos()是不分大小写的,那么<script就用不了了,但是xss标签有一大堆,方法也有一大堆

High

初次尝试无果,怎么试都无果,后面切换页面的时候发现这个

URL第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。
再根据Low的html源码可得
php界面对$_GET['default']进行了过滤
html会读取default=后的全部字符串substring(document.location.href.indexOf("default=")+8)
构造Payload:

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

源码审计

<?php// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {# White list the allowable languagesswitch ($_GET['default']) {case "French":case "English":case "German":case "Spanish":# okbreak;default:header ("location: ?default=English");exit;}
}?>

php页面只允许表单内容提交,没有问题。
但是有内鬼,终止执行

Impossible

源码审计

<?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("<option value='" + lang + "'>" + $decodeURI(lang) + "</option>");


不解码就不执行了,妙啊

XSS(Reflected)(反射型跨站脚本)

反射型XXS是一种非持久性的攻击,它指的是恶意攻击者往Web页面里插入恶意代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的目的。

Low

alert(1)

过(

源码审计

<?phpheader ("X-XSS-Protection: 0");// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Feedback for end user$html .= '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>

无过滤,直接拼

Medium

标签没了

上图片

反正图片噶了还有七七八八的标签和方法
源码审计

<?phpheader ("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$html .= "<pre>Hello ${name}</pre>";
}?>

只过滤了<script>,然后,我忘记大小写了

High

这回真得图片了

源码审计

<?phpheader ("X-XSS-Protection: 0");// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Get input$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );// Feedback for end user$html .= "<pre>Hello ${name}</pre>";
}?>

他好爱script
这回区分了大小写,无伤大雅

Impossible

源码审计

<?php// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {// Check Anti-CSRF tokencheckToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// Get input$name = htmlspecialchars( $_GET[ 'name' ] );// Feedback for end user$html .= "<pre>Hello ${name}</pre>";
}// Generate Anti-CSRF token
generateSessionToken();?>

htmlspecialchars()虽然默认不转义单引号,但是这个拼接的地方不支持我们用单号绕过

XSS(Stored)(存储型跨站脚本)

攻击者事先将恶意代码上传或储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。

Low

Name栏有长度限制,可以F12删除html的maxlength="10"
反正Low难度直接插就好了

源码审计

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

stripslashes() 函数删除由 addslashes() 函数添加的反斜杠。
没用,过

Medium

<script>alert(1)</script>噶了

<Script>alert(1)</Script>
比起能弹窗,我更想知道我的img哪去了

POST请求

txtName=<Script>alert(1)</Script>
mtxMessage=<img src=1 onerror="alert(1)" />

div

我的img捏
源码审计

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

我的img被strip_tags()噶了,呜呜呜
$name没有区分大小写

High

吔我img

源码审计

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

message又被噶了
name只置空script,img好啊

Impossible

源码审计

<?phpif( isset( $_POST[ 'btnSign' ] ) ) {// Check Anti-CSRF tokencheckToken( $_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();?>
token
stripslashes()
mysqli_real_escape_string()
htmlspecialchars()
参数化

CSP Bypass (CSP绕过)

CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。
两种启用 CSP的方法

HTTP 响应头信息的Content-Security-Policy字段
通过网页的标签

Low

看一眼

再多一眼就会爆炸

根据CSP策略

script-src 'self' 
https://pastebin.com 
hastebin.com 
www.toptal.com 
example.com 
code.jquery.com 
https://ssl.google-analytics.com ;

可以包含以上域名的js代码
https://pastebin.com这个站创建还挺方便的
在new paste插入xss,然后加上tags(长度大于2)点击创建即可

再复制创建好的链接回到DVWA

有了,但是不执行

我不明白啊

小寄不算寄
源码审计

<?php$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com hastebin.com www.toptal.com example.com code.jquery.com https://ssl.google-analytics.com ;"; // allows js from self, pastebin.com, hastebin.com, jquery and google analytics.header($headerCSP);# These might work if you can't create your own for some reason
# https://pastebin.com/raw/R570EE00
# https://www.toptal.com/developers/hastebin/raw/cezaruzeka?>
<?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>
';

他好温柔,怕你创建不了特地准备了两个测试用例
但是我都执行不了,大抵是出不了网罢,算了,我不知道

Medium


还是先看CSP头

script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';

unsafe-inline:允许使用内联资源,如内联<script>元素,javascript:URL,内联事件处理程序(如onclick)和内联<style>元素。必须包括单引号。

nonce-source:仅允许特定的内联脚本块,nonce=“TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA”

稍微百度一下使用方法
即可得到Payload:
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert("XSS")</script>

源码审计

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

还是贴心的给出了教程

High

化繁为简了

Content-Security-Policy:script-src 'self';

粗看看不出什么,只能看到有两个solveSum是相同的

尝试bp抓包,将solveSum改为xss语句,执行成功

具体还得看网页引用的high.js

function 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

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

可以发现除了按钮,还能通过post传参拼接xss语句
已知



再结解释下XSS的原因

$callback = $_GET['callback'];echo $callback . "(".json_encode($outp).")";

jsonp.php?callback=solveSum
callback的值会原样输出回html页面造成XSS

Impossible

源码审计
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生成,且固定(默认ChangeMemd5(rot13())


再根据token的生成原理以及错误信息可以大胆猜测提交的token要改为successmd5(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 = "<p style='color:red'>Well done!</p>";} else {$message = "<p>Invalid token.</p>";}break;}} else {$message = "<p>You got the phrase wrong.</p>";}} else {$message = "<p>Missing phrase or token.</p>";}
}

if ($token == md5(str_rot13("success")))
猜测之中,简单,过

Medium

此时token变成了XXeMegnahCXX,逆向首尾加俩X

关于token,比较复杂的方法就是去逆向他的加密逻辑,来达到伪造数据,懒狗一点就是直接修改数据,调用函数
先更改id='phrase'的value值

控制台直接调用medium.js

再回看token,成功改变

提交成功

源码审计

case 'medium':if ($token == strrev("XXsuccessXX")) {$message = "<p style='color:red'>Well done!</p>";} else {$message = "<p>Invalid token.</p>";}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 = "<p style='color:red'>Well done!</p>";} else {$message = "<p>Invalid token.</p>";}break;

跟js的加密过程一样就是了,逆向,拼接,sha256

Impossible


挡不住啊,前端js再牛,终归也是开源的,谁都能看见,稍微有效点的方法可能也就是冷门的混淆了,不知道混淆套娃后的js还能不能用,总之,能用后台生成token就尽量用后台,前台看看数据就好哩。

完结撒花

前前后后水了一周,中间还查了好多怪东西。
总之还是很强的,漏洞的利用和修复都涵盖到了。

本文链接:https://my.lmcjl.com/post/12423.html

展开阅读全文

4 评论

留下您的评论.