In this non-technical post, I talk about why red teaming doesn't have to turn you into the villain, discussed various non-technical aspects of responsible red teaming.
6 min read
Marcus Aurelius once wrote: "The impediment to action advances action. What stands in the way becomes the way."
I think about this quote a lot when hunting bugs or conducting simulations for clients. Sometimes the universe just aligns. A file upload feature here, a missing validation there, a configuration file in an unusual location. Each weakness, unremarkable on its own. Together? Sometimes forms potential infrastructure takeover.
A few months ago (September '25), my collaborator Swapnil and I were working on a bug bounty program for a cloud hosting provider. They offered VPS services and other usual services that cloud hosting providers offer.
For context: I'm a red team operator and security researcher who goes by "kr1shna4garwal" (Krishna Agarwal). I spend my days (and nights, let's be honest) trying to break things that shouldn't break and acting like a state-sponsored threat actor sometimes.
Act 1: A Feature That Changed Everything
This program was particularly dominated by my friend Swapnil (shoutout to this great mysterious man!). He'd been working on it for a long time, reporting a bunch of access control and cross-site scripting issues, so one day he invited me to hunt on this together.
We worked together on this for a few months and together reported a few more issues back in time, like oVirt panel credential exposure due to issue in async request processing, that's a story for another day.
One day (on 29th September), scrolling through the customer panel, I noticed something new. They had added a support ticket system. customers could now raise support requests directly from the dashboard. Behind the scenes, they were using Jira, though.
So the support ticket system takes a Summary, Description, and a "revolutionary" attachment feature. Fair enough. I lorem ipsum'd both fields and attached a beautiful, non-harmful image.
Here is the redacted POST request:
POST /[Somefile].php HTTP/2Host: customer.REDACTED.comCookie: [...My Cookies...]Content-Length: 682X-Requested-With: XMLHttpRequestUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36Accept: application/json, text/javascript, */*; q=0.01Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0nA5NXTKKKAAOBGTOrigin: https://customer.REDACTED.comAccept-Encoding: gzip, deflate, br------WebKitFormBoundary0nA5NXTKKKAAOBGTContent-Disposition: form-data; name="action"open_a_ticket------WebKitFormBoundary0nA5NXTKKKAAOBGTContent-Disposition: form-data; name="token"[...MY Access Token...]------WebKitFormBoundary0nA5NXTKKKAAOBGTContent-Disposition: form-data; name="summary"Quo usque tandem, patientia nostra?------WebKitFormBoundary0nA5NXTKKKAAOBGTContent-Disposition: form-data; name="message"Nec dubitamus multa iter quae et nos invenerat.------WebKitFormBoundary0nA5NXTKKKAAOBGTContent-Disposition: form-data; name="file[]"; filename="fox.jpeg"Content-Type: image/jpeg[...JPEG...]------WebKitFormBoundary0nA5NXTKKKAAOBGT--
The file went through, and I refreshed the page. I found a hyperlink in my support request that, when clicked, initiated a separate POST request to download my file. Interesting!
The request was:
POST /[Somefile].php HTTP/2Host: customer.REDACTED.comCookie: [...My Cookies...]Content-Length: 107X-Requested-With: XMLHttpRequestUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36Accept: */*Content-Type: application/x-www-form-urlencoded; charset=UTF-8Origin: https://customer.REDACTED.comAccept-Encoding: gzip, deflate, braction=download_file&token=[...MY Access Token...]&download_url=https://support.REDACTED.com/_servicedesk/customers/secure/attachment/[...Some Kind of Integer id...]/fox.jpeg?fromIssue=[...My Issue Number...]
Act 2: The SSRFing
There it was. That beautiful, terrible parameter that got my attention immediately: download_url.
For those unfamiliar, seeing a parameter like download_url, which by design seems to take input as a URL, is a red flag for Server-Side Request Forgery (SSRF)!
I replaced the value of the download_url parameter with my out-of-band server, and sure enough, I received the HTTP request, confirming Server-Side Request Forgery. Next up, I could scan for internal services here. But destiny had other plans.
Act 3: file:// Magic
In this materialistic world, HTTP and HTTPS aren't the only URL protocols. Some are more dangerous than others. The file:// protocol, for instance, allows access to the local filesystem.
Most HTTP libraries support multiple protocols. PHP's file_get_contents(), cURL, and similar functions will happily process file:// URLs unless explicitly filtered.
Now, this is where the so-called philosopher inside me appreciated the elegance of the universe. A Server-Side Request Forgery that accepts the file:// protocol is essentially a Local File Inclusion (LFI) vulnerability in disguise. Imagine if I had just stopped and reported this. Surely this was a high-severity finding, certainly. But my goal was critical.
Let me show you what's happening behind the scenes, based on my understanding, here is pseudo-code I imagined
function download_req($action, $token, $download_url) { $user = validate_user_token($token) $foo = curl_init() curl_setopt($foo, CURLOPT_URL, $download_url); curl_setopt($foo, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($foo); // curl_exec foo! curl_close($foo); if (!$user) { return error_resp("Invalid Token"); } // If token is invalid, no further processing if ($action === 'download_file') { $file_content = $result; return $file_content; // return response }}?>
The application was essentially acting as a proxy, It takes a URL, fetches its content and return it to the user in response.
PHP has a feature called stream wrappers that can chain filters together. In recent years, many researchers have discovered ways to use these chains to achieve arbitrary code execution through file read. But here, the application wasn't processing PHP filters. Afterwards, I tried several other techniques such as gopher:// and other potentially dangerous protocols, log poisoning, SSH keys, etc. But all attempts failed miserably.
Act 4: When One Door Closes
Someone said it right: when one path closes, examine or carve another. We couldn't get code execution through conventional means, unfortunately. But we had arbitrary file read, and the next most valuable thing to read is configuration files.
Modern web applications are essentially configured secrets wrapped in code. credentials, API keys, encryption keys. All stored somewhere, in some form, on the server.
I started fuzzing for the usual suspects like .env, database.php, config.php in usual paths like /, /var/www/html, /usr/share/nginx/html, and so on.
All returned empty responses. Unfortunately.
From past whitebox based assessments, I learned that developers sometimes deploy applications in various ways. Docker containers often mount application code to /app or /opt/app, sometimes developers find /opt/ convenient for organization, and many other such non-standard patterns exist.
From here, I guessed many, but got hit at:
download_url=file:///opt/app/config.php
And here we go! It returned about 300 lines of response containing the disaster.
I got everything, including (but not limited to) configuration values, credentials, secrets, API keys, tokens. Everything in plaintext. This single file contained everything.
Just for clarity, here are the items we got: LiveChat credentials, Kayako credentials, Jira Service credentials, cPanel credentials, Plesk credentials, ISPManager credentials, Foreman infrastructure credentials (spanning 13 regions: RU, NL, DE, IS, US, UK, FR, TR, ES, IT, CH, PL, FI), database credentials, RabbitMQ credentials, PayPal/BitPay/Stripe/Coinbase credentials, email credentials, GitHub credentials, PowerDNS keys, encryption keys, and more.
Do I even need to write the impact in a fancy manner here? This is complete potential infrastructure takeover. We stopped right after getting the credentials and reported it right away. Using these credentials and the Local File Inclusion issue, I could have gained control over all their mentioned financial platforms, databases, repositories, panels, and application PHP source codes, respectively.
Attack Vector (AV:N): Network - exploitable remotely over the internet
Attack Complexity (AC:L): Low - no special conditions required beyond basic authentication
Privileges Required (PR:L): Low - only requires a standard user account
User Interaction (UI:N): None - attacker can exploit without user participation
Scope (S:C): Changed - the vulnerability affects resources beyond its original security scope
Confidentiality Impact (C:H): High - total disclosure of all files on the system
Integrity Impact (I:H): High - with exposed credentials, attacker can modify any data
Availability Impact (A:H): High - attacker can disrupt or destroy services using exposed credentials
Responsible Disclosure
We documented everything and reported the vulnerability to their Chief Technology Officer. He responded promptly and immediately took action.
Timeline:
29 September '25 - Noticed the new support system feature
30 September '25 - Discovery and reported
1 October '25 - They disabled the support functionality entirely
8 October '25 - Bounty awarded: $4,500
12 October '25 - They redesigned the attachment download functionality entirely
23 November '25 - Publishing this writeup
Disclaimer: All details have been anonymized or changed to protect the affected organization. No actual credentials or identifying information are included.