freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

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

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

FreeBuf+小程序

FreeBuf+小程序

Java代码审计 —XSS跨站脚本
2022-01-14 14:57:18
所属地 广东省

一般来说,XSS的危害性没有SQL注入的大,但是一次有效的XSS攻击可以做很多事情,比如获取Cookie、获取用户的联系人列表、截屏、劫持等。根据服务器后端代码的不同,XSS的种类也不相同,一般可以分为反射型、存储型以及和反射型相近的DOM型。漏洞危害有:窃取Cookie,键盘记录,截屏,网页挂马,命令执行。

审计思路

(1)收集输入、输出点。

(2)查看输入、输出点的上下文环境。

(3)判断Web应用是否对输入、输出做了防御工作(如过滤、扰乱以及编码)。

(4)通过功能、接口名、表名、字段名等角度做搜索

XSS的触发位置

xss的产生必定含有输入点,所以只需要定位用户的输入点,即可快速地进行跟踪发现漏洞。request.getParameter(param)或“${param}”获取用户的输入信息。输出主要表现为前端的渲染,我们可以通过定位前端中一些常见的标识来找到它们,然后根据后端逻辑来判断漏洞是否存在。

JSP表达式

<%=变量%>是<%out.println(变量);%>的简写方式,<%=%>用于将已声明的变量或表达式输出到网页外,以下两种写法所实现的效果是相同的

<%out.println(msg);%>
源码:
<% int msg = 131; %>
<%out.println(msg);%>
输出:
131

<%=msg%>
源码:
<% int msg = 111; %>
<%=msg%>
输出:
111

所以可以写成

<%String msg = request.getParameter('msg');%>
<%=msg%>

EL表达式

EL(Expression Language,表达式语言)是为了使JSP写起来更加简单。

<%=request.getParameter("username") %>等价于${param.username}
image.png

<c:out>标签

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

<c:out value="${user.getUsername()}"

<c:if>标签

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

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

<c:forEach>标签

<c:forEach>标签的作用是迭代输出标签内部的内容。它既可以进行固定次数的迭代输出,也可以依据集合中对象的个数来决定迭代的次数

<table>
<tr><th>名字</th><th>说明</th><th>图片预览</th></tr>
<c:forEach items="${data}"var="item">
<tr><td>${item.advertName}</td><td>${item.notes}</td><td><img src="${item.defPath}"/></d></tr>
</c:forEach>
</table>
<ul>
<li><a href='?nowPage=${nowPage-1}'><-上一页</a></i>
<c:forEach varStatus="i" begin="1"end="${sumPage}">
<c:choose>
<c:when test="${nowPage==i.count}">
<li class='disabled'>${i.count}</li>
</c:when>
<c:otherwise>
<li class='active'><a href='?nowPage=${i.count}'>${i.count}</a><li>
</c:otherwise>
</c:choose>
</c:forEach>
<li><a href="?nowPage=${nowPage+1}'>下一页-</a></li>
</ul>

ModelAndView类的使用

ModelAndView类用来存储处理完成后的结果数据,以及显示该数据的视图,其前端JSP页面可以使用“${参数}”的方法来获取值:

@RequestMapping("mvc")
@Controller
public class TestRequestMMapping{
	@RequestMapping(value="/getMessage")
	publicModelAndView getMessage(){
		ModelAndView modelAndView = new ModelAndView();
		modelAndView.setViewName("messgae");
		modelAndViewaddObject("meggage", "HelloWorld");
		return modelAndView;
	}
}

Model类的使用

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

Public String index1(Model model){
    Model.addAtribute("result", "后台返回");
    return "result";
}

反射型XSS

从上面的代码可以看到,产生XSS的最主要原因是因为没有对用户的输入进行过滤后直接输出,所以在代码审计的时候,我们只需要通过搜索特定的关键字和数据交互点,然后判断这些数据是否可控以及输出位置,当数据可控且可以直接在浏览器页面输出时可以进一步构造XSS攻击代码

前端导致XSS代码段

<%
String name = request.getParameter("name");
out.println(name)
%>

后端导致XSS代码段

public void Message(HttpServletRequest req, HttpServletResponse resp) {
	String message = req.getParameter("msg");
	try{
		resp.getWriter().print(message);
	} catch (IOException e) {
		e. printStackTrace();
	}
}

无论是前段还是后端都可以发现,输出语句没有进行任何过滤就直接把用户输入给输出了

