年始あたりにMongoDBへのランサム狙いが頻発したらしいのとその対策

http://www.barracuda.co.jp/column/detail/742
AWSに置いたMongoDBサーバにアタックが続いているという報告

https://www.mongodb.com/blog/post/how-to-avoid-a-malicious-attack-that-ransoms-your-data
公式のアタック対策をまとめたブログ記事

 で、公式のアタック対策ブログ記事が日本語化されていなかったので適当な訳を作った。ところどころ日本語が適当。

=========================================
さいきん、MongoDBのセキュリティ設定が不十分な公開インスタンスに、悪意の持った攻撃が行われていると報告が来る。攻撃者はデータベースを消し、復旧のために身代金を要求してくる。

もしあなたのデータベースがアタックされたと思っているなら、下記のステップを踏んでほしい。

これらの攻撃は、セキュリティ保護の拡張設定をMongoDBに行うことで防げる。あなたはセキュリティ設定を正しく行う必要があるが、ドキュメントがそれを手助けしてくれるだろう。以下に関係のあるドキュメント、その他役立つリソースへのリンクを記す。

・セキュリティに関しては私たちのセキュリティマニュアルがある。さらに、MongoDBユニバーシティのカリキュラムの中でもセキュリティのオンライントレーニングを行っている。
・私たちのセキュリティチェックリストを使ってほしい。認証の利用、アクセスコントロール、ネットワーク制限、その他重要なベストプラクティスについて書かれている。
・もっとも人気のあるMongoDB(RPM)インストーラは、デフォルトではアクセスをローカルホストに制限している。他のインストール手段を使う場合も、この設定を適用しろ。
+つまりネットワークに乗っける場合、MongoDBのポートを直接公開せず、リバプロ使えってこと?+
MongoDBクラウドマネージャMongoDBOpsマネージャは継続的バックアップを提供してくれる。ユーザはクラウドマネージャでインターネット公開をアラート通知するように設定できる。

・直近リリースのMongoDB3.4は認証設定を保護されていないシステムに適用できるようになっている、ダウンタイム以外で。
MongoDB Atlasは多重レベルでのセキュリティを提供している。これは堅牢なアクセスコントロールやAmazon VPCs、VPC Peering、IP whitelists、TLS/SSLによる暗号化、その他オプションの暗号化によってネットワーク隔離が施されている。
・私たちはセキュリティインシデントのあったユーザに対して脆弱性レポートを提供し、啓蒙を行っている。このあたりについてはこちら
・もしセキュリティに関するベストプラクティスに興味があれば、わたしたちのセキュリティアーキテクチャホワイトペーパを読んだり、セキュリティハブに行ってみてねん。

攻撃への対処に関しての推奨ステップ
攻撃者がデータにアクセスしてしまったかを判断するには?
・もしアクセスコントロールが正しく行われているなら、攻撃者はデータにはアクセスできていないはずです。セキュリティチェックリストを読み、アタックポイントになりそうなところをレビューしてください。
・データベースとコレクションを確認して下さい。最近のケースでは、それらがdropされ、身代金を要求する一件のデータに置き換わっていました。
・アクセスコントロールが有効になっているなら、システムログに認証されていないアクセスや不審なログが残っています。


もしあなたがセキュリティの不十分なMongoDBインスタンスを運用していて、データが漏洩していたら
・あなたが商用サポートを受けているならうちのサービスエンジニアに連絡して。
・まず最初にやるべきは、このあとの不正なアクセスを防ぐこと。さあセキュリティチェックリストだ。
usersInfoコマンドを使って、ユーザの増減が起こっていないか調べよう。
・ログを追って攻撃された時間を調べよう。データをドロップしたコマンド、ユーザをいじくったコマンド、身代金要求のデータを作ったコマンドなど。
・もし定期バックアップがあるなら最新のものでリストアできる。攻撃までのあいだに変更されたデータを調べる必要もある。もしOpsManagerCloudManagerをバックアップに使っているなら、直ちに攻撃前の状態に戻せるかもしれない。
・もしバックアップを取っていなかったり、リストアが成功しないなら、データは永久に失われたことになる。
・攻撃者がデータベースの情報を丸ごと持って行ったと仮定し、内部セキュリティの運用に気を付ける。
comment: 0

