<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=2109257909350072&amp;ev=PageView&amp;noscript= 1">

Event

CSS2026でデモ発表しました――その脆弱性、本当に呼ばれているか?

目次

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 行だけ変えました。

  • 例 1:StringSubstitutor.createInterpolator().replace(入力) を呼ぶアプリ
  • 例 2:WordUtils.capitalize(入力) だけを呼ぶアプリ

バージョンだけを見る道具は、例 1 にも例 2 にも同じ警告を出します。けれども、展開の機能を使っているのは例 1 だけです。

ここで言う「到達可能性」は、インターネットから届くかどうかではありません。アプリのコードから起点の関数まで、呼び出しがつながっているかどうかです。

どう調べるか:起点の関数から逆にたどる

調べる順番は次のとおりです。

  1. CVE(公表された脆弱性の番号)ごとに、解析の起点にする関数を選びます。脆弱性を直したコードの変更記録を見て、そこで書き換えられた関数を選びました。Text4Shell では StringLookupFactory.addDefaultStringLookups で、危険な展開の機能を既定で使える状態にしていた関数です。攻撃のときに実際に危険な処理をするのは別の関数(ScriptStringLookup.lookup)で、起点の関数とは一致しません。そのため、起点の関数への道が見つかっても、攻撃が成り立つことまでは確かめていません。
  2. その関数を出発点に、「この関数を呼んでいるのは誰か」をくり返し調べます。
  3. アプリ自身のコードに着いたら「影響あり」とし、そこまでの道を画面に出します。

逆から始める理由は 2 つあります。1 つは、起点の関数が入ったライブラリが手元にあれば、アプリの処理が始まるところが分からなくても調べ始められることです。もう 1 つは、アプリの処理が始まるところから順にたどると、つながらない部分が出ることです。たとえば Web の要求を受ける処理は、アプリのコードではなくフレームワークが呼ぶので、main から順にたどってもそこへ着かないことがあります。

一方で、Java の標準ライブラリ(JDK)のメソッドの中身は、速さのために読みません。この割り切りが、後で述べる見落としの原因の 1 つになっています。

同じ commons-text 1.9 を入れた 2 つのアプリで、起点の関数から呼び出し元をさかのぼると、例 1 は App.main に着いて道が出て、例 2 はアプリに着かず道が出ない

デモでは、Text4Shell の起点として addDefaultStringLookups のほかに、利用者が直接呼ぶ StringSubstitutor.replace も一緒に渡しました。そのため例 1 の画面には道が 2 本出ます。

  • [1] App.main → replace
  • [2] App.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 つの場合を見せました。

例 3 と例 4:クラスの名前を、コードに書くか設定から読むか

Java には、クラスやメソッドを文字列の名前で選んで呼ぶ書き方(reflection)があります。例 3 と例 4 は、どちらも reflection で createInterpolator を呼ぶアプリで、違いはクラスの名前の取り方の 1 行だけです。

例 3 と例 4 の違いはクラス名を取る 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 Log4Shell:判定は正しいが、出た道は攻撃の道ではない

例 5 は、log4j(Java で広く使われるログ出力のライブラリ)の 2.14.1 を入れ、LogManager.getLogger(App.class).error(入力) を呼ぶアプリです(Log4Shell、CVE-2021-44228)。解析には、Log4Shell を直したコードの変更記録で書き換えられた関数 2 つを起点として渡しました。外部のサーバーからオブジェクトを取り寄せる処理(JndiManager.lookup)と、ログの文字列を組み立てる処理(MessagePatternConverter.format)です。

Log4Shell のデモで、解析が出した道は 2 本とも、このアプリでは使われない JndiContextSelector を通り、実際の攻撃の道は出なかった

判定は「影響あり」で、このアプリについては正しい結果です。ただ、画面に出た道は 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 件で、解析が道を見つけられるかを数えました。

試験用の関数が動くことを確かめた 86 件の解析結果。道が見つかったのは 75 件。見つからない 11 件のうち 7 件は、JDK の側から呼ばれる形だった

道が見つかったのは 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(言い切らない判定)にとどめ、機械が読む形の結果では「調査中」にしています。

この結果をどう使えるか

研究の途中経過として、いま言えることをまとめます。

  • 「影響あり」のときは、アプリから起点の関数までの道が出ます。どの関数を通るかが見えるので、本当にその道を通るかを、途中から自分で確かめ始められます。ただし例 5 のように、出た道が実際の攻撃の道とは限りません。また、起点の関数に着くことと、攻撃が成り立つことは別です。
  • 「影響なし」のときは、指定した関数への道が見つからなかったという結果です。見落とす形が残っているので、それだけで対応の優先度を下げることはお勧めしません。
  • reflection の警告が出た場所は、呼び先を自分で確かめる出発点になります。ただし、警告が出ない形もあります。

おわりに

Go アプリについては、同じ考え方の機能を FutureVuls で提供しています。2026年10月14日に、Go アプリで見つかった脆弱性について、アプリのコードから脆弱な関数まで呼び出しの道があるかをタスク一覧とタスク詳細に出す機能を、ベータ版として公開しました(リリースノート、到達可能性解析(ベータ版)のマニュアル)。CSIRT プランのオーガニゼーションで使えます。この記事で紹介した Java の解析は、まだ研究の段階です。

JDK の側から呼ばれる形には、まだ手を入れていません。実アプリでどれだけ判定が変わるかも、まだ測れていません。研究室訪問や共同研究のご相談を歓迎します。

執筆者プロフィール

神戸康多

フューチャー株式会社

Cyber Security Innovation Group シニアアーキテクト

神戸康多(かんべ こうた)

役割

OSS の脆弱性スキャナ「Vuls」の作者。FutureVuls チームのリーダーとして、脆弱性データと ソフトウェアサプライチェーンのリスク管理に取り組んでいる。

略歴

2019 年にソフトウェアジャパンアワード、2022 年に Google Open Source Peer Bonus を受賞。 Black Hat Asia Arsenal、HITCON、VulnCon 2026、OWS 2026 などで登壇。 依存ライブラリの保守状況を判定する OSS「uzomuzo」を開発している。

  • 脆弱性管理: CVSS だけに頼らない、到達可能性分析と SSVC を使ったリスクに基づく優先順位付け
  • ソフトウェアサプライチェーンセキュリティ: 依存ライブラリの保守状況と乗っ取りの兆候の検出
  • エンジニアリング: 脆弱性対応の判断と作業の自動化

この記事をシェアする