首先我们大致了解一下 dvwa 的代码结构(以 xss 的为例,其他类似)

dvwa 每个页面有一个 index.php ,在 index.php 中有这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$vulnerabilityFile = '';
switch( $_COOKIE[ 'security' ] ) {
case 'low':
$vulnerabilityFile = 'low.php';
break;
case 'medium':
$vulnerabilityFile = 'medium.php';
break;
case 'high':
$vulnerabilityFile = 'high.php';
break;
default:
$vulnerabilityFile = 'impossible.php';
break;
}

require_once DVWA_WEB_PAGE_TO_ROOT . "vulnerabilities/xss_r/source/{$vulnerabilityFile}";

它通过判断 cookie 中的 security 来决定将参数提交到哪一个 php 脚本上,所以我们看源码需要看 low、medium、high、impossible 这些后端脚本如何处理参数。

Reflected XSS

low

黑盒测试

  1. 探测 xss

    构造一个独一无二且不会被识别为恶意代码的字符串用来提交到页面,这里构造 kidding,并提交。

    看页面回显以及使用浏览器审查工具进行代码审查,寻找构造的字符串是否在页面中显示。

    我们可以看到页面上回显:Hello kidding,而且在 Elements 中我们发现 Hello kidding 这个文本,并未使用字符串包裹,也就是说,我们输入

    , 从最早的< 到最晚的 t ,中间全部被过滤掉了,只剩下一个 >。

    impossible

    黑盒测试

    直接测试:<script>alert(/xss/)</script>

    回显:Hello <script>alert(/xss/),它将你输入的特殊字符都完好的回显出来,到了这种时候,你心里要有一句话:是时候放弃了...

    我们在开发者工具中定位,并 edit as html 发现,果不其然,大于小于号都给实体化了,嗯,不愧是 impossible。

    代码讲解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?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
    $html .= "<pre>Hello ${name}</pre>";
    }
    // Generate Anti-CSRF token
    generateSessionToken();
    ?>

    htmlspecialchars() 函数把预定义的字符(" & <> ' )转换为 HTML 实体。这样就基本杜绝了 XSS。

    此外,在此函数中,默认不编码单引号,要加上 ENT_QUOTES 参数才会编码单引号。在 dvwa 中不编码单引号没有风险,但是在其他场景下,单引号就可能会被利用

    Stored XSS

    温馨提示:存储型 xss 会一直有,对后续使用 dvwa 不友好,还要自己去数据库删除,可以选择 Setup/Reset DB 来重置 dvwa

    黑盒测试

    首先,我们测试 Message 栏,我们在 Name 栏输入 Hello, 在 Message 栏输入 <script>alert(/xss/)</script>,成功弹窗。

    然后,我们测试 Name 栏,我们尝试在 Name 栏输入 <script>alert(/xss/)</script>,但是发现它限制了长度(一般,你在输入那里输着输着,输不动了,就是前端限制),打开开发者工具,我们看到:

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

    字符串长度最长为 10,此长度限制为前端限制,我们有两种方式突破

    1. 直接 F12 在 Elements 中的对应长度处,改长度
    2. burp suite 抓包,直接改 name 的值

    然后,测试弹窗成功。

    代码讲解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?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
    //代码基本同上(我这里不抄过来了)

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

    ?>

    trim() 函数

    移除字符串两侧的空白字符或其他预定义字符。

    语法:trim(string,charlist) 若无 charlist, 则移除字符串两侧的空白字符,否则,移除 charlist 中的字符。

    stripslashes() 函数

    删除由 addslashes() 函数添加的反斜杠。

    提示:该函数可用于清理从数据库中或者从 HTML 表单中取回的数据。

    mysqli_real_escape_string 函数

    mysqli_real_escape_string() 函数转义在 SQL 语句中使用的字符串中的特殊字符,用来防止 SQL 注入,仅在写数据库的时候才需要调用这个函数。

    可以看到,对输入并没有做 XSS 方面的过滤与检查(做了对 SQL 执行方面的检查,所以不太好进行 SQL 注入),且存储在数据库中,因此这里存在明显的存储型 XSS 漏洞。

    medium

    黑盒测试

    首先测试,Message,测试以下 payload:

    1
    2
    3
    4
    <script>alert(xss)</script>
    <img src=@ onerror=alert(/xss/)>
    <iframe onload=alert(1)>
    <body onload=alert('XSS')>

    均未成功,放弃

    测试 Name,测试

    1
    2
    3
    <script>alert(xss)</script>

    <scrIpt>alert(xss)</sCript>

    大写那个测试弹窗,看来对 Message 做了较强的过滤,对 Name 只做了过滤小写 script 标签的黑名单。

    当然,双写也能绕过,这里就不再做演示了。

    代码讲解

    大致代码与上面 low 相同,不同的相信我们呀猜出来了。

    $message 多了一步 htmlspecialchars 的处理

    1
    $message = htmlspecialchars( $message );

    $name 多了过滤 <script> 的操作

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

    high

    黑盒测试

    从 medium 我们知道 Message 是不可能了,接着尝试 Name, 大写与嵌套失败,使用别的标签,比如:img,iframe,body 标签绕过成功。

    代码讲解

    1
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );

    可以看到,这里用正则完全地过滤了 script 标签,所以我们使用 script 标签不再奏效。

    impossible

    黑盒测试

    使用以下 payload 进行测试,

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

    回显:

    我们看到 <> 均被实体化编码了,凉凉。

    代码讲解

    不出所料,Name 和 Message 参数都进行了 htmlspecialchars 处理。

    一个坏习惯:

    测试留言板要一个个测试,不要两个一起都输入 xss payload 测试,很能会相互影响,这里就因为二者挨着,相互影响,导致测试失败。

    DOM XSS

    未完待续...

    总结回顾:

    1. 测试漏洞时,要一个一个点的测试,不要相邻的点就一起测试,不然,如果他们在代码中是一起的,容易相互影响。

    2. 修复方式: 输入过滤:提倡白名单,在服务端对用户的输入做好限制

      输出过滤:如果是输出到 html 中,那进行 html 编码;如果是输出到 js 中,js 转义

    3. 存储型 XSS 一旦存储上每次访问对应地方都会触发,很烦。尤其在测试别人的网站的时候,不要使用弹窗的 XSS payload,最好使用 XSS 平台检测。