
Advisory for CVE-2026-51385. Needed to publish it as GRAPHIFY hasnt recognized the advisory neither publish it, and MITRE assigned CVE-2026-51385, this is the advisory for it.
CVE-2026-51385에 대한 권고. GRAPHIFY가 권고를 인식하지 못하고 게시하지도 않았으며 MITRE가 CVE-2026-51385를 할당했기 때문에 이를 게시해야 했습니다. 이것이 해당 CVE에 대한 권고입니다.
ES: 이것은 제 첫 CVE였습니다. 솔직히 말하자면, ANTI SSRF 함수의 논리를 깨는 방법을 스스로 발견했고, 정말로 첫 CVE를 게시하고 싶었습니다. 그래서 이것이 보안에 어떤 영향을 미칠 수 있을지 고민하고 정당성을 찾아 MITRE에 제출했고, 그들은 이를 수락했습니다. 이것이 권고입니다.
EN: This was my first CVE, to be honest i found myself the way to brake the logic in the anti-SSRF function, and i really wanted to publish the CVE so i tough how it could affect the security, and found a justification, send it to MITRE and they accept it. This is the advisory.
보고서는 부분적으로 AI로 작성되었지만, 사람이 감독했음을 명심하십시오
graphify에서 DNS 리바인딩(TOCTOU)을 통한 SSRF.
패키지: graphify (PyPI: graphifyy), 저장소 Graphify-Labs/graphify
영향받는 구성 요소: URL 수집 경로, graphify add <url>
영향받는 버전: >=0.3.2, <=0.4.29
수정된 버전: 0.5.4 (커밋 dd86271, PR #591 / #592)
CVSS 3.1: 8.3 (High), CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CWE: CWE-918 (SSRF), CWE-367 (TOCTOU 경쟁)
제보자: Arturo Melgarejo Galindo, 독립 보안 연구원 (@Arturo0x90)
graphify add <url>은 많은 페처(fetcher)들이 그렇듯이 SSRF로부터 스스로를 보호하려고 합니다: 호스트 이름을 확인하고, 결과 IP를 차단 목록(loopback, RFC1918, link-local, 예약됨)과 대조한 후, 검사에 통과하는 경우에만 가져옵니다.
문제는 검증한 IP가 실제로 연결하는 IP와 다르다는 점입니다. 검증은 한 번의 DNS 조회를 수행하고, 그 다음 requests.get(hostname)은 두 번째 독립적인 조회를 수행합니다. 호스트 이름에 대한 DNS 영역을 소유하고 있으며 매우 낮은 TTL로 두 개의 A 레코드(하나는 공용, 하나는 내부)를 제공하는 경우, 확인자는 각 질의에 대해 다른 응답을 합법적으로 제공할 수 있습니다. 첫 번째 응답은 차단 목록을 통과합니다. 두 번째 응답은 실제 소켓이 연결되는 곳입니다. 이것이 전체 버그입니다. "IP를 확인한 후 이름으로 연결"하는 논리를 우회하는 전형적인 리바인딩이며, 수정 방법은 검증된 IP를 연결에 고정하는 것입니다. 이것이 0.5.4에서 수행한 작업입니다.
이 점에 대해 솔직하게 말하고 싶습니다. 단독으로 보면 생각보다 약해 보입니다.
사람이 앉아서 이미 신뢰하는 URL을 graphify add에 입력한다면, 이는 거의 무용지물입니다. 그 사람이 원한다면 graphify를 자신의 127.0.0.1로 직접 가리킬 수 있습니다. 리바인딩 트릭이 필요하지 않으며, 그 그림에는 공격자가 없습니다.
graphify가 운영자가 선택하지 않은 URL을 대상으로 실행되는 순간 실제 취약점이 됩니다. 이 도구의 경우 이는 드문 경우가 아니라 정상적인 사용 방식입니다. graphify는 AI 어시스턴트가 소비하는 지식 그래프를 구축하므로, 실제 흐름은 일부 프로세스가 사용자를 대신하여 graphify add를 호출하는 것입니다. 에이전트, CI 작업, README의 URL 목록을 탐색하는 스크립트, 또는 다른 모델의 출력에서 나온 URL 등이 있습니다. 이러한 모든 경우에 공격자는 입력 문자열을 제어하며 사람은 이를 검사하지 않았습니다.
이것이 중요한 경우입니다. 입력을 신뢰할 수 없게 되고 리바인딩 경쟁에서 이기면, 가져오기는 검증한 공용 주소 대신 내부 주소로 전달됩니다. 구체적으로 다음에 도달할 수 있습니다:
127.0.0.1 및 loopback에 바인딩된 모든 것,169.254.169.254 (클라우드 인스턴스 메타데이터),100.64.0.0/10. 따라서 이 범위는 경쟁조차 필요 없으며, 해당 범위를 가리키는 일반 A 레코드만으로 충분합니다.그리고 이러한 주소에 도달하는 것은 그곳에 무엇이 있는지에 따라 피해로 이어집니다. 많은 내부 및 개발 서비스는 데이터를 반환하거나 단순 GET으로 상태를 변경하는 GET 엔드포인트를 노출합니다. 따라서 graphify 가져오기가 잘못된 경로와 쿼리 문자열로 그중 하나에 도달하면, 이는 단순한 읽기가 아닙니다. 내부 대상이 Jenkins scriptText, Flask/Django 디버거(--debug), 관리 패널 또는 메타데이터 엔드포인트인 경우, 그 잘못된 GET은 공격자가 경계 내부에서 행동하는 것입니다. graphify가 그들을 대신하여 요청을 하는 대리자(deputy)입니다.
이것이 자체로 RCE를 제공한다고 주장하는 것은 아닙니다. 그 자체로는 그렇지 않습니다. 그러나 loopback, IMDS, RFC1918 및 CGN을 자동화된 수집 경로에서 가리킬 수 있는 완전한 SSRF는 정확히 이러한 공격이 구축되는 기본 요소입니다. 그리고 이것이 보고하는 요점입니다.
Tavis Ormandy의 공개 rbndr.us harness를 사용했습니다. 이는 각 확인 시 두 IP 사이를 전환하는 호스트 이름을 제공합니다. 7f000001.08080808.rbndr.us는 8.8.8.8과 127.0.0.1 사이를 번갈아 가며 제공합니다.
내부 대상(여기서는 데모용 loopback)에서 무언가를 시작합니다:
sudo python3 -m http.server 80
그런 다음 수집을 트리거하고 경쟁이 성공할 때까지 재시도합니다. 약 4~5번 중 1번 꼴로 성공하며, 간단한 루프로 99% 이상까지 올릴 수 있습니다:
for i in {1..20}; do
graphify add http://7f000001.08080808.rbndr.us/ && break
sleep 1
done
성공한 시도에서 graphify는 호스트 이름이 먼저 IP 차단 목록을 통과했음에도 불구하고 127.0.0.1:80의 로컬 서버가 반환한 모든 것을 수집합니다.
0.5.4 (dd86271)에서 수정 커밋됨.Arturo Melgarejo Galindo, 독립 보안 연구원.