ConoHaにMongoDBサーバを立てた

 MongoDBを使いたかった。ほんとはMongoDBをホスティングしてるサービスを使いたかったのだが、それだとお値段がお遊びにならない額になるのでconohaのVPSで立てた。適当なデータベースと適当なユーザーを追加して適当にホスティングするメモ(適当==適切)。
・MongoDBのポート変更
・MongoDBにデータベース追加
・データベースのユーザ追加
・遠隔でクライアントを用意して接続、ちょっといじってみる


 conohaのVPSで選べるイメージの中にMongoDBがあるのでそれを選んでVPSを立ち上げた。OSはCentOSの7系だった。
 まずはユーザー追加など基本的なことを済ませる。そのあとのはなし。

・MongoDBのポート変更
 MongoDBのデフォルトのポートは27017である。接続用クライアントはこのことを知っているので、サーバのアドレスをポート抜きで渡すと勝手に27017経由でつないでくれる。しかしこれ、なんと警視庁が「MongoDBを狙ったポートスキャンが頻発している」とリリースを出したことがあるので、ポート変更はしておいたほうがいいと考えている。なのでポート変更。
 MongoDBの設定ファイルである/etc/mongod.confをテキストエディタで開くと、ネットワーク設定があるので、そこのポート番号を編集する。
/etc/mongod.conf

#network interfaces
net:
port: 27017

 ついでにその真下にあるであろうデータベースをいじるユーザの権限を、次の作業のためにちょっといじる。一時的に、つなげられれば誰でもいじれる状態にしてしまう。
security:

#authorization: enabled
authorization: disabled

https://docs.mongodb.com/manual/reference/configuration-options/#security.authorization
 設定を書き換えたら、MongoDBのプロセスを立ち上げなおし。
systemctl restart mongod

 ポートが変わったのでファイヤーウォールを設定する。設定ファイルの書き換え。
/etc/firewalld/zones/public.xml

<port protocol="tcp" port="27017"/>

 設定ファイルを書き換えたらファイヤーウォールの立ち上げなおし。
systemctl restart firewalld



・データベースのユーザ追加
 MongoDBにVPS内からつなぐ。コマンドで"mongo"と打って、MongoDBに接続する。そしてユーザ追加。roleは"read"や、書き込み権限まで付与したければ"readWrite"など。
db.createUser({user:userName, pwd:password, roles:[{role:role, db:dbName}]});


 認証確認。
db.auth(userName, password);

 もろもろ確認。
use admin;

db.system.users.find();

 ユーザが追加できたらctrl+CでMongoDB接続から抜ける。MongoDBの設定ファイルでユーザ権限を元に戻して、MongoDBを立ち上げなおす。


・遠隔でクライアントを用意して接続、ちょっといじってみる
 Pythonでpymongoを使って接続してみる。
from pymongo import MongoClient

client = MongoClinet(ip, port)
db = client.get_database(dbName)
db.authenticate(user, password)

 適切にユーザが作成されていれば、コレクション作成やドキュメント挿入ができるはず。
db.create_collection("foobar")

db.foobar.insert({"foo":"bar"})
db.foobar.count()


 ととのった。
comment: 0

Dockerは本番運用でばかり使うものでもないという話

 今年のデヴサミではDockerを使って運用環境を便利にしたってな話をぼちぼち聞いた気がする。一方で、Dockerなんて本番環境の運用で使うんじゃないよ、っていう話題が出てきたりしている。デヴサミでDockerを導入したって発表をしてたところは、自分のところで、自分たちの目的に合うように、しっかり知見を貯めていた。その上で本番環境への導入をしていた。お上から「なんじゃ、Dockerを入れると工数削減できるらしいじゃないか」という鶴の一声で唐突に、走っているプロジェクトに入れることになれば災厄になりかねないとは思う。
 そんな話題もあるが、Dockerってすごく便利だと思っていて、本番運用だけで使うものでもないなーと考えている。なので「とりあえず」でDockerを使えるような自分(イチ開発者)なりの使い方を書いておく。先に一つ書いておくと、GUIがないとダメなようならちょっとつらいものではある。

 まず、OSはなんでもいいがDockerは入っているものとする。
 たとえばMySQLを使っているが、MongoDBをちょっと検討したくて、軽くさわってみたくなったとする。通常なら、開発マシンや仮想マシンにインストールして立ち上げたりする。DockerならMongoDBサーバの用意にコマンド一つ。
