朋友的 WordPress 網站是五年前幫他架設在 AWS EC2 上,這五年來沒有特別的維護與升級,終於(?)在今年的一月被駭客入侵,網站出現一大堆亂七八糟的帳號和色情文章。於是開始拯救與升級 legacy 架構的挑戰,此篇分享整個遷移和升級過程,大家一起來看看吧!

原始狀況描述

  • 2018 年架設後就沒有再做特別的維護
  • EC2 是老舊的 Ubuntu 16.04
  • WordPress v4.9.3 和 php 7.0 fpm 直接安裝在 host 上,透過 nginx 作反向代理
  • AWS RDS 作為資料庫實例 ,運行老舊的 MySQL 5.7,有開啟自動備份
  • 圖片等上傳檔除了本機 EBS 有一份,S3 上也會 sync 一份
  • 今年一月時網站被駭客透過 WordPress 漏洞入侵,建立很多假帳號,並注入一堆有問題的程式碼和檔案
  • 因網站被注入,效能大幅下降導致 SEO 變差,從 google search console 中看到點擊率在一月被入侵後開始受到嚴重影響
拯救被入侵的 WordPress 網站!

拯救被入侵的 WordPress 網站!

緩解入侵

首先必須先緩解掉持續的暴力破解,先幫朋友申請 Cloudflare 並擋在 EC2 前面,再依照之前的文章 防範 WordPress 被攻擊的 8 個方法 設定好防火牆,開啟 on attack 模式,大量阻擋可疑 request。

修改成現代化架構

以目前 Web 架設概念應該要盡可能的讓資料與應用伺服器分離,也就是說應用伺服器應該是無狀態、隨時可替換的。並使用 docker,讓 application 跑在 container 裡面。運行環境跟著 container,某方面來說使 host 與 application 解依賴,因此 host 隨時可更換,不用擔心系統升級等問題。

但 WordPress 先天限制,User 上傳的圖檔放在 WordPress source code 目錄之中,若要將上傳的圖片檔案改放置 S3 上可能需要魔改 WordPress 或加裝套件。

比較簡單的作法是把使用者上傳相關的資料夾從 container 掛載出來 實現分離,並將掛載出來的資料 放在另一顆 EBS 上面 。未來若有需要,直接拔掉 EBS 接到另一台 EC2 上即可。

但你懂得,有時候會需要進去手改一下 wp-config.php 或其他主題相關的 source code,因此我會把 WordPress container 中整個 /var/www/html 掛出來,這樣要調整比較方便,不怕 container 移除後重啟導致資料不見。

拯救被入侵的 WordPress 網站!

另外擋案上傳和 php 運行的設定需要調整,因此把 upload.iniphp.ini-production 也掛載出來。

   wp:
     depends_on:
       - db
     image: wordpress:5.0.3-php7.3-fpm
     container_name: bgs_wp
     volumes:
       - ./wp_html:/var/www/html
       - ./php_cfg/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
       - ./php_cfg/php.ini:/usr/local/etc/php/php.ini-production
     networks:
       wp:
     restart: always
     logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "3"
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: <user>
       WORDPRESS_DB_PASSWORD: <user>
       WORDPRESS_DB_NAME: <db>

資料庫改使用 docker 的方式啟動,將 /var/lib/mysql 資料夾掛載出來(一樣掛到另一顆 EBS 上),讓資料庫檔案可以儲存在外部,避免 container 重建後消失。

同時建立定期備份 script,透過 host cronjob 觸發,將 db dump 到 /tmp/db_dump 再上傳至 S3。 後面會描述備份的實作方式。

這裡把 RDS 拿掉的原因是朋友的網站流量沒有大到需要開獨立的資料庫,透過 docker 全部起在同一台 EC2 上,雖然會喪失 RDS 自動備份功能,但卻能省下原本一半以上的 cost。同時自己建立簡單的備份機制並不難,因此這部份最後決定這樣作。

   db:
     image: mysql/mysql-server:8.0.22
     container_name: bgs_db
     volumes:
       - ./mysql:/var/lib/mysql
       - ./db_dump:/tmp/db_dump
     restart: always
     networks:
       wp:
     command: mysqld --default-authentication-plugin=mysql_native_password
     logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "3"
     environment:
       MYSQL_ROOT_PASSWORD: <pwd>
       MYSQL_DATABASE: <db>
       MYSQL_USER: <user>
       MYSQL_PASSWORD: <pwd>

