SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

Log4ShellのためのLDAP

はじめに

こんにちは。SST研究開発部の小野里です。いよいよ入社2年目になってしまい、新入社員特権でチヤホヤされる時期も終わってしまったので戦々恐々としています。

さて、私はつい先日まで2021年度新卒研修最後の課題として、Log4ShellのPoCを作って動かすというものに挑戦していました。無事に動作させてローカル環境で任意コード実行を行うところまで完了しましたが、ここまで来るのに非常に長く険しい道のりがありました。特に苦労したのはLog4Shellのために利用するLDAPについての理解で、

  • 枯れた技術すぎて情報が少なく
  • 現役で使う人は大体強い人なので入門記事も少なく
  • アカウント管理以外の使い方についての情報はほぼ無い

というような状況。生半可な調べ方では糸口すら掴めませんでした。不適切な言い方になるかもしれませんが、脆弱性を悪用する人間は本当に勤勉なんだなと感心する次第です。
本記事では、私のような初学者に向けて、Log4ShellでLDAPがどのように使われていたのか?について私が調べた内容を書き残しておこうと思います。Log4shellのために要求されるLDAPの知識を、本記事でざっくりと説明していきます。
「LDAPって何?」「Java Schemaって何?」「Log4Shellの概要は分かったけど、結局何でRCEが起きるの?」といったレベル感の方におススメです!

「LDAPについては知っている」という方は、よりLog4Shellの方にフォーカスした記事も書いているのでそちらへどうぞ!

https://techblog.securesky-tech.com/entry/2022/04/21/what-happened-log4shelltechblog.securesky-tech.com

※注意!!!
本記事の内容は脆弱性の理解・セキュリティ技術の向上のために記載しているものであり、決して悪意ある攻撃の実行を示唆するものではありません。実際に攻撃手法を試してみる場合、必ず自分の管理下にあるサーバとの通信に限ってください。

目次

LDAPの基礎知識

LDAPはLightweight Directory Access Protocolの略で、ディレクトリサービスに接続するために使用されるプロトコルのことです。本記事ではLDAPサーバ含むソフトウェアなどの付随する環境も総称してLDAPと呼ぶことにします。
ディレクトリサービスとは何か、については他に詳しい解説記事がたくさんあるので調べて頂いた方が早いかと思いますが、一言で表すなら「階層型で文字列情報を管理するサービス」です。一般的なRDBとは異なり、PC内部でのディレクトリ構造のように情報を階層化して管理します。

例えば、

  • securesky-tech.comというドメインを持つ
    • Secure Sky Technologyという会社の
      • Research and Developmentという部署に所属するAさんとBさん
      • Salesという部署に所属するCさん

の3人がいたとしましょう。この時、これらの情報はLDAP上では以下の図のように表現できます。

ディレクトリ構造図

ここで、それぞれの頭についているcn,ouといった文字列は、それぞれの属性がどのような意味を表すかを示す名前となっています。属性は非常に膨大な種類がありますが、有名なものだと以下のような種類があります。

名前 意味
dc Domain Component。ツリーの頂点であるドメイン名を構成する要素
o Organization。会社名、組織名などを表す
ou Organizaton Unit。部署名などを表す
cn Common Name。ユーザ名など、その他の情報を表す

また、LDAPでは1つ1つの情報(図でいうところの四角い枠1つ分)を「エントリ」と呼びます。各エントリには代表値となる属性(RDN:Relational Distinguished Name)があり、1つ1つのエントリはDN(Distinguished Name)でその位置を示します。DNは、指し示したいエントリから一番上の階層までのRDNをつなぎ合わせて表現します。上記の図を例にすると、Aさんのエントリの

  • RDNはcn=A
  • DNはcn=A,ou=Research and Development,o=Secure Sky Technology,dc=securesky-tech,dc=com

となります。この時、RDNはcnといった名前を表すような値でなくても構いません。同じ階層で被りが無ければよいので、上記の図だとmail属性をRDNとして、AさんのDNをmail=hoge@example.com,ou=Research and Development,o=Secure Sky Technology,dc=securesky-tech,dc=comと表現することも可能です。とはいえ、一般的にはcn属性のようなそのエントリ自体の名前に相当するものが使用されることが多いです。

LDAP含むディレクトリサービスの特徴として、「データの変更がしづらく、アクセスが早い」という事が挙げられます。そのため、頻繁にデータが変更されず、素早い応答が要求される用途として、ローカルネットワーク内でのアカウント管理などに主に用いられています。