docker run -p 27017:27017 mongo

これで少し待てばローカルにMongoDBサーバが立ち上がる。あとは自分の使いたい言語でMongoDBとつなぐライブラリを用意してつなぐだけ。Pythonでならpymongoをpipなどでインストールして下記。
from pymongo import MongoClient

client = MongoClient()

 Dockerのコマンドでやっているのは、コンテナと呼ばれるものを走らせる指示と、そのコンテナとクライアントのポートの結び付けだ。ここではmongoというコンテナを走らせろと指示をしており、mongoというコンテナイメージを持ってなければパブリックリポジトリへイメージを探しに行く。
 パブリックリポジトリはOSベンダやらなんやら様々なソフトベンダがイメージを提供している。UbuntuやCentOS、Debianなどのイメージから、RailsやDjangoなどのWebフレームワークがすぐ使えるもの、MySQLやMongoDBなどDBがすぐに使えるものなど、さまざまなものがそろっている。

 コンテナは仮想化の一種なので、依存関係的な意味ではクライアント環境は汚れていない。ただコンテナイメージは使いたいソフト+それが実行される環境(OSなど)の容量を持つため、物理的な容量は多少持っていかれる。けど動作としてはたいして重くはならない。コンテナによる仮想化は、従来の仮想化を使うより軽くなる仕組みになっている。昨今のDockerの盛り上がりを受けて、コンテナとしてAlpineというセキュリティを考慮された数MB程度しかないOSを使うのが流行っている。

 Dockerを使うことで、容易にちょっと触ってみたいDBサーバを用意できた。これをこのまま開発用のDBにしてもいいだろう。仮想環境に用意されたので、依存関係でクライアント環境を汚す心配はない。



 Dockerの使い方をもう一つ。Webアプリケーションの実行環境を用意してみる。例としてここではNodejsで。
 ぼくは開発にWindowsを使っている。Macを開発に使っている人も多数いるだろう。けどそんな人たちの本番サーバはなにかしらのLinuxディストリビューションであることが大半だ。開発環境と本番環境のOSが違うってのは嫌だ。そろえたい。これまでメジャーであったVirtualBoxのようなもので、仮想マシンを立ち上げる?それは手間だし、仮想マシン内でコマンドをいじっているうちに、その環境が秘伝のタレになってきて、本番環境で再現できない可能性だってある。じゃあDockerを使う。

 Dockerを使う前にアプリを準備する。ここでは簡単なもので済ませる。Nodejsで走らせるとポート8000番で待ち受け、Hello World!を返す単純なやつ。
./scripts/app.js

const http = require('http');

const port = 8000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});

server.listen(port);

console.log(`Server running at port:${port}/`);


 続いてDockerfileを用意する。ここにはどんな環境を用意して、アプリ実行前にやる事前準備をして、ではいざ実行というプロセスを書くことになる。
./Dockerfile

# Nodejsが入っているAlpine OSを使う
FROM node:7.10.0-alpine

# RUNに続くコマンドを実行する
RUN mkdir /app

# クライアントのカレントディレクトリにあるファイルをコンテナの/appへコピー
COPY . /app

# コンテナでコマンドが走るときのカレントディレクトリを設定
WORKDIR /app/scripts

# ポート待ち受け設定
EXPOSE 8000

# あとで走らせるコマンド(Nodejsの実行)
CMD node app.js

 以上二つで必要なファイルはそろった。./Dockerfileと./scripts/app.jsの二つのファイルがカレントディレクトリ以下にある。二つコマンドを打てば、アプリを動かすサーバが立ち上がる。
 まずコンテナイメージの作成。Dockerfileで最終行のCMDの前までを実行し、環境イメージが作成される。
docker build -t node_app

 環境イメージができれば、あとはそれを使って実際に走るコンテナを作る。
docker run -p 8000:8000 node_app

 アプリ作成後は、デプロイまで単純な二つのコマンドで済んでいる。便利。

 実際にDockerでNodejsのWebアプリやるなら、ユーザを用意して、実行はそっちに切り替えようねという話など参考になるのが下記。
