
CVE-2026-96512 (SudoTimeWarp) の再現およびPoCスクリプト。呼び出し元のTZがsudoersのNOTBEFORE/NOTAFTERウィンドウをずらす問題と、緩和策のチェックを含む。
sudo: 呼び出し元の TZ が NOTBEFORE/NOTAFTER を決定する
SudoTimeWarp (CVE-2026-96512) の再現資料。Date_Spec タイムスタンプが末尾の Z を省略している sudoers ルールは mktime() によって変換されるが、この関数は呼び出しごとに getenv("TZ") を再読み込みする。
sudo は setuid-root であり、呼び出し元の environ が execve() をそのまま通過するため、非特権の呼び出し元が自身の有効期間の評価に用いられるタイムゾーンを選択できてしまう。
| 名称 | SudoTimeWarp |
| CVE | CVE-2026-96512 |
| CVSS v3.1 | AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — 7.8 High |
| CWE | CWE-863 (登録上); 機構は CWE-807 |
| 影響範囲 | sudo 1.8.20 から 1.9.17p2 まで、および 1820a349 より前の main |
| 修正版 | 1820a349 (2026-08-29) |
| ずれ幅 | 片方向あたり最大 24 h 59 m 59 s; 合計区間 49 h 59 m 58 s |
根本的な問題 — TZ が NOTBEFORE/NOTAFTER に影響する — は、Tencent Xuanwu Lab の XlabAI Team、Atuin Automated Vulnerability Discovery Engine、および Guannan Wang、Zhanpeng Liu、Guancheng Li によって非公開で報告され、コミット db669167c (2026-03-14) でクレジットされている。
SudoTimeWarp (CVE-2026-96512) は、db669167c が不完全であるという発見である。 このコミットが導入した保護は glibc のタイムゾーンキャッシュ — ひいてはログのタイムスタンプ — をカバーするが、認可が決定される gentime.c:156 の mktime() には届いていない。そのコミットを既に含むツリー上でも、ウィンドウのずれは完全に再現可能である。メンテナは 1820a349 のメッセージでこの点を記録している:
以前の変更は「mktime() 関数が呼び出されるたびに TZ 環境変数を再読み込みするため、効果がなかった」
sudoers ルールが (1) 呼び出し元にコマンドを許可し、(2) NOTBEFORE= または NOTAFTER= を持ち、(3) タイムスタンプを Z 接尾辞なし、かつ明示的なオフセットなしで記述していること。
項目 (3) は特殊なものではない。sudoers のマニュアルは接尾辞なしの形式をサポートされる拡張として記載し、4 つの例示タイムスタンプの 1 つとして 20151201235900 を掲載しており (docs/sudoers.man.in:1820)、この形式はプロジェクト自身の回帰テストコーパス (plugins/sudoers/regress/testsudoers/test13.sh) にも現れる。
必ず使い捨てのコンテナまたは VM 内で。 ここにあるすべてのスクリプトは /etc/sudoers を書き換える。バックアップと復元は行うが、そこでミスをすると実機から締め出される — スクリプトは --i-know を渡さない限りコンテナ外での実行を拒否する。
docker build -t sudotimewarp .
docker run --rm -it sudotimewarp
またはイメージをビルドせずに:
docker run --rm -it -v "$PWD:/m" debian:trixie bash -c \
'apt-get update >/dev/null && apt-get install -y sudo >/dev/null && bash /m/poc.sh'
影響を受けるビルドでの想定出力:
=== probes ===
rule valid, no TZ : uid=0(root) gid=0(root) groups=0(root)
rule expired, no TZ : sudo: a password is required
rule expired, TZ=UTC : sudo: a password is required
rule expired, TZ=XXX24 : uid=0(root) gid=0(root) groups=0(root)
expired + 'Z', TZ=XXX24 : sudo: a password is required
XXX は任意の 3 文字のタイムゾーン略称であり、24 は POSIX オフセットである。ファイルは関与せず、存在する必要もない — 悪用可能なチャネルはインラインの POSIX 文字列のみである。tzfile 形式 (TZ=:/tmp/evil.tz およびその変種) は setuid 下で glibc の __libc_enable_secure ガードによって拒否され、ずれはちょうど 0 秒となる。これが本件を CVE-2014-9680 と分かつ点である。
1〜3 行目は対照であり、これが重要である。パースエラーもバグと同じ ALLOW を生むからである。
poc.sh は影響を受ける場合は 0、そうでない場合は 1、対照が成立しなかった場合は 2 で終了する。
poc.sh は非対話的に実行できるよう NOPASSWD を使用している。これはバグの条件ではない。分割スクリプトは特権境界を明示的に示す — パート 1 は管理者が正当に行うことだけを行い、パート 2 は非特権ユーザとして実行され、いかなる特権も使用しない:
bash repro-admin.sh escalation # as root: writes the policy
su - poc -c 'bash /poc/repro-attacker.sh'
bash repro-admin.sh --cleanup
repro-admin.sh は 4 つのシナリオを取る:
パスワードを要求するルールの場合、呼び出し元は依然として PAM を通じて認証し、誤ったパスワードは依然として失敗する。これは認証バイパスではない — 動くのは認可の決定である。
PR:L であり、PR:N ではない。TZ 値で拒否される。C:L/I:N/A:N)。7.8 のベクタは、日付付きルールが呼び出し元の恒久的アクセスより広い場合を評価している。約 25 時間の上限はアクセスウィンドウに適用され、影響の持続時間には適用されない。その中での 1 回の成功した使用で、ウィンドウを超えて存続する永続化を確立するのに十分である。
すべての NOTBEFORE/NOTAFTER タイムスタンプに Z を付加する — これにより timegm() 分岐が強制される。
grep -rE 'NOT(BEFORE|AFTER)=' /etc/sudoers /etc/sudoers.d/
sudo -l で監査してはならない。これはすべての Date_Spec を gmtime() 経由で整形し、常に Z を付加する (plugins/sudoers/display.c:229)。そのため NOTAFTER=20260827221423 と書かれたルールは NOTAFTER=20260827221423Z と表示される。出力は、まさに判定を左右する細部を正規化して消してしまう。cvtsudoers と fmtsudoers も同じ挙動をする。/etc/sudoers を直接読むこと。
1820a349: https://github.com/sudo-project/sudo/commit/1820a349687522f51023d1ae5925125f59679a8c2026-08-28 にメンテナへ報告、期限は設定せず。候補パッチは同日に返ってきた。公開修正は 2026-08-29 に着地。CVE は CNA-LR を務める Red Hat によって割り当てられ、2026-09-23 に公開された。エンバーゴはない。ここにあるものはすべて修正コミット以来公開されている。
SudoTimeWarp / CVE-2026-96512: Ermenson Junior、独立研究、Red Hat により「Independent security research」として記録。根本問題の原報告は Tencent Xuanwu Lab の XlabAI Team、Atuin Automated Vulnerability Discovery Engine、および Guannan Wang、Zhanpeng Liu、Guancheng Li に帰属する。
防御目的で公開: ホストが影響を受けているかの確認、および Z による緩和の検証のために。
| 行 | 確認内容 |
|---|
| 1 | ルールがそのウィンドウ内でそもそも機能すること |
| 2 | 4 行目が存在しない場合の拒否が本当に NOTAFTER に由来すること |
| 3 | TZ の設定自体が原因ではないこと — TZ=UTC は TZ なしと同じ判定をする |
| 4 | バグ: 期限切れのルールが root として実行される |
| 5 | 境界: ドキュメント化された Z があると timegm() 分岐が取られ、失敗する |
| シナリオ | ポリシー |
|---|
expired (デフォルト) | ルール 1 つ、NOTAFTER は 1 時間前、ゾーンなし |
valid | 同じルールがまだウィンドウ内 — 対照 |
expired-z | 同じ期限切れルールにドキュメント化された Z を付けたもの — 対照、影響を受けない形式 |
escalation | 狭い恒久的許可と期限切れの広い許可 — メンテナンスや緊急時アクセス許可が実際に取る形 |