nginx 的部分

  • /etc/nginx/conf.d 掛出來,提供 nginx 的設定檔
  • /etc/nginx/ssl 掛出來,提供 SSL key 和 cert
  • /var/www/html 和 WordPress container 掛到同一個目錄上,讓 nginx 可以 pass fastcgi 外,也能直接 host wp-contentwp-includes 的靜態檔
   nginx:
     depends_on:
       - wp
     image: nginx:1.23.3
     container_name: bgs_nginx
     restart: always
     ports:
       - "80:80"
       - "443:443"
     networks:
       wp:
     volumes:
       - ./wp_html:/var/www/html
       - ./nginx:/etc/nginx/conf.d
       - ./ssl:/etc/nginx/ssl
     environment:
       TZ: Asia/Taipe

三者結合起來的 docker-compose.yml 如下

version: '3'

services:
   db:
     image: mysql/mysql-server:8.0.22
     container_name: bgs_db
     volumes:
       - ./mysql:/var/lib/mysql
       - ./db_dump:/tmp/db_dump
     restart: always
     networks:
       wp:
     command: mysqld --default-authentication-plugin=mysql_native_password
     logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "3"
     environment:
       MYSQL_ROOT_PASSWORD: <pwd>
       MYSQL_DATABASE: <db>
       MYSQL_USER: <user>
       MYSQL_PASSWORD: <pwd>

   wp:
     depends_on:
       - db
     image: wordpress:5.0.3-php7.3-fpm
     container_name: bgs_wp
     volumes:
       - ./wp_html:/var/www/html
       - ./php_cfg/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
       - ./php_cfg/php.ini:/usr/local/etc/php/php.ini-production
     networks:
       wp:
     restart: always
     logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "3"
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: <user>
       WORDPRESS_DB_PASSWORD: <user>
       WORDPRESS_DB_NAME: <db>

   nginx:
     depends_on:
       - wp
     image: nginx:1.23.3
     container_name: bgs_nginx
     restart: always
     ports:
       - "80:80"
       - "443:443"
     networks:
       wp:
     volumes:
       - ./wp_html:/var/www/html
       - ./nginx:/etc/nginx/conf.d
       - ./ssl:/etc/nginx/ssl
     environment:
       TZ: Asia/Taipei

networks:
   wp:
     name: wp
     driver: bridge

方向有了,開始下手吧!

有了遷移的目標,接下來開始把 legacy 架構的 WordPress 搬過去,同時需要達到以下目標:

  • 駭客 inject 的 source code 不能帶過去
  • WordPress 要從 4.9.3 升級到 5.0.3 版 (目前套用主題最高支援版本)
  • 文章、圖片不能丟失

那就開始動手囉!

Step 1:準備好 new EC2,安裝 docker

這部份不特別描述,大致就是去 aws web console 開一台新的 EC2,更新系統、設定好防火牆、SSH key 和安裝 docker

Step 2:dump 資料庫與複製 uploads 資料

在 aws web console 新建一顆 EBS 並 attach 到 old EC2 上,再到 old EC2 中下指令 format 後掛載

# 確認新的 EBS 裝置名稱
$ lsblk
# 新增的 EBS 完全空白,需要分割並建立磁區
$ sudo mkfs -t xfs /dev/nvme1n1
# 建立 /data folder
$ sudo mkdir /data
# 將 EBS 掛到 /data
$ sudo mount /dev/nvme1n1 /data
# 建立 bgs 資料夾,等一下要搬移資料使用
$ sudo mkdir /data/bgs
# 改變資料夾權限
$ sudo chown ubuntu:ubuntu /data/bgs

透過 old EC2 連接 RDS,將 db dump 到 EBS 上。

$ docker run -it --rm -v /data/bgs/db_dump:/tmp/db_dump mysql:8.0.32  bash -c "mysqldump -h xxxx.xxxx.us-west-2.rds.amazonaws.com -u <user> -pxxxxxxx db > /tmp/db_dump/db_dump.sql --set-gtid-purged=OFF --column-statistics=0

因為我使用 MySQL 8 的 image 對 5.7 作 dump,若直接跑會看到以下錯誤

mysqldump throws: Unknown table 'COLUMN_STATISTICS' in information_schema (1109)

