sudoersのNOPASSWDを絞り込んだら、PCが2回勝手に落ちた話

sudo systemctl restart bluetoothと打ったら、パスワードを聞かれずに通った。何かがおかしい。ここから、GTFOBinsの調査、SSH鍵の作り直し、rrsyncとの格闘、そしてOpenSSHのソースコードを読むところまで進んだ調査の記録。最後には、原因が分からないままPCが2回勝手に落ちるという、後味の悪いオチが付く。 きっかけ: パスワードを聞かれない Bluetoothの調子が悪くてsudo systemctl restart bluetoothを打った。いつもならパスワードを聞かれるはずが、そのまま通った。直近でsudoを使った覚えもない。おかしいと思ってsudo -n -l(パスワード無しで、自分に許可されているsudoコマンドを一覧表示する)を叩いてみた。 ユーザー takashi は takashi-pc 上で コマンドを実行できます (ALL : ALL) ALL (root) NOPASSWD: /usr/bin/rsync, /usr/bin/systemctl, /usr/bin/poweroff Copy /etc/sudoers.d/配下に、rsync・systemctl・poweroffの3つが、パスワード無しで、引数を問わず実行できる設定が入っていた。 GTFOBinsで確認した危険性 GTFOBinsは、Unix系のバイナリを悪用した権限昇格の手口をまとめたデータベースだ。systemctlとrsyncのページを見ると、この設定がどれだけまずいかがすぐ分かる。 systemctl: 自分でsystemdのサービスファイルを作り、systemctl link→systemctl enable --nowで登録・起動すれば、そのサービスはroot権限で動く。あるいはSYSTEMD_EDITOR環境変数に自分のスクリプトを指定した状態でsystemctl editを叩くと、そのスクリプトがrootで呼ばれる。 rsync: --rsh(または-e)オプションで、任意のコマンドを「リモートシェル」として起動できる。sudo rsync -e /bin/sh ...のような形で、rootのシェルを直接取れる。 つまりこの設定は、一般ユーザーのまま実質rootになれる、という状態だった。poweroffだけは被害が限定的(シャットダウンのみ)だが、残り2つは深刻。 誰がこれを使っていたのか このPCで動いている自動化を一通り確認したが、systemd timerにもcronにも、この3つのNOPASSWDに依存しているものは見当たらなかった。 答えは、別のマシン(自宅のミニPC、以下NAS)側にあった。backup-archという、このPCの/home /boot /etcを毎晩NASへrsyncでバックアップするスクリプトが、SSH経由でこのPCに対してsudo rsyncを実行していた。バックアップ完了後、sudo systemctl stop/startでコンテナを一時停止する処理と、sudo poweroffで自動シャットダウンする処理も持っていた。 backup-archの最初のコミットは2025年10月。当時はコーディングエージェントに書かせたもので、SSH経由のsudo実行を前提にした設計だった。systemctlを使っていた「バックアップ前後にコンテナを止める」処理は、後になって「そもそも止める対象がバックアップ範囲に含まれておらず、意味が無かった」という理由で既に削除されていたが、sudoers側の許可だけが広いまま残っていた。 絞り込みの設計: rrsyncとSSH強制コマンド rsyncとsystemctlのNOPASSWDを外し、その代わりに、必要な操作だけをできる形に絞り込むことにした。 rsyncにはrrsync(restricted rsync)という、rsync本体に同梱されている補助スクリプトがある。SSHのauthorized_keysで、特定の鍵に対してcommand="rrsync -ro /path/to/dir"のように強制コマンドを指定すると、その鍵で接続してきたクライアントが何を要求しても無視され、指定した1つのディレクトリへの読み取り専用アクセスだけが許可される。 ただしrrsyncは、1回の起動につき1つのディレクトリしか制限できない。今回は/home /boot /etcの3か所が必要だったので、ディレクトリごとに専用の鍵を1本ずつ、計3本作った。加えて、シャットダウン専用の鍵をもう1本。合計4本の鍵、それぞれに対応する強制コマンドをauthorized_keysに設定した。 command="sudo /usr/bin/rrsync -ro /home",restrict ssh-ed25519 AAAA... nas-backup-home command="sudo /usr/bin/rrsync -ro /boot",restrict ssh-ed25519 AAAA... nas-backup-boot command="sudo /usr/bin/rrsync -ro /etc",restrict ssh-ed25519 AAAA... nas-backup-etc command="sudo /usr/bin/poweroff",restrict ssh-ed25519 AAAA... nas-backup-poweroff Copy sudoers側も、引数まで完全一致させた形にする。 ...

暗号化されたWaydroidのキャッシュを諦めて、PSNの非公開APIから友人とのチャット履歴を救出した話

