概要
自前のサーバーでブログを開設した途端、世界中から怪しい攻撃が来た話を読んで、自サーバのログも調べてみたら同様のクラッキング目的と思われるアクセスが来ていたことが確認できました。
その中で、phpMyAdminのユーザ名とパスワードを当てずっぽうで当てにきている攻撃がたくさんあったので、それらについて攻撃された回数をカウントし、ランキングしてみました。
解析
さくらVPSで契約したサーバ(CentOS 6.10)のApache HTTPサーバのログ(/var/log/httpd/access_log)を取得しました。
中身を見てみると下記のようにphpMyAdminに当てずっぽうのユーザ名とパスワードでアクセスが試みられていることがわかります。
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:57:44 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz3edc5tgb&server=1 HTTP/1.1" 200 14538 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:57:46 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@123&server=1 HTTP/1.1" 200 14546 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:57:59 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@2wsx&server=1 HTTP/1.1" 200 14533 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:58:03 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@WSX&server=1 HTTP/1.1" 200 14543 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:58:06 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@WSX3edc&server=1 HTTP/1.1" 200 14538 "-" "Mozilla/5.0"
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:58:09 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@wsx&server=1 HTTP/1.1" 200 14533 "-" "Mozilla/5.0"
pythonを用いてこれらの情報を抽出しヒストグラム情報を作りました。
pythonコードは末尾に付録として載せておきます。
解析対象ログの基本情報
取得期間:2018/09/26 ~ 2020/01/22
全体の行数:658858行
うちphpMyAdminのパスワードを当てずっぽうで当てにきている攻撃と思われるもの:186499行
ユーザ名の攻撃回数(全25種類)
順位 | ユーザ名 | 回数 |
---|---|---|
1 | root | 185504 |
2 | wordpress | 183 |
3 | admin | 138 |
4 | wp | 47 |
5 | blog | 45 |
6 | pma | 45 |
7 | shop | 43 |
8 | money | 42 |
9 | popa3d | 40 |
10 | joomla | 39 |
11 | http | 35 |
12 | ueer | 35 |
13 | project | 35 |
14 | nginx | 33 |
15 | apache | 33 |
16 | sql | 33 |
17 | db | 32 |
18 | nas | 32 |
19 | shopdb | 31 |
20 | dbs | 31 |
21 | web | 28 |
22 | backupdb | 6 |
23 | wordspress | 5 |
24 | backup | 2 |
25 | backups | 2 |
パスワードの攻撃回数(全953種類のうち上位30種類)
順位 | パスワード | 回数 |
---|---|---|
1 | pass | 408 |
2 | password | 372 |
3 | 361 | |
4 | admin | 359 |
5 | 123 | 347 |
6 | root | 344 |
7 | 123456 | 326 |
8 | welcome | 323 |
9 | r00t | 322 |
10 | monkey | 322 |
11 | whatever | 322 |
12 | abc123 | 321 |
13 | aa123456 | 321 |
14 | 123123 | 319 |
15 | mysql | 318 |
16 | login | 318 |
17 | 111111 | 318 |
18 | password123 | 318 |
19 | 1234567890 | 317 |
20 | access | 317 |
21 | 666666 | 316 |
22 | apache | 315 |
23 | oracle | 315 |
24 | 654321 | 315 |
25 | root123 | 315 |
26 | 123qwe | 314 |
27 | 1234567 | 314 |
28 | 12345678 | 314 |
29 | pass123 | 314 |
30 | letmein | 313 |
(ついでに)攻撃されたパスとその回数(全2種類)
順位 | パス | 回数 |
---|---|---|
1 | /phpmyadmin/index.php | 173687 |
2 | /phpMyAdmin/index.php | 12812 |
考察
全ランキングはこちらにおいておきます。
ユーザ名はrootが圧倒的多数で、ほかはwordpress、blogなどのブログ関連用語、admin、pmaなどデータベース関連用語、apacheなどサーバ関連用語などが並んでいる感じでした。
sshサーバへの攻撃ログを解析した記事と比べるとsshへの攻撃では人名やOS名などが狙い撃ちされているのに対し、phpMyAdminへの攻撃ではブログ関係用語が狙われているなど、微妙に傾向の違いがあって面白いです。
パスワードのトップ3は「pass」「password」「」(なし)でした。
突出してどれかが多いということはなくありがちなものが全体的にまんべんなく狙われている感じでした。
こちらで言われているような「最悪なパスワード」の傾向とも一致しているように思えます。
パターンとしては下記のようなものがありました。
- 有名名詞(PC関連) - root、mysql、access、apahe、oracleなど
- 有名名詞(一般) - welcome、monkey、freedom、money、dragonなど
- 数字羅列 - 123456、123123、111111、1234567890、666666など
- 「password」またはその派生 - pass、password、Password、passw0rd、p@$$w0rd123など
- キーボード順番押し - qwerty、qazwsx、qazxcv!@#、zaq1zaq1、qwertなど
- 上記の組み合わせ - abc123、aa123456、password123、123qwe、admin123など
ついでに攻撃対象となったパスも集計しました。
攻撃対象となっていたのは以下の2種類だけでした。
- /phpmyadmin/index.php
- /phpMyAdmin/index.php
不正アクセス防止のための狙われるアクセス先リスト
や
狙われやすいURLについて
の記事で言及されていたパスと比べるとバリエーションが少ないのは、今回が"pma_password="が含まれているという条件で行を抽出したあとの結果であるためと考えられます。実際抽出条件を「"phpMyAdmin"が含まれていること」にしてログを眺めてみると、今回挙げた2種類以外のパスへのアクセスも試みられていることがわかりました。
おわりに
僕はセキュリティの知識はほぼないので「どうしたら安全か」はわかりませんが、少なくともデフォルトパスにおいたデータベースにありがちなユーザ名とパスワードをセットで使うのはやめようと思いました。
本文は以上です。
ここまでお読みいただきありがとうございました。
以下は付録です。
付録
ログから必要情報を抽出するためのpythonコードです。
今回は下記のような行を解析対象としました。
xxx.xxx.xxx.xxx - - [04/Oct/2018:02:58:03 +0900] "GET /phpMyAdmin/index.php?pma_username=root&pma_password=1qaz@WSX&server=1 HTTP/1.1" 200 14543 "-" "Mozilla/5.0"
"pma_password="が含まれていれば解析対象とみなします。
ここからスペースや特定の文字、キーワードで分割するなどして、情報を抽出します。
情報とその取り出し方の例を示します。
ここはログファイルの書き出しの設定などによって異なる可能性があるので適宜修正してください。
情報 | 取り出し方 |
---|---|
ipアドレス | スペースで分割して0番目 |
タイムスタンプ | "["と"]"で囲まれた文字列 |
Method (GET, POSTなど) | スペースで分割して6番目。ただし初めの文字がダブルクオテーションなのでそれは削除する |
phpMyAdminのパス | スペースで分割されたうち7番目の要素から"?"の直前まで |
ユーザ名 | "pma_username="と"&"の間の文字列 |
パスワード | "pma_password="と"&"の間の文字列。(ただしこの部分のqueryでurlが終了している場合があり、そのときは後ろの"&"が存在しないため、スペースの直前までとする) |
全部の行がこのやり方で抽出できているとは限らないのですが、多少例外があっても影響は少ないだろうと思って無視しています。そのあたりは適当です。
要素を取り出したあと、注目する情報(今回はphpMyAdminのパス、ユーザ名、パスワード)の回数を数えます。
最後に降順にソートして標準出力に表示します。
ソースコード全体は下記のような感じです。
from collections import defaultdict
with open('/path/to/access_log','r') as f:
logs = f.readlines()
# phpMyAdminへの攻撃の抽出
pma_attacks = [log for log in logs if 'pma_password' in log]
# ip, time_stamp, method, path, username, passwordを抽出
# ログの形式に合わせて適宜書き換える
extracted_pma_infos = []
for pma in pma_attacks:
#ipアドレスはスペース区切りの0番目の要素
ip = pma.split(' ')[0]
#タイムスタンプは"["から"]"までの文字列
time_stamp = pma.split('[')[1].split(']')[0]
#Method(POST、GETなど)はスペース区切りの6番目の要素から1文字目を削除したもの
method = pma.split(' ')[5][1:]
#pathはスペース区切りの6番目の要素のうち"?"の直前までの文字列
path = pma.split(' ')[6].split('?')[0]
#ユーザ名は"pma_username="から直近の"?"までの文字列
username = pma.split('pma_username=')[1].split('&')[0]
#パスワードは"pma_password="から直近の"?"までの文字列
password = pma.split('pma_password=')[1].split('&')[0]
#パスワードが最後のクエリの場合の例外処理
#このとき、直近の"?"が存在せずログの末尾まで含まれてしまうので、スペースの直前できる
if ' ' in password:
password = password.split(' ')[0]
extracted_pma_infos.append([ip,time_stamp,method,path,username,password])
# 攻撃されたパス、ユーザ名、パスワードのヒストグラムを作る
pathlist = defaultdict(int)
unlist = defaultdict(int)
pslist = defaultdict(int)
for pma in extracted_pma_infos:
path = pma[3]
un = pma[4]
ps = pma[5]
pathlist[path]+=1
unlist[un]+=1
pslist[ps]+=1
# 引数のヒストグラムデータ(dict_obj)を解析して降順のリストを作る関数
def orderedlistFromDict(dict_obj):
count = list(set([dict_obj[v] for v in dict_obj]))
count.sort()
orderedlist = []
for c in count[::-1]:
for key in dict_obj:
if dict_obj[key] == c:
orderedlist.append((key,dict_obj[key]))
return orderedlist
# 攻撃されたパス、ユーザ名、パスワードの回数順のリスト
ordered_path = orderedlistFromDict(drlist)
ordered_un = orderedlistFromDict(unlist)
ordered_ps = orderedlistFromDict(pslist)
# パスランキングの表示
for val in ordered_path:
print(val[0],val[1],sep=',')
# ユーザ名ランキングの表示
for val in ordered_un:
print(val[0],val[1],sep=',')
# パスワードランキングの表示
for val in ordered_ps:
print(val[0],val[1],sep=',')