不正アクセスログ集計ツール②

不正アクセスに対する対応は当サイトでは悪質な(連続で何度もログインアタック攻撃をするなど)場合、手動でblocklist(ufwの場合、/etc/ufw/before.rules)に放り込むことにしている。そのため集計対象としてはブロックリストに含まれるIPアドレスは対象外としたい。よってapacheユーザーでブロックリストを読み込む必要があるが、こちらのファイルはroot権限でしか開くことが出来ない。apacheユーザーを管理者グループに入れることも考えたが、ここはひとつ。

  1. root権限でブロックリストを読み込むコマンドを作成する(getblocklist)。
  2. apacheユーザーに getblocklistコマンドだけパスワードなしでsudo出来るようにする。
  3. apacheユーザー権限で動作するpythonプログラム(djangoなど)でsubproccessにより上記コマンドを実行し、結果をリストとして得る関数を作る

で、バッチリではないかと、この方針で進めることとする。

1.root権限でブロックリストを取得するコマンド

これは全く問題なしで、以前のblockset.pyより該当部分のみを抜き出したプログラムとなる。

getblocklist.py

ipアドレスを利用するクラスipv4addressはライブラリとして分離している。

2.apacheユーザーに実行権限付加

表示プログラムを/usr/local/sbinなどに置きapacheユーザー(www-data)にsudo実行権限を与える

www-data        ALL=(ALL)       NOPASSWD:/usr/local/sbin/getblocklist.py

www-dataパスワード未設定のsu不可ユーザーなのでsudo ALLでも他のコマンドは実行出来ない。

3. apacheユーザー権限でリストを取得する

subproccessモジュールを利用して標準出力を得るには、Popenでプロセスを起動してプロセスの標準出力を接続する必要がある。プロセスのstdoutは一行づつ取り出すイテレータとなるので、mapによりipv4adrsetのリストとして取得している。(また、なぜかプロセスの出力がバイトとなっているので、UTF-8で文字列にデコードする必要があった”x.decode('UTF-8').rstrip('\n')”)

import subprocess

get_blocklist_proccess_cmd = ['sudo','/usr/local/sbin/getblocklist.py']

# root権限でブロックリスト取得コマンドを走らせて標準入力を得る
def get_blocklist():
    proc = subprocess.Popen(get_blocklist_proccess_cmd, stdout=subprocess.PIPE)
    bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
    return bipset

良く分かっていなかったのだけど、関数からmapのまま返すとイテレータなので関数の外にでてもプロセスが終わってないのね。。listとして全部取得してプロセスを正常に終らせておく必要がありましたとさ。

不正アクセスログ集計ツール①

目的

不正アクセスのログを集計してポータルの管理ページに結果を表示する

不正アクセスの定義

今回の場合はWEBページの認証機構へのログインアタックとする。パスワードを知っていて入って来てしまう場合は対象外。この場合はログイン履歴表示機能で様子をみることが可能とする。

機能

  • djangoポータルページの管理グループのみ表示(ページへのリンクも管理グループのみに表示)
  • django認証ログからの集計と、Wordpressのプラグイン(User Login History)の認証履歴データベーステーブルからの履歴も取得し、合計として集計する。
  • 集計開始日時ー終了日時、リストアップするための認証失敗回数(5回など)設定。
  • 特定のIPアドレスのアクセス詳細を表示ー上記リスト表示にボタン追加

その他機能追加として、直接関係は無いが

  • エラー404などのページカスタマイズ
  • djangoいろいろ勉強用テストページ

現在の所、最終的な完成形は上記のような仕様になるが、管理ページの追加なので訪問者には何も関係が無いです。その他思いついたら仕様追記する予定。

開発に関しては、djangoのプロジェクトディレクトリをgit管理とし、GitHubプライベートリポジトリ経由でsshログインユーザーにclone。VScodeで編集してテストブランチとしてpush。apacheユーザーでプロジェクトディレクトリ(テスト用仮想サーバー)にpullして動作試験。デバッグを行い完成形(公開レベルになった段階)をmasterブランチにmergeしてGitHubにpush。公開用プロジェクトディレクトリにpullして運用するという流れ。

単純にapacheユーザーのプロジェクトをVScodeのssh-remoteで編集したいという欲求のため、このような複雑は構造となってしまった。GitHubの使い方の勉強できて丁度良いか。

djangoチュートリアルメモ

