HacKerQWQ的博客空间

javaweb代码审计学习(xss)

Word count: 3.4kReading time: 17 min
2021/11/06 Share

环境配置

https://github.com/cn-panda/JavaCodeAudit/tree/master/%E3%80%9003%E3%80%91XSS%20%E6%BC%8F%E6%B4%9E%E5%8E%9F%E7%90%86%E4%B8%8E%E5%AE%9E%E9%99%85%E6%A1%88%E4%BE%8B%E4%BB%8B%E7%BB%8D

下载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
32
33
#创建数据库
create database sec_xss charset utf8;
#创建表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for message
-- ----------------------------
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`mail` varchar(255) DEFAULT NULL,
`message` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of message
-- ----------------------------
BEGIN;
INSERT INTO `message` VALUES (1, 'panda', 'panda@cnpanda.net', '这是⼀个测试储存型
XSS 的项⽬');
INSERT INTO `message` VALUES (2, 'test', 'test@test.com', '测试数据 2。测试功能是
否正确');
INSERT INTO `message` VALUES (3, 'test_last', 'last@cnpanda.net', '最后⼀次测试,
测试⽆误,则完成');
INSERT INTO `message` VALUES (4, '熊猫', 'admin@cnpanda.net', '你好!这⾥有⼀个新的
短消息请注意查收!');
INSERT INTO `message` VALUES (5, 'lalala', 'lalala@qq.com', '啦啦啦啦啦啦啦啦绿绿
\r\n啦啦啦啦啦啦啦啦绿绿');
INSERT INTO `message` VALUES (6, 'xss', 'xss@xss.xss', ' \' test');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

src/main/java/com/sec/dao/impl/MessageInfoDaoImpl.java23行和69行修改数据库连接账号密码

image-20211106160744494

主页如下:

image-20211106160832933

  • 如果遇到数据库timezone错误,可以将连接修改为

    1
    jdbc:mysql://localhost:3306/sec_xss?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC

漏洞原理

页面提供了插入字符串的功能时,如果字符串含有javascript代码且特殊字符如尖括号没有被转义,就会被页面执行,造成XSS漏洞。危害有窃取其他用户的Cookies、截屏、劫持等等,javascript能做的事情都可以做。分为反射型、存储型和DOM型XSS。

EL表达式

EL(Expression Language,表达式语言),为了使JSP写起来更简单,提供了在JSP中简化表达式的方法。JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。

image-20211129233540405

<c:out>标签

<c:out>标签用来显示一个表达式的结果,与<%=%>相似,它们的区别就是<c:out>标签可以直接通过”.”操作符来访问属性。

1
<c:out value="customer.address.street">

<c:if> 标签

<c:if>标签判断表达式的值,如果表达式的值为 true 则执行其主体内容。

1
2
<c:if test="${user.salary > 2000}"
<p>我的工资为:value="${user.salary}"</p>

<c:forEach>标签

这些标签封装了Java中的for,while,do-while循环。

相比而言,<c:forEach>标签是更加通用的标签,因为它迭代一个集合中的对象。

<c:forTokens>标签通过指定分隔符将字符串分隔为一个数组然后迭代它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<title>c:forEach 标签实例</title>
</head>
<body>
<c:forEach var="i" begin="1" end="5">
Item <c:out value="${i}"/><p>
</c:forEach>
</body>
</html>

模型类的使用

ModelAndView

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("mvc")
@Controller
public class EL {
@RequestMapping(value="/getMessage")
public ModelAndView getMessage(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("hello");
modelAndView.addObject("message","HelloWorld");
return modelAndView;
}
}

ModelMap的使用

使用java.util.Map实现,可以根据模型属性的具体类型自动生成模型属性的名称

1
2
3
4
5
public String testmethod(String someparam,ModelMap model){
//将数据放置到ModelMap类的model对象中,第二个参数可以是任何Java类型
Model.addAttribute("key",somparam);
return "success";
}

Model类的使用

Model类是一个接口类,通过attribute添加数据,存储的数据域范围是requestScope

1
2
3
4
public String index1(Model model){
Model.addAttribute("result","后台返回");
return "success";
}

反射型XSS

image-20211106161111973

此处提供了将用户输入显示在页面中的功能,对应源码如下

image-20211106161209015

在search.jsp页面中通过msg获取用户输入并传给/search路由

image-20211106161318952

经过service方法最终将请求传递到Message方法,此处的

1
resp.getWriter().print(message)

将用户输入直接回显到页面上,直接输入payload

1
<script>alert("HacerQWQ");</script>

就能被浏览器执行

image-20211106161619078

存储型XSS

通过servlet将用户的输入作为字符串插入到数据库,然后查询记录的时候回显到页面上,造成存储型XSS。一般存在的地方是博客或者业务数据记录中。

image-20211106165351416

前端是个插入数据的框,在Message中输入payload

1
<script>alert(1);</script>

对应后端代码src/main/java/com/sec/dao/impl/MessageInfoDaoImpl.java处的MessageInfoStoreDao方法

image-20211106165535336

这里虽然经过预编译,但是预编译只能防止sql注入,对于xss的payload没有作用,本质上还是将数据本身插入到数据库当中

同样在MessageInforShowDao中将记录取出回显到message.jsp中

image-20211106165831206

image-20211106165845045

最终还是会触发存储型XSS攻击

image-20211106170103840

修复方案

产生漏洞的主要原因是javascript标签被解析,所以可以通过使标签失效来达到防御的目的,主要有以下几种

  • 保留语意,将输⼊的特殊字符转译存储到数据库,缺点是可能会对数据库或⽂件系统产⽣⼀些不必要的垃圾信息
  • 过滤掉特殊字符,只保留正常数据,缺点是有些时候⽤户需要输⼊特殊字符,不能保证数据原始性
  • 输⼊限制,含有特殊字符的数据不能够输⼊

全局过滤器

主要是借助了web.xml中的filter配置项进行过滤,添加如下内容

1
2
3
4
5
6
7
8
<filter>
<filter-name>XssSafe</filter-name>
<filter-class>com.sec.filter.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XssSafe</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

这里的url-pattern配置/的话只会匹配类似/login,如果是/*的话就能匹配如/login.jsp的路径

XSSFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.sec.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

//XssFilter实现:
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XSSHttpServletRequestWrapper((HttpServletRequest) request), response);
}
}

XSSHttpServletRequestWrapper.java

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.sec.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;

//XssHttpServletRequestWrapper实现
public class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XSSHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@SuppressWarnings("rawtypes")
public Map<String, String[]> getParameterMap(){
Map<String,String[]> request_map = super.getParameterMap();
Iterator iterator = request_map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry me = (Map.Entry)iterator.next();
String[] values = (String[])me.getValue();
for(int i = 0 ; i < values.length ; i++){
values[i] = xssClean(values[i]);
}
}
return request_map;
}
public String[] getParameterValues(String paramString)
{
String[] arrayOfString1 = super.getParameterValues(paramString);
if (arrayOfString1 == null)
return null;
int i = arrayOfString1.length;
String[] arrayOfString2 = new String[i];
for (int j = 0; j < i; j++){
arrayOfString2[j] = xssClean(arrayOfString1[j]);
}
return arrayOfString2;
}
public String getParameter(String paramString)
{
String str = super.getParameter(paramString);
if (str == null)
return null;
return xssClean(str);
}
public String getHeader(String paramString)
{
String str = super.getHeader(paramString);
if (str == null)
return null;
str = str.replaceAll("\r|\n", "");
return xssClean(str);
}
private String xssClean(String value) {
//ClassLoaderUtils.getResourceAsStream("classpath:antisamyslashdot.xml", XssHttpServletRequestWrapper.class)
if (value != null) {
// NOTE: It's highly recommended to use the ESAPI library and
// uncomment the following line to
// avoid encoded attacks.
// value = encoder.canonicalize(value);
value = value.replaceAll("\0", "");
// Avoid anything between script tags
Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid anything in a src='...' type of expression
scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid anything in a href='...' type of expression
scriptPattern = Pattern.compile("href[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome </script> tag
scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid eval(...) expressions
scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid expression(...) expressions
scriptPattern = Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid javascript:... expressions
scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid vbscript:... expressions
scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid onload= expressions
scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
}
return value;
}
}

规则设置不全的话就容易被绕过,例如:这里只是替换了javascript为空,如果是双写的的话还是能直接执行的

payload:

1
<img src=x onerror="javasjavascriptcript:alert('HackerQWQ')">

image-20211106173435187

使⽤打包好的java库

这里使用的是owasp的Encode插件

maven插件下载地址:https://mvnrepository.com/artifact/org.owasp.encoder/encoder/1.2.3

用法:https://javadoc.io/doc/org.owasp.encoder/encoder/latest/index.html

配置上跟上面的差不多,这里使用注解@WebFilter效果一样

XssFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sec.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

//XssFilter实现:
@WebFilter(filterName = "XssFilter",urlPatterns = "/*")
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(new XSSHttpServletRequestWrapper((HttpServletRequest) request), response);
}
}

XSSHttpServletRequestWrapper.java

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.sec.filter;

import org.owasp.encoder.Encode;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;

//XssHttpServletRequestWrapper实现
public class XSSHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XSSHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@SuppressWarnings("rawtypes")
public Map<String, String[]> getParameterMap(){
Map<String,String[]> request_map = super.getParameterMap();
Iterator iterator = request_map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry me = (Map.Entry)iterator.next();
String[] values = (String[])me.getValue();
for(int i = 0 ; i < values.length ; i++){
values[i] = xssClean(values[i]);
}
}
return request_map;
}
public String[] getParameterValues(String paramString)
{
String[] arrayOfString1 = super.getParameterValues(paramString);
if (arrayOfString1 == null)
return null;
int i = arrayOfString1.length;
String[] arrayOfString2 = new String[i];
for (int j = 0; j < i; j++){
arrayOfString2[j] = xssClean(arrayOfString1[j]);
}
return arrayOfString2;
}
public String getParameter(String paramString)
{
String str = super.getParameter(paramString);
if (str == null)
return null;
return xssClean(str);
}
public String getHeader(String paramString)
{
String str = super.getHeader(paramString);
if (str == null)
return null;
str = str.replaceAll("\r|\n", "");
return xssClean(str);
}

private String xssClean(String value) {
if (value != null) {
value= Encode.forHtml(value);
}
return value;
}
}

拦截成功

image-20211106180333282

commons.lang包的StringEscapeUtils类

在这个包中有个StringUtils 类,该类主要提供对字符串的操作,对null是安全的,主要提供了字符串查
找、替换、分割、去空⽩、去掉⾮法字符等等操作。存在两个函数可以供我们过滤使⽤。

在maven中添加依赖

1
2
3
4
5
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>

用法:http://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringEscapeUtils.html

  1. StringEscapeUtils.escapeHtml(string)
    
    1
    2
    3
    4
    5
    6
    7

    使⽤HTML实体,转义字符串中的字符。

    如`"`转换为`"`等

    2. ```
    StringEscapeUtils.escapeJavaScript(string)
    使⽤JavaScript字符串规则转义字符串中的字符。 如对`'`进行转义=>`\'`

效果也是不错的

image-20211106181049227

案例分析(CVE-2018-19178)

漏洞概述

描述:

1
In JEESNS 1.3, com/lxinet/jeesns/core/utils/XssHttpServletRequestWrapper.java allows stored XSS via an HTML EMBED element, a different vulnerability than CVE-2018-17886.

com/lxinet/jeesns/core/utils/XssHttpServletRequestWrapper.java中的xss过滤可以用embed标签绕过

环境搭建

项目地址:https://gitee.com/root8088/jeesns1.3

  1. 创建数据库

    1
    create database jeesns charset utf8mb4;
  2. 执行数据库脚本。数据库脚本在jeesns-web/database/jeesns.sql目录下。

    1
    source /path/to/project/jeesns-web/database/jeesns.sql
  3. 导入到maven项目

  4. 设置项目编码为utf-8,选择jdk1.7版本或以上,不要选择jre。

  5. 然后在jeesns-web/src/main/resources/jeesns.propertis ⽂件中修改数据库的账号密码以及后台路径

  6. 编译项目。在eclipse中,右键点击项目名,选择Run as - Maven build...Goals填入clean package,然后点击Run,第一次运行需要下载jar包,请耐心等待。

  7. 部署项目。将项目部署到Tomcat7或以上版本,启动Tomcat。

  8. 访问系统。前台地址:http://localhost:8080/;用户名:admin,密码:jeesns,登录成功之后,在右上角展开有个'管理',点击即可进入后台管理。

本地没搭建成功,不费这个时间了

漏洞分析

问题出现在com/lxinet/jeesns/core/utils/XssHttpServletRequestWrapper.java

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
private String cleanXSS(String value) {
//first checkpoint
//(?i)忽略⼤⼩写
value = value.replaceAll("(?i)<style>", "&lt;style&gt;").replaceAll("(?
i)</style>", "&lt;&#47;style&gt;");
value = value.replaceAll("(?i)<script>", "&lt;script&gt;").replaceAll("
(?i)</script>", "&lt;&#47;script&gt;");
value = value.replaceAll("(?i)<script", "&lt;script");
value = value.replaceAll("(?i)eval\\((.*)\\)", "");
value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']",
"\"\"");
//second checkpoint
// 需要过滤的脚本事件关键字
String[] eventKeywords = { "onmouseover", "onmouseout", "onmousedown",
"onmouseup", "onmousemove", "onclick", "ondblclick",
"onkeypress", "onkeydown", "onkeyup", "ondragstart",
"onerrorupdate", "onhelp", "onreadystatechange", "onrowenter",
"onrowexit", "onselectstart", "onload", "onunload",
"onbeforeunload", "onblur", "onerror", "onfocus", "onresize",
"onscroll", "oncontextmenu", "alert" };
// 滤除脚本事件代码
for (int i = 0; i < eventKeywords.length; i++) {
// 添加⼀个"_", 使事件代码⽆效
value = value.replaceAll(eventKeywords[i],"_" + eventKeywords[i]);
}
return value;
}
}

使用黑名单的方式对数据进行过滤,由于标签数量大,属性也多,因此一旦不能完全对敏感字符进行过滤,就会造成XSS注入

payload

1
2
3
4
<img src="x" ONERROR=confirm(0)>
<svg/onLoad=confirm(1)>
<object
data="data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTs8L3NjcmlwdD4=">

修复漏洞

采用StringEscapeUtils或者OWASP的Encode插件

CATALOG
  1. 1. 环境配置
  2. 2. 漏洞原理
  3. 3. EL表达式
    1. 3.1. <c:out>标签
    2. 3.2. <c:if> 标签
    3. 3.3. <c:forEach>标签
  4. 4. 模型类的使用
    1. 4.1. ModelAndView
    2. 4.2. ModelMap的使用
    3. 4.3. Model类的使用
    4. 4.4. 反射型XSS
    5. 4.5. 存储型XSS
  5. 5. 修复方案
    1. 5.1. 全局过滤器
    2. 5.2. 使⽤打包好的java库
    3. 5.3. commons.lang包的StringEscapeUtils类
  6. 6. 案例分析(CVE-2018-19178)
    1. 6.1. 漏洞概述
    2. 6.2. 环境搭建
    3. 6.3. 漏洞分析
    4. 6.4. 修复漏洞