objectClassとは

LDAPの各エントリは、色々な属性を持ちます。先ほどの図の例だと、Aさんのエントリは

  • cn=A
  • userPassword=hoge
  • mail=hoge@example.com

の3つの属性を持っています。しかしながら、どのエントリも完全に自由な属性を持っていいわけではありません。例えば、ドメイン名を示すdc=securesky-tecn,dc=comエントリが、userPassword属性を持つことはあるでしょうか?当然ながら、dc=securesky-tech,dc=comエントリはユーザではないので、userPassword属性は必要ありません。もしこれが自由に設定できてしまったらLDAPのエントリ構造はたちまちカオスになってしまいますし、各種バグの原因にもなりかねないでしょう。

このような事態を防ぐために、LDAPの各エントリにはobjectClassという属性を設定します。これは「そのエントリがどのような属性を持つか」を定義する属性です。「属性を定義する属性」というと少々分かりづらいかもしれませんが、オブジェクト指向プログラミングに慣れた方は「クラスのフィールド定義」のようなものだと考えると分かりやすいかもしれません。クラスの作成時にそのクラスがどのようなフィールドを持つかを定義するのと同じように、objectClass属性でそのエントリがどのような属性を持つかを定義します。例えば、オブジェクト指向で考えた時にAさん、Bさん、Cさんのような人たちを示すクラスEmployeeを作るとします。上図の例通りにEmployeeクラスのフィールドを定義するなら、このクラスは次のように実装できるでしょう。

pubic class Employee{
    String cn;
    String userPassword;
    String mail;
    //以下省略
}

同じように、LDAPのエントリにobjectClass=employeeが指定されていたとします。このemployeeというobjectClassの定義では、「このobjectClassを持つエントリはcn,userPassword,mailの3つの属性を持たなければならない」と定義されているとします。このobjectClassが指定されていることで、Aさん~Cさんの各エントリは必要な属性を持ち、それ以外の余計な属性を持たなくて済むのです。

employeeというobjectClassは架空のものですが、実際のobjectClassは様々な情報が表現できるように多くの種類があります。自分で新たなobjectClassを定義することも可能ですが、少々面倒なので実際には既存のobjectClassを組み合わせて使うことが一般的です。例えば、ユーザアカウントを表すエントリを示すaccountというobjectClassは、次のように定義されています。

account
必須属性:uid
任意属性:description,host,l,o,ou,seeAlso

これは、このobjectClassを持つエントリにはuid属性が必須であり、それ以外に必要であればdescription,host,l,o,ou,seeAlsoの属性を入れてもよい、という事です。先ほど「組み合わせて使う」と書いた通り、objectClassは1つのエントリに複数設定することもできます。これにより、必須属性が増えたり、任意で付与できる属性の幅が広がって様々な情報を表現することが可能になります。

objectClassについてだけでも非常に奥が深いため、本記事での解説はここまでとします。objectClassについてもっと知りたい方は、以下の記事を参考にしてみてください。

Schemaとは

さて、ここまで属性とobjectClassの話を書いてきましたが、これらはどこで定義されていて、どうやって確認すればいいのでしょうか?その疑問を解決するのがSchemaです。Schemaは属性やobjectClass自体の定義を記したものです。LDAPサーバなどをインストールするとファイルとして一緒にダウンロードされます。OpenLDAPの場合、/etc/ldap/schemaの中に様々なschemaファイルがあります(環境によって異なる場合があります)。
例を挙げて見てみましょう。OpenLDAPの/etc/ldap/schema/core.schemacn属性の定義を見てみます。

attributetype ( 2.5.4.3 NAME ( 'cn' 'commonName' )
       DESC 'RFC2256: common name(s) for which the entity is known by'
       SUP name )

まずは先頭のattributetypeです。これは属性の定義であることを表しています。次の2.5.4.3という数字ですが、これはOIDと呼ばれる値で、その属性やオブジェクトクラスを一意に示すIDです。これは国際的に決まっている値なので、ユーザが勝手に変更することは出来ませんし、(基本的には)自分で勝手に生成することも出来ません。上記の場合、cn属性のOIDは世界中どこでも2.5.4.3という事になります。次にNAMEです。これは見て分かる通り、その属性の表記名と正式名称を表しています。cn属性は正式にはcommonName属性だという事が分かります。DESCも同様に見て分かる通り、その定義の概要を表しています。上記の場合は「RFC2256で採択された、一般名を表す属性」であることが分かります。最後にSUPですが、これは基底属性(規定objectClass)を表しています。どの属性、objectClassから継承されて定義されたものなのかを示す値で、上記の場合はcn属性はname属性を継承して定義された属性だという事が分かります。