基本的にはdjangoサイトのチュートリアルをそのままトレース。やった事だけをメモしていこうかと。。。

  • 仮想環境への移行 source bin/activate
  • djangoインストール : pip install Django
  • バージョン確認:python -m django –version → 3.2.4
  • プロジェクト作成:django-admin startproject mysite
  • とりあえずテストサーバー起動:python manage.py runserver 0:8000
    外部からアクセスするため(ただしファイヤーウォールで接続制限をかけること)
  • 「Invalid HTTP_HOST header」エラーが出るので、setting.pyを修正
    ALLOWED_HOSTS = [‘*’]
    デフォルトでは空になっているので、自サイト以外からはアクセスできないようだ
  • チュートリアルに沿ってアプリケーション作成
    python manage.py startapp polls
  • ここまで作った状態で、プロジェクト下とライブラリのdjango下をそれぞれgitの管理化に入れて、pythonの勉強を兼ねて、動作の仕組みを解析する予定。

loggerの件(pythonモジュール)

プログラムのデバッグや各種情報を出力するのにprintを使用したり、エラー出力のためにsys.stderr.writeを使った板が、今後djangoなどWEBアプリ開発を念頭に入れて、loggingモジュールを使用できるよう勉強していきたい。

ここのページを参考にすると、なるほどいつもはモジュールをまんまimportしているが、それだとインスタンスを生成せずに使用してしまう可能性もある。なるべく使用するのみをimportするような習慣をつけておいたほうがよさそう。

×import logging
×logger = logging.getLogger(__name__)

〇from logging import getLogger, StreamHandler, DEBUG, INFO
〇logger = getLogger(?__name__)

あとはおまじないの様に、下記コードを追加して出力先とレベルを設定するとよいそうだ

logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBEG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

#
#
logger.info('inform')
logger.error('error occerd')

setLevelは2か所あるので、引数をDEBUGなどを変数に格納して、一括で変えられるようにしてもいいかもしれない。

勉強がてら、上記修正をblockset.pyに適用して、sys.stderr.write()の部分をlogger.info()に変更。動作自体は特に変化はないが、ちゃんと作ってる感じが良いかも・・・
日に日に、情報コピペの覚書投稿になりつつある・・・・

python環境整備

ubuntuのインストール直後はpythonコマンドはpython2.7系、3.8系のpythonを使いたければpython3と打たなければならなかった。まぁ仮想環境を構築すれば良いんだけど、サーバー設定環境等全ユーザーで3系を使いたいので、対策した時の覚書き

ググったサイトを参考に、update-alternativesコマンドを使用してデフォルトの起動バージョンを3系とした。

root@server:~# update-alternatives --config python
There are 2 choices for the alternative python (providing /usr/bin/python).

  Selection    Path                Priority   Status
------------------------------------------------------------
  0            /usr/bin/python3.8   2         auto mode
  1            /usr/bin/python2.7   1         manual mode
* 2            /usr/bin/python3.8   2         manual mode

Press <enter> to keep the current choice[*], or type selection number:

こんな感じでpython3が選択されている

ここで、勉強は各種実験用に個人ディレクトリにpython仮想環境を構築(venvを使用)

xxxx@server:~$ python -m venv venv
xxxx@server:~$ cd venv/
xxxx@server:~/venv$ ls
bin  include  lib  lib64  pyvenv.cfg  share
xxxx@server:~/venv$ source bin/activate
(venv) xxxx@server:~/venv$ ls
bin  include  lib  lib64  pyvenv.cfg  share
(venv) xxxx@server:~/venv$ which python
/home/xxxx/venv/bin/python

まずはpipのアップデート

(venv) xxxx@server:~/venv$ pip install -U pip
Collecting pip
  Downloading pip-21.1.2-py3-none-any.whl (1.5 MB)
     |████████████████████████████████| 1.5 MB 4.0 MB/s
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 20.0.2
    Uninstalling pip-20.0.2:
      Successfully uninstalled pip-20.0.2
Successfully installed pip-21.1.2

あとは色々モジュールを入れたりすれば良いですが、折角なので最新バージョンを使用出来るようにと

こちらを参考にpyenv仮想環境を構築した。

git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cd ~/.pyenv
git pull

下記のように環境変数を設定するよう.profileを書き換えて再ログイン

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init --path)"' >> ~/.profile

pyenv install -l | grep 3.9
  3.9.0
  3.9-dev
  3.9.1
  3.9.2
  3.9.3
  3.9.4
  3.9.5

最新は3.9.5なのでこれをインストール

pyenv install 3.9.5

なんか色々エラーが出たらコンパイルのためのライブラリが足らんようです。当方の場合はbz2が足りなかったので、

