はじめに
こんにちは。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についてもっと知りたい方は、以下の記事を参考にしてみてください。
- OpenLDAPの設計:OpenLDAPで始めるディレクトリサーバ構築(1)(1/3 ページ) - @IT
- 3.1.1 オブジェクトクラス定義
- オブジェクトクラスのリファレンス (Sun Directory Services 3.1 管理ガイド)
Schemaとは
さて、ここまで属性とobjectClassの話を書いてきましたが、これらはどこで定義されていて、どうやって確認すればいいのでしょうか?その疑問を解決するのがSchema
です。Schemaは属性やobjectClass自体の定義を記したものです。LDAPサーバなどをインストールするとファイルとして一緒にダウンロードされます。OpenLDAPの場合、/etc/ldap/schema
の中に様々なschemaファイルがあります(環境によって異なる場合があります)。
例を挙げて見てみましょう。OpenLDAPの/etc/ldap/schema/core.schema
のcn
属性の定義を見てみます。
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.schema
のaccount
objectClassの定義を見てみます。
objectclass ( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ host ) )
まずは先頭のobjectclass
ですが、これは文字通りobjectClassの定義であることを表しています。次の数字は先ほどと同じくOID
。NAME
はこの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を持つという事になります。上記のaccount
objectClassの場合、単体でエントリを作成することが出来るという事が分かります。次のMUST
・MAY
は、前章のobjectClassの説明で書いた必須属性と任意属性です。account
objectClassを持つエントリはuserid
属性が必須であり、任意でdescription
~host
までの属性を持つことが出来るという事が分かります。objectClassの定義についてはこちらの記事が参考になります。
Schemaを理解する上で大切なのは「属性の定義もobjectClassの定義も同じSchemaファイルに含まれている」という事です。Schema、objectClass、属性の違いをしっかりと理解することがLDAPを理解する第一歩となります。図で表すと以下のようになります。
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.SerializableClass
、javaCodebase
属性はhttp://localhost:8080/
なので、実体となるクラスファイルはhttp://localhost:8080/com/company/SerializableClass.class
で参照できる必要があります。
また、objectClassも見てみましょう。ここで設定されているobjectClassはjavaContainer
とjavaSerializedObject
の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にも入っていない属性はエントリに設定できないのでした。しかし、javaContainer
とjavaSerializedObject
を合わせてもMUSTにcn
とjavaSerializedData
が設定されているだけです。javaClassName
とjavaCodebase
属性は何故エントリに設定できるのでしょうか?答えは、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
参考文献
- 5.4.2.3 オブジェクトクラスの定義
- Java オブジェクトを保管するための LDAP サーバー構成 - IBM Documentation
- RFC 2713 - Schema for Representing Java(tm) Objects in an LDAP Directory
- More on the LDIF Format
- RFC 2849 LDIF input - IBM Documentation
- OpenLDAPの設計:OpenLDAPで始めるディレクトリサーバ構築(1)(1/3 ページ) - @IT
- 3.1.1 オブジェクトクラス定義
- オブジェクトクラスのリファレンス (Sun Directory Services 3.1 管理ガイド)
- スキーマってなんだろう 2007/10/06 OSC2007Tokyo/Fall
- 【LDAP基礎用語】DCとは?OUとは?バインドDN,ベースDN,サフィックスとは?匿名バインドとは?ldapsearchのオプション | SEの道標
- us-16-MunozMirosh-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE