Account Takeover due to DNS Rebinding

VoorivexVoorivex
6 min read

Hello guys, after a long time, I decided to write a blog post. I chose a vulnerability that I recently uncovered in Hashnode. As you may have already noticed, I set up this blog on Hashnode. Naturally, when I use a third-party service like this, I spend a few hours checking their security as a concerned customer.

The first eye-catching feature in Hashnode is cross-domain authentication. Personally, when I hunt, I always look for authentication class vulnerabilities, which I’m stronger in, especially when an authentication token is transferring among different places. Hashnode has an option for blog owners to have their own domain, as you are reading this post on blog.voorivex.team and not the Hashnode website.

It's not a big deal. By setting a simple cname record in the DNS server, you can verify that the domain belongs to the user, and the traffic will be redirected to Hashnode servers:

From a backend perspective, everything is the same, except for the host header of the HTTP packet. Since the IP address is the same for all of Hashnode’s blogs, Hashnode uses the host header to determine which blog should be loaded. However, from a browser's perspective, the URLs are different. We all know that browsers store data based on the Origin:

  • https://hashnode.com

  • https://blog.voorivex.team

So, if a user enters credentials and logs into the Hashnode website, they should repeat the procedure to log into blog.voorivex.team too. To be user-friendly, Hashnode uses an authentication transfer, which means when somebody has an authentication session on the Hashnode website, they will automatically get an authentication session on other CNAMed domains too. But how?

Cross-Domain Authentication

  1. The user opens hashnode.com/authenticate equipped with JWT authentication Cookie. The HTTP request has a parameter named next which is responsible for redirecting the user back:

     GET /authenticate?next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1
     Host: hashnode.com
     Cookie: redacted
     User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0
     Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
     Accept-Language: en-US,en;q=0.5
     Accept-Encoding: gzip, deflate, br
     Referer: https://blog.voorivex.team/
     Upgrade-Insecure-Requests: 1
     Sec-Fetch-User: ?1
     Te: trailers
     Connection: close
    

    The response:

     HTTP/2 307 Temporary Redirect
     Age: 0
     Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
     Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data:
     Date: Mon, 15 Apr 2024 15:25:10 GMT
     Location: https://hashnode.dev/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F
     Referrer-Policy: origin-when-cross-origin
     Server: Vercel
     Strict-Transport-Security: max-age=63072000
     X-Content-Type-Options: nosniff
     X-Frame-Options: deny
     X-Matched-Path: /authenticate
     X-Vercel-Cache: MISS
     X-Vercel-Id: fra1::pdx1::pldpk-1713194710837-255be6ebcd6a
     Content-Length: 112
    
     https://hashnode.dev/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F
    
  2. There is an intermediate phase here which Hashnode checks token and next URL before redirecting the user back:

     GET /identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1
     Host: hashnode.dev
     Cookie: redacted
     User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0
     Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
     Accept-Language: en-US,en;q=0.5
     Accept-Encoding: gzip, deflate, br
     Referer: https://blog.voorivex.team/
     Upgrade-Insecure-Requests: 1
     Sec-Fetch-User: ?1
     Te: trailers
     Connection: close
    
  3. If the user has a valid JWT authentication Cookie and legitimate next URL value, they will be redirected back to the value of next parameter (in this case, blog.voorivex.team which is legitimate in Hashnode)

     HTTP/2 307 Temporary Redirect
     Age: 0
     Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
     Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data:
     Date: Mon, 15 Apr 2024 15:25:11 GMT
     Location: https://blog.voorivex.team/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https://blog.voorivex.team/
     Referrer-Policy: origin-when-cross-origin
     Server: Vercel
     Set-Cookie: redacted
     Max-Age=315360000; Domain=.hashnode.dev; HttpOnly
     Strict-Transport-Security: max-age=63072000
     X-Content-Type-Options: nosniff
     X-Frame-Options: deny
     X-Matched-Path: /identity
     X-Vercel-Cache: MISS
     X-Vercel-Id: fra1::pdx1::8tmnc-1713194711393-f85cf186df76
     Content-Length: 110
    
     https://blog.voorivex.team/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https://blog.voorivex.team/
    
  4. The user opens blog.voorivex.team with an Authentication Token and if it is a valid token, they will be provided with a JWT authentication Cookie