sudo apt install -y libbz2-dev libsqlite3-dev

あとググって足らなさそうなものも入れておくと、無事インストール終了

pyenv global 3.9,5   -----  3.9,5 を使用する
pyenv global system  -----  システムにインストールされているバージョンを使用

python -V

バージョン確認して無事変更されていたら、さらにvenvによる仮想環境も構築しておく

python -m venv venv
cd venv
source bin/activate
python -V

deactivate

こっちの方がライブラリが探しやすいが、pyenvで最新版のpythonが使う環境も欲しいということでの2本立てとしてみた。ライブラリ何度も入れないといけなさそうだけど、色々体で覚えていけそうw

サーバー設定作業メモ

何をやったか忘れないように、メモメモ記事を残すことにした。これも投稿数稼ぎかなw

ポータルサイト http://www.marochanet.org/ の djangoアップデート作業

ウェブユーザでpython仮想環境に入り、まずはpipのアップデート

(venv0) xxx@jagha:~/html/python$ python -m pip install --upgrade pip

次に仮想環境のアップデート可能なモジュールリストをとる(一部表示)

(venv0) xxx@jagha:~/html/python$ pip list -
Package                Version Latest Type
---------------------- ------- ------ -----
……
Django                 3.0.8   3.2.4  wheel
django-bootstrap4      2.2.0   3.0.1  wheel
django-hitcount        1.3.2   1.3.3  wheel
django-tinymce         3.0.2   3.3.0  wheel
……

他にもリストアップされたモジュールを全てアップデートする

(venv0) xxx@jagha:~/html/python$ pip install -U (module name)

ここで、リストには無いがデータベースエンジンとの接続に必要なmysqlclientのアップデートに失敗。

これに関してはpython側のライブラリが不足していたため下記コマンドをrootで打って、ライブラリをインストールする

apt-get install python3-dev libmysqlclient-dev

無事mysqlclientのアップデートに成功して終了。

メールボックス整理ツール

本サーバーでは個人の興味や勉強を兼ねてメールサーバーを運用している。昔はプロバイダメールはPOPで端末に落としてくるのが主流で、例えば家と職場、携帯電話で同じ受信メールを処理することが難しかったし、Exchangeを始めIMAPも使われ始めたが、サーバー負荷が高いため普通のプロバイダでは利用できない場合が殆どだった。今ならフリーのアカウントなどWEBメールを初めとして受信メールをサーバーを置くタイプが多くなってきた。

私が使用しているプロバイダメールも今はWEBメール対応でサーバーにメール本体を置くことが出来るが、やはりPOPがデフォルトのようだ。そこで、まだPOPしか使えない頃から、外部にサーバーを立ち上げて(一昔流行ったおうちサーバーや、現在の当方のような「さくらVPS」)プロバイダからのメール自動転送(昔はPOP)を受けてIMAPサービスを行っている。

サーバーの引越においてもメールデータの移行も必要であった。引越ついでにサーバー上のユーザーのメールボックス形式をmboxからIMAPとの親和性の高いMaildir形式に移行する。二つの形式には大きな違いがあり、

  • mboxのメールボックスは巨大な1ファイル、Maildirはメール1通1ファイル
  • mboxのメールボックスフォルダ階層はファイルシステムに準ずる、Maildirはフォルダの階層はピリオドで接続した構造で表現され、ファイルシステム的には一階層である
  • mboxの受信トレイは/var/spool階層のユーザー名ファイル、Maildirは各ユーザーディレクトリのMaildir直下に配信される
  • どちらも漢字フォルダ名はutf-7で、端末では普通読めない。

そのあたりの作業を簡単に行えるような行き当りばったりツールをまたpythonで作成した。pythonのmailboxライブラリを使用するとmbox形式やMaildir形式のメールボックスを操作することが出来、相互に移動したりフォルダに振り分けたり出来る。というライブラリお任せで作成したツール「mailboxutil」についてまた追々と解説・覚書きを投稿したいと思う。

このツールをつかってメールボックス移行を行ったのは実際には前のサーバーで約1年前CentOS6からCentOS8にOS変更した時でした。まぁ、話がややこしくなるので、今回やったことにさせてくださいw
ていうか、このタイミングでCentOSサポート終了のお知らせは正直凹みました。。。時間を返せーー!と。
(CentOS同士でもメジャーバージョンを一個飛ばしということもありアップグレードインストールは諦めて、バックアップ→新規インストール→データリストアをやっていたのでした。。。)

gzipモジュール組込み

