Back to the blog
Vulnerability12 min readUpdated

7-Step Exploit Chain to Full SaaS Admin Takeover

A recent pentest of an established B2B SaaS: 7 small flaws chained into a full pre-auth admin takeover from zero — no user, no password.

Diego Melo, author
Diego Melo

Security researcher and founder of No Vuln

7-Step Exploit Chain to Full SaaS Admin Takeover

Imagine opening the internet, going to your SaaS's address, and — without a login, without a password, without clicking “forgot my password,” without any account on your platform — becoming the master administrator of your application in 5 minutes. Reading all your customers' data. Editing contracts. Changing settings. Resetting passwords.

That's exactly what we found in a recent pentest of an established B2B SaaS with an active customer base of hundreds and millions in ARR. The chain has 7 links. Each link, on its own, looks small. In sequence, they turn into a catastrophe.

This post is the anonymized technical breakdown of what happened — because the 7 links recur in Brazilian B2B SaaS with worrying frequency. If you run a web platform, it's worth reading.

The scenario (anonymized)

  • Application: B2B SaaS in production, active customer base, enterprise contracts
  • Stack: PHP + Adianti Framework (a Brazilian framework popular in ERPs and administrative platforms)
  • Database: multi-tenant MySQL
  • Infra: shared server, PHP 8.x, Apache
  • Consolidated engagement severity: 53 findings — 12 critical, 17 high, 8 medium, 9 low, 7 informational

The client had been through other audits before. “It was clean,” they said. Under focused adversarial research, we found 53 distinct vulnerabilities. This post focuses on one of the 12 criticals: the 7-link chain that becomes a pre-auth master-admin takeover. Other critical findings will be the subject of their own posts.

The chain in 1 minute

#LinkSeverity in isolationRole in the chain
1A file upload service reachable without authenticationMediumPoint of entry
2Weak extension validation (accepts .phar)MediumFools the filter
3Upload directory inside the webroot + PHP execution allowedMediumLets you run what you uploaded
4The application.ini file is readable by the web processHighExposes secrets
5Password hashing in unsalted MD5HighLets you forge a credential
6A REST API validating the token by shared secret (not a signed JWT)MediumMakes it possible to create an admin session
7Filesystem-based PHP session, with no ID rotation on loginMediumAdmin session hijack

Each row is “medium” in isolation. Combined → Catastrophic critical.

The story, step by step

Link 1 — the back door with no key

The first discovery was an endpoint:

POST /engine.php?class=AdiantiUploaderService

The Adianti Framework exposes certain classes as “public” via a configuration file. The dev team marked the upload service as public — probably to allow avatar uploads at signup. The result: anyone on the internet could upload files onto the server. No login. No token. Nothing.

The physical equivalent: a back door to the office, no lock, and nobody watching.

Link 2 — the filter that filters badly

The service validated the extension of the uploaded file. It blocked .php. But it accepted .phar — Phar is a PHP archive format that acts as a packaged executable. The server didn't tell them apart.

The physical equivalent: the back door had a “no sneakers allowed” sign, but let sandals through. Same problem, different outfit.

Link 3 — the folder where everything executes

The upload landed in /var/www/html/tmp/. By Apache default, everything in that folder could be executed as PHP — including the .phar we'd just uploaded.

The result: the attacker uploads a file, then accesses that file by its URL, and the server runs the code they wrote.

1. POST /engine.php?class=AdiantiUploaderService   ← uploads payload.phar
2. GET  /tmp/payload.phar                           ← the server executes it
3. The attacker now runs commands on the server     ← RCE confirmed

In plain terms: the attacker now runs code on your server as if it were their own server.

Link 4 — the vault of secrets with no door

With RCE in hand, the attacker reads the application's configuration file:

[general]
seed = "abc123def456..."           ← used to sign tokens
rest_key = "789xyz..."             ← key for the REST API
db_password = "..."

These secrets should have been protected. But the PHP process that serves the site has read permission on that file (it needs it, to run). Since the attacker now runs as the PHP process, they read everything.

In plain terms: the intruder found the owner's little notebook, with every password written down.

Link 5 — passwords that turned to wet sugar

The password hashes in the database used unsalted MD5. MD5 is a hash algorithm from 1992 — broken for password use since ~2010. Without a salt, any common password shows up in rainbow tables (precomputed tables) in seconds.

Worse: with the “seed” from application.ini in hand, the attacker can forge the admin's password directly without needing to crack anything. Knowing the algorithm + having the key = you can invent whatever hash you want.

In plain terms: your users' passwords were kept behind a 90s bicycle lock. Anyone opens it.

Link 6 — the API that trusted the shared secret

