反射型 XSS Low 源码:
1 2 3 4 5 6 7 8 9 10 11 <?php header ("X-XSS-Protection: 0" );if ( array_key_exists ( "name" , $_GET ) && $_GET [ 'name' ] != NULL ) { 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" );if ( array_key_exists ( "name" , $_GET ) && $_GET [ 'name' ] != NULL ) { $name = str_replace ( '<script>' , '' , $_GET [ 'name' ] ); echo "<pre>Hello {$name} </pre>" ; } ?>
这段源代码的核心就在于:
1 $name = str_replace ( '<script>' , '' , $_GET [ 'name' ] );
这一行代码把我们读入的 <script> 串给过滤掉了(换成了空字符串),所以我们可以用一个不需要 <script> 标签的注入方式,这里有一个办法就是通过 img 标签甩出一个报错来注入恶意代码:
1 <img src =x onerror =alert(1) >
还可以通过 <svg> 标签:
除了上面说的,还有一种更简单的方式,由于过滤用的 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" );if ( array_key_exists ( "name" , $_GET ) && $_GET [ 'name' ] != NULL ) { $name = preg_replace ( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i' , '' , $_GET [ 'name' ] ); 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 if ( array_key_exists ( "name" , $_GET ) && $_GET [ 'name' ] != NULL ) { checkToken ( $_REQUEST [ 'user_token' ], $_SESSION [ 'session_token' ], 'index.php' ); $name = htmlspecialchars ( $_GET [ 'name' ] ); echo "<pre>Hello {$name} </pre>" ; } 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' ] ) ) { $message = trim ( $_POST [ 'mtxMessage' ] ); $name = trim ( $_POST [ 'txtName' ] ); $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)) ? "" : "" )); $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)) ? "" : "" )); $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>' ); } ?>
这个比较简单,毕竟是 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' ] ) ) { $message = trim ( $_POST [ 'mtxMessage' ] ); $name = trim ( $_POST [ 'txtName' ] ); $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 ); $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)) ? "" : "" )); $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>' ); } ?>
跟反射型那边一样啊,匹配过滤掉 <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' ] ) ) { $message = trim ( $_POST [ 'mtxMessage' ] ); $name = trim ( $_POST [ 'txtName' ] ); $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 ); $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)) ? "" : "" )); $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>' ); } ?>
这里的构造方式和利用方式都跟前面一样,结合一下就行了。
Impossible 其实没啥好看的了,改的地方跟反射型 XSS 那边一样,不过这里的 high 构造的感觉略显粗糙,毕竟只过滤掉一个,虽然现实中还是很有可能出现这种情况的。
还是学到了一些的,比如在前端上的限制可以直接修改来进行绕过,虽然 payload 跟反射型那边都一样,毕竟都是 XSS。
感觉在技巧上的提升跟反射型那边是重合的,但是在思想上(或者说叫方法论上)学到了一些有意思的东西,对整个攻击链的把握和对攻击目标多个环节的了解以及利用,攻击的主要方式是 XSS,但是攻击面上主要的难点或者说绕过的点确实在前端对长度的限制,也就是说发现漏洞之后如何利用其实也是攻击的目标,或者说攻击面里的一部分。
DOM XSS Low 源码:
源代码约等于没有,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 if ( array_key_exists ( "default" , $_GET ) && !is_null ($_GET [ 'default' ]) ) { $default = $_GET ['default' ]; 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 if ( array_key_exists ( "default" , $_GET ) && !is_null ($_GET [ 'default' ]) ) { switch ($_GET ['default' ]) { case "French" : case "English" : case "German" : case "Spanish" : 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 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 里输入的内容在传输过程会默认被浏览器给编码,然后在前端代码里如果不解码的话,编码之后的内容是无法作为一个正常的代码运行的,毕竟 <> 啥的都被编码掉了。