
One Missing Parameter Cost Me Six Hours (PortSwigger Lab)
I spent six hours trying to upgrade a non-admin user to admin, convinced I was missing some clever bypass. The gap turned out to be one field in a request body I’d already looked at twice.
This is a PortSwigger lab on multi-step process access control. The setup: an admin panel with a user upgrade flow. You pick a user, hit upgrade, then confirm on a second screen before the change actually goes through. Not counting the admin login and accessing the admin panel, that’s two steps. The goal was to login as wiener (my non-admin account) and upgrade it to admin without ever having admin access to begin with.
What I tried that didn’t work
I followed and wrote out the steps the admin flow actually takes, so I could inspect each step individually. Checked the change-email route for anything reusable. Tried hitting the admin and admin-roles paths directly with different HTTP methods. Added the referrer header with the value I’d seen during the legitimate admin flow. Went through the HTML and JS on every relevant page. Tried looking for where the user list was being fetched from. Tried the user-ID-in-params trick that had worked on an earlier lab. None of this brought any results and just got me more frustrated.
The thing is, I was going at this problem with the assumption that in this scenario, I was a hacker with no idea of how the admin system actually worked when upgrading users. And that the lab giving me access to the admin credentials was just to hint towards any probable vulns. Why I slipped into that line of thinking, I have no idea.
As I watched the hours tick by on my laptop clock, I grew increasingly aware of the painful fact that some LLM somewhere could probably one-shot this problem. That I could end my suffering by taking a knee before the mighty oracle called Claude. And you as a reader are probably wondering why I didn’t submit. Well I was determined to actually learn. I had told myself going into this, that I was doing it to grow, not to feel like I was growing. That meant poring over documentation, analyzing every single possibility I could think of, understanding why every move I made worked or didn’t work.
All of that will only make me more productive when I finally integrate LLMs into my workflow. So no shortcuts. We go hard or we don’t go anywhere.
The actual find
I ended up intercepting every request in the full flow individually, from login through to the final confirmation, instead of guessing at which single request mattered. Comparing them side by side, the difference wasn’t in the headers like I initially thought. It was in the body of the very last request triggered at the confirmation stage:
Opening the confirmation screen sends:
username=wiener&action=upgrade
Actually confirming the upgrade sends:
action=upgrade&confirmed=true&username=wiener
The first time I inspected this I actually thought the answer was in the Referer header. That was a dead-end unfortunately, even when i tried with the other headers, despite my numerous testing of different endpoints.
My testing did, however, reveal that the answer was in the cookie. After trying everything I could think of, I assumed there was something obvious in the request that i was probably missing, so i just grabbed the confirmed final request from the admin user flow, and grabbed the session cookie from my non-admin user, pasted it into the confirmation request, and was greeted with a much needed 302 response.
Root cause
I know the lab was about an unprotected step in this multi-step process, and it makes sense that swapping the cookie would solve the problem. But that only told me the endpoint didn’t care whose session was making the request. It didn’t tell me why my own manual attempts, using my own valid cookie, kept failing with a 401. I wasn’t going to let my six hours disappear without finding out what the actual differentiator was.
Turns out it wasn’t headers, despite how convinced I’d been earlier. It was the request body. I’d tried smaller fetches to the endpoint to get a feel for what was going on, but never actually forged and sent the finished request with the confirmed field included. Send the request without confirmed=true and you get rejected, even with a fully authenticated cookie. Add that one field back in, same cookie, same account, and it succeeds. So the endpoint wasn’t checking who was asking at all. It was only checking whether the request was shaped a specific way, a much shorter list of requirements than “must be logged in as admin,” which meant none of the actual gatekeeping from earlier in the flow, the admin login, the panel access, the user selection, was being enforced at this step whatsoever.
In practice, the issue here was clear, even if the request body was the differentiator, there was insufficient protection at this step of the whole process. The developer assumed that getting to this phase would mean the user had to have:
- logged in as an admin
- accessed the admin panel
- selected a user to upgrade/downgrade
While everything up to the final point had proper access control, the utter lack of protection at the final step ultimately made the entire security system useless as the attacker (that’s us 😀) could simply bypass all that beautiful auth and send the final finished payload and get what they want, which was privilege escalation in this case.
The fix
This is pretty clear cut. Do not assume behavior when setting up access controls. The user can and will find ways to behave in ways you didn’t expect or take actions in orders you did not expect. Every step of a multi-step flow should have it’s own independent access controls implemented. Each step should assume the user is unauthorized by default and check to see if they have the necessary permissions to perform their related actions and/or access their related resources.
What I learned
I’d like to say this lab taught me persistence 😂. But yeah, six hours of thorough and deliberate hunting for the simple obvious bug didn’t actually leave me feeling like I wasted time. I was able to apply everything I had learned from the previous labs, checking for the vulnerabilities I had learned, using BurpSuite as an essential tool in my workflow. All of that feels really close to what I think real life security work looks like. I have no regrets. Looking forward to learning more.
Cover photo by Agence Olloweb on Unsplash