USC CTF Fall 2024 Writeups

Published on

colors (crypto)


This was a rather simple challenge. The message is easily recognizable as base64. After that hex --> binary --> ASCII, which will give the flag.

Flag: CYBORG{tR0jans_love_C4rdinal_@nd_G0ld}

iRobot (web)


Another simple challenge. By the name one can guess that it hints at checking the robots.txt file. Heading over to that file gives this:

          
User-agent: *
Disallow: /hidden/flag.txt
          
        
Going over /hidden/flag.txt gives the flag.

Flag: CYBORG{robots_txt_is_fun}

weirdtraffic (forensics)


We've been given a .pcapng file. Opening it in Wiresharks shows some connection captures. First I tried to find parts of the flag through a string search, which in turn actually gave the flag.

Flag: CYBORG{hping3-is-a-cool-tool}

pineapple (forensics)


Another Wireshark challenge. Opening it in Wireshark shows a lot of connections. I mean a lot. I started by filtering out the http connections first and found few.

We can see that the victim has tried to make several POST requests. One of those requests to / has a forms with usernmae, filename and filepw fields. There's also an .7z compressed file, which can be extracted. To extract the file right click on the stream then Follow > HTTP Stream. A pop-up will appear containing the data contents of the stream. Click on Save as and save the file on your computer. Make sure that the data as showed is raw (since it is binary file, saving it in a specific encoding may lead to data loss as not every encoding has every characters). Now the thing is that the file we just saved has some response text and also the raw bytes for the .7z file. To get rid of the response text, open the file in some text-editor such as vim or nano. Then manually remove the unwanted contents.
Now we check the file type:
Now we try to extract the file:
Okay.? There's a password? If you remeber correctly, there was a filepw field in a form found earlier. Checking one of the POST requests, we are able to see the form data submitted to the server.
This gives the filepw as conjoined_TRIANGLES. Using this to extract the .7z file gives us a image file containing the file.

Flag: CYBORG{pe4cefaRe_4x09}

think_twice (forensics)


From description it is evident that we have to use exiftool. Using exitfool to extract the data gave a base64 string in the Software section of the metadata. Decoding it gave another base64 string, decoding further gave the flag.

Flag: Cyb0rg{McCarthy}

decipherium (crypto) (Solved post-CTF)


The give file contains the following text:

TeSbILaTeSnTeNoISnTeCsCsDyICdTeIISnTeLaSbCdTeTeTeLaTeSbINoTeSbSbInICdTeBaSbSbISnIYbSbCdTeXeINoSbSbTeHoTeITeFmTeITeMdITeSbICsEr
From observation, it seems to be the symbols for elements for the periodic table. Converting it to their corresponding atomic numbers gave this:
52 51 53 57 52 50 52 102 53 50 52 55 55 66 53 48 52 53 53 50 52 57 51 48 52 52 52 57 52 51 53 102 52 51 51 49 53 48 52 56 51 51 53 50 53 70 51 48 52 54 53 102 51 51 52 67 52 53 52 100 52 53 52 101 53 52 51 53 55 68

At first it seems to be a simple hex but I was so wrong at that moment. Decoding it into hex didn't work; neither converting the whole number to bytes did. It was not until the end of the CTF I realized that we needed to simply convert each no inot its ASCII representation and de-hex the result.

