PaperCut CVE-2026-81578 と 82078 ドライバ名から始まる連鎖

印刷管理ソフト PaperCut NG / MF に2件の脆弱性がある。CVE-2026-81578CVE-2026-82078 で、CISA の KEV カタログには 2026年8月31日に2件そろって収載され、是正期限は 2026年9月14日とされている。KEV に載っているということは、実際に攻撃で使われたことが確認されているという意味である。

この2件は、別々に読むと危険度を読み違えるという点で教材になる。片方は認証の不備で、影響は「一部のシステム設定を変更できる」まで。もう片方は任意コード実行だが、成立には「システム設定パラメータを操作できること」が要る。それぞれの結論と前提が、きれいに噛み合っている。

この記事では、噛み合っている箇所を追い、最後に ベンダが公開した侵害の痕跡(IoC)を手元のインストールディレクトリから探すスクリプトを置く。実際に動かして出力まで確認したものである。攻撃コードは書かない。扱うのは欠陥の形であって、動いているサーバを壊す手順ではない。

2件が言っていること

まず NVD の記述をそれぞれ見る。

CVE-2026-81578(CWE-306 重要機能に対する認証の欠如。NVD は CWE-305 で登録):

PaperCut MF と PaperCut NG の Web 管理インターフェースに、不適切なアクセス制御の脆弱性が存在する。特定の条件下で、管理機能を対象とした未認証のリモート要求が、アクセス検証の完了前にバックエンドの処理を起動しうる。これにより、未認証のリモートの攻撃者が特定のシステム設定を変更できる。

CVE-2026-82078(CWE-470 外部から制御される入力によるクラス/コードの選択):

PaperCut MF と PaperCut NG のデータベース接続ユーティリティに、安全でない動的クラス読み込みの脆弱性が存在する。アプリケーションは、設定可能なドライバ名に基づいてデータベースドライバのクラスを実体化するが、承認済みドライバの許可リストと照合していない。攻撃者がシステム設定パラメータを操作できる場合、これによりアプリケーションのクラスパス上にある任意の Java バイトコードが、PaperCut サーバプロセスのセキュリティコンテキストで実行されうる。

81578 の結論が「システム設定を変更できる」で、82078 の前提が「システム設定パラメータを操作できる」である。 前提と結論が同じ場所を指している。

対象バージョンは2件とも同じで、NVD の範囲は 24.1.9 未満 / 25.0.2 以上 25.0.12 未満 / 26.0.2 以上 26.0.4 未満。ベンダの告知はもっと単純に「PaperCut NG と PaperCut MF の全バージョンが対象」と書いている。

CVSS は評価方式によって数字が入れ替わる。

CVE NVD の CVSS 3.1 ベンダの CVSS 4.0
CVE-2026-81578 9.8 Critical 8.8 High
CVE-2026-82078 9.1 Critical 9.4 Critical

この差を見て「どちらが本命か」を決めようとすると間違える。 2件は連鎖するので、片方だけ塞ぐ選択肢が存在しない。実際、ベンダは両方の「Mitigated in」を同じ緊急パッチと書いている。

ドライバ名からコードが動く、とはどういうことか

82078 の中身は、Java を書いたことがあれば見覚えのある形をしている。JDBC のドライバは、伝統的にクラス名の文字列から読み込む

1
2
3
// 設定ファイルから読んだ文字列で、クラスを名指しして読み込む
String driver = config.get("database.driver"); // 外から来る値
Class.forName(driver); // その名前のクラスを初期化する

Class.forName は、名前で指定されたクラスを読み込んで静的初期化子を走らせる。つまり、そのクラスに static { ... } ブロックがあれば、インスタンスを作らなくても中身が動く

ここに許可リストが無いと、database.driver に書ける文字列は「JDBC ドライバのクラス名」ではなく「クラスパス上の任意のクラス名」になる。読み込まれた時点で静的初期化子が動くので、あとは攻撃者が用意したクラスがクラスパスに乗っているかどうかだけの問題になる。

正しい形は、名前を受け取ったうえで照合してから使うことである。

1
2
3
4
5
6
7
8
9
10
11
12
// 承認済みドライバの許可リストと照合してから読み込む
private static final Set<String> ALLOWED_DRIVERS = Set.of(
"org.postgresql.Driver",
"com.microsoft.sqlserver.jdbc.SQLServerDriver",
"org.apache.derby.jdbc.EmbeddedDriver"
);