out.println(name)
resp.getWriter().print(message);

定位到后接着判断其中调用的参数是否可控,可以发现,message的值来源于前端get方法传入的msg参数,同时并未对传入的数据进行任何的处理就进行输出,因此是完全可控的因此我们只需找到对应的路由,并通过GET方法传入包含XSS有效载荷的URL,以控制“resp.getWriter().print(message)中的message参数为XSS有效载荷。对于常规的Java项目,通过web.xml可快速地找到对应方法的路由关系

<servlet>
    <description></description>
    <display-name>search</display-name>
    <servlet-name>search</servlet-name>
    <servlet-class>com.sec.servlet.InfoServlet</servlet-class>
</servlet>

payload
search?msg=<script>alert(1)</script>

储存型XSS

储存型XSS和反射型XSS的原理是一样的,区别在于储存型XSS会把payload存储在服务器,每一次访问内容就有触发payload的可能,所以相比反射型XSS,存储型XSS的危害更大。反射型XSS需构造恶意URL来诱导受害者点击,而存储型XSS由于有效载荷直接被写入了服务器中,且不需要将有效载荷输入到URL中,往往可以伪装成正常页面,迷惑性更强。因此存储型XSS漏洞对于普通用户而言很难及时被发现。
一般XSS会在数据库读取数据,然后渲染为HTML,此时就会被浏览器引擎解析其中的恶意数据,所以一般储存型XSS在一些留言板,文章,个性签名等地方比较易受攻击。
在挖掘存储型XSS漏洞时,要统一寻找“输入点”和“输出点”。由于“输入点”和“输出点”可能不在同一个业务流中,在挖掘这类漏洞时,可以考虑通过以下方法提高效率。
(1)黑白盒结合。
(2)通过功能、接口名、表名、字段名等角度做搜索。

实例一

以下代码来自于《网络安全Java代码审计实战》
对一个DEMO进行审计,发现存在show将用户的留言打印,在web.xml可以找到对应的类

<servlet>
<description></description>
<display-name>show</display-name>
<servlet-name>show</servlet-name>
<servlet-class>com.sec.servlet.ShowServlet</servlet-class>
</servlet>
public void ShowMessage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    MessageInfoService nsginfo = new MessageInfoServiceImpl();
    List<MessageInfo>msg = msgInfo.HessageInfoShowService();

    if(msg != null){
        req.setAttribute("msg", msg);
        req.getRequestDispatcher("/message.jsp").forward(req,resp)
        return;
    }
}

再观察jsp可以发现jsp把messageinfo中的name、mail以及message取出并渲染到浏览器

<%
	List<MessageInfo> msginfo = (ArrayList<MessageInfo>)request.getAttribute("msg");
	for(MessageInfo m:msginfo){
%>
<table>
		<tr><td class="klytd">留言人:</td>
			<td class ="hvttd" <%=m.getName() %></td>
		</tr>
		<tr><td class="klytd"> e-mail:</td><td class ="hvttd"> <%=m.getMail() %></td>
			</tr>
		<tr><td class="klytd">内容:</td><td class ="hvttd"> <%=m.getMessage() %></td></tr>
</table><% } %>
</div>

我们这时候已经确定了输出点,是未经过滤的,然后我们要找到输入点,查看在输入的过程和处理的过程有没有对传入的参数进行过滤,从上面的代码可以看到,对msg参数使用setAttribute方法进行了存储,然后通过getRequestDispatcher将其重定向至message.jsp文件进行输出

追踪msg的值,发现是通过msgInfo.MessageInfoShowService()得到的,步入方法可以发现MessageInfoShowService得到的值又是通过调用了MessageInfoShowDao方法得到的

public List<MessageInfo> MessageInfoShowService(){
    List<MessageInfo>msg = msginfo.MessageInfoshowDao();
    return msg;
}

继续步入MessageInfoshowDao方法,发现和数据库进行连接相关、以及对SQL语句进行预编译的代码,并分别初始化了messageinfo和messageinfo,将从SQL语句从查询出来的数据(name,mail,message)传递到msg中,再将获得所有数据的msg传递给messageinfo,最终返回messageinfo

