2026年10月20日、CSS2026(コンピュータセキュリティシンポジウム2026)のデモンストレーションセッションで、Java アプリが脆弱なライブラリの関数を本当に呼んでいるかを調べる研究を、実際に解析を流して撮った録画とポスターの紙面で紹介しました。解析は、起点に選んだライブラリの関数から呼び出し元を逆にたどり、アプリまで続く呼び出しの道があれば、その道を画面に出します。実際には呼ばれているのに道を見つけられない書き方がまだ残っているので、道が見つからなくても「影響なし」とは言い切りません。
ここで言う「影響あり」は、コードから推定した呼び出しの道の候補が見つかった、という意味です。その道が実際に実行されることや、攻撃者が悪用できることを証明したものではありません。脆弱なバージョンが入っているという警告を消すものではなく、どれから直すかを考えるための材料を 1 つ足すものです。
| 項目 | 内容 |
|---|---|
| イベント | CSS2026 デモンストレーションセッション(アクトシティ浜松) |
| 日付 | 2026年10月20日(火) |
| 番号 | DP-40(公式ページの表記。ポスターの表記は DEMO-40) |
| 題目 | 「その脆弱性、本当に呼ばれているか? — Java 逆向き到達可能性解析の実機デモ」 |
| 発表者 | 神戸康多(フューチャー株式会社) |
依存ライブラリの脆弱性を調べる道具の多くは、脆弱なバージョンが入っているだけで警告を出します。警告の数は多く、どれから直すかを決めるのが大変です。
デモでは、Text4Shell(CVE-2022-42889)を例にしました。Apache Commons Text の 1.5〜1.9 では、文字列の中に script: で始まる置き換えの指示(ドル記号と波かっこで囲む書き方)があると、それを展開する機能が既定で有効になっていました。攻撃者が操作できる文字列を、検証せずにこの置換の機能に渡すと、コードを実行されるおそれがありました(Apache Commons Text のセキュリティ情報)。この commons-text 1.9 を入れた小さな Java アプリを 2 つ用意し、アプリのコードを 1 行だけ変えました。
StringSubstitutor.createInterpolator().replace(入力) を呼ぶアプリWordUtils.capitalize(入力) だけを呼ぶアプリバージョンだけを見る道具は、例 1 にも例 2 にも同じ警告を出します。けれども、展開の機能を使っているのは例 1 だけです。
ここで言う「到達可能性」は、インターネットから届くかどうかではありません。アプリのコードから起点の関数まで、呼び出しがつながっているかどうかです。
調べる順番は次のとおりです。
StringLookupFactory.addDefaultStringLookups で、危険な展開の機能を既定で使える状態にしていた関数です。攻撃のときに実際に危険な処理をするのは別の関数(ScriptStringLookup.lookup)で、起点の関数とは一致しません。そのため、起点の関数への道が見つかっても、攻撃が成り立つことまでは確かめていません。逆から始める理由は 2 つあります。1 つは、起点の関数が入ったライブラリが手元にあれば、アプリの処理が始まるところが分からなくても調べ始められることです。もう 1 つは、アプリの処理が始まるところから順にたどると、つながらない部分が出ることです。たとえば Web の要求を受ける処理は、アプリのコードではなくフレームワークが呼ぶので、main から順にたどってもそこへ着かないことがあります。
一方で、Java の標準ライブラリ(JDK)のメソッドの中身は、速さのために読みません。この割り切りが、後で述べる見落としの原因の 1 つになっています。
デモでは、Text4Shell の起点として addDefaultStringLookups のほかに、利用者が直接呼ぶ StringSubstitutor.replace も一緒に渡しました。そのため例 1 の画面には道が 2 本出ます。
App.main → replaceApp.main → createInterpolator → …(クラスの初期化処理など)… → addDefaultStringLookups図に描いたのは [2] だけです。途中の「クラスの初期化処理」は、そのクラスを最初に使うときに 1 回だけ走る処理のことです。例 2 では、さかのぼってもアプリから呼んでいる場所に着かないので、道が見つかりません。画面の判定の行は次のとおりです(行頭の「例 1」「例 2」は説明用に付けた印で、画面には出ません)。
例 1 == CVE-2022-42889 == AFFECTED
例 2 == CVE-2022-42889 == NOT AFFECTED (best-effort)
例 2 の「best-effort」は、言い切らない判定であることを示す印です。この判定を機械が読める形(VEX という、製品が脆弱性の影響を受けるかを書く形式)で出すときも、「影響なし」ではなく「調査中」を出しています。
例 1・例 2 で「呼ぶか呼ばないか」で結果が分かれることを見せた後、呼び先がコードの外で決まる 2 つの場合を見せました。
Java には、クラスやメソッドを文字列の名前で選んで呼ぶ書き方(reflection)があります。例 3 と例 4 は、どちらも reflection で createInterpolator を呼ぶアプリで、違いはクラスの名前の取り方の 1 行だけです。
例 3 では、Class.forName に渡すクラス名("org.apache.commons.text.StringSubstitutor")と、getMethod に渡すメソッド名("createInterpolator")の両方が、コードに文字列で書いてあります。この形なら、解析は、名前で選んだメソッドを実際に呼ぶ invoke の呼び先を createInterpolator と決められるので、道をつなげて「影響あり」にします。例 4 のようにクラス名を設定から読むと、どのクラスが呼ばれるかは実行するまで決まらないので、道はそこで切れ、判定は「影響なし(best-effort)」になります。
どちらのアプリにも、次の 2 行の警告が出ます(関数名の表記を短くしています。画面では先頭に java|demo| が付き、引数の型も出ます)。
WARN: reflection-call: demo.App.main -> java.lang.Class.forName
WARN: reflection-call: demo.App.main -> java.lang.reflect.Method.invoke
この警告は「ここで道が切れた」という印ではなく、「ここで reflection を使っている」という印です。道がつながったかどうかは、判定と道の表示で見ます。
また、名前が書いてあればいつでもつながるわけではありません。例 3 では、続けて sub.getClass().getMethod("replace", …) のように、実行時のオブジェクトからクラスを取って replace を呼んでいます。この形はまだつなぎません。例 1 では 2 本出た道が、例 3 では createInterpolator を通る 1 本だけになるのはこのためです。
例 5 は、log4j(Java で広く使われるログ出力のライブラリ)の 2.14.1 を入れ、LogManager.getLogger(App.class).error(入力) を呼ぶアプリです(Log4Shell、CVE-2021-44228)。解析には、Log4Shell を直したコードの変更記録で書き換えられた関数 2 つを起点として渡しました。外部のサーバーからオブジェクトを取り寄せる処理(JndiManager.lookup)と、ログの文字列を組み立てる処理(MessagePatternConverter.format)です。
判定は「影響あり」で、このアプリについては正しい結果です。ただ、画面に出た道は 2 本とも JndiContextSelector という部品を通っていました。log4j は、ロガーの土台を用意する部品(ContextSelector)を設定で差し替えられる作りになっていて、log4j-core 2.14.1 にはその実装が 5 つあります。このアプリで動くのは既定の ClassLoaderContextSelector だけです。けれども解析は設定を読まないので、5 つ全部を呼び先の候補にし、特別な設定をしたときだけ使われる JndiContextSelector を通る道を出しました。
実際の攻撃で使われるのは、ログに書く入力の中の、jndi: で始まる置き換えの指示(外部のサーバーからオブジェクトを取り寄せる指示)を JndiLookup が展開する道です。この道は解析に出ませんでした。理由はまだ確かめていません。
同じ道は、LogManager.getLogger() を呼ぶだけで何もログを出さないアプリにも出て、「影響あり」になりました。こちらは、実際には影響が無いのに「影響あり」と出た例です。画面の道は「コードから推定した候補」であり、実行されることや悪用できることを示すものではありません。
デモの解析は 1 回 11〜26 秒でした。commons-text と log4j を使う小さなアプリ 5 個を、出力の形や解析の設定を変えて合計 31 回解析した幅です(2026年10月15日、Docker の中、必要なライブラリは手元に取得済みの状態)。大きな実アプリでの時間ではありません。
呼び出し方を 24 通りに分け、それぞれに書き方の違いを用意して、小さな Java プログラム(Java 21)を合計 86 件作りました。どれも、実行すると起点の関数に見立てた試験用の関数が確かに動くことを、2026年10月16日に確かめています。この 86 件で、解析が道を見つけられるかを数えました。
道が見つかったのは 75 件で、見つからない 11 件のうち 7 件は、アプリのコードからではなく、JDK の側から呼ばれる形でした。残りの 4 件は、reflection と、親クラスから受け継いだメソッドの形です。
この数え方には 3 つの条件があります。道の検出は 10月5日の版で数えました。例 3 の形をつなぐようにした 10月15日の改良の後には数え直していないので、その後に見つかるようになった件があるかは分かりません。また、試験用に作った呼び出し方の中での内訳で、実アプリでどれだけ見落とすかを表す数字ではありません。
JDK の側から呼ばれる形とは、Java の一般的な仕組みで言えば、たとえば次のような場面です。並べ替えの比較方法を書いた部品を Collections.sort(list, 比較の部品) に渡すと、比較の処理はアプリのコードからではなく、JDK の並べ替えの中から呼ばれます。この解析は速さのために JDK のメソッドの中身を読まないので、このような形では、比較の処理の呼び出し元をさかのぼると JDK の中で止まり、アプリまで着かないことがあります。この形の直し方の案はありますが、まだ入れていません。
ここまでの見落としには、警告が出ないものがあります。JDK の側から呼ばれる形では、「JDK の中身は読んでいない」という同じ 1 行が毎回出るだけで、どこで道が切れたかは画面から分かりません。reflection にも、警告の対象に入っていない書き方があります。たとえば、呼ぶ処理をあとから差し込める動的 Proxy や、メソッドを値として持ち回って呼ぶ MethodHandle を使った呼び出しには、警告が出ません。
そのため、Java の「影響なし」はすべて best-effort(言い切らない判定)にとどめ、機械が読む形の結果では「調査中」にしています。
研究の途中経過として、いま言えることをまとめます。
Go アプリについては、同じ考え方の機能を FutureVuls で提供しています。2026年10月14日に、Go アプリで見つかった脆弱性について、アプリのコードから脆弱な関数まで呼び出しの道があるかをタスク一覧とタスク詳細に出す機能を、ベータ版として公開しました(リリースノート、到達可能性解析(ベータ版)のマニュアル)。CSIRT プランのオーガニゼーションで使えます。この記事で紹介した Java の解析は、まだ研究の段階です。
JDK の側から呼ばれる形には、まだ手を入れていません。実アプリでどれだけ判定が変わるかも、まだ測れていません。研究室訪問や共同研究のご相談を歓迎します。