String driver = config.get("database.driver");
if (!ALLOWED_DRIVERS.contains(driver)) {
throw new IllegalArgumentException("unsupported JDBC driver: " + driver);
}
Class.forName(driver);

否定リスト(危険そうな名前を弾く)では足りないのがこの種の欠陥の要点である。クラスパスに何が乗るかはアプリの構成で変わるので、「危険なクラスの一覧」を先に書くことができない。書けるのは「使ってよいクラスの一覧」のほうだけである。

なお、Class.forName に限った話ではない。設定値から実体を選ぶ仕組みは全部同じ形をしている。 Python の importlib.import_module(name)、Node.js の require(name)、.NET の Type.GetType(name)。いずれも「名前は設定から来るが、照合はしていない」という状態を作れる。自分のコードで、外から来た文字列がそのまま読み込み対象の名前になっている箇所があれば、この記事はそこの話でもある。

IoC に欠陥の形が出ている

ベンダが公開した侵害の痕跡は、上の推測が当たっていることを裏づけている。server.log に残る文字列として挙がっているのはこれらである。

1
2
3
4
5
6
ERROR No suitable driver found for jdbc:no:x
ERROR DatabaseUtils - Database error looking up cardID: VALUES CAST
DB URL: jdbc:derby:memory:pwn;create=true
DB URL: jdbc:no:x DB Driver: <5文字のランダム名>
Database error looking up cardID: VALUES CAST(X'cafebabe
Database error looking up cardID: VALUES CAST('

DB Driver: の後ろが5文字のランダム名になっているのが、まさに「ドライバ名にドライバでないものを書いた」痕跡である。jdbc:no:x は実在しない JDBC URL なので、接続そのものは失敗する。接続の成否は攻撃者にとってどうでもよく、クラスが読み込まれれば目的は達している。

cafebabe は Java クラスファイルの先頭4バイトのマジックナンバーである。手元で確かめられる。

1
2
3
$ printf '\xca\xfe\xba\xbe\x00\x00\x00\x34' > /tmp/x.class
$ xxd -l 4 /tmp/x.class
00000000: cafe babe ....

つまり VALUES CAST(X'cafebabe という行は、クラスファイルの中身が SQL のリテラルとして流し込まれていたことを示している。Derby のインメモリデータベース(jdbc:derby:memory:pwn;create=true)を作って、そこを経由してバイト列を運んでいる形である。

ディスク側の痕跡も同じ命名規則で残る。

1
2
3
<install>\server\lib\<5文字>.class
<install>\server\data\content\<5文字>.cmd
<install>\server\data\content\<5文字>.out

server/libクラスパスに含まれるディレクトリである。ここに .class を置いてから、その名前をドライバ名として指定すれば、82078 の条件が揃う。

侵害後の挙動としては、pc-app.exe(Linux では pc-app)が cmd.exe を子プロセスとして起動し、whoami & ver から tasklistnltest /dclist へと進む一連のコマンドが観測されている。エンドポイント保護がここで止めて端末を隔離した例もある、とベンダは書いている。

そして、痕跡は消される。 ベンダの告知は「これらのファイルは攻撃の進行に伴って攻撃者に片づけられることがあるので、存在しないことは侵害されていないことの証明にならない」と明記している。server.log 自体が消えている、不自然に切り詰められている、というのも痕跡の一つに挙がっている。

IoC を手元で探すスクリプト

上の文字列とファイル名の形を、インストールディレクトリから探すだけのスクリプトである。判定はしない。 見つけた箇所を出すだけで、あとは人が読む。Python 3.9 以降で追加のインストールなしに動く。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python3
"""PaperCut NG/MF の公開 IoC を server.log と install ディレクトリから探す。
使い方: python3 scan.py <PaperCut install dir>
ベンダの緊急告知が挙げた文字列とファイル形だけを見る。判定はしない。"""
import re
import sys
from pathlib import Path

LOG_MARKERS = [
"No suitable driver found for jdbc:no:x",
"Database error looking up cardID: VALUES CAST",
"DB URL: jdbc:derby:memory:pwn;create=true",
"DB URL: jdbc:no:x",
]
CAFEBABE = "VALUES CAST(X'cafebabe"
FIVE_CHAR = re.compile(r"^[A-Za-z0-9]{5}\.(class|cmd|out)$")


def scan_log(path: Path) -> list[str]:
hits = []
if not path.exists():
return ["server.log that does not exist: %s" % path]
text = path.read_text(encoding="utf-8", errors="replace")
for line_no, line in enumerate(text.splitlines(), 1):
for m in LOG_MARKERS + [CAFEBABE]:
if m in line:
hits.append(f"{path.name}:{line_no}: {m}")
return hits


def scan_files(root: Path) -> list[str]:
hits = []
for sub in ("server/lib", "server/data/content"):
d = root / sub
if not d.is_dir():
continue
for f in sorted(d.iterdir()):
if f.is_file() and FIVE_CHAR.match(f.name):
hits.append(f"{sub}/{f.name} ({f.stat().st_size} bytes)")
return hits


def main() -> int:
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
found = scan_log(root / "server" / "logs" / "server.log") + scan_files(root)
for h in found:
print("HIT", h)
print(f"-- {len(found)} indicator(s). Absence is not proof of safety.")
return 0


if __name__ == "__main__":
raise SystemExit(main())

痕跡を模した構成に対して実行すると、次のように出る。

1
2
3
4
5
6
7
$ python3 scan.py /path/to/papercut
HIT server.log:2: No suitable driver found for jdbc:no:x
HIT server.log:3: Database error looking up cardID: VALUES CAST
HIT server.log:3: VALUES CAST(X'cafebabe
HIT server/lib/aB3xQ.class (8 bytes)
HIT server/data/content/aB3xQ.cmd (0 bytes)
-- 5 indicator(s). Absence is not proof of safety.

痕跡の無い構成では 0 件になる。

1
2
$ python3 scan.py /path/to/clean
-- 0 indicator(s). Absence is not proof of safety.

最後の行をわざと同じ文にしてある。0 件と「無事」は違うからで、ベンダ自身がそう書いている以上、出力でそこを曖昧にしてはいけない。server.log が存在しない場合も HIT として出すようにしてあるのは同じ理由で、ログが消えていることそのものが痕跡だからである。

FIVE_CHAR[A-Za-z0-9]{5} に絞ってあるのは、正規のファイルを拾いすぎないためである。逆に言えば、攻撃者が6文字の名前を使えば外れる。この手の検知は形の一致であって、網羅ではない。

何をするか

  1. 緊急パッチ Release 3 を当てる。 ベンダは Release 3 を「過去の緊急リリースをすべて含む累積版」と説明しており、初期の緊急パッチや Release 2 で止まっている環境も対象としている。Release 3 では SAML ログインの不具合と、外部カード照合における従来の Microsoft SQL Server ドライバ対応という2件の後退も併せて修正されている。
  2. 外部カード照合を使っているなら、パッチ後の挙動を確認する。 82078 が起きたのはまさにその機能まわりのユーティリティで、Release 3 はそこに手を入れている。
  3. 上のスクリプトなり手作業なりで痕跡を見る。 未認証で設定を書き換えられ、それがコード実行に連鎖する形なので、パッチより前に足場を作られている前提で動くのが安全側である。
  4. 管理画面をインターネットから直接届かない位置へ移す。 PaperCut の Application Server は社内利用が前提で、外部公開が要る構成はまれである。ただしこれは到達経路を狭めるだけで、社内から届く攻撃者には効かない。

この形を自分のコードで探す

最後に一般化しておく。この2件が組み合わさったのは偶然ではなく、境界の引き方が2箇所でずれていたためである。

  • 81578 は「検証の完了前にバックエンドの処理が起動する」。認証はあるが、副作用より後ろにあった。
  • 82078 は「許可リストと照合せずにクラスを実体化する」。検証が無かった

自分のコードで探すなら、質問は2つでよい。

「認証チェックは、副作用を起こす行より前にあるか。」 ミドルウェアで認証しているつもりでも、ハンドラの入口で設定を読み込む・キャッシュを温める・接続を張るといった処理が先に走っていないか。

「外から来た文字列が、読み込む対象の名前になっていないか。」 クラス名・モジュール名・テンプレート名・シリアライザ名。名前で実体を選ぶ仕組みは、許可リストとセットでなければ成立しない。


この記事の CVE の詳細と、クラウド各社・Linux ディストリごとの対応状況は セキュリティ情報まとめ にも整理してある。

出典(いずれも2026年9月7日確認)


PaperCut CVE-2026-81578 と 82078 ドライバ名から始まる連鎖
https://blog.hashito.biz/2026/09/07/papercut-cve-2026-81578-82078-jdbc-driver-name-rce/
著者
hashito
作成日
2026年9月7日
著作権