Q1:
注入一个cat /flag,为什么要写成lipsum.globals[‘os’].popen(‘cat /flag’).read()
在模板里从 Jinja2 默认能访问到的对象出发,沿着 Python 对象模型一路找到 os 模块,再执行命令并把输出读回来。
拆开看:
1 | {{ lipsum.__globals__['os'].popen('cat /flag').read() }} |
1. 为什么不能直接写 os.popen(...)?
在 Jinja2 模板上下文里,默认并没有把 Python 的 os 模块暴露成变量。你直接写:
1 | {{ os.popen('cat /flag').read() }} |
模板引擎会认为 os 是一个未定义变量,通常报错或渲染为空。SSTI 要做的就是:从模板里能访问到的对象,找到 Python 全局命名空间,再拿到危险模块。
2. lipsum 是什么?
lipsum 是 Jinja2 自带的一个全局函数,用来生成 Lorem Ipsum 文本。默认情况下,Jinja2 模板里可以直接调用:
1 | {{ lipsum() }} |
所以它是一个模板里“天然可访问”的 Python 函数对象。
3. __globals__ 是什么?
在 Python 中,函数对象有一个属性 __globals__,它返回这个函数定义时所在模块的全局命名空间字典。
lipsum 定义在 Jinja2 的 jinja2.utils 模块里,而这个模块顶部 import os,所以:
1 | lipsum.__globals__['os'] |
就相当于拿到了 jinja2.utils 模块里的 os 模块。
换句话说:
1 | lipsum.__globals__ |
4. 为什么用 ['os'] 而不是 .os?
因为 __globals__ 是一个字典。字典取键可以用:
1 | d['os'] |
也可以用属性方式:
1 | d.os |
但方括号更明确,而且当键名不是合法标识符时必须用方括号。这里写 ['os'] 就是标准的字典取值。
5. 为什么要 .popen(...).read()?
os.popen('cat /flag') 会通过 shell 执行命令,并返回一个类似文件的对象。命令输出在管道里,必须调用 .read() 才能把输出读成字符串。
如果不加 .read():
1 | {{ lipsum.__globals__['os'].popen('cat /flag') }} |
模板最终渲染出来的可能是类似:
1 | <os._wrap_close object at 0x...> |
而不是 /flag 的内容。
如果换成 os.system('cat /flag'),它返回的是退出码,命令输出通常直接写到服务器进程的标准输出,不一定进入 HTTP 响应,所以 SSTI 里更常用 popen(...).read()。
6. 整条链路的含义
1 | lipsum |
所以它不是固定咒语,而是:找一个模板里能访问到的 Python 函数,用它的 __globals__ 跳到 Python 全局空间,再找 os、subprocess、__builtins__ 等危险对象。
不同环境里可用的“跳板”可能不同,比如 cycler、joiner、namespace、Flask 的 config、url_for 等,都可能被拿来找 __globals__。lipsum 只是因为 Jinja2 默认提供、比较常用。
防御上,根本办法是不要把用户输入当模板渲染,使用 render_template 传变量,启用沙箱,限制模板全局对象,并让 Web 进程最小权限运行。以上仅用于授权测试或 CTF 场景。
评论