发布于2022年11月4日3年前 Java代码审计 —XXS跨站脚本 一般来说,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}<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> payloadsearch?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,最终返回messageinfopublic 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调用了MessageInfoStoreDaopublic 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,然后提交抓包内容当我们已返回主页就会发现弹窗而且由于我们设置的是网站的标题,即http报文中http头的tittle的位置,所以不管访问那个页面都会弹窗打开数据库可以观察到,在zrlog库中website表的title字段的值就是我们插入的paylaod通过抓包我们已经确定了输入的位置为/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()); } 继续审计AdminRoutespublic 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}的形式输出,未做处理,导致了XSSDOM型XSSDOM型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.URLdocument.locationdocument.refererdocument.from输出点evaldocument.writedocument.InnerHTMLdocument.OuterHTMLXSS漏洞修复前面已经讲过导致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 实现二全局的XSSFilterpackage 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("&", "&").replace("<", "<").replace("'", "'"); } }; 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实体编码在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("&"); break; case '"': sb.append("""); break; case '\'': sb.append("'"); break; case '<': sb.append("<"); break; case '>': sb.append(">"); 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示例:反射型的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片段,然后分析其中的标签和属性是否合法即可。参考文章:XSS漏洞 (zhishihezi.net)本文作者:Gh0st1nTheShell
创建帐户或登录后发表意见