This page looks best with JavaScript enabled

ImaginaryCTF 2021

 ·  ☕ 12 min read

ImaginaryCTF 2021 Banner

This weekend I participated in ImaginaryCTF 2021 with WreckTheLine. We finished third out of 1018 entrants, the final team to complete all 55 challenges and hit 11330 points, missing second place by 3.5 minutes.

System Hardening 5

“CyberPatriot but run by roo fanatics… What could go wrong…”

At 450 points, System Hardening 5 was in the highest value category of challenges. It was in the pattern of CyberPatriot, where teams download and run a VM, find and fix all of the vulnerabilities. A scoring engine checks the scored items every minute or so and reports to the scoreboard.

As with CyberPatriot, we start with the readme. The major constrants today are:

  • No updates at all. Makes sense, that’s time-consuming and uninteresting.
  • Critical services: Remote desktop, print spooler.
  • Don’t disable SMB or the scoring engine may break


Let’s start1 with the Forensics questions, as the readme says, so we don’t accidentally destroy artifacts we’ll need to answer them.

An [sic] user on this system was compromised, allowing rooReaper to break in. What is the username of this user?

Well, let’s look around. Event log? Exploit artifacts? Let’s check this other file on the desktop first, text-messages-from-mom.txt.txt [sic].

MOMtotallynotrooreaper: hi roo! this is definitely mom here
rooYay: oh hi mom didnt see you there all too well
MOMtotallynotrooreaper: ?
rooYay: nvm, what did you text me for
rooYay: and is this a new number? i dont see the message history
MOMtotallynotrooreaper: uhh yea this is a new number
rooYay: oh ok
MOMtotallynotrooreaper: can you disable your firewall? and enable smb pls and disable your antivirus
rooYay: ok whatever you say mom…
rooYay: ill look up some tutorials
MOMtotallynotrooreaper: NO DONT LOOK AT TUTORIALS
rooYay: ok i wont what do i do
MOMtotallynotrooreaper: ill show you
MOMtotallynotrooreaper: btw what is rooPOG’s password
MOMtotallynotrooreaper: i heard he loves sharing
rooYay: its just password1337 nothing fancy
MOMtotallynotrooreaper: thanks!

Looks like rooYay fell for the classic “hi this is your mother” phish, and gave out rooPOG’s password.

That also gives us the answer to the next forensics question:

What is the password of the compromised user from the previous question?

password1337 nothing fancy

The other forensics questions require some research.

What is the CVE ID of the vulnerability that allowed rooReaper to escalate privileges?

I poked around in rooPOG’s home directory for a bit, finding xconsole.exe and vlib.dll in Downloads\foobar. The latter was detected as Win32/Mamson.A!ac, which doesn’t obviously use any exploits. I enabled Windows Defender and started a scan, and before I had found anything in the event viewer, it had found C:\temp\PrintNightmareLPE.exe.

Print Nightmare, CVE-2021-34527, is a recent vulnerability that allows a remote attacker to install a malicious printer driver configuration DLL to gain arbitrary code execution as SYSTEM. A variant, CVE-2021-1675, is useful for local exploitation, and was accepted as the answer.

Both of these drop the payload, in this case vlib.dll, in C:\Windows\System32\spool\drivers\x64\3\, where Defender found it alongside reverse_64bit.dll which it detected as Win64/Meterpreter.B. I removed the whole folder, along with C:\temp\ and the contents of rooPOG’s Downloads folder.

The final forensics question seemed like a steganography problem, but referred to another challenge in the CTF:

The background image on the Desktop contains a secret message. What is it?
HINT: It’s related to another challenge in this CTF, a reversing challenge.

The wallpaper was background.png in rooYay’s Pictures folder. It consisted of a grid of tiles, in a repeating pattern Axxxx, where there were 3 options for x. No obvious pattern was shown by stegsolve, and it wasn’t clear how to decode information in the tile pattern. I put it aside until a teammate, JaGoTu, solved roolang, which seemed potentially-related. Sure enough:

JaGoTu: roolang was just a vm that was calculating fibonacii very slowly
JaGoTu: can you send the raw png somewhere?
JaGoTu: cause the roolang basically runs png files

They ran their roolang solution on the PNG, and sent back the output: Hello, and welcome to the roos’ server!

PUPs (Not the Fun Kind)

With the forensics questions out of the way, let’s continue through the readme. We have a list of needed services and software, so let’s remove anything that’s not on it, starting with the faux-Clippy in the corner of the screen.

First, the Startup folder for All Users in C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\. Two shortcuts:\

  • chksec - Shortcut.lnk to C:\Windows\System32\chksec.exe
  • essential - Shortcut.lnk to C:\Windows\System32\essential.bat

Delete the shortcuts, kill the processes, delete the Clippy executable. essential.bat printed some ascii art text, copied itself to the current user’s start menu Startup folder, opened a video, and then paused. Delete the batch file from System32 and from rooYay’s %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\.

Installed Programs

There’s a couple shortcuts on the desktop to software that wasn’t mentioned as required in the readme, so let’s remove those. I replaced Chrome with Firefox portable, since the roos prefer IE and I very much do not. Nothing else in add/remove programs looked relevant.

I also looked through the running processes, but nothing else there raised suspicion.


Next, let’s deal with users, which is easiest from compmgmt.msc/lusrmgr.msc.

  • Create roocursion as requested in the readme
  • Rename the local administrator account as requested (can be done here or in group policy)
  • Make sure nobody unauthorized is in Administrators or other privileged groups
  • Make sure no non-builtin users exist who aren’t mentioned, poking through their home folders before deleting
  • Disable the Guest account, and rename for good measure
  • Change the password for all users

I’ll mention here that CyberPatriot often uses significantly outdated best practices and ignores how things would be done in the real world, and we also don’t know what this challenge will score on, so a lot of the things I check and changes I make to this machine are excessive, paranoid, annoying, or otherwise just weird. If we lose points for something we can easily undo the change.

Services and Firewall

Interspersed with everything else, I poke around services.msc and the control panel to make sure everything is sane. Enable the firewall MOMtotallynotrooreaper told us to disable, then make sure there’s no nonstandard or scary rules enabled. Enable cloud protection and sample sumbission for Defender, make sure the scan finished. Right click My Computer and click Properties, check $PATH and other environment variables for anything weird, make sure Data Execution Prevention is enabled for all applications, and disable NetBIOS.

Group Policy

Before I forget, let’s make sure we’ve mitigated Print Nightmare. The MSRC post about it recommends updating (oops, can’t), disabling the print spooler (oops, can’t), or disabling inbound remote printing with Group Policy.

I fire up gpedit.msc, open Computer Configuration -> Windows Settings -> … Where’s Security Settings?

This is where I spent about half my time on this challenge. When launching gpedit.msc, Security Settings was missing. secpol.msc would show the tree starting at Windows Settings instead of Security Settings, and also omit the latter. Many answers on the internet offered a simple regsvr32 wsecedit.dll, which doesn’t work as the DLL doesn’t have the correct entrypoints for that. This is where I ran dism /online /cleanup-image /restorehealth and sfc /scannow, which didn’t find any problems.

I never did figure out what was done to break that, as I eventually gave up and tried importing an exported policy, and later started setting the policy registry entries by hand. After a while, I opened the mmc snapin again, and Security Settings was there!

It seems that it’s only broken for the rooYay user, and after setting UAC to require a password, I was unintentionally elevating as rooAstro, for whom it worked correctly. Something in my profile was breaking it. Some windows LD_LIBRARY_PATH type thing? Something in HKEY_CURRENT_USER? It didn’t directly affect security, so I put it aside again.

From there, I could much more easily continue with group policy, basically just going through every option in Security Settings -> Account Policies and Local Policies and some folders in Administrative Templates -> Windows Settings, setting everything to the most paranoid sane option.

  • Remember 24 passwords

  • 60 day maximum password age

  • 10 day minimum password age

  • 14 character minimum length

  • Must meet complexity requirements

  • No reversible encryption

  • 15 minute account lockout duration

  • 10 attempt lockout threshold

  • 15 minute counter reset timer

  • Enable all auditing

  • Go through User Rights Assignment and restrict everything as much as possible

  • Hey, why is Everyone allowed to take ownership of files??

  • You know what, let’s just disable the Administrator account

  • I don’t like Microsoft account signin

  • More audit everything

  • Prevent users from installing printer drivers (well… try to)

  • Restrict CD-ROM access to locally logged-on users

  • Require CTRL+ALT+DEL, force inactivity limit, require strong encryption

  • You get the idea.

Critical Services

And let’s not forget to secure our critical services. The MSRC Print Nightmare article points me to Administrative Templates -> Printers to disable “Allow Print Spooler to accept client connections”. There have been a few RDP vulnerabilities, but none with straightforward workarounds besides patching, so let’s just be sure to flip on all of the RDP server security policies. Lastly, we weren’t disallowed from disabling old versions of SMB, so use the powershell command in the docs, to check for SMBv1 and see it’s already disabled.

Other Stuff

In an advanced challenge, in addition to sfc and dism to look for “creatively modified” system files, hijackthis can be helpful. In case of a well-disguised network backdoor, it’s also good to check open sockets.


And with that, I had 91 points, 20/21 items, and the flag appeared in the score report. There was one scored item I never found, worth 9 points. I’ll be waiting for a writeup from tirefire, the only person with 100 points, to see what I missed!


