一般来说,XSS的危害性没有SQL注入的大,但是一次有效的XSS攻击可以做很多事情,比如获取Cookie、获取用户的联系人列表、截屏、劫持等。根据服务器后端代码的不同,XSS的种类也不相同,一般可以分为反射型、存储型以及和反射型相近的DOM型。漏洞危害有:窃取Cookie,键盘记录,截屏,网页挂马,命令执行。
(1)收集输入、输出点。
(2)查看输入、输出点的上下文环境。
(3)判断Web应用是否对输入、输出做了防御工作(如过滤、扰乱以及编码)。
(4)通过功能、接口名、表名、字段名等角度做搜索
xss的产生必定含有输入点,所以只需要定位用户的输入点,即可快速地进行跟踪发现漏洞。request.getParameter(param)或“${param}”获取用户的输入信息。输出主要表现为前端的渲染,我们可以通过定位前端中一些常见的标识来找到它们,然后根据后端逻辑来判断漏洞是否存在。
<%=变量%>是<%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(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类用来存储处理完成后的结果数据,以及显示该数据的视图,其前端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类是一个接口类,通过attribue()添加数据,存储的数据域范围是requestScope:
Public String index1(Model model){
Model.addAtribute("result", "后台返回");
return "result";
}
从上面的代码可以看到,产生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会把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,然后提交
抓包内容
当我们已返回主页就会发现弹窗
而且由于我们设置的是网站的标题,即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());
}
继续审计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和反射型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漏洞一般有以下几种方法。
实现一
编写全局过滤器实现拦截,并在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("&", "&").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>
在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();
}
类似的还有谷歌的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可以实现类似于全局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片段,然后分析其中的标签和属性是否合法即可。