Here is the flow of cross-domain authentication:

DNS Rebinding

As observed, in the final stage of the flow, if the token is valid, the user will receive a JWT authentication Cookie. If the next URL can be manipulated, the GUID token can be stolen, leading to an account takeover. Although the checker function in the backend is quite secure, let's address this question: how does Hashnode define legitimate domains? How does the next_check() function operate? Upon investigation, it was found that Hashnode verifies the next URL against its database, which includes lists of custom domains. Therefore, an attacker could bind a legitimate domain to their account to manipulate the whitelist. Here's the attacking scenario via DNS Rebinding:

  1. The attacker binds a legitimate domain pointing to hashnode.network, in this case https://attacker.voorivex.team

  2. Since this URL is considered legitimate, the next URL can also be set as https://attacker.voorivex.team

  3. The attacker then changes the DNS value of attacker.voorivex.team to their own IP address (Before launching the attack, they obtain a valid SSL certificate)

  4. Despite changing the DNS records, the next URL remains valid as https://attacker.voorivex.team. It may take a significant amount of time for Hashnode to update its database. Since the attacker does not remove their domain from the whitelist, just manipulates DNS records, the domain remains considered legitimate in Hashnode's system

  5. The attacker provides the victim with a link: https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F. If the victim clicks on this link, their GUID token will be stolen. The attacker can then produce a JWT using the stolen GUID token, gaining unauthorized access to the victim's account

DNS Rebinding

I'm not going to explain the DNS rebinding vulnerability here; a quick search will give you plenty of information. The important questions for me were:

How is the next parameter value validated in the backend? Can the value be anything, or does it just match against the database? What if I change the DNS records of the verified domain?

As observed, in the final stage of the flow, if the token is valid, the user will receive a JWT authentication Cookie. If the next URL can be manipulated, the GUID token can be stolen, leading to an account takeover. Although the checker function in the backend is quite secure, let's address this question: how does Hashnode define legitimate domains? How does the next_check() function operate? Upon investigation, it was found that Hashnode verifies the next URL against its database, which includes lists of custom domains. Therefore, an attacker could bind a legitimate domain to their account to manipulate the whitelist. Here's the attacking scenario via DNS Rebinding:

  1. The attacker binds a legitimate domain pointing to hashnode.network, in this case https://attacker.voorivex.team

  2. Since this URL is considered legitimate, the next URL can also be set as https://attacker.voorivex.team

  3. The attacker then changes the DNS value of attacker.voorivex.team to their own IP address (Before launching the attack, they obtain a valid SSL certificate)

  4. Despite changing the DNS records, the next URL remains valid as https://attacker.voorivex.team. It may take a significant amount of time for Hashnode to update its database (or DNS cache). Since the attacker does not remove their domain from the whitelist, just manipulates DNS records, the domain remains considered legitimate in Hashnode's system

The attacker provides the victim with a link: https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F. If the victim clicks on this link, their GUID token will be stolen. The attacker can then produce a JWT using the stolen GUID token, gaining unauthorized access to the victim's account.

Automation

I wrote a Python script to automatically change the DNS record and then open the malicious URL on behalf of the victim. This ensures that the attack will work every time if the victim opens the attacker’s website. In terms of CVSS calculation, the attack complexity is low, making the overall vulnerability score 8.0 (High). The script:

Responsible Disclosure

Immediately after finding the flaw, I reported it to Hashnode. There is no obvious BBP for Hashnode, but a brief documentation worked for me. After about 20 days, they patched the vulnerability and issued a bounty to me.

33
Subscribe to my newsletter

Read articles from Voorivex directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Voorivex
Voorivex

I work as an instructor and hunter, leading a small team to grow, learn, and innovate.