The lab
here is how portswigger describes it
This website has an unauthenticated admin panel at /admin, but a front-end system has been configured to block external access to that path. However, the back-end application is built on a framework that supports the X-Original-URL header.
To solve the lab, access the admin panel and delete the user carlos.
what an access control vulnerability is?#
access control is the layer that decides who is allowed to do what. authentication proves who you are, access control decides what that identity is permitted to touch, and when it is done badly you end up able to reach pages or perform actions that should have been off limits.
a common way to get this wrong is to enforce the rule based purely on the requested url. here the site has an admin panel at /admin, and rather than properly protecting it, a front end system simply blocks any request whose path is /admin. the back end behind it has no such opinion, it will happily serve the panel to anyone who asks. so the only thing standing between us and admin is a front end that is making its decision off the url string, and that is a string we can influence more than the developers expected.
Step 1 - Confirm the admin path is blocked at the front end#
upon accessing the lab the home page shows a login and an admin panel button, and the description has already told us the shape of the setup.

following the admin panel link gets us refused.

the refusal comes back in plain text and reads like something the front end generated rather than the application itself, which matches the description. so the block lives at the front end, and if we can get our request to the back end while hiding the /admin path from that front end, we win.
Step 2 - Prove the back end honours X-Original-URL#
the description gives us the lever, the back end framework supports a header called X-Original-URL, which tells it to treat the path in that header as the real request path, overriding the one on the request line.
that is the crack. the front end inspects the actual request line, but the back end routes on the header. so we point the request line at something harmless and smuggle the path we care about inside the header.
first we test that the back end is really reading the header. we set the request line to / and add a header pointing at a path that does not exist:
GET / HTTP/1.1
X-Original-URL: /logicbreaker
the app comes back with a not found response.

that is exactly the proof we wanted. the request line / is a real page, so if the app were routing on that we would get the home page. instead it returned not found, which means it routed on /logicbreaker from the header. the back end is trusting X-Original-URL.
Step 3 - Reach the admin panel through the header#
now we aim the header at the path the front end was blocking. the request line stays as /, which the front end sees and happily allows, while the header carries /admin to the back end:
GET / HTTP/1.1
X-Original-URL: /admin
the front end only sees / on the request line, so it has nothing to block, and the back end reads the header and serves the admin panel.

Step 4 - Delete carlos and solve#
reaching the panel is not the goal on its own, we still need to run the delete action. the admin panel deletes a user through a path like /admin/delete with a username parameter, so we recreate that request the same way.
the path goes in the header, and the query string carries the target. one detail worth noting, the query string stays on the real request line rather than in the header:
GET /?username=carlos HTTP/1.1
X-Original-URL: /admin/delete

the front end sees only / with a harmless looking parameter, the back end routes /admin/delete and reads username=carlos, and the account is removed.

with this, the lab is solved!