主にSMTPへの不正アクセスを監視してファイヤーウォールにブラックリストを追加するpythonプログラム(blockset.py)はサーバー引越前より使用していたが、それまでに集計した不正アクセスIPアドレスリストは引越後も使用して対応していた。

リスト自体は5年くらい前からのものでかなり膨らんで来たしもう既に状況は変わっているかも知れないため、サーバーIPアドレスも変わったという機会もあり最初から収集を始めることとし、一度ブロックリストを空にしてみた。2~3日は特におかしなアクセスは無く油断していたら、夕べ辺りに大量にSMTPへログインアタックがいらっしゃいましたので下記に晒します。

root@jagha:~# blockset.py -u
use defaule.
New record : 31.210.20.7 (13)
New record : 31.210.20.18 (10)
New record : 31.210.20.23 (11)
New record : 31.210.20.39 (15)
New record : 31.210.20.41 (6)
New record : 31.210.20.54 (7)
New record : 31.210.20.93 (7)
New record : 31.210.20.109 (9)
New record : 31.210.20.111 (14)
New record : 31.210.20.155 (13)
New record : 31.210.20.162 (15)
New record : 31.210.20.173 (11)
New record : 31.210.21.3 (6)
New record : 31.210.21.4 (6)
New record : 31.210.21.9 (6)
New record : 31.210.21.76 (5)
New record : 31.210.21.82 (13)
New record : 31.210.21.96 (12)
New record : 31.210.21.195 (15)
New record : 31.210.21.220 (15)
New record : 31.210.21.235 (6)
New record : 31.210.21.242 (7)
New record : 31.210.21.243 (12)
New record : 45.133.1.22 (15)
New record : 45.133.1.50 (11)
New record : 45.133.1.58 (7)
New record : 45.133.1.73 (13)
New record : 45.133.1.100 (13)
New record : 45.133.1.102 (14)
New record : 45.133.1.109 (14)
New record : 45.133.1.127 (11)
New record : 45.133.1.138 (8)
New record : 45.133.1.192 (15)
New record : 45.133.1.203 (8)
New record : 45.133.1.204 (8)
New record : 45.133.1.206 (13)
New record : 45.133.1.214 (15)
New record : 45.133.1.218 (15)
New record : 45.133.1.239 (15)
New record : 45.133.1.241 (12)
New record : 45.133.1.242 (12)
New record : 45.133.1.247 (8)
New record : 45.144.225.54 (15)
New record : 45.144.225.95 (13)
New record : 45.144.225.204 (9)
New record : 45.144.225.205 (7)
New record : 45.144.225.206 (7)
New record : 136.144.41.70 (7)
New record : 136.144.41.87 (12)
New record : 136.144.41.132 (6)
New record : 136.144.41.162 (8)
New record : 136.144.41.166 (8)
New record : 136.144.41.181 (15)
New record : 136.144.41.209 (8)
New record : 195.133.40.31 (8)
New record : 195.133.40.34 (15)
New record : 195.133.40.41 (7)
New record : 195.133.40.63 (7)
New record : 203.159.80.60 (12)
New record : 203.159.80.61 (15)
New record : 203.159.80.77 (6)
New record : 203.159.80.190 (12)
update ufw before.rules
Firewall reloaded

こちらをネットマスク24ビットでくるめると、意外に同じノードからのご来店が多く、

31.210.20.0/24  <=  31.210.20.0
31.210.21.0/24  <=  31.210.21.0
45.133.1.0/24  <=  45.133.1.0
45.144.225.0/24  <=  45.144.225.0
136.144.41.0/24  <=  136.144.41.0
195.133.40.0/24  <=  195.133.40.0
203.159.80.0/24  <=  203.159.80.0

これだけになったので、この辺りの方々が迷惑なドメインということで一括ブラックリストとした

あと、もう一点、先日のlessコマンドにならってgzip圧縮されたログファイルも普通に解析できるように仕様追加した。cron起動されるlogrotateでまだ解析していないログファイルが圧縮アーカイブされてしまう可能性がある。

pythonのgzipモジュールを使用すると通常ファイルと同じように普通に読み書き出来るファイルハンドルが生成される。with構文を使用してファイル操作を行っている部分を下記にように変更した。

import gzip
import re

・・・・
    with open(args.logfile, 'r', encoding='utf-8') if not re.search('\.gz$', args.logfile) else gzip.open(args.logfile, 'rt') as fh:
        for line in fh:
            ・・・・

with構文の中でインライン条件分岐を行い、末尾が’gz’だった場合はgzipでオープンするという内容。