public List<MessageInfo> MessageInfoshowDao(){
    Connection conns = null;
    PreparedStatenent ps = null;
    ResultSet rs = null;
    List<MessageInfo>messageinfo = null;
    try{
        class forName("com.mysql.jdbc.Driver");
        conns = DriverManager.getconnection("jdbc:mysql://localhost:3306/sec_xss", "root", "root");
        String sql = "select * from message";
        ps = conns.prepareStatenent(sql);
        rs = ps.executeQuery();
        messageinfo = new Arraylist<Messagelnfo>();

        while(cs.next()){
            MessageInfo msg = new MessageInfo();
            meg.setName(rs.getString("name"));
            msg.setMail(rs.getString( "mail"));
            msg.setMessage(rs.getString( "message"));
            messageinfo.add(msg);
        }
    } catch (CLassNotFoundException e){...}catch (SQLException e) {...} finally(...}

    return messageinfo;
}

所以输出的流程就很清晰了,通过读取数据库里面的内容,最终渲染成html然后输出至浏览器,所以下一步我们需要寻找数据库插入数据的方法

通过搜索关键字可以找到MessageInfoStoreDao方法

public class MessageInfoDaoImpl implenents MessageInfoDao {

	public boolean MessogeInfoStoreDao(String nane, String mail, String messoge){
	
		Connection Conn = null;
		PreparedStatenent ps = nulL;
		boolean result = false;
		try {
			Class.forNane("con, mysql.jdbc.Driver");
			conn = DriverManager.getConnection("jdbc:mysqL://localhost:3386/sec_Xss", "root", "root");
			String sql = "INSERT INTO message (name, mail, message) VALUES (?,?,?)";
			ps = conn.prepareStatement(sql);
			
			ps.setString(1, name);
			ps.setString(2, mail);
			ps.setstring(3, message);
			ps.execute();
			
			cesult = true;
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e. printstackTrace();
		} catch (SQLException e) {
			//TODO Auto-generated catch btock
			e.printstackTrace();
		}finally{
			try

然后我们再去查看在那里被调用了MessageInfoStoreDao方法,可以发现MessageInfoStoreService调用了MessageInfoStoreDao

public boolean MessageInfoStoreService(String name, string mail, String message){
	return msginfo.MessageInfoStoreDao(name, mail, message);
}

通过查找可以发现在StoreXSS中对其进行了调用,并且MessageInfoStoreService中的三个参数全部直接来源于GET方法

public void StoreXss(HttpServletRequest req, HttpServletResponse resp throws ServletException, IOException{

	String nane = req.getParaneter("name");
	String mail = req.getParaneter("mail);
	String message = reg.getParaneter("message");
 
	if(!name.equals(null) && !mail.equals(null) && !message.equals(null)){
    	MessageInfoService msginfo = new MessageInfoServiceImpl();
    	msginfo.MessageInfoStoreService(name, mail, message);
    	
    	resp.getWriter().print("<script>alert(\"添加成功\")</script>");
    	resp.getWriter().flush();
    	resp.getWriter().close();
	}
}

由于没有进行任何的校验,所以只需要直接插入paylaod即可

实例二

对Zrlog1.1.9进行测试并审计

首先先将zrlog1.1.9进行部署安装,部署完成后打开管理员后台

在设置、网站设置中的网站标题处插入payload,然后提交
image.png

抓包内容
image.png

当我们已返回主页就会发现弹窗
image.png

而且由于我们设置的是网站的标题,即http报文中http头的tittle的位置,所以不管访问那个页面都会弹窗

打开数据库可以观察到,在zrlog库中website表的title字段的值就是我们插入的paylaod
image.png

通过抓包我们已经确定了输入的位置为

/zrlog/api/admin/website/update

通过web.xml可知该CMS都是通过com.zrlog.web.config.ZrLogConfig类来进行访问控制

<filter>
    <filter-name>JFinalFilter</filter-name>
    <filter-class>com.jfinal.core.JFinalFilter</filter-class>
    <init-param>
        <param-name>configClass</param-name>
        <param-value>com.zrlog.web.config.ZrLogConfig</param-value>
    </init-param>
</filter>

可以在WEB-INF/classes/com/zrlog/web/config/ZrLogConfig.class找到字节码文件

public void configRoute(Routes routes) {
    routes.add("/post", PostController.class);
    routes.add("/api", APIController.class);
    routes.add("/", PostController.class);
    routes.add("/install", InstallController.class);
    routes.add(new AdminRoutes());
}

继续审计AdminRoutes

public void config() {
    this.add("/admin", AdminPageController.class);
    this.add("/admin/template", AdminTemplatePageController.class);
    this.add("/admin/article", AdminArticlePageController.class);
    this.add("/api/admin", AdminController.class);
    this.add("/api/admin/link", LinkController.class);
    this.add("/api/admin/comment", CommentController.class);
    this.add("/api/admin/tag", TagController.class);
    this.add("/api/admin/type", TypeController.class);
    this.add("/api/admin/nav", BlogNavController.class);
    this.add("/api/admin/article", ArticleController.class);
    this.add("/api/admin/website", WebSiteController.class);
    this.add("/api/admin/template", TemplateController.class);
    this.add("/api/admin/upload", UploadController.class);
    this.add("/api/admin/upgrade", UpgradeController.class);
}

从中我们可以找到/api/admin/website对应的类为WebSiteController,继续对该类进行审计

@RefreshCache
public WebSiteSettingUpdateResponse update() {
    Map<String, Object> requestMap = (Map)ZrLogUtil.convertRequestBody(this.getRequest(), Map.class);
    Iterator var2 = requestMap.entrySet().iterator();

    while(var2.hasNext()) {
        Entry<String, Object> param = (Entry)var2.next();
        (new WebSite()).updateByKV((String)param.getKey(), param.getValue());
    }

    WebSiteSettingUpdateResponse updateResponse = new WebSiteSettingUpdateResponse();
    updateResponse.setError(0);
    return updateResponse;
}

用户输入的内容会被存放在requestMap当中,然后里面的值通过一系列处理进入了while循环,在循环体当中被updateByKV方法进行数据传输,在这一系列处理过程中未发现有对传入数据的过滤,因此进一步审计updateByKV方法,查看是否进行过滤
代码地址

public boolean updateByKV(String name, Object value) {
    if (Db.queryInt("select siteId from " + TABLE_NAME + " where name=?", name) != null) {
        Db.update("update " + TABLE_NAME + " set value=? where name=?", value, name);
    } else {
        Db.update("insert " + TABLE_NAME + "(`value`,`name`) value(?,?)", value, name);
    }
    return true;
}

可以发现updateByKV方法直接就对传入的参数对数据库进行插入更新,未对数据进行过滤、扰乱以及编码

到这里我们已经对输入点进行完整的审计,从中并未发现过滤输入的操作,下一步就要对输出点进行审计查看是否在输出点做了过滤

这套Web系统采用了MVC架构,其中的“V”(表现层)采用了jsp。我们对输出“网站标题”的位置进行审计,zrlog\include\templates\default\header.jsp

<h1 class="site-name">
    <i class="avatar"></i>
    <a href="${rurl}">${_res.title}</a>
    <span class="slogan">${webs.title}</span>
</h1>

发现直接以${webs.title}的形式输出,未做处理,导致了XSS

DOM型XSS

DOM型XSS和反射型XSS的展现形式相似,但是还是有区别,区别在于DOM型XSS不需要与服务器交互,只发生在客户端处理数据阶段,粗略地说,DOM XSS漏洞的成因是不可控的危险数据,未经过滤被传入存在缺陷的JavaScript代码处理。

<script>
    var pos = document.URL.indexOf("#")+1;
    var name = document.URL.substring(pos, document.URL.length);
    document.write(name);
    eval("var a = " + name);
</script>

DOM型常见的输入点和输出点
输入点

  • document.URL

  • document.location

  • document.referer

  • document.from
    输出点

  • eval

  • document.write

  • document.InnerHTML

  • document.OuterHTML

XSS漏洞修复

前面已经讲过导致XSS漏洞的主要原因是输入可控并且没有经过过滤便直接输出,因此防御XSS漏洞一般有以下几种方法。

过滤器

实现一
编写全局过滤器实现拦截,并在web.xml进行配置
配置过滤器

public class XSSFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void destroy() {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFiter(new XSSRequestWrapper((HttpServletRequest) request), response);
    }
}

实现包装类

import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class XSSRequestWrapper extends HttpServletRequestWrapper{
	public XSSRequestWrapper(HttpServletRequest servletRequest){
	super(servletRequest);
	}
	@Override
	public String[] getParameterValues(String parameter){
		String[] values = super.getParameterValues(parameter);
		if(values == null){
			return null;
		}
		int count = values.length;
		String[] encodedValues = new String[count];
		for(int i = 0; i < count; i++){
			encodedValues[i] = stripXSS(values[i]);
		}
		return encodedValues;
}
@Override
public String getParameter(String parameter){
	String value = super.getParameter(parameter);
	return stripXSS(value);
}
@Override
	public StringgetHeader(Stringname){
		String value = super.getHeader(name);
		return stripXSS(value);
	}
	private String stripXSS(String value){
		if(value != null{
			//NOTE: It's highly recommended to use the ESAPl ibrary and uncomment the following line to

实现二
全局的XSSFilter

package com.anbai.sec.vuls.filter;

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

public class XSSFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;

        // 创建HttpServletRequestWrapper,包装原HttpServletRequest对象,示例程序只重写了getParameter方法,
        // 应当考虑如何过滤:getParameter、getParameterValues、getParameterMap、getInputStream、getReader
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(request) {
            public String getParameter(String name) {
                // 获取参数值
                String value = super.getParameter(name);

                // 简单转义参数值中的特殊字符
                return value.replace("&", "&amp;").replace("<", "&lt;").replace("'", "&#039;");
            }
        };

        chain.doFilter(requestWrapper, resp);
    }

    @Override
    public void destroy() {

    }

}

web.xml添加XSSFilter过滤器:

<!-- XSS过滤器 -->
<filter>
  <filter-name>XSSFilter</filter-name>
  <filter-class>com.anbai.sec.vuls.filter.XSSFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>XSSFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

HTML实体编码

image.png
在Java中虽然没有内置如此简单方便的函数,但是我们可以通过字符串替换的方式实现类似htmlspecialchars函数的功能。

/**
 * 实现htmlSpecialChars函数把一些预定义的字符转换为HTML实体编码
 *
 * @param content 输入的字符串内容
 * @return HTML实体化转义后的字符串
 */
public static String htmlSpecialChars(String content) {
  if (content == null) {
    return null;
  }

  char[]        charArray = content.toCharArray();
  StringBuilder sb        = new StringBuilder();

  for (char c : charArray) {
    switch (c) {
      case '&':
        sb.append("&amp;");
        break;
      case '"':
        sb.append("&quot;");
        break;
      case '\'':
        sb.append("&#039;");
        break;
      case '<':
        sb.append("&lt;");
        break;
      case '>':
        sb.append("&gt;");
        break;
      default:
        sb.append(c);
        break;
    }
  }

  return sb.toString();
}

采用开源安全控制库(OWASP)企业安全应用程序接口(ESAPI)

类似的还有谷歌的xssProtect等

//HTML Context
String html= ESAPI.encoder().encodeForHTML("<script>alert('xss')</script>");
// HTML Attribute Context
String htmlAttr = ESAPI.encoder().encodeForHTMLAttribute("<script>alert('xss')</script>");
//Javascript Attribute Context
String jsAttr = ESAPI.encoder().encodeForJavaScript("<script>alert('xss')</script");

RASP XSS攻击防御

RASP可以实现类似于全局XSSFilter的请求参数过滤功能,比较稳定的一种方式是Hook到javax.servlet.ServletRequest接口的实现类的getParameter/getParameterValues/getParameterMap等核心方法,在该方法return之后插入RASP的检测代码。这种实现方案虽然麻烦,但是可以避免触发Http请求参数解析问题(Web应用无法获取getInputStream和乱码等问题)。
示例 - RASP对getParameter返回值Hook示例:
image.png
反射型的XSS防御相对来说比较简单,直接禁止GET参数中出现<>标签,只要出现就理解拦截,如

http://localhost:8000/modules/servlet/xss.jsp?input=<script>alert('xss');</script>

过滤或拦截掉<>后input参数就不再具有攻击性了。
但是POST请求的XSS参数就没有那么容易过滤了,为了兼顾业务,不能简单的使用htmlSpecialChars的方式直接转义特殊字符,因为很多时候应用程序是必须支持HTML标签的(如:<img><h1>等)。RASP在防御XSS攻击的时候应当尽可能的保证用户的正常业务不受影响,否则可能导致用户无法业务流程阻塞或崩溃。
为了支持一些常用的HTML标签和HTML标签属性,RASP可以通过词法解析的方式,将传入的字符串参数值解析成HTML片段,然后分析其中的标签和属性是否合法即可。
image.png

参考文章:XSS漏洞 (zhishihezi.net)

# web安全 # 代码审计 # XSS漏洞 # 漏洞复现及分析
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录