X

JerseyCTF 2025: Play-Fair!

Apr 02, 2025

author: @wxrth

Challenge Info:

Category: Binary Exploitation

TL;DR:

I ran the provided script on the encrypted message and got something that looked like a flag but didn’t start with jctf{, which was already suspicious. The middle looked fine, so I figured the grid was right, but the logic was off. Turns out the script was still encrypting instead of decrypting it was shifting right and down when it should’ve been shifting left and up. Flipped the signs, ran it again, and the real flag popped out.

The Challenge (Solution):

We’re handed some scrambled text

yjp}b{k{_vog1pnb2j31dhs1bsptln

Alongside a Python file that implements a Playfair cipher with a custom grid built from a seeded random shuffle.

import random
from random import randint
def reverseMe(p):
    random.seed(3211210)
    arr = ['j', 'b', 'c', 'd', '2', 'f', 'g', 'h', '1', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'y',
           'v', '3', '}', '{', '_']
    t = []
    for i in range(len(arr), 0, -1):
        l = randint(0, i-1)
        t.append(arr[l])
        arr.remove(arr[l])
        arr.reverse()
    for i in range(5):
        s = ''
        for j in range(5):
            s += t[5*i+j]
    o = ''
    for k in range(0, len(p)-1, 2):
        q1 = t.index(p[k])
        q2 = t.index(p[k+1])
        if q1 // 5 == q2 //5:
            o += t[(q1//5)*5 + ((q1+1)%5)]
            o += t[(q2//5)*5 + ((q2+1)%5)]
        elif q1 % 5 == q2 % 5:
            o += t[((q1//5 + 1) % 5 * 5) + (q1%5)]
            o += t[((q2//5 + 1) % 5 * 5) + (q2%5)]
        else:
            o += t[(q1//5)*5+(q2%5)]
            o += t[(q2//5)*5+(q1%5)]
    print(o)

if __name__ == '__main__':
    inp = "REDACTED"
	reverseMe(inp)

Also the note says this flag is in the format jctf{...}.

Step 1 - Play around:

Naturally, I just ran the script with yjp}b{k{_vog1pnb2j31dhs1bsptln as input and it printed out:

sytftbython_r3v3rsc3_qc2sh{31}

I noticed that the middle part was readable. I could clearly see the word r3v3rsc3, which looked like it might be part of the flag. But everything else looked off. The beginning didn’t even match the expected flag format jctf{..}.

That made me start questioning the logic of the Playfair cipher implementation in the script.

Step 2 - hmmmmm:

Since the output looked partially correct, I figured the character grid itself was probably fine especially because the middle decrypted cleanly.

So I went back to the code and looked closely at the Playfair logic:

if q1 // 5 == q2 // 5:
    o += t[(q1 // 5) * 5 + ((q1 + 1) % 5)]
    o += t[(q2 // 5) * 5 + ((q2 + 1) % 5)]
elif q1 % 5 == q2 % 5:
    o += t[((q1 // 5 + 1) % 5) * 5 + (q1 % 5)]
    o += t[((q2 // 5 + 1) % 5) * 5 + (q2 % 5)]
else:
    o += t[(q1 // 5) * 5 + (q2 % 5)]
    o += t[(q2 // 5) * 5 + (q1 % 5)]

This is clearly applying the encryption rules for a Playfair cipher:

But we’re supposed to be reversing the encryption not do it again. That explained why the output was jumbled at the edges.

Step 3 - Flip the directions:

Once I realized the script was still applying encryption logic, the fix was pretty straightforward I just needed to reverse the shift directions.

In Playfair decryption:

So I went into the code and changed the signs from + to - in the right places.

Here’s the updated decryption logic:

if q1 // 5 == q2 // 5:
    # change the sign from + to - to shift left instead of right
    o += t[(q1 // 5) * 5 + ((q1 - 1) % 5)]
    # change the sign from + to - to shift left instead of right
    o += t[(q2 // 5) * 5 + ((q2 - 1) % 5)]
elif q1 % 5 == q2 % 5:
    # change the sign from + to - to shift up instead of down
    o += t[((q1 // 5 - 1) % 5) * 5 + (q1 % 5)]
    # change the sign from + to - to shift up instead of down
    o += t[((q2 // 5 - 1) % 5) * 5 + (q2 % 5)]

With those changes, the script was no longer encrypting the encrypted text it was finally reversing it like it was supposed to.

Step 4 – Run it again:

After updating the shift directions, I ran the script again with the exact same input. And this time, the output was:

jctf{python_r3v3rs1ng_c22b3b1}

Nice. Got the flag :)

🚩 Final flag: jctf{python_r3v3rs1ng_c22b3b1}