KnightCTF 2025 Write Up

Before all

Team: QnQSec
Rank: 14/759
I participated in this CTF with team QnQSec, and I was assigned to solve those web challenges… which I don’t like them (in this CTF) as well.
They are easy but some are quite guessy, and I also encountered serveral infra issues…

Write Up

Knight Connect

An under maintainence live-chat app which only it’s login function was well implemented.
Take a close look at the loginUsingLink function in /app/Http/Controllers/AuthController.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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');
}

It’s verification based on the bcrypt value of email|time, which is 200% can be self modified and spoof…
But the part is what’s the bcrypt salt round?
Well… the result is that the round number is ten, WTF?

1
2
3
4
┌──(kali🐳kali)-[~/ctf/KightCTF/Knight-Connect]
└─$ grep -r 'BCRYPT_ROUNDS' .
./phpunit.xml: <env name="BCRYPT_ROUNDS" value="4"/>
./config/hashing.php: 'rounds' => env('BCRYPT_ROUNDS', 12),

Just sign my token with bcrypt(10 rounds), or easier just simply use online generator (link)

Btw, these are all possible admin emails … ?
image

Baby Injection

A black box challenge, url: http://172.105.121.246:5990/eWFtbDogSXRzIHlhbWwgYnJvLCBoYWNrIG1lIGlmIHlvdSBjYW4hISE=
Base64 decoded: yaml: Its yaml bro, hack me if you can!!!
Also, it’s a Flask web app
image
So my idea comes to the classic PyYaml deserialization vulneravility:
Info: https://net-square.com/yaml-deserialization-attack-in-python.html
Payload: yaml: !!python/object/apply:subprocess.getoutput ["ls -al"] (ofc, base64 encoded)
image
aha … WT …

Exceeding Knight

Another Laravel PHP application
After some observations, I found out that:

  • The flag is inside .env file
  • Debug mode on

Base on the challenge souce code, there are serveral methods to trigger an error message with .env leakage, I used the one in the calculator function:
/app/Http/Controllers/CalculatorController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CalculatorController extends Controller
{
public function index()
{
return view('calculator');
}

public function calculate(Request $request)
{
$request->validate([
'num1' => 'required|numeric',
'num2' => 'required|numeric',
'operation' => 'required|in:add,sub,mul,div',
]);

$num1 = $request->input('num1');
$num2 = $request->input('num2');
$operation = $request->input('operation');
$limit = env('MAX_CALC_LIMIT', 100000);

if ($num1 > $limit || $num2 > $limit) {
throw new \Exception("You have hit the calculation limit set in the .env file.");
}

$result = match ($operation) {
'add' => $num1 + $num2,
'sub' => $num1 - $num2,
'mul' => $num1 * $num2,
'div' => $num2 != 0 ? $num1 / $num2 : throw new \Exception("Division by zero is not allowed."),
};

return view('calculator', ['result' => $result]);
}
}

If the interger I input is greater than env('MAX_CALC_LIMIT', 100000);, an error response with environment variables would pop out.
image

Admin Access

A blackbox challenge with a password reset function, login to admin’s account to get flag!
I found that there’s an email address: wrapped with html comment below every response

1
<!-- kctf2025@knightctf.com -->

So kctf2025 and the email addr probably be the admin’s credential, and after some trial and error, if I self modified the Host Header in my reset request, my listener can get the password reset link

PortSwigger Host Header Attack (link)

Post to http://45.56.68.122:7474

image

Listener:
image

Flag:
image