同様に、objectClassの定義も見てみましょう。今度はOpenLDAPの/etc/ldap/schema/cosine.schemaaccountobjectClassの定義を見てみます。

objectclass ( 0.9.2342.19200300.100.4.5 NAME 'account'
        SUP top STRUCTURAL
        MUST userid
        MAY ( description $ seeAlso $ localityName $
                organizationName $ organizationalUnitName $ host )
        )

まずは先頭のobjectclassですが、これは文字通りobjectClassの定義であることを表しています。次の数字は先ほどと同じくOIDNAMEはこのobjectClassの名前がaccountであることを表しています。SUPも同様に、accountがtopというobjectClassから継承されたものであることを表しています。
先ほどとの大きな違いは次のSTRUCTURALです。これはこのobjectClassの種別を表しています。objectClassには次の3つの種別があります。

  • ABSTRUCT(抽象型)
    他のobjectClassを定義するためのobjectClassです。オブジェクト指向で言うところの「抽象クラス」に相当します。このobjectClassのみを使用してエントリを作ることは出来ません。
  • STRUCTURAL(構造型)
    エントリを作成することが出来るobjectClassです。エントリは必ず1つ以上のSTRUCTURALなobjectClassを持つ必要があります。
  • AUXILIARY(補助型)
    他のSTRUCTURALなobjectClassと併用してエントリを作成することが出来るobjectClassです。このobjectClassのみを使用してエントリを作ることは出来ません。

つまり、LDAPのエントリは1個以上のSTRUCTURALと、0個以上のAUXILIARYなobjectClassを持つという事になります。上記のaccountobjectClassの場合、単体でエントリを作成することが出来るという事が分かります。次のMUSTMAYは、前章のobjectClassの説明で書いた必須属性と任意属性です。accountobjectClassを持つエントリはuserid属性が必須であり、任意でdescriptionhostまでの属性を持つことが出来るという事が分かります。objectClassの定義についてはこちらの記事が参考になります。

Schemaを理解する上で大切なのは「属性の定義もobjectClassの定義も同じSchemaファイルに含まれている」という事です。Schema、objectClass、属性の違いをしっかりと理解することがLDAPを理解する第一歩となります。図で表すと以下のようになります。

Schemaの概念図

Java Schemaとは

やっと本記事の主題に入ります。ここまでLDAPの基礎からSchemaとは何かについてざっくりと説明してきました。ここで、もしかしたらこんな疑問をお持ちの方がいるかもしれません。「で、結局何がLog4ShellのRCEに繋がるの?」と。答えは簡単。Javaのオブジェクト情報を入れられる属性があるんです。

先述の通り、LDAPには色々な情報を表すため、多種多様なSchemaが用意されています。この中には、Javaオブジェクトを表す属性やobjectClassを集めた「Java Schema」があります。これはRFC2713で定義されています。RFCのIntroductionには、次のような言及があります。

For applications written in the Java programming language, a kind of data that is typically shared are Java objects themselves. For such applications, it makes sense to be able to use the directory as a repository for Java objects. The directory provides a centrally administered, and possibly replicated, service for use by Java applications distributed across the network.

つまり、LDAPサーバをJavaオブジェクトのリポジトリとして使うためのSchemaということですね。このSchemaはOpenLDAPの場合、/etc/ldap/schema/java.schemaに保存されています。このSchemaには、7個の属性と5個のobjectClassが定義されています。これらを使って、実際にLDAPサーバにJavaオブジェクトのシリアライズデータを入れたエントリを作ってみます。結果から書くと、このエントリは以下のような内容になります。

dn: cn=javatest,dc=test
objectClass: javaContainer
objectClass: javaSerializedObject
cn: javatest
javaSerializedData:: VGhpcyBpcyBzYW1wbGUgZGF0YS5JdCBkb2VzIG5vdCBtZWFuLg==
javaClassName: com.company.SerializableClass
javaCodebase: http://localhost:8080/

まずは属性から見てみましょう。javaSerializedData属性は、RFCではこう定義されています。

This attribute stores the serialized form of a Java object. The serialized form is described in [Serial].
This attribute's syntax is 'Octet String'.

