
Detaillierter Proof-of-Concept und Analyse von CVE-2021-28378, einer gespeicherten XSS-Schwachstelle in Gitea, die beliebige Code-Injektion, Privilegieneskalation und Remote-Code-Ausführung über Git-Hooks ermöglicht.
Details zu dieser CVE hier.
Diese CVE betrifft das Fehlen einer String-Escape-Funktion auf der Client-Seite, wenn Inhalte vom Server abgerufen werden. Sie ermöglicht es einem Angreifer, einfach beliebigen Code einzuschleusen, indem er einen Kommentar zu einem Issue oder einem Pull Request erstellt. Dies wurde mit dem Commit 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5 Ende Januar 2020 eingeführt, aber aufgrund eines Squash können wir nicht genau wissen, wer dies getan hat, um nach ähnlichen eingeführten Fehlern zu suchen.
Lassen Sie uns zunächst verstehen, woher diese Schwachstelle kommt. Dazu sehen wir uns den Commit an, der diese CVE behebt.
Vorher:
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>
`
Wir sehen, dass es kein HTML-Escaping für label.name, issue.repository.full_name, issue.title und body gibt, bei denen es sich um Zeichenfolgen handelt, die ein Benutzer manipulieren kann, und somit beliebigen Code einschleusen kann.
Beachten Sie, dass issue.repository.full_name nicht wirklich manipuliert werden kann, um Code einzuschleusen, da es zu viele Regeln einhalten muss ([\w._-]+), die wir mit dieser CVE nicht umgehen können.
Genau das behebt dieser PR, indem sie in htmlEscape umgewandelt werden.
Nachher:
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>
`
Werfen wir einen kurzen Blick auf die Datei web_src/js/features/contextpopup.js, um herauszufinden, wie die CVE ausgelöst wird.
...
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>
`
});
});
}
Was wir dort sehen, ist, dass für jeden Knoten mit der Klasse ref-issue ein anfälliges Popup hinzugefügt wird.
Nun müssen wir herausfinden, wo ein ref-issue an den Benutzer gesendet wird: Grep wir (und behalten nur interessante Ergebnisse, also keine Testdateien oder ähnliches)!
Die folgende Commit-Nummer entspricht dem Tag 1.12.4, der Standardversion aus der offiziellen docker-compose.yml-Datei, die von dieser CVE betroffen ist.
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")
Wir wissen nun, dass es keine statischen Inhalte mit der Klasse ref-issue gibt, sondern dass sie dynamisch generiert werden.
Die letzte Zeile ist nicht interessant, da es sich nur um eine Sanitizer-Richtlinienregel handelt. Die anderen sind interessant, wir werden sehen, warum.
Tauchen wir in modules/markup/html.go ein.
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"))
}
}
Wir müssen auch verstehen, was getIssueFullPattern macht.
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
}
Fassen wir zusammen, um zu verstehen, welche Aufgabe dort erledigt wird.
In fullIssuePatternProcessor müssen wir zunächst sicherstellen, dass ctx.metas nicht nil ist (was wir als wahr annehmen, da es von übergeordneten Funktionen/Methoden im Aufrufstack erstellt wird).
Dann prüfen wir, ob die Knotendaten einem Pfad wie http://mygitea.com/gituser/myrepo/issues/1 entsprechen. Wenn eine Übereinstimmung vorliegt, ersetzen wir sie durch einen Link.
Wenn es sich bei dem Link um dasselbe Benutzer- und Repository handelt, ersetzen wir ihn durch das Muster #<issue_id>, andernfalls durch <user>/<repo>#<issue_id>.
In beiden Fällen wird eine ref-issue-Klasse hinzugefügt, um das Popup zu generieren.
Die anderen Grep-Ergebnisse machen im Grunde dasselbe.
Da wir nun den Kontext haben – es geht darum, einen Link wie http://mygitea.com/gituser/myrepo/issues/1 durch einen kürzeren Link zu ersetzen – können wir ableiten, wo eine solche Aufgabe ausgeführt wird: bei Issues und PRs, wo Sie ein Label hinzufügen und/oder einen Kommentar posten können.
Für diese Annahme gehen wir davon aus, dass Sie ein Repository mit Schreibberechtigung haben.
Gehen Sie zu Ihrem Repository und erstellen Sie ein neues Label mit einem Namen, der beliebigen Code enthält (z. B. <script>alert('label.name')</script>).
Gehen Sie zu den Issues Ihres Repositorys und erstellen Sie einen neuen Issue mit beliebigem Code im Titel und im Text, und fügen Sie das vorherige Label hinzu.