spamが襲ってきた

緒戦は負けつづけ・・・

2009年の初めころ(このサイトが2008年の初めにオープンしたので1年位たった後かな)から、すこしづつ、#commnet 欄に spam が入ってきた。その都度消して対応してたけど、それも頻度が多くなって、#comment 自体を消して対応していました。(あまりコメントがはいるわけでもなかったので・・・)
9月になると今度は、Q&A?や新規ページに 書き込まれる様になってきました。そのため、アクセスログからドメインもしくはIPを探して、サーバー側でDeny(拒否)したり、パスワードページに設定したりしました。また、spam は 一度書き込んだwiki名をしつこく復活させてくるので、そのページをほとんど空にしてパスワードを入れたりして対応していました。
でもこの様な対応ではどうにもならない量のspamがやってきました。一日に50や100はページがつくられてしまうのです。
続いて、おこなったのが、bash と cron を利用して、定期的にページを消すものです。得に、Q&A?は、接頭語が決まってくるので、接頭語と更新時間を指定して消していました。(Q&A?には書き込みがなかったので、Q&A?自体を消していたのですが、その後もどんどん書き込まれていました為の対応でした。) これは、接頭語が決まっている段階では非常に効果を発揮しましたが、一時的に書き込まれるものはなんとしようもなく、また、その頃には、任意のwiki名で次々書き込まれていました。

spam とは 『誰が』『何の為に』『何をしているのか?』

spam と闘っているときに、spam の正体・・・一体『誰が』『何の為に』『何をしているのか』を考えていました。

spam とは 『誰』であるか? 

これがもっとも分かりません。accessログからわかるドメインやIPは多種多様で、判別不能です。どうやっているのかは後で考察するとしても、自身のIPやドメインから直接攻撃してくることはなく、ここから誰であるかを判別させることは不可能です。それでも考察してみます。
  1. 日本人、もしくは日本語を話す人ではない。(日本語で書き込まれることがない)
  2. 多くの人間の仕業ではない。(恐らく1~2の個人もしくは団体が行っている)
  3. 一通り以上のITリテラシーのある人間である。(むちゃくちゃ高い必要もないと思われます。私も同じことができるかもしれない・・・)
  4. パソコン機器やネット環境などの最低限の環境を備えることができる人間である。
  5. 屈折した人間である可能性が高い。(正常なモチベーションでできることではない)

spam は、『何の為に』それを行うのか?

これは一般的に3通り考えられるのでしょうか・・・。
  1. spamリンクのページに来たユーザーのクッキー情報等を取得し犯罪利用する(フィッシング目的)
  2. spam広告業として、リンクに誘う。もしくは、人気ページにリンクを貼ることで検索順位を向上させる。(いわゆるPageRankアルゴリズムのSEO対策・・・いまやかなり疑わしいのでしょうが。。。)
  3. 愉快犯的行為。異常な自己顕示欲の充足
上の二つは、両方の目的を持っている可能性があるのでしょう。いずれにしても、このやり方がその目的に対して意味のある行為とは思えないのですが・・・。

spam は 『何をしているのか?』

spamは、一体何をしているのか?
  1. URL(get)やフォーム(post)送信により攻撃対象のページに繰り返し書き込みをする。
  2. プログラムによる繰り返しを行っており、こちらが一つ一つ消せる様な頻度やしつこさではない。
  3. DNS(BIND)の再起利用が可となっているサイトを踏み台にして世界中のサイトから書き込んでくるので、IPやドメインを拒否してもほとんど意味が無い。

spamへの対応を考えてみる。

このあたりは、決定的な対応ではなく、どんな対応が考えらるかをちょっと考えてみたという内容です。。前述の初期の対応もこの辺から出発していますが、「決定的な対応」をした後、どうしてもすり抜けてくるIPやドメインがある場合には、考えてみるべき内容だと思っています「とにかく決定的な対応を行いたいだけだ」というなら読み飛ばしてください。

pukiwikiを選択する意味があるのか?

