私について
▪ アプリケーションセキュリティのエキスパート (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.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
name=andres
POST /hello HTTP/1.1
Host: example.com
Content-Type: application/json
{"name": "andres"}
Ruby ハッシュへのデコード
POST /hello HTTP/1.1
Host: example.com
Content-Type: application/json
{"name": {"foo": 1}}
前述のすべての場合では name 変数の型は String 型だった。しかし
我々はそれをハッシュ型に強制できる:
noSQL ODM の導入
MongoId ODM (Object Document Mapper) のようなフレームワーク
を用いる場合、開発者は以下のようなコードを記述できる:
以上のコードは Mongo データベースにクエリを送信し、user_id と
confirmation_token が一致する最初の登録情報を返す
post '/registration/complete' do
registration = Registration.where({
user_id: params[:user_id],
confirmation_token: params[:token]
}).first
...
POST /registration/complete HTTP/1.1
Host: vulnerable.com
Content-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' do
registration = Registration.where({
user_id: params[:user_id],
confirmation_token: params[:token]
}).first
...
POST /registration/complete HTTP/1.1
Host: vulnerable.com
Content-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>
私の本人確認をするために電話をくれ #3
HTTP リクエスト
確認コードは 357896
HTTP 応答
ようこそ admin!
電話検証のバイパス
ハッカーは電話検証をバイパスしたい。そのアイデアは:
▪ 管理者のスマートフォンをハックする
▪ 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.1
Host: api.twilio.com
Content-Type: application/json
X-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.1
Host: vulnerable.com
Content-Type: application/json
{"phone_number": "+1 (541) 754-3010"}}
POST /verify-my-phone HTTP/1.1
Host: evil.com
Content-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
end
end
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.1
Host: vulnerable.com
Content-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()
end
end
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.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&pa
yer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A1
2%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&notify_version=2.6&custom=665588975&payer_status=v
erified&address_country=United+States&address_city=San+Jose&quantity=1&ver
ify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-
1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3
238416&payment_type=instant&last_name=User&address_state=CA&receiver_email
=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9
T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&resi
dence_country=US&handling_amount=0.00&transaction_subject=&payment_gross=1
9.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==')
=> "x04bI"x1Aandres@bonsai-sec.comx06: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)
脆弱性は常にそこに潜んでいる
▪ あなたたちはツールよりも賢い。つまらない仕事は自動化して、
ソースコードのレビュー、アプリケーションの論理的欠陥や対象ア
プリケーションの性能の理解を深めることなどに集中しよう。
▪ あなたたちは顧客よりも賢い。彼らに確信させよう。あなたがソー
スコードからより多くの脆弱性を発見し、大きな投資対効果をもた
らすということを。
▪ あなたたちはそこら辺の開発者よりも賢い(セキュリティや脆弱性、
リスクに関する教育訓練を受けている) 。彼らがいかに善良であ
っても過ちを犯す。
andres@bonsai-sec.com
@w3af

[CB16] 難解なウェブアプリケーションの脆弱性 by Andrés Riancho