依照這篇討論,加上 --column-statistics=0 參數即可解決

接下來將 old EC2 中的 /var/www/html/wp-content/uploads 整個複製到 EBS 上

$ sudo cp -R /var/www/html/wp-content/uploads /data/bgs/

最後將 EBS 從 old EC2 上拔掉。先在 old EC2 上下 umount 後,再去 aws web console detach EBS。

$ sudo umount /data

Step3:restore 資料庫

在 aws web console 將 EBS attach 到 new EC2,並在 new EC2 中掛載。

# 建立 /data folder
$ sudo mkdir /data
# 將 EBS 掛到 /data
$ sudo mount /dev/nvme1n1 /data

因這顆 EBS 要一直給 new EC2 使用,需設定自動 mount after reboot。

首先查詢 EBS 的 UUID

$ sudo blkid
...
/dev/nvme1n1: UUID="680badd5-9f95-485a-b267-4b2aaddfd1bb" BLOCK_SIZE="512" TYPE="xfs"
...

修改 /etc/fstab 令 Ubuntu 開機時自動掛載 EBS

$ sudo nano /etc/fstab

在檔案最後一行增加以下這一行,儲存離開即可(請依照上一步查尋的 UUID 作替換)

UUID=680badd5-9f95-485a-b267-4b2aaddfd1b  /data  xfs  defaults,nofail  0  2

緊接著 docker 起 MySQL,restore 剛剛 dump 的資料

$ cd /data/bgs
$ docker compose up -d db
$ docker exec -it bgs_db bash -c "mysql -h localhost -u root -pxxxxxx -e \"CREATE DATABASE db;GRANT ALL PRIVILEGES ON db.* TO '<username>'@'%';\""
$ docker exec -it bgs_db bash -c "mysql -h localhost -u root -pxxxxxx db < /tmp/db_dump/db_dump.sql"

完成後,docker 再起 WordPress v4.9.3 + nginx,開啟瀏覽器進入網站。此步是先確定文章等資料有正常匯入,避免直接用 v5 的 WordPress 起可能遇到 migration 問題而不好確認 root cause 並排除。

$ docker compose up -d

此時網頁圖片消失是正常,因為我們還沒有把 uploads 資料夾放入正確路徑。

Step4:升級 WordPress

接下來要升級 WordPress 到 v5.0.3。必須先 down wp container, 並把 4.9.3 初始化的 wp_html 資料夾整個刪掉 ,再將 docker-compose.yml 中的 wp container 修改成 v5.0.3 ,up 啟動讓他重新初始化目錄。

# 移除 wordpress v4 container 
$ docker compose down wp
# 刪掉整個 wp source code
$ sudo rm -Rf wp_html
# 修改 docker compose 中 wordpress 的版本
$ nano docker-compose.yml
# 重新建立 wordpress v5 container
$ docker compose up -d

會這麼搞剛的原因是,Wordpress web 升級頁面預設只能 升級到最新版 ,以目前來說會是 v6.1.1,若要升級到指定版本,只能自己上傳原始碼並作替換。

思考了下,與其我手動上傳,不如清掉目錄檔案,並替換成對應版本的 image 讓他重新初始化來的方便。

替換完成後,瀏覽器再進入 WordPress 一次,此時出現需要 migrate db 提示,大膽給他按下去,不到五秒就會完成了。

升級一切順利,可以進入下一步。

Step 5:還原 uploads 資料

將剛剛從 old EC2 搬過來的 uploads 資料夾,移入 WordPress container 掛出來目錄中對應位置即可。記得修改權限成 www-data ,後續才能透過網頁上傳新的圖片

# 搬移 uploads 資料夾到正確位置
$ sudo mv uploads wp_html/wp-content/
# 修改權限,直接對整個 wp folder 下比較方便
$ sudo chown -R www-data:www-data wp-html

此時再重新進入網站圖片就能正確出現了!但外掛和主題沒有搬過來,此時頁面排版會全部亂掉!沒關係,只需要人工從 wordpress 後台重新安裝即可,如此也能避免將被駭客污染的程式碼帶過來。

清除駭客建立的假帳號

接下來要對假帳號作清理,但假帳號 (Subscriber) 多達一萬五千個!人工手動從 WordPress 後台刪除非常耗時