pukiwikiは、不特定多数の方が新たにページを作ったり、書き込みをすることを許した人間の善意の上に成り立つページを作るものだと思います。一人で作るだけ。他人には書き込ませないというなら、あえて選択する必要があるのかは考えてみる必要があると思います。第三者が書き込まない前提のページ作成であれば、spam攻撃の対象にはなりにくいと考えます。以下、検討すべきものを考えてみます。
  1. ブログではダメなのか?
  2. WordPressとかは?
  3. SiteDevは?
ひとのことを言えないんだけどね・・・・。書き込みがほとんどないし。#comment は消しまくったし・・・・(反省)

pukiwiki のデフォルトの機能で何ができるか考えてみる。

  1. cantedit にリストを加えてみる。pukiwiki.ini.php 305行 あたりにある以下の行に 編集を許可しないページ名を加えてみる。以下は、Tim Docks という名前のページを加えたところ
    // Page names can't be edit via PukiWiki
    $cantedit = array( $whatsnew, $whatsdeleted,"Tim Docks" );
    これでTim Docksというページは編集(新規作成)できなくなります。お分かりだと思いますが、これはまさにいたちごっこ。いったいくつリストに登録したらよいのか・・・と気が遠くなります。さらに、本当にその名前でページを作りたくても、つくれなくなります。
  2. 認証ページに設定する。pukiwiki.ini.php の247 行目あたりから始まる認証設定で、ページやコンテンツ(単語)を引っ掛けて認証させます。ただし、cantedit同様にいたちごっこです。

ApachでIPアドレスを拒否する

Ubuntuでいえば、たとえば、/etc/apache/conf.d/ 内に設定ファイルを書き(編集し)、特定のIPアドレスやドメインをDeny(拒否します。)たとえば、cssgroup.lv というドメインを拒否した場合の設定は、以下の様な感じ
<VirtualHost 192.168.99.6>
       ServerAdmin webmaster@felix-labo.org
       DocumentRoot /virtual/www/felix-labo.org
       Servername felix-labo.org
       <Directory /virtual/www/felix-labo.org>
               Order allow,deny
               Allow from all
               Deny from cssgroup.lv
       </Directory>
・
・
・
・
・
ただし、この方法は前述の通り、いろいろなドメインを踏み台にしてくる相手にはほとんど効果がありません。

どうやって、IPアドレスやドメインを特定するのか?

話が前後してしまいますが、IPアドレスやドメインはどうやって特定したら良いのでしょうか・・・。私が行っている方法です。
まずは、apache の access log を見ていきます。apache のアクセスログは、初期状態では、/etc/apache/site-enabled/000-default の中に場所が記載されています。この場所からログファイルを見つけて、less コマンドを使って見ていきます。
(access_log ファイルのあるディレクトリか・・・)
less ./access_log
ここで、最終行に移動します。
G
次に検索は
/cmd=edit
といった具合にやります。上記の検索は、cmd=edit を探すコマンドですが、pukiwikiのページの編集の際のURLに含まれるので、編集しようとしたアクセスログが一発でわかります。(新規ページをcmd=edit とせずに書き込んでいく場合には、これでは抽出できません。)検索語、ページを↑↓キーなどで移動すると、該当する単語が反転されています。
access_log を調べる以外の方法として、awstats等のページアクセス分析ツールによるIP・ドメイン抽出を行っています。得に、大量にURL等のテキストを書き込んでくるspamのIPやドメインは、ロードするバイト数が異常値になっているのですぐに分かります。多くの場合、これらのドメインは、.lv (ラトビア)や .de(デンマーク?)など、意味不明な国のアクセスです。この様にして特定したドメインをaccess_logの中で検索をかけると、はっきりとどのページがいつ作られたのかが分かってきます。

bash を cron に登録して消す。

