13年前の記事のリンクを貼りたいがためにサイト復旧しようとしたら、知らない住人(=バックドア)が4人いた件
2026年のワールドカップで、カーボベルデが旋風を巻き起こした。
初出場。人口55万人。アイスランドに次いで2番目に人口の少ないW杯出場国である。グループHでスペインを0-0に抑え、ラウンド32では前回王者アルゼンチンと延長戦までもつれて2-3で散った。
それを見ながら、私はソワソワしていた。
13年前に、うちのブログでカーボベルデのことを書いていたからだ。
2013年9月。日本でこの国名を知っている人がほぼいなかった頃である。
ドヤ顔でリンクを貼りたい。貼りたい。
だが貼れない。そのブログは500エラーで死んでいた。
最後にまともに触ったのは2018年。その前の更新は2013年。要するに10年以上放置していた。放置していた自覚すらなかった。それくらい忘れていた。
で、リンクを1本貼るために蓋を開けたら、中に知らない人が住んでいた。4人。
この記事は復旧手順の話ではない。開けたら何が出てきたかという話である。
念のため先に書いておくと、私は電気回路の出身で、Webサーバの運用はどちらかというと片手間だ。以下は専門家の調査レポートではなく、10年ぶりに自宅の物置を開けた人間の悲鳴である。
最初の異変は、誰にも読めないファイルだった
バックアップを取ろうと tar を回したら、1ファイルだけ読めない。
---------- 1 user group 66108 Aug 20 2013 wp-content/plugins/search.php
Change: 2023-06-03
権限0000。
オーナーである私にすら読めない。自分のサーバの自分のファイルなのに。
しかも2013年に作られたファイルが、2023年に権限だけ変更されている。10年間放置されて、ある日突然アクセス権を剥奪されている。
これは壊れたのではなく、隔離された跡だった。レンタルサーバの改ざん検知が、読めない状態にして封じ込めてくれていた。ありがとう。
権限を戻して中を見た。WSO Web Shell。ファイルマネージャ、コマンド実行、SQLクライアント一式が入ったバックドアである。
腹が立ったのは実装の丁寧さだ。検索エンジンのクローラを User-Agent で判定して404を返し、存在を隠す。error_log を無効化してログも残さない。
私のサイトより真面目に作られていた。
1人ではなかった
「隔離されてるなら大丈夫か」で終わらせなかったのが、この日唯一の手柄である。全部走査した。
| ファイル | 入居日 | 状態 |
|---|---|---|
wp-content/plugins/search.php |
2013-08-20 | 隔離済 |
wp-includes/pomo/admin.php |
2013-07-10 | 野放し |
wp-includes/pomo/feeds.php |
2013-07-10 | 野放し |
php5.php(公開ディレクトリ直下) |
2014-04-12 | 野放し |
4本。
検知ツールが捕まえていたのは、そのうち1本。
打率2割5分である。プロなら二軍だ。
しかも入居日が2013年7月10日に2件、その40日後に1件、さらに翌年に1件。定期的に増えている。増改築である。
隠れ方がうまい
wp-includes/pomo/ はWordPressの翻訳ライブラリで、正規の構成は6ファイル固定だ。
entry.php mo.php plural-forms.php po.php streams.php translations.php
ここに admin.php と feeds.php が混ざっていた。
コアの奥深く、10年間誰も開けない場所。ファイル名も「いかにもありそう」に寄せてある。屋根裏に住まれていた気分である。
なお feeds.php の中身は難読化されていて、eval を16進で書いて正規表現の /e 修飾子から呼ぶという凝った作りだった。
ただしこの修飾子、PHP 7で削除されている。
つまりこいつは、私と同じくPHPのバージョンアップに殺されていた。同志である。
一番ヤバかったのは「バックアップ」だった
backup/ というディレクトリがあった。2013年時点のスナップショットである。
そして私が自分で書いた引き継ぎメモには、こう書いてあった。
唯一の資産。削除しない
太字で。
開けたら、同じバックドアが3本、そのまま入っていた。
しかも公開領域である。
/backup/php5.php → HTTP 200
/backup/wp-includes/pomo/feeds.php → HTTP 200
/backup/wp-includes/pomo/admin.php → HTTP 200
全部生きていた。13年間、誰でも叩ける状態で。
考えてみれば当たり前だ。感染したあとに取ったバックアップは、感染ごと保存している。
そして厄介なのは、バックアップには「大事なもの」「消してはいけないもの」というラベルが貼られていることだ。だから誰も中を見ない。一番長く残る。一番安全そうな場所が、一番放置されていた。
直した瞬間、8年前のマルウェアを配信し始めた
コアを入れ替えて、サイトがようやく表示された。感動もつかの間、最新版に上げる前にバージョンを確認したら、こう出た。
name="generator" content="WordPress 4.5.13"
置いたのは5.9系である。
コアが巻き戻るわけがない。何かの見間違いかと3回確認した。
答えはHTMLの末尾に書いてあった。
</html><!-- Cache file was created on 29-03-18 22:48:26 -->
2018年3月29日に焼かれた静的HTMLを配信していた。
wp-content/ を丸ごと戻したので、2018年のキャッシュまで一緒についてきていた。キャッシュプラグインは「HTMLがあればPHPを通さず即返す」ので、律儀に8年前を再生していた。タイムカプセルである。誰も掘り起こしを頼んでいない。
問題はその中身だ。同じHTMLの末尾に、まったく無関係な外部サイト2つから jquery-update.php を読み込む <script> タグが刺さっていた。改ざんの定番で、ファイル名が無害そうなのも定番である。
数えたら40ファイル。
つまりサイトが復活した瞬間から、訪問者に配信していた。
直したつもりで、2018年の汚染をそのまま再現していた。復旧とは何かを考えさせられた。
ちなみに注入元はキャッシュではなく、現在有効なテーマの header.php と footer.php だった。キャッシュを消しても、次からはそこから出てくる。二段構えである。誰も頼んでいない。
濡れ衣も着せた
走査していたら、WordPressコアのファイルから医薬品スパムのキーワードが検出された。
一瞬、血の気が引いた。コアまで汚染されていたら話が全然違う。
中を見た。
general-template.php:13 * specialised header will be included.
spe-cialis-ed。
英単語である。
同じ理屈で、有名プラグインのボット判定コードが「クローラ回避の実装だ」として大量にヒットした。これも正規の機能である。
疑心暗鬼になった人間の目には、全部が怪しく見える。
そして、10年前から失敗し続けているものがあった
一番しみじみしたのはここだ。
復旧後、テーマから読み込んでいる外部スクリプトを全部叩いてみた。8件中3件は、参照先がもう存在していないか、httpsに対応していなかった。
内訳は、とっくにサービス終了した広告配信と、レンタルサーバが昔提供していたアクセスカウンタ。
2013年当時のパーツが、10年以上HTMLに書かれたまま、誰にも気づかれず、無言で失敗し続けていた。
ついでにWordPressと無関係なCMSの残骸も53MB出てきた。798ファイル。権限777。全開放。いつ置いたのか、まったく思い出せない。
放置していたサイトについて、私は「止まっている」と思っていた。誰も見ていないし、何も起きていない、と。
全然そんなことはなかった。
動いてほしくないものが4本動いていて、動いてほしいものが3本死んでいた。
放置は静止ではない。誰も見ていないところで、勝手に増えるものと勝手に壊れるものが、両方積もっていく。
犯人は分からない
正直に書く。
侵入経路は不明。 2013年当時のWordPressは3.x系で、プラグインも軒並み古かった。心当たりが多すぎて絞れない。容疑者が多すぎるという最悪の状況である。
10年間で何をされたかも不明。 WSOはファイル操作もDB操作もコマンド実行もできる。幸運にも管理者アカウントは正規の2件だけで、不正なユーザー追加は無かった。ただアクセスログは残っていないので、実際に何をされたかは追えない。
そもそも今回の500エラーの原因も不明。 ルートの .htaccess が、WordPress用ではない別フレームワークのテンプレートに書き換わっていた。
侵入者の仕業か、過去の自分の操作ミスか。
どちらもあり得るのが一番情けない。
検体は消さずに権限を落として保全した。証拠は残しておく。何の証拠になるのかは分からないが。
で、肝心のカーボベルデの記事はどうだったのか
そもそもの目的を思い出してほしい。ドヤ顔でリンクを貼ることである。
13年ぶりに自分の記事を読み返した。
2013年の私は、FIFAランキングを引いて「当時37位の日本より1つ上」と指摘し、アフリカネイションズカップ初出場でベスト8に入った実績を挙げ、フランスのリールでプレーする23歳の選手を「有望な選手だ」と紹介していた。
ここまでは良い。ファインプレーである。ドヤれる。
そしてこう書いていた。
実際ここ最近けっこー力をつけてきている国なのかもね。
「かもね」。
腰が引けている。13年後に本大会でスペインの27本のシュートを止める国に対して、「かもね」。
さらに悪いのはこの後だ。同じ記事の後半で、私はアフリカサッカーの組織運営のずさんさを延々と批判した挙げ句、こう締めていた。
世界の強豪をたびたび脅かす存在がポッとアフリカから飛び出してくるが、それらはあまり長持ちせず、すぐに沈没してしまう
カーボベルデはその後13年かけて、沈没せずにW杯本大会まで来た。
13年越しに、自分に反論された。
しかも、2013年の記事の主題は「カーボベルデが出場停止中の選手を起用してW杯予選を失格になった」件である。手続きの不備で本大会への道を自分で閉ざした国が、13年後に自力で初出場を勝ち取った。
そう考えると、記事の目の付け所は悪くなかった。書いた本人の見立てだけが外れていた。
結果
バックドア4本が稼働中 → 全て無害化(200 → 403)
テーマに注入スクリプト → 除去(配信0件)
2018年のマルウェア入りキャッシュ → 退避・再生成
公開領域に古いWordPress 6セット → 非公開領域へ退避(771MB)
データ損失はゼロ。記事294件は全行ハッシュ照合で一致した。守るものは守れた。
そして13年前の記事に、ようやくリンクが貼れる。
バックドア4本を駆除し、771MBを退避し、DBサーバを引っ越し、丸1日を溶かした。サッカーの記事1本にリンクを貼るために。
教訓は1行で済む。放置するな。
とはいえ放置していなければ、4人の同居人にも気づかなかったし、13年前の自分が腰の引けたことを書いていたことにも気づかなかった。そこは複雑である。
作業自体もそれなりに転げ回ったのだが、それは次の記事に書く。
参考
- 拙記事: カーボベルデ(Cape Verde)/2013年9月16日(※問題の記事)
- FIFA: アルゼンチン vs カーボベルデ ラウンド32 マッチレポート
- WordPress: サイトがハッキングされた場合の対処(公式ドキュメント)
- WordPress:
wp-includes/pomo/の正規構成(ソースリポジトリ) - WordPress: ドロップインとキャッシュディレクトリについて
この文章は原案・ディレクション:九頭龍、作文:AIで書いています。