拯救被入侵的 WordPress 網站!

查了一下,Wordpress 有提供 cli 工具,能透過 user type 過濾出 subscriber 直接作 batch delete。

但 WordPress image 預設沒有安裝 cli 工具,難道還要自己進去安裝?其實不用, WordPress 官方 還有另外推出 tag 為 cli 開頭的 docker image,可以直接起他呼叫指令。

為了後續方便維運操作,簡單寫一個 wp_cli.yml 專門起 wordpress cli container,並將 command 用 sleep infinity override,讓 container 啟動後可以活著但不做事,方便進入裡面下指令操作。

version: '3'

services:
   wp-cli:
     image: wordpress:cli-2.5.0-php7.3
     container_name: bgs_wp_cli
     volumes:
       - ./wp_html:/var/www/html
     networks:
       wp:
     restart: always
     command: sleep infinity
     logging:
      driver: "json-file"
      options:
        max-size: "1m"
        max-file: "3"
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: <user>
       WORDPRESS_DB_PASSWORD: <pwd>
       WORDPRESS_DB_NAME: <db>

networks:
   wp:
     name: wp
     driver: bridge
     external: true

進入 wordpress cli container 開始大量刪除假帳號吧!加參數 --number=10 先刪 10 個確認下,沒問題後拔掉參數一口氣全部刪除!

# at host
$ docker exec -it wp-cli bash
# in container
$ wp user delete $(wp user list --role=subscriber --field=ID --number=10) --reassign=1
拯救被入侵的 WordPress 網站!

刪除可能需要一小段時間,因為他是 一筆筆刪除 user ,沒有使用 bulk delete,刪除量越大越耗時

建立自動化備份

最後要建立自動化備份,首先是 db backup,一樣用 mysqldump 將 db dump 出來, tar 壓縮後,透過 aws cli container sync 到 s3 上,最後再清掉本機上的備份檔。

#!/bin/bash

docker exec -it bgs_db bash -c "mysqldump -h localhost -u root -pxxxxx db > /tmp/db_dump/db.sql"

cd /data/bgs/db_dump/
sudo tar zcvPf /data/bgs/db_dump/db_dump$(date '+%Y%m%d').tar.gz db_dump.sql

docker run --rm -it -v /data/aws:/root/.aws -v /data/bgs/db_dump:/db_dump amazon/aws-cli s3 cp /db_dump/db_dump$(date '+%Y%m%d').tar.gz s3://<bucket>/backup/db/

sudo rm /data/bgs/db_dump/db_dump.sql
sudo rm /data/bgs/db_dump/db_dump$(date '+%Y%m%d').tar.gz

再來是備份 uploads 裡的檔案,一樣透過 aws cli container sync 到 s3

#!/bin/bash
docker run --rm -it -v /data/aws:/root/.aws -v /data/bgs/wp_html/wp-content/uploads:/uploads amazon/aws-cli s3 sync /uploads s3://<bucket>/backup/uploads

將以上兩份 script 透過 crontab 定期執行來達成自動化備份

看看成效吧

自此搬移與升級就算完成了,清理掉駭客 inject 的程式碼並升級到 v5 後,本來首頁的 TTFB 高達 3.33 s,現在只需要 0.6s,下降了 80%

從 google search console 中能看到修復後點擊率開始回升

拯救被入侵的 WordPress 網站!

但 WordPress 5.0.3 還是很舊,因此後續需要朋友去 survey 合適的主題,替換後再作一次 WordPress 版本升級。

至此分享整個搬遷和升級過程,希望能幫助到有類似需求的人!

延伸閱讀

防範 WordPress 被攻擊的 8 個方法
WordPress 部落格被攻擊全記錄

Written by J
雖然大學唸的是生物,但持著興趣與熱情自學,畢業後轉戰硬體工程師,與宅宅工程師們一起過著沒日沒夜的生活,做著台灣最薄的 intel 筆電,要與 macbook air 比拼。 離開後,憑著一股傻勁與朋友創業,再度轉戰軟體工程師,一手扛起前後端、雙平台 app 開發,過程中雖跌跌撞撞,卻也累計不少經驗。 可惜不是那 1% 的成功人士,於是加入其他成功人士的新創公司,專職開發後端。沒想到卻在採前人坑的過程中,拓寬了眼界,得到了深層的領悟。