bash (シェルスクリプト・ウィンドウズ的に言うとバッチ)に特定のファイルを消すスクリプトを登録して、それをさらにcron に登録することで、定期的にファイルを消してくれます。spamfilter による『決定的な対応』を施す前も、施した後も、実は#tracker のpost送信と思われる書き込みが止められませんでした。#traker はspam 対策では、問題があるかもしれません。(現在試行錯誤中)書き込みのない、#tracker だったので、消したのですが、それでも書き込みが止まらず、この方法で定期的に消していました。(#tracker の対策については、後述したいと思います。)
すこし話がそれましたが、あまり有効な手段かどうかは疑わしいですが、一応備忘しておきます。まず、どこか適当なところに bash ファイルを作成します。例えば、/usr/local/sbin/ などに、下記の内容のファイルを作成します。
#!/bin/bash
find /(pukiwikiのルート)/wiki/ -mmin -2000 -name "EF*" -exec rm {} \;
find /(pukiwikiのルート)/cache/ -mmin -2000 -name "EF*" -exec rm {} \;
これは、2000分前以降 で EF・・・で始まるファイルを消せ というもので、消す場所は、wikiファイルのある、wiki/ と、 キャッシュがある cache/ のディレクトリです。 つくった後、実行権限を付与します。
sudo chmod +x /usr/local/sbin/rmqa
で実行は、
sudo /usr/local/sbin/rmqa
といった具合でやります。
次に、これを cron (クローン)にやらせます。cron は文字通り自身のクローンなので自身の代わりにスクリプトを実効してくれるものです。ウィンドウズ的に言うと タスクマネジャーというわけです。クローンは
sudo crontab -e
で起動する画面に一行一コマンド打ち込みます。さっさと消したいので以下のコマンドを打ち込みました。
*/10 * * * *  /usr/local/sbin/rmqa
これで、10分おきに、先ほどのスクリプトを実行してくれます。なお、この方法では #recent に履歴だけ残ってしまいます。10分おきでも3つくらい書き込まれることがあります。#recent は、
/pukiwiki/cache/recent.dat
の内容なので、中身で表示したくないものを消します。

spamfilter で 決定的な対策をする

美麻Wikiさんの公開してくれているpukiwiki用のスパムフィルターを施しました。結構な威力です。

準備

美麻wikiのspamfilter は akismet という spam fileter と、reCaptcha という 崩し文字の確認入力による機械的ページ生成の抑制機能の双方を併せ持つ優れたspamfileter です。akismet には WordPressからのApiキーの取得、reCaptcha は reCaptchaの公開キーと秘密キーの取得が必要です。

WordPress から akismet の APIキーを取得

http://en.wordpress.com/signup/から、必要情報を登録して、アカウントを取得します。ブログページも作ります。作成したアカウントでログインし、左上の『My Account』→『Edit Profil』とクリックすると、一番上に、
あなたの WordPress APIキー: ********. Don't share your API key, it's like a password.
と現れます。APIキーは、無闇に他人に教えてはいけません。

reCaptcha から 公開キー・秘密キーを取得

reCaptchaから、必要情報を登録し、ログインします。登録したドメイン名をクリックすると、公開キーと秘密キーが取得できているのが分かります。

nkf インストール

UTF-8 サーバーの場合には、文字コード変換が必要になってきます。文字コードを変換してくれる fkf パッケージをインストールします。
sudo apt-get install nkf

ダウンロード・インストール

美麻Wikiから1.4.7用のspamfilterファイルをダウンロードして、解凍(unzip)します。1.4.7 というフォルダーができあがるので、中身をそのままpukiwiki/ へ上書きコピーしますが、UTF-8 の場合には、事前に処理を施します。
(1.4.7 ディレクトリから)
$ cd ./lib
$ nkf -w ./akismet.class.php > ./akismet.class.php2
$ mv ./akismet.class.php2 ./akismet.class.php
$ nkf -w ./file.php > ./file.php2
$ mv ./file.php2 ./file.php
$ nkf -w ./init.php > ./init.php2
$ mv ./init.php2 ./init.php
$ nkf -w ./plugin.php > ./plugin.php2
$ mv ./plugin.php2 ./plugin.php
$ nkf -w ./recaptchalib.php > ./recaptchalib.php2
$ mv ./recaptchalib.php2 ./recaptchalib.php
$ nkf -w ./spamfilter.php > ./spamfilter.php2
$ mv ./spamfilter.php2 ./spamfilter.php
(もしかしたら、init.php と spamfilter だけでよいかも・・・・)。さらに、init.php を変更します。
vi ./init.php
56行目あたりから 冒頭に // UTF-8: とある部分を消します。
/////////////////////////////////////////////////
// INI_FILE: LANG に基づくエンコーディング設定

