Skip to content
KitploitKITPLOIT
ИнструментыЭксплойтыБлог
Log in
Отправить
ИнструментыЭксплойтыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

ЛентыКонтактыКонфиденциальность© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2021-28378 — Подробное доказательство концепции и анализ CVE-2021-28378, хранимой XSS-уязвимости в Gitea, позволяющей внедрение произвольного кода, повышение привилегий и удаленное выполнение кода через git-хуки. | Kitploit
Инструменты/GitHubGitHub/pandatix/cve-2021-28378
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийТестирование на ПроникновениеОбучение и ОбразованиеРазработка Полезной Нагрузки
GitHubpandatix/cve-2021-28378

CVE-2021-28378

Подробное доказательство концепции и анализ CVE-2021-28378, хранимой XSS-уязвимости в Gitea, позволяющей внедрение произвольного кода, повышение привилегий и удаленное выполнение кода через git-хуки.

Репозиторий
4148 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2021-28378

Подробности об этой CVE здесь.

Эта CVE связана с отсутствием экранирования строк на стороне клиента для контента, получаемого с сервера. Она позволяет атакующему легко внедрять произвольный код, создавая комментарий к issue или pull request. Проблема была внесена в коммите 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5 в конце января 2020 года, но из-за squash мы не можем точно узнать, кто это сделал, чтобы исследовать похожие внесённые баги.

Прежде всего, давайте разберёмся, откуда берётся эта уязвимость. Для этого посмотрим коммит, исправляющий эту 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>
`

Мы видим, что нет никакого экранирования HTML для label.name, issue.repository.full_name, issue.title и body, которые являются строками, управляемыми пользователем, и, следовательно, позволяют внедрять произвольный код. Обратите внимание, что 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 (мы полагаем, что это так, поскольку он создаётся вышестоящими функциями/методами в стеке вызовов). Затем мы проверяем, соответствует ли данным узла путь вида http://mygitea.com/gituser/myrepo/issues/1. Если совпадение есть, мы заменяем его ссылкой. Если ссылка относится к тому же пользователю и репозиторию, заменяем её на паттерн #<issue_id>, иначе — на <user>/<repo>#<issue_id>. В обоих случаях добавляется класс ref-issue, который нужен для генерации всплывающего окна.

Остальные результаты grep делают примерно то же самое.

Пора поиграть!

Теперь, когда у нас есть контекст — замена ссылки вида http://mygitea.com/gituser/myrepo/issues/1 на более короткую, — мы можем понять, где это происходит: в issues и PR, где можно добавлять метки и/или оставлять комментарии.

Для этого предположим, что у вас есть репозиторий с правами на запись.

Метка

Перейдите в свой репозиторий и создайте новую метку с именем, содержащим произвольный код (например, <script>alert('label.name')</script>).

Issue

Перейдите в раздел issues вашего репозитория и создайте новый issue с произвольным кодом в заголовке и теле, добавив ему предыдущую метку.

Выполним немного кода

Создайте новый issue в любом репозитории и добавьте в его тело ссылку на ваш внедрённый issue, например http://mygitea.com/gituser/myrepo/issues/1. Она будет заменена ссылкой, которую мы видели ранее, и когда кто-то наведёт курсор на ссылку, выполнится произвольный код: появится всплывающее окно, содержащее label.name.

Время эксплойта

CVE-2021-28378 доказанно позволяет внедрение произвольного кода. Давайте представим, что может сделать атакующий.

Скачать инструмент