
Prova di concetto dettagliata e analisi di CVE-2021-28378, una vulnerabilità XSS memorizzata in Gitea che consente l'iniezione arbitraria di codice, l'escalation dei privilegi e l'esecuzione remota di codice tramite git hooks.
Dettagli su questa CVE qui.
Questa CVE riguarda la mancanza di escaping delle stringhe lato client per i contenuti recuperati dal server. Consente a un attaccante di iniettare facilmente codice arbitrario creando un commento su un issue o una pull request. È stata introdotta nel commit 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5, alla fine di gennaio 2020, ma a causa di uno squash non possiamo sapere esattamente chi l'ha introdotta per cercare bug simili.
Prima di tutto, capiamo da dove arriva questa vulnerabilità. Per farlo, vediamo il commit che risolve questa CVE.
Prima:
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>
`
Possiamo vedere che non c'è alcun escaping html su label.name, issue.repository.full_name, issue.title e body, che sono stringhe manipolabili da un utente, e quindi possono iniettare codice arbitrario.
Nota che issue.repository.full_name non può essere realmente manipolato per iniettare codice poiché deve seguire troppe regole ([\w._-]+) che non possiamo bypassare con questa CVE.
Questo è esattamente ciò che la PR risolve convertendole in htmlEscape.
Dopo:
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>
`
Diamo un'occhiata al file web_src/js/features/contextpopup.js, per capire come attivare la 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>
`
});
});
}
Quello che vediamo è che per ogni nodo con la classe ref-issue, viene aggiunto un popup vulnerabile.
Ora, dobbiamo trovare dove un ref-issue viene inviato all'utente: facciamo un grep (e teniamo solo i risultati interessanti, quindi niente file di test o simili)!
Il seguente numero di commit corrisponde al tag 1.12.4, la versione predefinita del file docker-compose.yml ufficiale, che è affetto da questa 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")
Ora sappiamo che non esiste contenuto statico con la classe ref-issue, ma viene generato dinamicamente.
L'ultima riga non è interessante poiché è solo una regola di policy del sanitizer. Le altre sono interessanti, vedremo perché.
Analizziamo 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"))
}
}
Dovremo anche capire cosa fa 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
}
Riassumiamo per capire cosa fa questo codice.
In fullIssuePatternProcessor, dobbiamo prima assicurarci che ctx.metas non sia nil (cosa che assumiamo vera poiché viene costruito da funzioni/metodi più in alto nello stack).
Poi controlliamo se i dati del nodo corrispondono a un percorso come http://mygitea.com/gituser/myrepo/issues/1. Se c'è una corrispondenza, lo sostituiamo con un link.
Se il link riguarda lo stesso utente e repository, lo sostituiamo con il pattern #<issue_id>, altrimenti con <user>/<repo>#<issue_id>.
In entrambi i casi, viene aggiunta una classe ref-issue per generare il popup.
Gli altri risultati del grep fanno più o meno la stessa cosa.
Ora che abbiamo il contesto, che riguarda la sostituzione di un link come http://mygitea.com/gituser/myrepo/issues/1 con un link più corto, possiamo dedurre dove viene fatto questo lavoro: negli issue e nelle PR, dove puoi aggiungere un'etichetta e/o postare un commento.
Per questo, supponiamo di avere un repository con permessi di scrittura.
Vai al tuo repository e crea una nuova etichetta con un nome contenente codice arbitrario (ad esempio <script>alert('label.name')</script>).
Vai agli issue del tuo repository e crea un nuovo issue con codice arbitrario nel titolo e nel corpo, e aggiungi l'etichetta precedente.
Crea un nuovo issue su un qualsiasi repository e aggiungi nel suo corpo un link al tuo issue iniettato come http://mygitea.com/gituser/myrepo/issues/1.
Verrà sostituito dal link che abbiamo visto prima, e quando qualcuno passerà il mouse sopra il link, il codice arbitrario verrà eseguito: verrà mostrato un popup che contiene label.name.
La CVE-2021-28378 è dimostrata essere in grado di consentire l'iniezione di codice arbitrario. Immaginiamo cosa potrebbe fare un attaccante.