名前の通り、この属性はJavaオブジェクトのシリアライズデータを表します。ここで、他の属性と比較してjavaSerializedDataだけ:::になっていることに気づいたでしょうか。この::は、属性の値がbase64エンコードされたバイナリデータか、UTF-8のマルチバイト文字であるということを表しています。この場合は、シリアライズしたJavaオブジェクトのデータをbase64エンコードしたものを属性値としています。 次にjavaClassName属性です。これはシリアライズされたオブジェクトの元々のクラス名を表します。RFCに

This attribute stores the fully qualified name of the Java object's "distinguished" class or interface (for example, "java.lang.String").

とあるように、この属性値は完全修飾クラス名である必要があります。
最後にjavaCodebase属性です。この属性は、javaClassName属性で指定されたクラスファイルがどこにあるかを示しています。RFCには以下のように定義されています。

This attribute stores the Java class definition's locations. It specifies the locations from which to load the class definition for the class specified by the javaClassName attribute. Each value of the attribute contains an ordered list of URLs, separated by spaces. For example, a value of "url1 url2 url3" means that the three (possibly interdependent) URLs (url1, url2, and url3) form the codebase for loading in the Java class definition.
If the javaCodebase attribute contains more than one value, each value is an independent codebase. That is, there is no relationship between the URLs in one value and those in another; each value can be viewed as an alternate source for loading the Java class definition. See [Java] for information regarding class loading.

JavaオブジェクトのシリアライズデータをjavaSerializeData属性から読み込んだプログラムは、そのオブジェクトの元となるクラスファイル名をjavaClassName属性から取得し、そのクラスファイルの実体をjavaCodebase属性のURLから取得します。ここで注意してほしいのは、クラスファイルの実体を置く場所は完全修飾クラス名に沿わなければならないという事です。上記の例だと完全修飾クラス名はcom.company.SerializableClassjavaCodebase属性はhttp://localhost:8080/なので、実体となるクラスファイルはhttp://localhost:8080/com/company/SerializableClass.classで参照できる必要があります。

また、objectClassも見てみましょう。ここで設定されているobjectClassはjavaContainerjavaSerializedObjectの2つです。まずjavaContainerですが、これはJavaオブジェクトを表すエントリであることを示す、STRUCTURAL objectClassです。cn属性がMUSTとなっているだけのごく簡素なobjectClassです。次にjavaSerializedObjectですが、こちらも名前の通りJavaのシリアライズされたオブジェクトを表すobjectClassです。Schemaファイルでは以下のように定義されています。

objectclass ( 1.3.6.1.4.1.42.2.27.4.2.5
        NAME 'javaSerializedObject'
        DESC 'Java serialized object'
        SUP javaObject
        AUXILIARY
        MUST javaSerializedData )

さて、ここでobjectClassの基礎を思い出してみましょう。objectClassのMUSTにもMAYにも入っていない属性はエントリに設定できないのでした。しかし、javaContainerjavaSerializedObjectを合わせてもMUSTにcnjavaSerializedDataが設定されているだけです。javaClassNamejavaCodebase属性は何故エントリに設定できるのでしょうか?答えは、javaSerializedObjectの継承元となっているjavaObjectです。このobjectClassの定義は以下のようになっています。

objectclass ( 1.3.6.1.4.1.42.2.27.4.2.4
        NAME 'javaObject'
        DESC 'Java object representation'
        SUP top
        ABSTRACT
        MUST javaClassName
        MAY ( javaClassNames $ javaCodebase $
                javaDoc $ description ) )

ここではMUSTとしてjavaClassName属性が、MAYとして4つの属性が指定されています。つまり、javaSerializedObjectはこれを継承しているため、実質次のような形になります。

javaSerializedObject
必須属性:javaSerializedData, javaClassName
任意属性:javaClassNames, javaCodebase, javaDoc, description

まとめ

以上、非常に複雑で長い記事となってしまいましたが、Log4Shellのために要求されるLDAPの知識をざっくりと説明させていただきました。

Java Schemaに定義されている属性を使ってJavaのオブジェクトを表すエントリを作成、そのエントリを置いたLDAPサーバに攻撃対象からアクセスさせることでオブジェクトのデータをダウンロードさせ、RCEに繋げるのがLog4Shellの流れになります。

次回はこの脆弱性の発生機序について、Log4j側から見た流れを説明します。

https://techblog.securesky-tech.com/entry/2022/04/21/what-happened-log4shelltechblog.securesky-tech.com

参考文献