The application had a REST API authenticated by a shared secret (not a signed JWT, not OAuth — just “send the right secret, I'll trust you”). With the rest_key in hand (leaked in link 4), the attacker calls the API as if it were the server talking to itself.

The API allows creating an admin session — because that's how the master admin signs in. The attacker now has a valid master-admin session_id.

In plain terms: the intruder found the password to the building's internal intercom, called the front desk, and said “send the master key up to my apartment.” The front desk sent it.

Link 7 — the session that doesn't rotate

The attacker takes the freshly created session_id and pastes it into an anonymous browser. Because the application doesn't rotate the session ID between login and the authenticated state, the ID created by the API works directly in the browser.

The attacker reloads the page → they're in as the master admin. Reads all the data. Creates fake contracts. Deletes records. Everything the master admin can do, they can do.

In plain terms: the credential they forged over the phone worked straight at the turnstile. No questions asked.

The impact if exploited by a real attacker

The estimate we presented to the client:

  • Leak of sensitive personal data (Art. 5 + Art. 11 of Brazil's data protection law, the LGPD) of the entire customer base
  • Potential LGPD fine: up to 2% of revenue or R$ 50,000,000 per violation (cap)
  • Breach of enterprise contracts: SLA, DPA, security clauses
  • Mandatory notification to the ANPD (Brazil's National Data Protection Authority) within 3 business days (ANPD Resolution CD/ANPD No. 15/2024)
  • Notification to the affected data subjects (the whole base)
  • Post-incident churn: 4 in 10 Brazilian consumers don't come back after an incident (Akamai, 2024)
  • Response cost: incident team, legal, communications, forensics, customer retention
  • Reputation: years to recover

The chain was reported, validated with a reproducible PoC in 10 steps (a Postman collection), and fixed by the client within ~72h. As far as anyone knows, it was never exploited by a real attacker.

Why this slipped past earlier audits

3 practical reasons:

1. A scanner doesn't build a chain

Each link in isolation is medium — that is, an automated scanner would raise 5 disconnected “medium” alerts, which the team probably deprioritized (“we have worse things to fix”). No scanner connects “unauthenticated upload” + “.phar extension accepted” + “execution in the webroot” + “config.ini readable” + “unsalted MD5” + “REST API with a shared secret” + “session doesn't rotate” and says “this becomes an admin takeover.”

2. A checklist audit looks by category, not by chain

“ISO 27001 controls check”-style audits look at whether the company has controls in each category, not whether the chained controls survive a creative attacker. The result: the company would mark 90% of the checks green and still have the chain live.

3. Adianti Framework exposes a lot by default

Adianti has legacy defaults (from 2014–2018) that made sense for ERPs on an internal network. In a multi-tenant SaaS exposed on the public internet in 2026, those defaults become attack surface. If you don't know that a public, unauthenticated AdiantiUploaderService is a vulnerability, you won't go looking for it. Adianti has thousands of Brazilian installations with these same defaults.

How to audit it yourself (10 minutes)

If your platform uses Adianti (or any PHP framework), validate the 7 links:

  1. List all the “public” classes of your framework. Which are reachable without authentication? Each one is a potential point of entry.
  2. For upload services: validate extensions with an allow-list (not a block-list). Include .phar, .phtml, .pht, .svg, .html in the explicit block-list.
  3. Upload directory: is it outside the webroot? If inside, is PHP execution disabled (.htaccess or nginx config)?
  4. Configuration files: application.ini / .env / config.php — what are the permissions? Who can read them? If web PHP can read them, an attacker with RCE can read them.
  5. Password hashing: are you using password_hash() (PHP native, bcrypt/argon2)? Or MD5/SHA1?
  6. Internal APIs: is authentication by a signed JWT or a shared secret? If shared, it leaks along with the config.
  7. Sessions: does your framework rotate the session_id on every change of auth state? Native PHP does NOT rotate by default — it needs session_regenerate_id(true).

Each item takes ~1-2 minutes to check. If 3+ are vulnerable, you're exposed to this class of chain.

The most important lesson

The difference between the vulnerable team and the secure team isn't talent. It's an adversarial validation process:

  • Code review focused on chains, not isolated bugs
  • Periodic pentesting that validates “how” an attacker chains things
  • Updating a framework's legacy defaults
  • Automated tests that validate the entire authorization chain

If your platform is PHP + a popular framework (Adianti — the only Brazilian one of the bunch — Laravel, Symfony, CodeIgniter), or any legacy stack, it's worth auditing this pattern specifically before you discover it the hard way.

What to do today

  1. Map your framework's public classes/endpoints. What does each one do?
  2. Audit upload endpoints with the 7 tests above.
  3. If you don't have a documented pentest in the last 12 months, you don't know whether your platform is exposed. You only know that it hasn't been attacked yet.
  4. If you find 1+ vulnerable link, commission a pentest focused on chains.

To talk about a pentest focused on ATO chains:

If you want to jump straight to action: request a pentest. Mutual NDA within 24h.

Share

WhatsAppLinkedInX

Next step

Want to apply this to your system?

No Vuln runs in-depth penetration tests with the same methodology described in this article. Request a proposal: mutual NDA within 24h, scope defined on a technical call.