古い友人とのPlayStation Network(PSN)上でのチャットを、消えてしまう前に手元に残しておきたくなった。使っているのはPCの中の[Waydroid]で動くPS App。まずはローカルのキャッシュファイルを直接読みに行こうとしたのだが、そこから思いのほか長い調査になった。 ローカルのキャッシュは暗号化されていた Waydroidのアプリごとのデータは~/.local/share/waydroid/data/data/<パッケージ名>/以下にAndroidの/dataパーティションと同じ構造で保存されている。PS Appのフォルダを覗くと、databases/の中にそれらしいファイルが並んでいた。 fileコマンドで確認すると「OpenPGP Public Key」という判定が出て面食らったが、実際には単に先頭バイトが平文のSQLiteの磁気文字列(SQLite format 3)ではなかっただけだった。xxdで生のバイト列を見ると、最初から意味のないランダムな値が並んでいる。つまり中身は暗号化されていて、復号する鍵はアプリ側が握っている。ここは早々に諦めた。 アプリの画面を手で遡るのは、そもそも選択肢に入らなかった PSNのメッセージはサーバー側に保存されていて、PS4・PS5・スマホのPS Appのどれからでも同じ会話を見られる。だからPS Appを開いて手で遡れば、6年分だろうと全部読めることは最初から分かっていた。 問題はそこじゃない。何年分もの会話を、スクロールしながらスクリーンショットで残していくのがあまりに面倒だった、というだけの話だ。だから、ローカルの暗号化ファイルを諦めた後の次の一手は「手で遡る」ではなく、サーバーから直接、プログラムでメッセージを取得する方法を探すことだった。 PSNは1対1のDMも「Group」として扱っている PSNには非公式ながら実用に足るPythonラッパー(psnawp)がある。ソースを読むと、興味深いことが分かった。 The Group class manages PSN group endpoints related to messages (Group and Direct Messages). PSN内部では、複数人のグループチャットも1対1のダイレクトメッセージも、どちらも同じ「Group」という概念で扱われている。つまり友人との会話を取得するには、「その人とのGroup」を見つければよく、専用のDM用APIを探す必要はなかった。 ページネーションのパラメータ名を、独立した別実装から確定させる get_conversation()というメソッドでメッセージを取得できるが、レスポンスにはprevious・nextというカーソルらしきフィールドとreachedEndOfPageという終端フラグが含まれていた。ページネーションができる構造だとは分かったが、次のページを取るときに渡すクエリパラメータの正式な名前がドキュメントに載っていない。 最初はnextという名前を推測で使ったが、動かなかった。ここで、同じPSNの非公式APIを別言語(PHP)で実装している、完全に独立した別プロジェクト(Tustin/psn-php)のソースコードを確認した。該当箇所にはこう書かれていた。 if ($cursor != null) { $params['before'] = $cursor; } ... $this->update($this->totalCount, $results->messages, $results->previous); Copy 正しいパラメータ名はnextではなく**beforeで、渡す値はレスポンスのprevious**フィールドだった。ドキュメントに載っていない挙動でも、独立した複数の実装を突き合わせれば、推測に頼らず答えを確定できる。 実行環境まわりの小さなつまずき Arch Linuxではpip install --userがexternally-managed-environmentエラーで弾かれる(PEP 668)。素直にpython -m venvで仮想環境を作ってそこにインストールする。 認証にはNPSSOという64文字のトークンを使うが、ライブラリのドキュメントには「これはパスワードと同等に扱うべき」と明記されている。人に見せない・チャットに貼らない前提で、ローカルの環境変数として渡す運用にした。 もう一つ、正直に書いておく。 PSNには公式に公開されたAPIは無く、psnawpのようなライブラリはPS Appの内部通信をリバースエンジニアリングしたものだ。これはPSNの利用規約が禁じる「許可されていない自動化ツールの使用」に該当し、規約違反になる。 読み取り専用だから軽い、書き込みより安全、という話ではない。規約違反であること自体は読み書き問わず同じで、それを「相対的にリスクが低い」という言い方でぼかすべきではなかった。 結果と、届かなかったケース こうして書いたスクリプトを実行すると、6年近く前、会話グループが作られた時点まで遡って、1万件を超えるメッセージを数分で取得できた。手で遡っていたら、この量は現実的な時間では終わらなかったはずだ。 一方で、もう何年も連絡を取っていない別の知人に同じ方法を試したところ、Request is rejected due to all targets' settingsというエラーで弾かれた。これは相手の側のプライバシー設定(あるいはブロック)によるもので、こちらの実装や理屈の間違いではない。API がどれだけ正しく動いても、相手側の設定という現実の壁は越えられない。技術的な調査がどこまでも通用するわけではない、という当たり前の事実を確認する形になった。 ...