
Jira Service Management Server의 이메일 템플릿에서 템플릿 인젝션으로 인한 코드 실행 취약점
이메일 템플릿에서의 템플릿 인젝션으로 인한 Jira Service Management Server 코드 실행
저는 여기에 배포 및 디버그 방법을 안내해 놓았습니다. 참고하시면 됩니다.
이 CVE의 Description에도 명확히 나와 있듯이, 버그는 Email Template 기능에 있습니다. Admin 권한이 있는 사용자는 알림 이메일의 템플릿을 자유롭게 편집할 수 있습니다. 이 버그는 admin 권한이 필요하므로 RCE가 가능하더라도 그다지 심각하지는 않지만, 저는 재미를 위해 블로그를 쓰기로 결정했습니다 :) SSTI에 대해 공부하는 분들은 참고하시면 됩니다.
atlassian-jira-servicedesk-4.17.0-m0006-standalone 버전을 배포하고 4.18.0 버전과 무엇이 다른지 diff를 확인해 보았습니다:

이 많은 파일들을 보면,... 당연히 일일이 읽지 않습니다. 저는 매우 게으릅니다. 농담이고, 사실 보안 수정 사항 외에도 각 버전에는 기능의 업그레이드와 변경이 있습니다. 모든 것을 diff해서 읽는 것은 매우 시간이 많이 걸립니다. 물론 어떤 경우에는 어쩔 수 없이 모두 diff해서 읽어야 하지만, 이 경우에는 그렇게 하지 않았습니다 =)))
SendBulkMailSimpleNote.render 함수에서 컨텍스트 변수를 확인하여 활용할 수 있는 것이 무엇인지 확인할 수 있습니다:
제 개인적인 경험상, Utils, Manager, service 등의 키워드가 포함된 컨텍스트/클래스를 주로 확인합니다. $jirautils 컨텍스트(com.atlassian.jira.util.JiraUtils 클래스)에 public static <T> T loadComponent(String className, Class<?> callingClass) 메서드가 포함되어 있음을 발견했습니다:
문서를 찾아보며 이 메서드의 기능과 사용법을 알아보았습니다. 입력으로 String className이 들어가고 출력이 클래스이므로, 이 메서드가 클래스를 임의로 로드할 가능성이 있습니다:
System -> Email templates로 이동하여 현재 템플릿 파일을 다운로드합니다.
서로 다른 알림 유형에 사용되는 여러 템플릿이 있으므로, 여러 유형에 공통으로 사용할 수 있는 파일을 찾았습니다. 예를 들어 email\html\includes\header.vm
편집된 템플릿 파일을 업로드한 후 SendBulkMail을 사용하여 알림 이메일을 보냈습니다. 이메일에서 다음과 같은 출력을 받았습니다:
대략적으로 이 클래스에는 생성자가 없거나 허용되지 않는다는 의미입니다. 생성자(입력이 없는 생성자)가 있는 다른 클래스를 시도해 보았습니다:
결과는 예상대로였습니다. 클래스를 성공적으로 얻었습니다:
블랙리스트에 추가된 3개의 클래스가 있습니다:
org.springframework.expression.spel.standard.SpelExpressionParser,\
com.atlassian.jira.component.ComponentAccessor,\
com.atlassian.jira.plugin.ComponentClassManager
각 클래스를 확인해 보았을 때, SpelExpressionParser 클래스에 public SpelExpression parseRaw(String expressionString) 메서드가 있음을 발견했습니다. 잠시 구글링하여 사용 방법을 찾아보니 대략 다음과 같이 사용할 수 있었습니다:
#set($SpelExpressionParser = $jirautils.loadComponent('org.springframework.expression.spel.standard.SpelExpressionParser',$i18n.getClass()))
$SpelExpressionParser.parseRaw("T(java.lang.Runtime).getRuntime().exec('calc')")
보시다시피, parseRaw 함수는 org.springframework.expression.spel.standard.SpelExpression 클래스의 객체를 반환할 뿐, 아직 표현식을 실제로 렌더링하지는 않습니다. SpelExpression 클래스로 들어가 getValue 메서드를 확인했습니다:
@Nullable
public Object getValue() throws EvaluationException {
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
EvaluationContext context = this.getEvaluationContext();
return compiledAst.getValue(context.getRootObject().getValue(), context);
} catch (Throwable var4) {
if (this.configuration.getCompilerMode() != SpelCompilerMode.MIXED) {
throw new SpelEvaluationException(var4, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION, new Object[0]);
}
}
this.compiledAst = null;
this.interpretedCount.set(0);
}
ExpressionState expressionState = new ExpressionState(this.getEvaluationContext(), this.configuration);
Object result = this.ast.getValue(expressionState);
this.checkCompile(expressionState);
return result;
}
입력/출력을 보고, 저는 더 이상 문서를 보지 않고 바로 시도하기로 결정했습니다:
#set($SpelExpressionParser = $jirautils.loadComponent('org.springframework.expression.spel.standard.SpelExpressionParser',$i18n.getClass()))
$SpelExpressionParser.parseRaw("T(java.lang.Runtime).getRuntime().exec('calc')").getValue()
결과:
이로써 velocity의 샌드박스를 우회하여 RCE를 할 수 있게 되었습니다. 하지만 이 버그를 발견한 작성자의 입장에서 생각해 보면, 제가 설명할 수 없는 몇 가지 질문이 있습니다:
이 3개의 클래스를 조사해 보니, 이들은 하나의 체인을 형성할 수 있습니다:
ComponentAccessor.getComponentClassManager()
=> ComponentClassManager.newInstance(String className) (이 클래스는 임의의 클래스를 로드할 수 있음)
=> SpelExpressionParser
만약 작성자가 저와 같은 $jirautils를 사용했다면 다른 두 클래스는 필요하지 않을 것입니다. 그러나 작성자가 $jirautils를 사용하지 않았다면 어떻게 ComponentAccessor를 얻을 수 있었을까요???