First, Eth007, one of the challenge authors, let me know that the last scored item I couldn’t find was LSA protection.

Second, thank you to the iCTF team for selecting this as a prize-winning writeup!

New Technology

“If it’s not Windows New Technology, what else could NT stand for?”

This was a 300pt cryptography challenge in the form of a python script. It generates a random key consisting of 5 tuples, each a 512-bit prime p and an integer exponent $1 \le e < 4$.

def gen():
    private = []
    for _ in range(5):
        p = getPrime(512)
        e = getRandomRange(1, 4)
        private.append((p, e))
    return private, normalize(private)

The public key is derived by the normalize() function, which multiplies together p**e for each private key component.

def normalize(fac):
    n = 1
    for p, e in fac:
        n *= p**e
    return n

Since these are 512-bit primes, the resulting public key isn’t easy to factor.

The private key is put through the deriv() function, which has a nested loop based on divs().

def deriv(priv):
    res = 0
    for d1 in divs(priv):
        for d2 in divs(d1):
            res += normalize(d2) * phi(d2) * phi(div(d1, d2))
    return res

divs() takes the same List[Set[prime: int, exponent: int], ...] private key format and yields the cartesian product of them based on the range 0..e of their exponents:

def divs(fac, pre=None):
    if pre is None:
        pre = []
    if not fac:
        yield pre
        p, e = fac[0]
        for i in range(0, e + 1):
            yield from divs(fac[1:], pre + [(p, i)]
>>> priv = [(167, 1), (149, 2)]
>>> pprint(list(divs(priv)))
[[(167, 0), (149, 0)],
 [(167, 0), (149, 1)],
 [(167, 0), (149, 2)],
 [(167, 1), (149, 0)],
 [(167, 1), (149, 1)],
 [(167, 1), (149, 2)]]

For each entry in this list, d1, deriv() again calls divs(), and loops on that d2, adding normalize(d2) * phi(d2) * phi(div(d1, d2)) to a running total.

def phi(fac):
    res = 1
    for p, e in fac:
        if not e: continue
        res *= (p**(e - 1)) * (p - 1)
    return res

def div(a, b):  # a=d1, b=d2
    b = dict(b)
    res = []
    for p, e in a:
        assert e >= b[p]
        res.append((p, e - b[p]))
    return res

At some point here things started to smell funny - we’re collecting the sum of some math done on every divisor of every divisor… Just for fun, let’s see how this behaves when run all the way through with more manageable numbers, maybe 12 bits.

def gen_custom(count=5, size=512, max_exp=3):
    private = []
    for _ in range(count):
        p = getPrime(size)
        e = getRandomRange(1, max_exp+1)
        private.append((p, e))
    return private, normalize(private)
>>> priv, pub = gen_custom(5, 12, 3)
>>> priv
[(3251, 1), (3863, 1), (2917, 2), (3761, 3), (3989, 2)]
>>> pub
>>> key = deriv(priv)
>>> key
>>> from sage.all import *
>>> factor(pub)
2917^2 * 3251 * 3761^3 * 3863 * 3989^2
>>> factor(key)
2917^4 * 3251^2 * 3761^6 * 3863^2 * 3989^4

If you don’t have sagemath installed, you can also use a handy dandy webassembly ECM factorization tool.

The factors of the public key aren’t surprising, that’s the exact math we used to derive it from the private key, but possible to reverse since we used small numbers. On the other hand, what’s up with the private key? Each factor just has double the exponent? And since $x^2 * y^2 = (xy)^2$, and $(x^3)^2 = x^6$… Our private key is just the square of the public key??

>>> key == pub**2

Well then! Let’s assign those commented output values to variables, square the public key, get a new AES instance for decrypting, and…

pub = 0x281ab467e16cdedb97a298249bdd334f0cc7d54177ed0946c04ec26da111...
ciphertext = bytes.fromhex("d2463ccc52075674effbad1b1ea5ae9a9c8106f1...")
key = pub**2
cipher =
    iv=b"\0" * 16,
plaintext = cipher.decrypt(ciphertext)

And, drumroll… b'ictf{Would_number_theory_be_new_technology?}\x04\x04\x04\x04'! For good practice, let’s clean that up a bit by removing the padding and turning it back into a unicode string:

from Crypto.Util.Padding import unpad
print(unpad(plaintext, 16).decode("utf-8"))

And there we have it!


Thank you to my team for a friendly place to be the dumbest person in the room, and to the ImaginaryCTF platform folks and challenge developers for the interesting and challenging … challenges.

The header image is from the ImaginaryCTF 2021 site (repo), used with permission from Astro.

  1. I’ve taken some liberties with the order I present objectives here, since a writeup that followed my actual path would be impossible to follow. ↩︎