// MB_LANGUAGE: mb_language (for mbstring extension)
//   'uni'(means UTF-8), 'English', or 'Japanese'
// SOURCE_ENCODING: Internal content encoding (for mbstring extension)
//   'UTF-8', 'ASCII', or 'EUC-JP'
// CONTENT_CHARSET: Internal content encoding = Output content charset (for skin)
//   'UTF-8', 'iso-8859-1', 'EUC-JP' or ...

switch (LANG){
case 'en': define('MB_LANGUAGE', 'English' ); break;
case 'ja': define('MB_LANGUAGE', 'Japanese'); break;
case 'ko': define('MB_LANGUAGE', 'Korean'  ); break;    //←ここ
// See BugTrack2/13 for all hack about Korean support,   //←ここ
// and give us your report!                 //←ここ
default: die_message('No such language "' . LANG . '"'); break;
}

define('PKWK_UTF8_ENABLE', 1);              //←ここ
if (defined('PKWK_UTF8_ENABLE')) {
       define('SOURCE_ENCODING', 'UTF-8');
       define('CONTENT_CHARSET', 'UTF-8');
} else {
       switch (LANG){
       case 'en':
               define('SOURCE_ENCODING', 'ASCII');
               define('CONTENT_CHARSET', 'iso-8859-1');
・
・
・
これをサーバーへscpで上書きコピーしにいきます。現在 ./lib にいるので、一つディレクトリを上がります。
$ cd ../
$ scp -r ./*  ユーザー名@サーバー名:/(pukiwikiルート)/
(パスワード入力)
これで、/1.4.7/ の下にあるフォルダー・ファイルを上書きにいきます。
続いて、pukiwiki.ini.php の末尾に以下を書き込んでいきます。(Toby Soft Wikiさんからいただきました。
//spam filter
function isWindowsMachine()
{
 return (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
}
function isLocalTestMachine()
{
 // windows machine is local test environment
 return $_SERVER['REMOTE_ADDR'] === '127.0.0.1';
}

define('SPAM_FILTER_AKISMET_API_KEY', '<akismet APIキー>');

$spamFilterCond = '#useragent() || #filename() || #formname() ||
 (#onlyeng() && (#atag() || #urlnum())) || #urlbl() || #uaunknown()';

if (!isLocalTestMachine()) $spamFilterCond .= ' || #urlnsbl
 (SPAM_FILTER_URLNSBL_REG) || #urlnsbl(MY_SPAM_FILTER_URLNSBL_REG)';

define('SPAM_FILTER_COND', $spamFilterCond);

// my URLNSBL setting
define('MY_SPAM_FILTER_URLNSBL_REG', '/(\.theplanet\.com)/i');


$spamFilterCapchaCond = '#onlyeng() || #url() || #atag() ||  #acceptlanguage()';

if (!isLocalTestMachine()) $spamFilterCapchaCond .= ' || #ipdnsbl()';

define('SPAM_FILTER_CAPTCHA_COND', $spamFilterCapchaCond);

define('SPAM_FILTER_RECAPTCHA_PUBLICKEY',  '<reCAPTCHAから取得した公開鍵>');
define('SPAM_FILTER_RECAPTCHA_PRIVATEKEY', '<reCAPTCHAから取得した秘密鍵>');

// 0.7.5の公式のものから、サービス停止しているlist.dsbl.org を除外
define('SPAM_FILTER_IPDNSBL_DNS', 'niku.2ch.net,bsb.spamlookup.net,
 bl.spamcop.net,all.rbl.jp');

// setting full-path of nslookup
if (PHP_OS == "FreeBSD") {
 define('SPAM_FILTER_NSLOOKUP_PATH', '/usr/sbin/nslookup');
} elseif (isWindowsMachine()) {
 define('SPAM_FILTER_NSLOOKUP_PATH', 'nslookup');
}
ほとんど、そのままなのです(Toby Softさんありがとう)が、reCaptcha の条件だけ、
$spamFilterCapchaCond = '#onlyeng() || #url() || #atag() ||  #acceptlanguage()';
と、url の記述があれば、全部 変形文字の確認入力制限をかけています。