Here's the solve script:

          
elements = { "H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5, "C": 6, "N": 7, "O": 8, "F": 9, "Ne": 10, "Na": 11, "Mg": 12, "Al": 13, "Si": 14, "P": 15, "S": 16, "Cl": 17, "Ar": 18, "K": 19, "Ca": 20, "Sc": 21, "Ti": 22, "V": 23, "Cr": 24, "Mn": 25, "Fe": 26, "Co": 27, "Ni": 28, "Cu": 29, "Zn": 30, "Ga": 31, "Ge": 32, "As": 33, "Se": 34, "Br": 35, "Kr": 36, "Rb": 37, "Sr": 38, "Y": 39, "Zr": 40, "Nb": 41, "Mo": 42, "Tc": 43, "Ru": 44, "Rh": 45, "Pd": 46, "Ag": 47, "Cd": 48, "In": 49, "Sn": 50, "Sb": 51, "Te": 52, "I": 53, "Xe": 54, "Cs": 55, "Ba": 56, "La": 57, "Ce": 58, "Pr": 59, "Nd": 60, "Pm": 61, "Sm": 62, "Eu": 63, "Gd": 64, "Tb": 65, "Dy": 66, "Ho": 67, "Er": 68, "Tm": 69, "Yb": 70, "Lu": 71, "Hf": 72, "Ta": 73, "W": 74, "Re": 75, "Os": 76, "Ir": 77, "Pt": 78, "Au": 79, "Hg": 80, "Tl": 81, "Pb": 82, "Bi": 83, "Po": 84, "At": 85, "Rn": 86, "Fr": 87, "Ra": 88, "Ac": 89, "Th": 90, "Pa": 91, "U": 92, "Np": 93, "Pu": 94, "Am": 95, "Cm": 96, "Bk": 97, "Cf": 98, "Es": 99, "Fm": 100, "Md": 101, "No": 102, "Lr": 103, "Rf": 104, "Db": 105, "Sg": 106, "Bh": 107, "Hs": 108, "Mt": 109, "Ds": 110, "Rg": 111, "Cn": 112, "Nh": 113, "Fl": 114, "Mc": 115, "Lv": 116, "Ts": 117, "Og": 118 }
ctext = "TeSbILaTeSnTeNoISnTeCsCsDyICdTeIISnTeLaSbCdTeTeTeLaTeSbINoTeSbSbInICdTeBaSbSbISnIYbSbCdTeXeINoSbSbTeHoTeITeFmTeITeMdITeSbICsEr"

elements_list = []
i = 0
while i < len(ctext):
    if 65 <= ord(ctext[i]) <= 90:
        if i + 1 < len(ctext) and 97 <= ord(ctext[i + 1]) <= 122:
            elements_list.append(ctext[i:i + 2])
            i += 2
        else:
            elements_list.append(ctext[i])
            i += 1

numbers = [elements[i] for i in elements_list]
hex_string = "".join([chr(i) for i in numbers])
print(b''.fromhex(hex_string).decode("utf-8"))
          
        

Flag: CYBORG{PERI0DIC_C1PH3R_0F_3LEMENT5}

unpopcorn (crypto)


We are given a python script which encrypts the flag of the challenge.


m = 57983
p = int(open("p.txt").read().strip())

def pop(s):
    return map(lambda x: ord(x)^42, s)

def butter(s):
    return map(lambda x: x*p%m, s)

def churn(s):
    l = list(map(lambda x: (x << 3), s))
    return " ".join(map(lambda x: "{:x}".format(x).upper(), l[16:] + l[:16]))

flag = open("flag.txt").read().strip()
message = open("message.txt", "w")
message.write(churn(butter(pop(flag))))
message.close()
        

  • The flag is first "pop"ed , i.e., each character is XORed with 42.
  • Then each character of the XORed flag has been multiplied with $p$ then taken modulo $m$.
  • Finally the resulting flag has been converted shifted 3-bits to the left, then converted to hex, rotated saved.
  • To solve the problem we need to apply each problem in its reverse order. Both pop() and churn() functions can be easily reversed; the problem lies with the butter() function since we do not know the value of $p$. Mathematically if $y$ is the result of the operation in butter(), then:
    $$ \begin{align*} x \cdot p &\equiv y &\mod m \\ x &\equiv y \cdot p^{-1} &\mod m \end{align*} $$ To find $p^{-1}$ we need to find $p$ which, unfortunately we don't have. But what we do have is that we know the flag format and hence, given that m is a small number, we can bruteforce values of $p^{-1}$ and for each flag retrieved we can see if it fits the format correctly or not. Below is the solve script for this:

    
    global m
    m = 57983
    ctext = [
        0x3FB60, 0x4F510, 0x42930, 0x31058, 0xDEA8, 0x4A818, 0xDEA8, 0x1AA88,
        0x65AE0, 0x1C590, 0x17898, 0x1C590, 0x29170, 0x3FB60, 0x55D10, 0x29170,
        0x42930, 0x6A7D8, 0x4C320, 0x4F510, 0x5FC0, 0x193A0, 0x4F510, 0x2E288,
        0x29170, 0x643F8, 0x31058, 0x6A7D8, 0x4A818, 0x1AA88, 0x1AA88
    ]
    
    def reverse_churn(s):
        dr = s[-16:] + s[:-16]
        return list(map(lambda x: (x >> 3), dr))
    
    def reverse_butter(s, p_inv):
        return list(map(lambda x: x * p_inv % m, s))
    
    def reverse_pop(s):
        try:
            return "".join(map(lambda x: chr(x ^ 42), s))
        except ValueError:
            return ""
    
    rev_churned = reverse_churn(ctext)
    
    for p_inv in range(1, m):
        try:
            rev_buttered = reverse_butter(rev_churned, p_inv)
            flag = reverse_pop(rev_buttered)
            if "CYBORG{" in flag:
                print(flag)
        except:
            continue
            

    Flag: CYBORG{R1Ch_BUTT3RY_SUSTENANC3}

    D'Lo (crypto)


    I might first add that this was the challenge I enjoyed most out of all crypto challeneges. The chall.sage contains the following:

    from Crypto.Util.number import *
    
    FLAG  = b"REDACTED"
    
    p = getPrime(256)
    q = getPrime(256)
    e = 7
    n = p*q
    d = int(pow(e, -1, (p-1)*(q-1)))
    
    c = pow(bytes_to_long(FLAG), e, n)
    print(f"n = {n}")
    print(f"c = {c}")
    print(f"d_low = {hex(d)[70:]}")
    
    """
    n = 9537465719795794225039144316914692273057528543708841063782449957642336689023241057406145879616743252508032700675419312420008008674130918587435719504215151
    c = 4845609252254934006909399633004175203346101095936020726816640779203362698009821013527198357879072429290619840028341235069145901864359947818105838016519415
    d_low = b9b24053029f5f424adc9278c750b42b0b2a134b0a52f13676e94c01ef77
    """
  • The program generates two 256-bit prime numbers $p$ and $q$.
  • It arbitraly takes the modulus, $e$ to be $7$.
  • Then it calculates the private modulus $d$, encrypts the flag and print the lower bits of $d$.
  • At first, given the small size of $e$, a small public exponent attacks come to mind. But given the similar size of $c$ the attack might not be feasible.
    Recall that $e$, $d$ and $N$ are related by the relation: $$ \begin{align*} e \cdot d &\equiv 1 \mod{\phi(N)} \\ \end{align*} $$ where $\phi(m) = (p-1)(q-1) = N-(p+q-1)$ is called the Euler's Totient Function.
    So, $$ \begin{align*} e \cdot d = k \phi(N) + 1 \\ e \cdot d - k \phi(N) = 1 \\ \end{align*} $$ for some positive integer $k$.
    Now $d \leq \phi(N)$ since $d = modInv(e,\phi(N))$. This implies that $1 \leq k \leq e$. Hence we will have to check for only $e$ values of $k$. Also we've been given some least significant bits of $d$ (let it be $b$). Therefore $d$ can be written as, $$ d=d_{0} + 2^{b} \cdot d_{1} $$ where $d_{0} = d \mod{2^{b}}$ is the known bits of $d$ and $d_{1}$ represents the unknown bits. Putting this value in the initial equation we get, $$ \begin{align*} e \cdot (d_{0} + 2^{b} \cdot d_{1}) - k \phi(N) = 1 \\ e \cdot d_{0} + e \cdot 2^{b} \cdot d_{1} - k \phi(N) = 1 \\ e \cdot d_{0} + e \cdot 2^{b} \cdot d_{1} - k \cdot (N-(p+q-1)) = 1 \\ \end{align*} $$ Put $X=p+q$, $$ \begin{align*} e \cdot d_{0} + e \cdot 2^{b} \cdot d_{1} - k \cdot (N-(X-1)) = 1 \\ k \cdot X = k \cdot N + k - e \cdot d_{0} + 1 + e \cdot 2^{b} \cdot d_{1} \\ \end{align*} $$ Further to remove the $d_{1}$ term, we solve the above equation modulo $2^{b}$ as $e \cdot 2^{b} \cdot d_{1} \equiv 0 \mod 2^{b}$. $$ \begin{align*} k \cdot X \equiv k \cdot N + k - e \cdot d_{0} + 1 \mod{2^{b}} \\ e \cdot d_{0} \cdot X - k\cdot X \cdot (N-X+1) + k \cdot N \equiv X \mod{2^{b}} \\ \end{align*} $$ Thus $p$ and $q$ be factorized easily and then the problem reduces to textbook RSA problem.
    
    d = int("0xb9b24053029f5f424adc9278c750b42b0b2a134b0a52f13676e94c01ef77",16)
    e = 7
    N = 9537465719795794225039144316914692273057528543708841063782449957642336689023241057406145879616743252508032700675419312420008008674130918587435719504215151
    known_bits = 240
    X = var('X')
    d0 = d % (2 ** known_bits)
    P. = PolynomialRing(Zmod(N))
    
    for k in range(1, e+1):
        try:
            results = solve_mod([e * d0 * X - k * X * (N - X + 1) + k * N == X], 2 ** 240)
            for m in results:
                f = x * 2 ** known_bits + ZZ(m[0])
                f = f.monic()
                roots = f.small_roots(X = 2 ** (N.nbits() / 2 - known_bits), beta=0.3)
                if roots:
                    x0 = roots[0]
                    p = gcd(2 ** known_bits * x0 + ZZ(m[0]), N)
                    q = N / ZZ(p)
                    print('p =', ZZ(p))
                    print('q =', N / ZZ(p))
                    assert ZZ(p)*(N/ZZ(p)) == N
                    break
        except:
            pass
    # p = 100571592176913563473553598439895415391332870938827636562155584701290520956889
    # q = 94832601466810038823671344791144082145009301280785700275762290283318790228359

    
    from Crypto.Util.number import long_to_bytes
    
    e = 7
    c = 4845609252254934006909399633004175203346101095936020726816640779203362698009821013527198357879072429290619840028341235069145901864359947818105838016519415
    n = 9537465719795794225039144316914692273057528543708841063782449957642336689023241057406145879616743252508032700675419312420008008674130918587435719504215151
    
    p = 100571592176913563473553598439895415391332870938827636562155584701290520956889
    q = 94832601466810038823671344791144082145009301280785700275762290283318790228359
    
    phi = (p-1)*(q-1)
    z = pow(e, -1, phi)
    m = pow(c, z, n)
    
    print(long_to_bytes(m))
    #CYBORG{H0w_w3ll_d0_y0u_th1nk_d'lo_w1ll_d0_7h15_53ason??}
                

    Flag: CYBORG{H0w_w3ll_d0_y0u_th1nk_d'lo_w1ll_d0_7h15_53ason??}

    beer sales (osint)


    Just search something like "Beer sale orlando august 2024"

    Submit the URL as flag.

    Flag: CYBORG{ftp://www.myflorida.com/pub/llweb/Beer4.pdf}

    TommyCam (osint)


    Wayback machine had a link to the first archive of the site. There is a link to view weather details via TommyCam. Clicking on that link has a link for another page which details the technical information of TommyCam.

    Flag: CYBORG{Toshiba 5200 80386}

    television (osint)


    Reversing searching the image by Google reveals a similar image at this link. From there, searching for "the radiant assembly of god church name" showed this link. From there we can see the name of the building in a below section. Searching for the name lands us on a Wikipedia page where the name of the architect is mentioned.

    Flag: CYBORG{Alfred Rosenheim}

    Tommy's Artventures (osint)


    First let's see what Tommy's upto. Seems like he just likes AI generated art.

    Well, there's a login page too. Since I don't have an account, I will sign up for one and then login.
    After I login, there's a "Curate" button and a "Log out" button. When "Curate" is clicked it gives an error and says that only admin can curate.
    If we see the localstorage we can see that there's a cookie stored. From the description we first read in the challenge, it seems that we have to forge the session cookie into thinking that we are the admin.
    From the format the cookie seems to be an JWT encoded cookie. Decoding it on jwt.io gives us the follwing results.
    So we need only change the username to admin then encode the cookie. Since we have the flask server's secret this is no heavy task.
    > flask-unsign --sign --secret "4a6282bf78c344a089a2dc5d2ca93ae6" --cookie "{'user': 'admin'}"
    Now changing this cookie with the one on the actual server would work. Now we can access the /curate button and get the flag.

    Flag: CYBORG{oce4n5_auth3N71ca7i0N}