http://postd.cc/lessons-building-node-app-docker/


 さいきん開発の遊びに使っていたPCを修理に出したので、手元に予備マシンしかなかった。これだとDBの準備がめんどうだなーと思っていたが、Dockerですんなり解決した。
 Dockerは仮想化の一種であり、クライアント環境を汚さないという恩恵がある。あとはHyper-Vみたいなので仮想マシン一台を丸々立ち上げるより、いろいろ軽い。だから本番のデプロイが便利になる以外にも使い道はたくさん出てきそう。


知らないおじさんの作ったDockerfileとかイメージを無邪気に使っちゃダメ。セキュリティ的に。
comment: 0

MongoDBのセキュリティを考える(続き)

MongoDBのセキュリティを考える
 前回にMongoDBのセキュリティを考えた。Pythonで、どういうことが起こるとまずいかを書いた。今回はPythonの軽量WebフレームワークFlaskでそのまずい状況が起こるかを調べた。コードを含めてまとめておく。


 起こるとまずいのは、JSONとなる辞書型のオブジェクト中のkey,valueのうちのvalueのほうに、文字列や数値での条件ではなく、式で条件が与えられてしまうこと。下記で示すような変数some_valueに文字列か数値が入れば、それは通常の一致検索に使われる。
db.members.find({"id":some_value})

some_valueに辞書型で{"$ne":""}という形で値を入れられると、SQLInjectionでいうところの" OR 't' = 't' "のごとく、多数のレコードをひっかけるような条件となってしまう。

 まずいことがFlaskで起こるかを調べるため、WebアプリケーションとしてのFlaskを使ったコードを用意した。
from flask import Flask

from flask import request

app = Flask(__name__)

form = """<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
</head>
<body>
<form action="/" method="POST">
ID: <input type="text" name="user" />
PW: <input type="password" name="pw" />
<input type="submit" value="send" />
</form>
</body>
</html>
"""

@app.route("/", methods=['GET', 'POST'])
def hello():
if request.method == 'GET':
return form
elif request.method == 'POST':
user = request.form.get("user")
print(request.form)
is_str = str(isinstance(user, str))
print(user)
return is_str

if __name__ == "__main__":
app.run()