この方法、他の用途のファイル読み込みでも応用が考えられる。もう一つ分岐を増やして、ファイル名が空白であった場合は標準入力(sys.stdin)とするなども出来そうだ。

ブロックリスト自動生成(番外)

前回のPythonプログラムの補足説明というか、WEBページで図を使って説明するための練習の様な投稿になります。WEBで表示出来るオブジェクトとしては通常段落と画像が主なものになりますが、画像はビットマップ形式が多く説明用の図には向かないかなと思っていたが、よくよく調べるとHTML5からベクタ図形の表示が出来るようになってるんですね。ほうほう。

ということで、SVGについて少し勉強してみることにした。中身はjavascriptのようで、WEBに貼付けるときは普通に画像扱いで良いとか。作成用のツールはInkscapeというフリーソフトがあるので早速インストールして使用してみた。

illustratorとかと同じように使えそうなスグレモノだけど、あいにく自分はillustrator系は使い慣れていないので、一から勉強するよりは、既存のドローソフトで簡単に描いたものをSVGに変換して貰うだけでも便利そうだ。

簡単なものならWMF(ウィンドウズメタファイル)、EMF(拡張メタファイル)も読み込めるのでBschとかで描いた回路図も変換出来そう。

ということで前回のPythonネタの続き

msks = list(map(lambda n: int('1'*n+'0'*(8-n), 2), [8 if (mask - i*8) >= 8 else (mask - i*8) if (mask - i*8) > 0 else 0 for i in range(4)]))

これは、整数値のネットマスク幅を、IPアドレスリストと同じ形式のマスクビット配列に変換する1文となる。(例えば23 → 11111111.11111111.11111110.00000000

[8 if (mask - i*8) >= 8 else (mask - i*8) if (mask - i*8) > 0 else 0 for i in range(4)]

これが1から32までの数字mskに対して、8ビットずつ4分割し、足りない分は余りと、ゼロとする構文。mskを8で割って、配列の上から「商」の数だけ8として、次に「余り」残りをゼロとする。

map(lambda n: int('1'*n+'0'*(8-n), 2), LIST)

上記配列の要素に対し上位を1で埋めたビット表記に変換して、2進数で数値化

例)7 → 11111110 → 0xFE

これを図で説明すると下図の様になる

なんか余計に分かりにくくなったような。まぁ今回は自分で思い出せれば良いか~くらいのつもりでまとめておきます。今後のツール活用に期待で。

ブロックリスト自動生成その3(素人のサーバー管理)

自動生成プログラムの機能はオプション‘-h’を付けると確認することが出来る。この辺は普通のコマンドラインプログラムっぽく作ってみました。

root@xxx:~# blockset.py -h
usage: blockset.py [-h] [-n] [-f] [-d] [-u] [-o MASK,COUNT] [-c COUNT] [-a ADDRESS] [logfile]

blockset.py collection of unauthorized access list and restart firewall(ufw)

positional arguments:
  logfile               input log file default:/var/log/mail.log

optional arguments:
  -h, --help            show this help message and exit
  -n, --none            Check only mode.
  -f, --force           bloclist force update, unless new record.
  -d, --debug           debug print mode
  -u, --update          blocklist update and restart firewall(ufw)
  -o MASK,COUNT, --orderck MASK,COUNT
                        ipset order ckeck
  -c COUNT, --count COUNT
                        abuse count
  -a ADDRESS, --address ADDRESS
                        individually address entry to blacklist
root@xxx:~#

  • -h(–help)
    ヘルプの表示、引数やコマンドラインオプション説明
  • -n(–none)
    ログファイルの検査のみで何もしない
  • -f(–foce)
    ログの検査で不正アクセスが無かった場合でも、ファイヤーウォール設定ファイルへのブロックリスト追加を続行する(ネットマスクによるリスト整理時に使用する)
  • -u(–upadte)
    ファイヤーウォール設定ファイルの更新とプロセスリスタートを行う
  • -o MASK,COUNT (–oderck MASK,COUNT)
    ブロックリストを再検査し、指定したマスクビットにてグループ化して、一定数以上のものをリストアップする。-fオプションで該当アドレスをマスクビットでまとめてファイヤーウォール設定ファイルを更新する、ファイヤーウォールの再起動には-uオプションを併用する
  • -c N(–count N)
    検査対象ログにて不正アクセスがN回以上あった場合にリストアップする
  • -a ADDRESS(–address ADDRESS)
    IPアドレスを直接指定して登録する、ログ検査と併用する仕様上、便宜的に100回不正アクセスがあったことにしている
続きを読む…