Spring Data Commonsにおける任意コード実行の脆弱性(CVE-2018-1273)
藤村 匡弘
サイバーセキュリティエンジニアリング部の藤村です。 4月にリクルートテクノロジーズに入社し、脆弱性検査を業務で担当しています。セキュリティ業務は未経験で入社しましたが、今回は自己紹介と検査学習を兼ねて先日発生した脆弱性について記事を書きました。
先日、Spring Data Commonsにおけるリモートで任意のコードが実行される脆弱性(CVE-2018-1273)が公表されました。この記事では、Spring Data Commonsにおいて実際にリモートでコードが実行されるまでのプロセスをコードベースで解説します。
要約
- Spring Frameworkで採用されているSpring Data Commonsで任意コード実行の脆弱性(CVE-2018-1273)が公表されました。
- Spring FrameworkはGitHubのframeworkタグが付いているProjectの中でもTop10に入るほどの人気のWeb Application Frameworkです。
- Spring Frameworkには、全てのSpring製品で共通の記述をすることができる、Spring Expression Language(略してSpEL)という仕組みがあります。
- Javaにはリフレクションと呼ばれる、プログラムの実行中に文字列などから動的に処理を行うことができる機能があります。SpELを評価する際に内部的にリフレクションの仕組みが利用されます。
- Spring Data Commonsでは、特定のリクエストをSpELとして評価させることが可能であり、SpELからリフレクションを悪用することで内部的に文字列をJavaクラスに変換し実行することで、任意コード実行の脆弱性となりました。
- 初期の対策では、特定のリクエストからのSpELではリフレクションが出来ないように修正されましたが、SpELが実行できることは変わらず、SpELで記述することができる正規表現によって別の脆弱性(ReDoS)が生まれました。
脆弱性(CVE-2018-1273)とは
2018年4月10日 (現地時間) 、Pivotal Software は、Spring Data Commons に関する複数の脆弱性情報を公開しました。公開された情報によると、Spring Data Commons には複数の脆弱性があり、脆弱性を悪用されると、実行しているアプリケーションサーバの実行権限で、リモートから任意の OS コマンドが実行されるなどの可能性があります。詳細は、Pivotal Software からの情報を参照してください。
引用: Spring Data Commons の脆弱性に関する注意喚起
Spring Data Commonsにおけるリモートで任意のコードが実行される脆弱性はSpring Frameworkによって作られたWebアプリケーションに対して特殊な細工をしたパラメータを送るとサーバ上で任意のコマンドを実行することができるというものです。
リモートから任意のOSコマンドが実行できる脆弱性は一般的にRCE(Remote Code Execution)と呼称され、非常に危険度が高いものになります。過去に、Apache Strutsなどでも同様の脆弱性が発生しニュースなどにも取り上げられておりました。
この脆弱性についての詳細はPivotalから報告されており、影響を受けるとされているバージョンは以下の様になっております。
・Spring Data Commons 1.13 to 1.13.10 (Ingalls SR10)
・Spring Data REST 2.6 to 2.6.10 (Ingalls SR10)
・Spring Data Commons 2.0 to 2.0.5 (Kay SR5)
・Spring Data REST 3.0 to 3.0.5 (Kay SR5)
Spring Data Commonsとは
Spring Data Commonsは、Spring Dataのコア部分を担っており、Spring DataはMVCフレームワークのモデル(M)に相当するライブラリです。このライブラリはSpring Frameworkを用いてRESTAPIなどを開発するアプリケーションで広く利用されているライブラリです。
検証
プロジェクトの構成
実際に今回発生したSpring Data CommonsのPoC(攻撃が可能なことを実証したコード。Proof of Conceptと呼ばれる)を動作させるために、実際にSpring Frameworkを導入し、環境を構築します。
今回、利用するライブラリはspring-boot:1.5.12とspring-data-commons:1.13.10を利用しております。
以下のようなPoCを動作させるために必要なものを構築し、検証を進めていきます。
ディレクトリ構成
└── src
└── main
└── java
└── com
└── example
├── VulnApplication.java
└── VulnerableController.java
1 2 3 4 5 6 |
@SpringBootApplication public class VulnApplication { public static void main(String[] args) { SpringApplication.run(VulnApplication.class, args); } } |
1 2 3 4 5 6 7 8 9 10 |
@RestController public class VulnController { @PostMapping(path = "/account") public void doSomething(Account account) { System.out.println(account.getName()); } interface Account { String getName(); } } |
Springは http://localhost/account?name=masahiro の様なAccountモデルのリクエストパラメータを受け取るAPIを作る際には、Controllerクラスでpathの宣言と、内部に取得したいパラメータのgetterをinterface型やClass型で宣言する必要があります。後述しますが、この脆弱性はClass型で宣言した場合には動作せず、Interface型で宣言した場合に動作することが確認できています。
脆弱性の分析
0. PoCを動かして見る
初めにこのアプリケーションの正常な動作から確認をしてきます。このWebアプリケーションはhttp://example.com/account?name=john_doe といったリクエストを受け取った際に、nameのパラメーターであるjohn_doeの文字列が標準出力されます。
図1正常な動作
それでは実際にPoCをlocalhostで動作させてみましょう。アプリの内部の挙動を把握できるようにするために、Javaのデバッグができる環境としてIntelliJ IDEAを利用しています。
1 |
curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('touch /tmp/poof')]=test" |
これを動作させると図2のような結果になります。 図2では、terminalでPoCを実行し、背面にアプリケーションのログを表示しています。実際にlsコマンドなどで確認すると/tmp/poofが作成されていることがわかります。アプリケーションとしてはExceptionで終了して500 Internal Server Errorを返してしまっていますが、PoCの実行完了には影響しません。
図2 PoCの動作画面
先ほどのリクエストは内部的にRuntimeクラスのインスタンスを呼び出してRuntimeからexec関数を実行しています。 次はどのようにしてリクエストパラメータがロードされてRCEに繋がるか確認していきましょう。
脆弱性を検証する際にPoCの動作箇所を特定することができればブレークポイントなどを利用したトレースが行えます。今回実行したPoCでは、Runtimeクラスを実行しているためブレークポイントを設定しにくくなっています。そこで、ブレークポイントを設定したテストクラスを作成し、それを呼び出すようにPoCを変更したいと思います。
1 2 3 4 5 |
public class RCETest { public static void test() { System.out.println("-------------------- VULN LOG ----------------------");// ● ブレークポイント } } |
リクエストするパラメータはRuntimeのクラスを呼び出すのではなくRCETestクラスを呼び出してtest()メソッドを実行します。
1 |
curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('com.example.RCETest').test()]=test" |
それでは実際に動作させてスタックトレースを見ましょう。
図3 スタックトレース
このスタックトレースをいくつかのレイヤーに分けて見ていきます。図4では、以下のように3つのパートに分けて例示しています。
- SpELがクラスへ変換し実行されるまで
2. リクエストがSpELとして評価されるまで
3. 脆弱性が動作するハンドラーが選択されるまで
図4
解説は実際にRCEが動作する部分から検証を行い、その後どの様にして危険な文字列が入り込んだのかを検証していきたいと思います。
1. SpELがクラスへ変換し実行されるまで
初めにリクエストパラメータとして渡された文字列がSpELとして評価された後に、どの様にしてクラスとして評価されるのかを確認していきます。
SpelExpressionクラス
まずは実際に「#this.getClass().forName(‘com.example.RCETest’).test()」という文字列がspring内で動作する部分から見ていきましょう。図4に1で示す赤枠で囲まれた部分になります。
ソースコードを見る前にPoCからどの様に動いているか予測します。まず、先頭についている「#this」これは標準のjavaでは動作せずSpringの中で利用されているSpEL(Spring Expression Language)と呼ばれるもので動作します。
7.5.10 Variables/ The #this and #root variables
PoCからこの攻撃はSpELを経由して動作していることがわかりますので、はじめにSpELのパッケージを利用しているところから脆弱性を追いかけていきます。初めは図4の赤枠1のスタックトレースについて下から見ていきましょう。
下図は
1 |
setValue:445, SpelExpression (org.springframework.expression.spel.standard) |
のスタックトレースです。
図5 SpelExpressionのスタックトレース
図5の右下のVariablesを見るとSpelExpressionのexpressionメンバ変数(String型)の中にはname[#this.getClass().forName(‘com.example.RCETest’).test()]がして代入されており、ソースコードのsetValueメソッドの第二引数のvaluesにはtestが代入されていることがわかります。
これはVulnControllerが「/account」へのリクエストパラメータとしてname[#this.getClass().forName(‘com.example.RCETest’).test()]=testをKey/Value型の文字列として評価し、SpELに入力しているからです。 まだこの段階ではexpressionが文字列型なので、#this.getClass().forName(‘com.example.RCETest’).test()はプログラムとして認識されていません。
このメソッドの最後で、expression変数を別のクラスで作り直しthis.ast.setValueを用いてast(CompoundExpressionクラス)に代入しています。
CompoundExpressionクラス
次に先ほどCompoundExpressionクラスに代入された#this.getClass().forName(‘com.example.RCETest’).test()文字列を追いかけていきましょう。
図6 CompoundExpressionクラス
図6はCompoundExpressionクラス内の処理です。setValueメソッドの内部処理として、図6上部のgetValueRefメソッドが呼ばれており、その中で#this.getClass().forName(‘com.example.RCETest’).test()文字列が評価されていき 図6 右下のVariablesにある通り、valueの中に#this.getClass().forName(‘com.example.RCETest’)で呼び出されたcom.example.RCETestクラスが格納されていることがわかります。実際にcom.example.RCETestクラスを生成しているのは51行目のgetValueInternalメソッドです。このメソッドは並行してクラスだけではなく.test()の部分も評価しています。
ReflectiveMethodExecutorクラス
いかにも動的にメソッドを実行しそうなクラス名なReflectiveMethodExecutorクラスがSpring Frameworkとして最後に動作しています。
図7 ReflectiveMethodExecutorクラス
図7 右下のvariablesのthis.methodを見るとRCETestクラスのtestメソッドがロードされていることがわかります。
以上が、PoCの文字列をSpELに入力してから実行されるまでの流れです。次はSpELにPoCのリクエストが流れるまでを確認していきます。
2. リクエストがSpELとして評価されるまで
次に図4に2で示したピンク枠のSpELにリクエストが入る処理を確認していきます。 DataBinderはリクエストを受け取るHandlerからデータを処理するクラスまでのデータの転送を行います。実際にデータを渡している処理はMapDataBinderのsetPropertyValue処理になります。
MapDataBinderクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@Override public void setPropertyValue(String propertyName, Object value) throws BeansException { if (!isWritableProperty(propertyName)) { throw new NotWritablePropertyException(type, propertyName); } StandardEvaluationContext context = new StandardEvaluationContext(); context.addPropertyAccessor(new PropertyTraversingMapAccessor(type, conversionService)); context.setTypeConverter(new StandardTypeConverter(conversionService)); context.setRootObject(map); Expression expression = PARSER.parseExpression(propertyName); PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty(); TypeInformation<?> owningType = leafProperty.getOwningType(); TypeInformation<?> propertyType = owningType.getProperty(leafProperty.getSegment()); propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType; if (conversionRequired(value, propertyType.getType())) { PropertyDescriptor descriptor = BeanUtils .getPropertyDescriptor(owningType.getType(), leafProperty.getSegment()); MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1); TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0); value = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor); } expression.setValue(context, value); } |
13行目のExpression expression = PARSER.parseExpression(propertyName);が評価され、 expressionの中には#this.getClass().forName(‘com.example.RCETest’).test()という文字列が入ります。
最後にexpression.setValueクラスが評価され、expressionにcontextとtestメソッドの文字列が格納され、SpELへと変換されていきます。
次に実際にこの脆弱性が発現する入り口について確認して行きましょう。
3. 脆弱性が動作するハンドラーが選択されるまで
HandlerMethodArgumentResolverCompositeクラス
ここまでの説明で、SpEL式が評価される処理に入った場合、実際に指定したJavaコードが実行されるところを確認しました。最後にこのSpEL式を評価する処理にどういう条件で入ってしまうのかを解説します。
Spring Frameworkでは、ユーザからのリクエストを処理する際に、対象となる処理に適した処理(Handler)を選択する処理が存在しています。その内部処理を図4の2で示した薄紫枠の処理を追いながら確認していきます。
はじめにどのHandlerが選択されるのかを確認します。どのHandlerを利用してリクエストを受け付けるかはHandlerMethodArgumentResolverCompositeクラスのHandlerMethodArgumentResolverメソッドで解決されます。
図8 HandlerMethodArgumentResolverCompositeクラス
Handlerは 図8 右下variablesのthis.argumentResolversに格納されている30個のHandler(バージョンによって数や順序が異なります)の中から、135行目のif (methodArgumentResolver.supportsParameter(parameter))の条件式が真の時に解決されるHandlerが選択され利用されます。
どの様な時にこの条件式が真になり、脆弱性がある処理に移るのかを確認します。下記のコードは条件式で評価されているsupportsParameterメソッドのコードです。
MethodArgumentResolverクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@Override public boolean supportsParameter(MethodParameter parameter) { if (!super.supportsParameter(parameter)) { return false; } Class<?> type = parameter.getParameterType(); if (!type.isInterface()) { return false; } // Annotated parameter if (parameter.getParameterAnnotation(ProjectedPayload.class) != null) { return true; } // Annotated type if (AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null) { return true; } // Fallback for only user defined interfaces for (String prefix : IGNORED_PACKAGES) { if (ClassUtils.getPackageName(type).startsWith(prefix)) { return false; } } return true; } |
supportsParameterメソッドの中で、今回の脆弱性を検証する際に重要となるのは10行目if (!type.isInterface())の制御文となります。処理しようとしているパラメータを格納するクラスの型がInterfaceではない場合、falseが返されこのHandlerは利用されません。Interfaceである場合、あらかじめ定義されたホワイトリストのIGNORED_PACKAGESに一致しなければtrueが返り、Handlerが選択されることになります。
これにより、VulnControllerクラスのパラメータを受けとる定義をする処理がInterface型であればこの脆弱性が発現することがわかります。実際にこの脆弱性を検証しているサンプルコードも以下のようにInterfaceで定義されています。
1 2 3 |
interface Account { String getName(); } |
脆弱性への対応
脆弱性が公開されてから修正されるまでの流れについて追いかけていきます。
CVE-2018-1273は2018/4/10に脆弱性レポートが公開され、2018/4/17に修正版がリリースされました。次に修正されたパッチの内容について検証を行います。
パッチの検証
今回の脆弱性に対してパッチが当てられたのは、Spring Data CommonsのMapDataBinderクラスです。 修正の大きな違いは、SpELを評価するContextがStandardEvaluationContextクラスからSimpleEvaluationContextクラスに変換されたことです。
- StandardEvaluationContextクラスはSpELのリフレクションを許可し、全てのオブジェクトの参照を可能にし、全てのメソッドに対して実行権限があります。
- SimpleEvaluationContextクラスはSpELに入力されたプロパティ名に対して、読み込みと明示的に指定された値に対する書き込みしか許可せずリフレクションは行えません。
このContextの変更により、「2. リクエストがSpELとして評価されるまで」で示した処理が変更され、RCEが実行されていたリフレクションベースの構文が評価されないため、攻撃が成立しないようになりました。
パッチに存在した脆弱性
最後に修正パッチに存在した脆弱性について軽く触れておこうかと思います。
前述したパッチにより、完全に脆弱性が直ったように思えますが、まだ脆弱性が残されていました。
修正により、外部からの入力によるSpELを用いたリフレクションができなくなりましたが、修正後でも外部からの入力をSpELとして評価をすることが可能です。これにより修正されたSpring Data Commonsで新たな脆弱性 CVE-2018-1257が発生しています。ReDoSと呼ばれる正規表現を悪用した、DoS攻撃になります。
CVE-2018-1257のPoC
1 |
curl -X POST http://localhost:8080/account -d "name['aaaaaaaaaaaaaaaaaaaaaaaa!'%20matches%20'%5E(a%2B)%2B%24']=test" |
この脆弱性の原因は、SpELの機能として正規表現を評価することができ、変更されたSimpleEvaluationContextクラスでも同様に正規表現が利用できた為です。
この脆弱性を直すために任意の閾値を超えてパターンマッチ処理を行う正規表現を停止する処理が加えられています。 commit log
この脆弱性は弊社リクルートテクノロジーズ の西村(にしむねあ)が発見・報告しました。
現在は、ReDoSの脆弱性も含め修正が行われたバージョンがリリースされているので、まだパッチを適用していない場合、再度パッチを適用することをお勧めします。
以上がSpring Data CommonsにおけるRCEの脆弱性解説になります。
ここまで読んでくださり、ありがとうございます。
最後に
今回の脆弱性解説記事はセキュリティエンジニアとして働く第一歩として非常に良い機会になりました。今後は社内の強いエンジニアの方々に学び、セキュリティエンジニアとして貢献していければと思います。