为啥要防止XSS攻击
先上一段标准解释(摘自百度百科)。
“XSS是跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。”
相信以上的解释也不难理解,但为了再具体些,这里举一个简单的例子,就是留言板。我们知道留言板通常的任务就是把用户留言的内容展示出来。正常情况下,用户的留言都是正常的语言文字,留言板显示的内容也就没毛病。然而这个时候如果有人不按套路出牌,在留言内容中丢进去一行
<script>alert(“hey!you are attacked”)</script>
那么留言板界面的网页代码就会变成形如以下:
<html>
<head>
<title>留言板</title>
</head>
<body>
<div id=”board”
<script>alert(“hey!you are attacked”)</script>
</div>
</body>
</html>
那么这个时候问题就来了,当浏览器解析用户输入的代码那一行时会发生什么呢?答案很显然,浏览器并不知道这些代码改变了原本程序的意图,会照做弹出一个信息框。就像这样。
XSS的危害
其实归根结底,XSS的攻击方式就是想办法“教唆”用户的浏览器去执行一些这个网页中原本不存在的前端代码。
可问题在于尽管一个信息框突然弹出来并不怎么友好,但也不至于会造成什么真实伤害啊。的确如此,但要说明的是,这里拿信息框说事仅仅是为了举个栗子,真正的黑客攻击在XSS中除非恶作剧,不然是不会在恶意植入代码中写上alert(“say something”)的。
在真正的应用中,XSS攻击可以干的事情还有很多,这里举两个例子。
窃取网页浏览中的cookie值
在网页浏览中我们常常涉及到用户登录,登录完毕之后服务端会返回一个cookie值。这个cookie值相当于一个令牌,拿着这张令牌就等同于证明了你是某个用户。
如果你的cookie值被窃取,那么攻击者很可能能够直接利用你的这张令牌不用密码就登录你的账户。如果想要通过script脚本获得当前页面的cookie值,通常会用到document.cookie。
试想下如果像空间说说中能够写入xss攻击语句,那岂不是看了你说说的人的号你都可以登录(不过某些厂商的cookie有其他验证措施如:Http-Only保证同一cookie不能被滥用)
劫持流量实现恶意跳转
这个很简单,就是在网页中想办法插入一句像这样的语句(比如评论区,留言,…):
<script>window.location.href="http://www.baidu.com";</script>
那么所访问的网站就会被跳转到百度的首页。
早在2011年新浪就曾爆出过严重的xss漏洞,导致大量用户自动关注某个微博号并自动转发某条微博。具体各位可以自行百度。
利用与绕过
那xss漏洞很容易被利用吗?那倒也未必。
毕竟在实际应用中web程序往往会通过一些过滤规则来组织代有恶意代码的用户输入被显示。
不过,这里还是可以给大家总结一些常用的xss攻击绕过过滤的一些方法,算是抛砖引玉。(以下的绕过方式皆通过渗透测试平台Web For Pentester 演示)
大小写绕过
这个绕过方式的出现是因为网站仅仅只过滤了
利用语句:
http://192.168.1.102/xss/example2.php?name=<sCript>alert("hey!")</scRipt>
利用过滤后返回语句再次构成攻击语句来绕过
这个字面上不是很好理解,用实例来说。
如下图,在这个例子中我们直接敲入script标签发现返回的网页代码中script标签被去除了,但其余的内容并没有改变。
于是我们就可以人为的制造一种巧合,让过滤完script标签后的语句中还有script标签(毕竟alert函数还在),像这样:
http://192.168.1.102/xss/example3.php?name=<sCri<script>pt>alert("hey!")</scRi</script>pt>
发现问题了吧,这个利用原理在于只过滤了一个script标签。
并不是只有script标签才可以插入代码
在这个例子中,我们尝试了前面两种方法都没能成功,原因在于script标签已经被完全过滤,但能植入脚本代码的不止script标签。
例如这里我们用<img>
标签做一个示范。
我们利用如下方式:
http://192.168.1.102/xss/example4.php?name=<img src='w.123' onerror='alert("hey!")'>
就可以再次愉快的弹窗。原因很简单,我们指定的图片地址根本不存在也就是一定会发生错误,这时候onerror里面的代码自然就得到了执行。
以下列举几个常用的可插入代码的标签。
<a onmousemove=’do something here’>
当用户鼠标移动时即可运行代码
<div onmouseover=‘do something here’>
当用户鼠标在这个块上面时即可运行(可以配合weight等参数将div覆盖页面,鼠标不划过都不行)
类似的还有onclick,这个要点击后才能运行代码,条件相对苛刻,就不再详述。
编码脚本代码绕过关键字过滤
有的时候,服务器往往会对代码中的关键字(如alert)进行过滤,这个时候我们可以尝试将关键字进行编码后再插入,不过直接显示编码是不能被浏览器执行的,我们可以用另一个语句eval()来实现。eval()会将编码过的语句解码后再执行,简直太贴心了。
例如alert(1)编码过后就是
\u0061\u006c\u0065\u0072\u0074(1)
所以构建出来的攻击语句如下:
http://192.168.1.102/xss/example5.php?name=<script>eval(\u0061\u006c\u0065\u0072\u0074(1))</script>
主动闭合标签实现注入代码
来看这份代码:
乍一看,哇!自带script标签。再一看,WTF!填入的内容被放在了变量里!
这个时候就要我们手动闭合掉两个双引号来实现攻击,别忘了,javascript是一个弱类型的编程语言,变量的类型往往并没有明确定义。
思路有了,接下来要做的就简单了,利用语句如下:
http://192.168.1.102/xss/example6.php?name=";alert("I am coming again~");"
效果如图:
回看以下注入完代码的网页代码,发现我们一直都在制造巧合。。
先是闭合引号,然后分号换行,加入代码,再闭合一个引号,搞定!
组合各种方式
在实际运用中漏洞的利用可能不会这么直观,需要我们不断的尝试,甚至组合各种绕过方式来达到目的。
XSS分类
介绍完一些常用的绕过方式,再倒回来讲一下XSS分类,因为下面讲具体的应用时会用到。
XSS攻击大致上分为两类:
一类是反射型XSS,又称非持久型XSS,
一类是储存型XSS,也就是持久型XSS。
什么是反射型XSS
其实,我们上面讲XSS的利用手段时所举的例子都是非持久型XSS。
也就是攻击相对于访问者而言是一次性的,具体表现在我们把我们的恶意脚本通过url的方式传递给了服务器,而服务器则只是不加处理的把脚本“反射”回访问者的浏览器而使访问者的浏览器执行相应的脚本。
也就是说想要触发漏洞,需要访问特定的链接才能够实现。
什么是储存型XSS
它与反射型XSS最大的不同就是服务器再接收到我们的恶意脚本时会将其做一些处理。
例如储存到数据库中,然后当我们再次访问相同页面时,将恶意脚本从数据库中取出并返回给浏览器执行。这就意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此储存型XSS的危害会更大。
还记得在文章开头提到的留言板的例子吗?那通常就是储存型XSS。当有人在留言内容中插入恶意脚本时,由于服务器要像每一个访客展示之前的留言内容,所以后面的访客自然会接收到之前留言中的恶意脚本而不幸躺枪。
这个过程一般而言只要用户访问这个界面就行了,不像反射型XSS,需要访问特定的URL。
防范手段
都说知己知彼方能百战不殆,知道了xss攻击的原理那么防御的方法也就显而易见了。
首先是过滤。对诸如<script>、<img>、<a>
等标签进行过滤。
其次是编码。像一些常见的符号,如<>在输入的时候要对其进行转换编码,这样做浏览器是不会对该标签进行解释执行的,同时也不影响显示效果。
最后是限制。通过以上的案例我们不难发现xss攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。
案例代码
SSM配置过滤器是在web.xml中的
<!--Xss过滤 , 添加到所有过滤器后面-->
<filter>
<filter-name>XssFilter</filter-name>
<!-- 过滤器位置 -->
<filter-class>com.hd.rcugrc.product.oa.filter.xss.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XssFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
而springboot配置方式
- 在启动类上添加
@ServletComponentScan
注解 - 在过滤器上添加
@WebFilter(urlPatterns = "/*",filterName = "filter1")
xss工具类
package com.xss;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
public class XSSUtils {
private static Map<String,Integer> map=new HashMap(){{
put("<script>(.*?)</script>",Pattern.CASE_INSENSITIVE);
put("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("<script>|</script>",Pattern.CASE_INSENSITIVE);
put("<script(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("eval\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("expression\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("javascript:",Pattern.CASE_INSENSITIVE);
put("vbscript:", Pattern.CASE_INSENSITIVE);
put("onload(.*?)=",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
put("<|>",Pattern.MULTILINE | Pattern.DOTALL);
}};
// 防止防止站点脚本注入工具,替换非法字符
public static <T>T cleanXSS(T value,Class<T> cl) {
if (value != null) {
String s = JSON.toJSONString(value);
for (Map.Entry<String, Integer> stringIntegerEntry : map.entrySet()) {
Pattern scriptPattern = Pattern.compile(stringIntegerEntry.getKey(), stringIntegerEntry.getValue());
s = scriptPattern.matcher(s).replaceAll("");
value = JSON.parseObject(s, cl);
}
}
return value;
}
// 防止防止站点脚本注入工具, 如果有特殊字符或者脚本那么报错
public static void cleanXSSError(Object value) throws Exception {
if (value != null) {
String s = JSON.toJSONString(value);
for (Map.Entry<String, Integer> stringIntegerEntry : map.entrySet()) {
Pattern scriptPattern = Pattern.compile(stringIntegerEntry.getKey(), stringIntegerEntry.getValue());
boolean matches = scriptPattern.matcher(s).find();
if (matches) {
throw new Exception("有非法字符"+stringIntegerEntry.getKey()+"存在...禁止接下来操作---请检查添加的数据,将非法数据清除后在操作");
}
}
}
}
// 防止防止站点脚本注入工具,如果有含特殊字符或者脚本那么返回true,
public static boolean cleanXSSErrorBool(Object value) {
if (value != null) {
String s = JSON.toJSONString(value);
for (Map.Entry<String, Integer> stringIntegerEntry : map.entrySet()) {
Pattern scriptPattern = Pattern.compile(stringIntegerEntry.getKey(), stringIntegerEntry.getValue());
boolean matches = scriptPattern.matcher(s).find();
if (matches) {
return true;
}
}
}
return false;
}
}
xss过滤器XssFilter
package com.xss;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* XSS攻击过滤器
* @author huanmin
*
*/
@WebFilter(urlPatterns = "/*",filterName = "XssFilter")
public class XssFilter implements Filter {
// 忽略权限检查的url地址
private final String[] excludeUrls = new String[]{
"null"
};
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String pathInfo = req.getPathInfo() == null ? "" : req.getPathInfo();
//获取请求url的后两层
String url = req.getServletPath() + pathInfo;
//获取请求你ip后的全部路径
String uri = req.getRequestURI();
//注入xss过滤器实例
HttpServletRequest httpServletRequest = new XssFilterWrapper((HttpServletRequest)servletRequest);
//过滤掉不需要的Xss校验的地址
for (String str : excludeUrls) {
if (uri.contains(str)) { //排除不需要过滤的路径
filterChain.doFilter(servletRequest, response);
return;
}
}
//xss过滤
filterChain.doFilter(httpServletRequest, response);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void destroy() {}
}
xss处理器XssFilterWrapper
package com.xss;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public class XssFilterWrapper extends HttpServletRequestWrapper {
public XssFilterWrapper(HttpServletRequest request) {
super(request);
}
/**
* 对参数处理 get 提交时候,防止站点脚本注入
* @param name
* @return
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return XSSUtils.cleanXSS(value,String.class);
}
/**
* 对数值进行处理 get post 提交时候 效验参数
* @param name
* @return
*/
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
int length = values.length;
String[] escapseValues = new String[length];
for (int i = 0; i < length; i++) {
escapseValues[i] = XSSUtils.cleanXSS(values[i],String.class);
}
return escapseValues;
}
return super.getParameterValues(name);
}
// 请求头里过滤 提交时候 效验表单,防止站点脚本注入
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return XSSUtils.cleanXSS(value,String.class);
}
/**
* 主要是针对HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE 获取pathvalue的时候把原来的pathvalue经过xss过滤掉
*/
@Override
public Object getAttribute(String name) {
// 获取pathvalue的值
if (HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE.equals(name)) {
Map uriTemplateVars = (Map) super.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (Objects.isNull(uriTemplateVars)) {
return uriTemplateVars;
}
Map newMap = new LinkedHashMap<>();
uriTemplateVars.forEach((key, value) -> {
if (value instanceof String) {
newMap.put(key, XSSUtils.cleanXSS((String) value, String.class));
} else {
newMap.put(key, value);
}
});
return newMap;
} else {
return super.getAttribute(name);
}
}
}
注意事项:
-
可能会遇到 拦截时候 (HttpServletRequest)servletRequest 转换失败,因为有可能是其他的地方也实现了HttpServletRequestWrapper 并且在你这个Filter之前运行的
比如: public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper
这是SpringSecurity权限架构内部的实现,解决办法:你在将这个类继承一下就行了内部该怎么操作就怎么操作就行 -
还有些情况,前端提交数据没有通过controller提交的,比如我遇到的使用DWR这个框架提交的数据, 这个框架数据内部不走Filter导致无法处理XSS问题, 解决办法: 使用注解方式, 单独给这个提交方法处理
-
如果还有其他拦截不到的情况,都可以使用注解方式单独的处理
AOP注解方式
package com.xss;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Xss {
}
package com.xss;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Enumeration;
@Component
@Aspect
public class XssAop {
//这里需要注意了,这个是将自己自定义注解作为切点的根据,路径一定要写正确了
@Pointcut(value = "@annotation(com.xss.Xss) || @within(com.xss.Xss)") //拦截类和方法
public void xss() {
}
//JoinPoint joinPoint
@Before("xss()")
public void xssAfter() throws IOException {
System.out.println("---------------注解-xss");
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 获取请求头
Enumeration<String> enumeration = request.getHeaderNames();
StringBuffer headers = new StringBuffer();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getHeader(name);
headers.append(name + ":" + value).append(",");
}
XSSUtils.cleanXSS(headers.toString(), String.class);
//获取url 的参数
StringBuffer params = new StringBuffer();
String queryString = request.getQueryString();
if (StringUtils.isNotBlank(queryString)) {
params.append(URLEncoder.encode(queryString, "UTF-8")).append(",");
}
XSSUtils.cleanXSS(params.toString(), String.class);
//获取post Body 数据
String body = getRequestBodyData(request);
XSSUtils.cleanXSS(body, String.class);
}
//获取请求体body的内容
public static String getRequestBodyData(HttpServletRequest request) throws IOException {
BufferedReader bufferReader = new BufferedReader(request.getReader());
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = bufferReader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}