Upgrade to Pro — share decks privately, control downloads, hide ads and more …

高橋メソッドでCVE解説シリーズ 〜CVE-2026-112816〜

高橋メソッドでCVE解説シリーズ 〜CVE-2026-112816〜

高橋メソッドでCVEを解説する (CVE-2026-112816)
opus5.5で生成

Avatar for Hakkadaikon

Hakkadaikon

November 01, 2026

More Decks by Hakkadaikon

Other Decks in Programming

Transcript

  1. 公式の記述(CVE レコード) Improper certificate validation in the HTTPS listener in

    Canonical LXD 4.0.14, 5.0.10, 5.21.8, 6.9 before 4.0.15, 5.0.11, 5.21.9, 6.10.1 allows an unauthenticated attacker to authenticate as any trusted client and gain full access to the cluster and managed virtual machines via a crafted certificate chain. 認証されていない攻撃者が 細工した証明書チェーンで 信頼済みの任意のクライアントとして認証され クラスタと仮想マシンをすべて操作できる 出典: cve.org CVE-2026-112816(CNA: Canonical)
  2. 深刻度の評価(2026-11-02 時点) 評価元 Canonical(CNA) 評価 CVSS 4.0: 10.0 Critical Exploitation:

    none Automatable: yes CISA(SSVC) Technical Impact: total NVD 独自スコアなし Ubuntu CVE トラッカ Priority Medium ー 備考 AV:N/AC:L/AT:N/PR:N/UI:N、影響はすべて High 悪用は未確認。自動化しやすく、影響は全面的 状態は Deferred(NVD 側の分析は保留) deb パッケージとしては 22.04 以降に収録なし。本体は snap で配布 LXD を snap で使っている環境では、CNA の 10.0 をそのまま受け止めてよい 報告者は Stéphane Graber。Incus(LXD から分かれたプロジェクト)から LXD へ共有された 出典: cve.org / NVD API / ubuntu.com/security/CVE-2026-112816 / discourse.ubuntu.com 告知 90557
  3. ① LXD の API は 2 つの入口を持つ 同じホストの lxc コマンド

    別のマシンの 他のク ラスタメンバ lxc / UI / Unix ソケット ファイルの権限で守る) ( HTTPS(既定 8443/tcp) クライアント証明書で守 る) デーモン で動く) LXD (root ( 今回の舞台は HTTPS の入口。 core.https_address を設定したときだけ開く クラスタを組むと、メンバ同士もこの HTTPS でつながる API で何ができるか: コンテナや VM の作成・削除・中でのコマンド実行など、ほぼ全部
  4. ② 公開鍵と署名 持っている人 できること 秘密鍵 本人だけ 署名を作る 公開鍵 誰でもよい 署名を確かめる

    「この公開鍵に対応する秘密鍵を持っている」ことは、その場で署名させれば確かめられる 公開鍵を持っているだけでは署名は作れない TLS のクライアント認証もこの方法で本人かどうかを確かめる
  5. ③ 証明書は配って使う サーバの証明書は、接続するだけで誰でも取れる # openssl s_client -connect lxd.example.com:8443 </dev/null 2>/dev/null

    \ | openssl x509 -noout -subject -fingerprint -sha256 証明書は公開鍵を相手に渡すための入れ物。持っていても本人の証明にはならない クライアント証明書も同じ。登録のために管理者へ渡したり、設定ファイルに置いたりする 本人かどうかを決めるのは、対応する秘密鍵で署名できるか
  6. ④ mTLS: クライアントも証明書を出す(TLS 1.3) クライアント サーバ(LXD) ClientHello ServerHello / サーバの

    Certificate / CertificateRequest 証明書のリスト) Certificate( 秘密鍵での署名) CertificateVerify( 署名をリストの先頭の公開鍵で確かめる Finished クライアントが送るのは証明書 1 枚ではなくリスト。ここが今回の核心 RFC 8446 Section 4.4.2 / 4.4.3
  7. ⑤ Certificate メッセージの中身 メッセージ 本人の証明書 Certificate +----------------------------------+ | [0] (leaf)

    | ← +----------------------------------+ | [1] CA | ← [0] +----------------------------------+ | [2] CA ... | ← +----------------------------------+ 中間 の証明書 さらに上の中間 秘密鍵を持っていると証明するのはこれだけ に署名した CA。検証の手がかり 任意。送る側が好きに並べられる RFC 8446: 送り手自身の証明書は必ず先頭。後ろは先頭を検証するための補助 CertificateVerify の署名で確かめられるのは 0 の秘密鍵だけ 1 以降は、誰の証明書でもクライアントが自由に入れられる RFC 8446 Section 4.4.2 "The sender's certificate MUST come in the first CertificateEntry in the list."
  8. ⑥ LXD は CA ではなく「指紋の一覧」で信頼する 管理者がクライアントを登録する(トークン方式など) 登録済みの一覧 指紋 証明書の SHA-256)

    # lxc config trust add # ( = lxc config trust list 既定では、CA の署名ではなく登録した証明書そのものと一致するかで判断する クライアント証明書は自己署名でよい だから Go の TLS ライブラリに「チェーンの検証」は任せず、LXD が自分で照合する その自前の照合に穴があった
  9. 場面 1: サーバの TLS 設定 func ServerTLSConfig(cert *shared.CertInfo) *tls.Config {

    config := shared.InitTLSConfig() config.ClientAuth = tls.RequestClientCert config.Certificates = []tls.Certificate{cert.KeyPair()} config.NextProtos = []string{"h2"} // Required by gRPC /* ... CA ClientCAs ... */ return config } モードなら を設定 Go の tls.Config は C で言えば設定用の構造体 RequestClientCert = 「クライアント証明書を求める。でも検証はしない」 署名の確認はする。チェーンが正しいかは見ない canonical/lxd タグ lxd-6.10.1 lxd/util/net.go:57-73
  10. Go のドキュメントにもそう書いてある // RequestClientCert indicates that a client certificate should

    be requested // during the handshake, but does not require that the client send any // certificates. RequestClientCert // PeerCertificates are the parsed certificates sent by the peer, in the // order in which they were sent. The first element is the leaf certificate // that the connection is verified against. PeerCertificates []*x509.Certificate は受け取ったリストそのまま 「検証に使ったのは先頭(leaf)」と明記されている PeerCertificates Go 1.21.6 src/crypto/tls/common.go:264-273, 324-327(最新版も同じ記述)
  11. 場面 2: Go は先頭の証明書で署名を確かめる signed := signedMessage(sigHash, clientSignatureContext, hs.transcript) if

    err := verifyHandshakeSignature(sigType, c.peerCertificates[0].PublicKey, sigHash, signed, certVerify.signature); err != nil { c.sendAlert(alertDecryptError) return errors.New("tls: invalid signature by the client certificate: " + err.Error()) } TLS 1.3 のサーバ側、CertificateVerify の処理 使う公開鍵は peerCertificates[0] だけ つまり保証されるのは「クライアントは 0 の秘密鍵を持っている」ことだけ Go 1.21.6 src/crypto/tls/handshake_server_tls13.go:946-951
  12. チェーン検証は今回の設定では走らない if c.config.ClientAuth >= VerifyClientCertIfGiven && len(certs) > 0 {

    opts := x509.VerifyOptions{ Roots: c.config.ClientCAs, /* ... */ } for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) } chains, err := certs[0].Verify(opts) チェーンの検証は VerifyClientCertIfGiven 以上のときだけ LXD は RequestClientCert (それより弱い設定)なのでスキップ 1 以降に何が入っていても、ハンドシェイクは成功する Go 1.21.6 src/crypto/tls/handshake_server.go:885-897
  13. 場面 3: 受け取った証明書を全部集める if len(r.TLS.PeerCertificates) > 0 { // Convert

    list of peer certificates to map. peerCertificateFingerprints := make([]string, 0, len(r.TLS.PeerCertificates)) peerCertificates := make(map[string]x509.Certificate, len(r.TLS.PeerCertificates)) for _, c := range r.TLS.PeerCertificates { f := shared.CertFingerprint(c) peerCertificates[f] = *c peerCertificateFingerprints = append(peerCertificateFingerprints, f) } の冒頭 0 だけでなく、おまけの 1 以降も指紋の一覧に入れている (*Daemon).Authenticate() canonical/lxd タグ lxd-6.10.1 lxd/daemon.go:489-498
  14. 場面 3 の続き: どれか 1 枚が一致すれば OK authTLS := func(protocol

    string, certGetter func(...string) map[string]x509.Certificate) (*request.RequestorArgs, error) { matchedCerts := certGetter(peerCertificateFingerprints...) if len(matchedCerts) == 0 { return nil, nil } else if len(matchedCerts) > 1 { return nil, api.StatusErrorf(http.StatusBadRequest, "Client sent too many credentials") } /* matchedCerts 1 matchedFingerprint */ trusted, _ := util.CheckMutualTLS(peerCertificates[matchedFingerprint], matchedCerts) if trusted { /* ... */ return &request.RequestorArgs{ Trusted: trusted, Protocol: protocol, Username: matchedFingerprint, /* ... */ }, nil のただ つのキーを に取り出す 送られた全部の指紋を信頼済み一覧と突き合わせ、1 枚だけ一致したらその人として認証 canonical/lxd タグ lxd-6.10.1 lxd/daemon.go:501-538(一部省略)
  15. CheckMutualTLS: 中身はバイト列の比較 func CheckMutualTLS(cert x509.Certificate, trustedCerts map[string]x509.Certificate) (bool, string) {

    err := certificateInDate(cert) if err != nil { return false, "" } for fingerprint, v := range trustedCerts { if subtle.ConstantTimeCompare(cert.Raw, v.Raw) == 1 { return true, fingerprint } } return false, "" } C で言えば「期限内か見て、 memcmp(cert, trusted, len) == 0 か」 この証明書の秘密鍵を相手が持っているかは見ていない(見られない) 秘密鍵の確認は TLS 層の仕事。そして TLS 層が確認したのは 0 だけ canonical/lxd タグ lxd-6.10.1 lxd/util/http.go:214-231(ログ出力を省略)
  16. 攻撃の流れ 攻撃者 脆弱版) LXD( Certificate: [0] 攻撃者の証明書, [1] 被害者の証明書 攻撃者の秘密鍵で署名)

    CertificateVerify( Go: [0] の公開鍵で署名 OK チェーン検証はしない → 接続成立 GET /1.0/instances など LXD: 200 OK 指紋 {攻撃者, 被害者} を照合 一致は被害者の 1 枚だけ → 被害者として認証
  17. C で書くとこういう関数 の秘密鍵を持っていることは 層が確認済み /* certs[0] TLS */ bool authenticate(const

    cert_t *certs[], size_t n, const char **user) { for (size_t i = 0; i < n; i++) { /* ← */ const trusted_t *t = trust_store_find(certs[i]); if (t != NULL) { *user = t->name; /* */ return true; } } return false; } 全部見る 被害者の名前が入る 正しくは、確認済みの 枚だけを見る /* 1 */ const trusted_t *t = trust_store_find(certs[0]); 説明用の擬似コード。LXD の実コードではない
  18. 複数一致チェックでは防げない } else if len(matchedCerts) > 1 { return nil,

    api.StatusErrorf(http.StatusBadRequest, "Client sent too many credentials") } 弾くのは「信頼済みの証明書が2 枚以上一致したとき」 攻撃者の証明書 0 は信頼一覧に無いので、一致は被害者の 1 枚だけ このチェックは「誰として扱うか迷う」ケースへの対策であって、なりすまし対策ではない
  19. CA モード(server.ca)でも防げない if protocol != request.ProtocolCluster && d.endpoints.NetworkCert().CA() != nil

    { trusted, _, _ = util.CheckCASignature(peerCertificates[matchedFingerprint], d.endpoints.NetworkCert()) CA 署名の確認も照合と同じ被害者の証明書に対して行う 被害者の証明書は本物なので、CA の署名も本物。確認は通る core.trust_ca_certificates=true の経路も、送られた全部の証明書を順に調べていた canonical/lxd タグ lxd-6.10.1 lxd/daemon.go:523-524, 570-583
  20. 同じ形の照合をしていた場所(修正コミットの対象) ファイル lxd/daemon.go lxd/cluster/tls.go lxd/api_vsock.go lxd-agent/server.go lxd/certificates.go lxd/identities.go 役割 メイン

    API の認証 クラスタメンバ同士の認証 VM 内エージェントからの接続(vsock) VM 内エージェント側の認証 証明書の削除、トークンでの自己登録 TLS 識別情報の作成 登録処理では逆に最後の要素 [len-1] を使っていた箇所もある(これも 0 に統一) canonical/lxd PR #19426(stable-6)の変更ファイル一覧
  21. なりすました後にできること 被害者と同じ権限で LXD の API を使える 管理者の証明書なら、コンテナや VM の作成・削除・中でのコマンド実行がすべてできる LXD

    は root で動いている。特権コンテナを作ってホストのディスクをつなぐ、などの手順でホストの root まで届きうる クラスタなら全メンバが対象 Depending on the privilege of the victim user, the attacker may gain root access to the host machine. 出典: Canonical の告知(discourse.ubuntu.com 90557)
  22. 同じ形のループは 2014 年からある for i := range r.TLS.PeerCertificates { if

    d.CheckTrustState(*r.TLS.PeerCertificates[i]) { return true } } return false LXD に証明書認証が入ったコミット(2014-11-18)にすでにこの形がある 当時の設定は RequireAnyClientCert 。これもチェーンを検証しない CVE レコードの影響範囲は直近のバージョンだけを挙げているが、古いバージョンでもソースを見る と同じ形 canonical/lxd commit cdc8b7c76b "Introduce the bulk of certification code"
  23. 新しい関数: GetPeerCertificate // GetPeerCertificate returns the first peer certificate in

    the request TLS connection states peer certifi func GetPeerCertificate(r *http.Request) (*x509.Certificate, error) { if r.TLS == nil { return nil, api.StatusErrorf(http.StatusBadRequest, "%w", ErrNoTLS) } if len(r.TLS.PeerCertificates) == 0 { return nil, api.StatusErrorf(http.StatusBadRequest, "%w", ErrNoCertificate) } } return r.TLS.PeerCertificates[0], nil TLS 層が署名で確認した 0 だけを返す 認証に関わるすべての場所で、この関数を経由するように変えた canonical/lxd commit eb029fcc13 "lxd/request: Add util for getting request peer certificate."(lxd/request/request.go)
  24. メイン API の認証の差分 - if len(r.TLS.PeerCertificates) > 0 { peerCertificateFingerprints

    := make([]string, 0, len(r.TLS.PeerCertificates)) peerCertificates := make(map[string]x509.Certificate, len(r.TLS.PeerCertificates)) for _, c := range r.TLS.PeerCertificates { f := shared.CertFingerprint(c) peerCertificates[f] = *c peerCertificateFingerprints = append(peerCertificateFingerprints, f) } + if peerCertificate != nil { + peerCertificateFingerprint := shared.CertFingerprint(peerCertificate) ... matchedCerts := certGetter(peerCertificateFingerprints...) + matchedCerts := certGetter(peerCertificateFingerprint) ... trusted, _ := util.CheckMutualTLS(peerCertificates[matchedFingerprint], matchedCerts) + trusted, _ := util.CheckMutualTLS(*peerCertificate, matchedCerts) canonical/lxd commit c1cec66f7e "lxd: Use only client leaf certificate for authentication (main API)."
  25. 修正後: 同じ攻撃はどうなるか 段階 TLS の署名確認 照合に使う証明書 信頼一覧との一致 結果 脆弱版 0

    攻撃者の証明書で OK 0 と 1 の両方 1 被害者が一致 被害者として認証 1 に誰の証明書を入れても、もう認証には使われない 修正版 同じ 0 だけ 一致なし 未認証
  26. 修正コミット(2026-10-15 作成、Mark Laing) commit eb029fcc13 e8868eb4d1 d213ffaf0f 5943ab3268 c1cec66f7e 0a34960776

    e10aeca6ce 1584e897d3 内容 を追加 lxd-agent: leaf だけで認証 クラスタ: leaf だけで認証 vsock: leaf だけで認証 メイン API: leaf だけで認証 証明書の自己更新・削除・トークン登録も leaf だけ 統合テスト: leaf だけで信頼・認証されることを確認 GetPeerCertificate() の単体テスト GetPeerCertificate() 公開リポジトリでは PR #19426 として 2026-10-30 に stable-6 ブランチへ取り込まれた github.com/canonical/lxd(2026-11-02 に確認)
  27. 修正済みバージョン(snap) チャンネル 6/stable 5.21/stable 5.0/stable 4.0/stable 2/stable 3/stable snap lxd

    lxd lxd lxd microcloud microcloud 修正版 6.10.1 5.21.9 5.0.11 4.0.15 2.1.5 3.4.1 いずれも 2026-10-29 リリース。candidate から stable へ段階的に配信中 2026-11-02 に確認した時点では、公開 git の lxd-6.10.1 などのタグ自体には修正コミットが入って いない。ソースから自分でビルドしている場合は要注意 出典: discourse.ubuntu.com 告知 90557 / github.com/canonical/lxd のタグとブランチ
  28. ① 更新と確認 いまのバージョン # snap list lxd microcloud 更新 #

    sudo snap refresh lxd sudo snap refresh microcloud # MicroCloud を使っている場合 更新後のバージョンを確認(上の表の修正版以上か) # snap list lxd snap は自動更新されるが、保留(hold)していないか確認する クラスタは全メンバを更新する
  29. ② 入口を絞る で待ち受けているか # HTTPS lxc config get core.https_address 使っていないなら閉じる

    ソケットだけになる) # (Unix lxc config unset core.https_address 必要なら FW で 8443/tcp に届く送信元を管理用ネットワークに限る 今回のバグに限らず、API がインターネットから見えないことが一番効く
  30. ③ 信頼済みの証明書と操作の記録を確認する 信頼済みのクライアント 識別情報) # ( lxc auth identity list

    最近の操作 誰の名前で何が行われたか) # ( lxc operation list lxc monitor --type=lifecycle # これから起きる操作を監視 使っていない証明書は削除する。なりすましの対象を減らせる 管理者権限の証明書を必要な人数に絞る 心当たりのないインスタンスや操作が無いか確認する
  31. 参考資料 CVE レコード: https://www.cve.org/CVERecord?id=CVE-2026-112816 NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-112816 Ubuntu: https://ubuntu.com/security/CVE-2026-112816 Canonical の告知:

    https://discourse.ubuntu.com/t/security-advisory-critical-vulnerability-found-incanonical-lxd-and-microcloud/90557 GitHub Security Advisory: https://github.com/canonical/lxd/security/advisories/GHSA-99jp-jwmc65p4(CVE レコードに記載。2026-11-02 時点では非公開で閲覧できない) ソース: https://github.com/canonical/lxd(タグ lxd-6.10.1、PR #19426) 修正コミット: eb029fcc13 / c1cec66f7e / d213ffaf0f / 5943ab3268 / e8868eb4d1 / 0a34960776 由来コミット: cdc8b7c76b "Introduce the bulk of certification code"(2014) Go crypto/tls: https://pkg.go.dev/crypto/tls(引用は Go 1.21.6 のソース) RFC 8446 (TLS 1.3) Section 4.4.2 / 4.4.3