上記を書いたファイルをsrv.pyとしてPythonで実行する。
python svr.py


 認証フォームを想定したもので、ルート(http://localhost:5000/)にアクセスするとID、パスワードを入れるinputが表示される。ここで入れたIDが、さきほどの辞書型の{"id":some_value}でいうところのsome_valueの位置に入る。前回でクライアントから送信された値が、サーバ側で辞書型として得られるとまずいということはわかっているので、今回はDBに問い合わせはしない。値が辞書型で得られてしまうことがあるかを確かめる。
 FlaskでPOSTされた値は、request.form.get("key_name")でえられる。key_nameに"user"と入れれば、フォームに入れた値が文字列型で得られる。これが辞書型になってしまうようなケースがあるだろうか。
 PHPやNodejsではここらでまずいことができる。だからPythonではどうだろうと試してみた。PHPやNodejsで問題が起こるのは、inputのnameが"user"から"user[$ne]"に書き換えられたとき。そのときにサーバサイドでkey_nameの値がまずい形で取り出されてしまう。ブラウザの開発者ツールで”user[$ne]"をセットしてPOSTし、サーバサイドでその値を取り出そうとしてみる。
request.form.get("user")

取り出せなーい。なぜ?print(request.form)してみればわかる。
ImmutableMultiDict([('user[$ne]', 'foo'), ('pw', 'var')])

"user"というキーのフォーム要素はなかったということ。これなら少なくとも{"id":some_value}のsome_valueの値として、条件式となる辞書型の値となることはない。

 PHPではまずいことが起きうる。それが@ITで開設されている。
「JSON文字列へのインジェクション」と「パラメータの追加」
 PHPは変数を、配列として宣言するという手順を経ずにいきなり配列として使える。かなりゆるい。それがMongoDBと組み合わせることで穴となりうる。Pythonでは起こらなかったことを考えると、MongoDBのみに非がある脆弱性とはいいがたい。

 Flaskでなら、問題はそうそう起きなさそうである。さらに一般化していえば、検索条件に使うJSONに条件として値を入れるとき、型を確認してあればInjectionのようなことはできない。SQLでいうところのプレースホルダがないのもここらが理由だろう。
comment: 0

MongoDBのセキュリティを考える

 MongoDBをけっこう積極的に使っている。そのためにセキュリティについてもある程度考えていたが、ここで改めてそれをまとめておく。SQL Injectionみたいなのがないのかってあたりなど。


 Pythonで公式のドライバであるPymongoを使って、認証突破の手掛かりになるようなことができないか試してみる。下記SQLのようなことがMongoDBでできるかどうか。
SELECT * FROM users WHERE name = '(入力値)';

SELECT * FROM users WHERE name = 't' OR 't' = 't';

 まず準備。Pymongoをインストールした上で、MongoDBを起動して、接続する。さらに、ユーザデータとなるものを一件インサートする。
from pymongo import MongoClient


client = MongoClient()
db = client.test_database

db.members.insert({"_id":"kuma", "pw":"foo"})

 これでkumaというユーザがいる状態になった。このkumaを引っ張り出す基本的なクエリは下記の通り。
member_id = "kuma"

db.members.find_one({"_id":member_id})


 下記のクエリは該当データがないのでなにも取ってこれない。
member_id = "john doe"

db.members.find_one({"_id":member_id})


 上記を書き換えて「"john doe"がIDではない」という条件を与えてみる。下記のようになるが、これでkumaも引っ張ってこれる。
member_id = {"$ne":"john doe"}

db.members.find_one({"_id":member_id})

ユーザIDを条件として入れるべきところを、「○○ではない」という条件に書き換えてしまうのがMongoDBのクラックの仕方で最たるものだろう。

 上記はmember_idに辞書型で条件を与えているのでユーザkumaも取ってこれる。これが辞書型ではなく、きちんと文字列型で与えられるなら、当然kumaは取ってこれない。
member_id = '{"$ne":"john doe"}'

db.members.find_one({"_id":member_id})

つまり、条件を与えるところは文字列型できっちり与えられるように固めて、そこを辞書型で与えられたりしないようにしておけということになる。それを考えると、やってはいけないのはクライアント側からJSONでデータを送らせて、それをなにも考えずにJSON.loadsで辞書型データにして扱ってしまうことだ。
 例として、クライアントからJSONを送られたとして、サーバ側でそれを辞書構造のデータにパースして扱うコードを書いてみる。 
ob = json.loads('{"_id":"john doe"}')

db.members.find_one({"_id":ob["_id"]})

 これがアウトなコードの例である。JSONの_idの値はクライアント側で任意のものにできる。ということは下記のようにしてしまえば、ユーザkumaはクエリにひっかかってくる。
ob = json.loads('{"_id":{"$ne":"john doe"}}')

db.members.find_one({"_id":ob["_id"]})

 ググってみるとクライアント側でフォームデータをJSONにしたいというニーズは結構あるようだが、MongoDBを使っている場合はそれは危険。
https://www.google.co.jp/search?q=form+to+json
 もし使っているWebフレームワークが、GETで送られてきた値を、空気を読んで辞書型で返してくれるなんて危なっかしい機能があったら回避策を取ろう。PythonでMongoDBを使っている場合、条件式は辞書型で与えられる。条件の値が入る部分に文字列型で値が入るということが保証されればいい。
 PHPはGETで送られてきた値を配列型にキャストして返してくることがあるので、脆弱性が生まれる要因になっている。PHPでMongoDBを使う場合は要注意。このあたりはもはやPHPの言語仕様との相性の悪さか。


 あと覚えておくべきこととしては、MongoDBにJavaScriptを渡すと実行してくれるなんて機能もある。db.members.find("this._id == "kuma")って書けばユーザkumaを取ってこれるが、悪用も考えておくべきである。まあこのJSをちゃんとJSONになるようにカーリーブラケット"{}"で囲んでやればそれでOK。あとは心配ならサーバサイドでのJS実行を止めてしまえばいい。
https://docs.mongodb.com/manual/core/server-side-javascript/
MongoDBがJSを実行できるってのは仕様であって、それが不安になるなら選択肢は用意されている。


 MongoDBを使う場合、クエリなどに入れる値の型は最低限注意するべきことだろう。文字列が入るべきところに、別の型で条件式とか入れられたりするような設計はしてはいけない。暗黙のキャストを多用せずに、型を意識してやっていけばインジェクションみたいなものは避けられるだろうと考えている。
comment: 0