这篇文章的灵感来自于Harmj0y在 SO-CON 2020 上的精彩演讲。一段时间以来,我一直想深入研究使用 Jenkins 来自动化构建红队工具,但最近完成了 RTO 考试,我觉得是时候玩一玩了!
Harmjoy 引用的 Gist 可以在此处找到。
在开始这个迷你项目之前,我的目标是构建一个相当简单的 CI 管道,以:
为了能够获取此代码并按照我们的意愿在各种其他项目/存储库中重用它,因此模块化是这里的一个关键目标。
这篇文章和OffensivePipeline项目之间有很多相似之处,但我想在 Jenkins 中扩展我的知识,而不是使用我已经很熟悉的 C#。我也觉得随着我进一步扩展这个项目,Jenkins 可能会在未来提供更多的灵活性。
在我们开始之前有一个重要的警告,我意识到本指南只涉及一些非常基本的混淆。生成的二进制文件仍然很容易检测到,但本指南应该重点介绍一些基础知识!
那里有很多安装 Jenkins 的指南,所以我不会在这里强调这一点。XenoSCR的一篇博文帮助我开始安装和配置 Jenkins,以及设置基本管道。
如前所述,我希望能够在 Jenkins 中编译项目,因此自然而然地 MSBuild 将成为执行此操作的主要候选者。像往常一样,StackOverflow包含有关如何在 Jenkins 中设置 MSBuild 的指南,我将在下面介绍。
首先,让我们去Manage Jenkins -> Plugin Manager
。我的页面中没有 MSBuild 条目Global Tool Configuration
,所以我必须从Plugin Manager
下载完成后,我们将通过转到并向Manage Jenkins -> Global Tool Configuration
下滚动到 MSBuild 部分来添加我们的配置。
单击“添加 MSBuild”并填写 MSBuild 路径的详细信息。确保使用 MSBuild 的路径来安装 Visual Studio,我最初将其设置为 v4.0.3019 的路径,但我遇到了很多问题,无法正确编译项目。
正如 Will 在他的演讲中所描述的,我们将使用“管道”来执行此编译。在 Jenkins Dashboard 中,我们可以单击 New Item,然后选择“Pipeline”。
对于一个基本项目,让我们使用下面的代码示例。这将从 GitHub 下载 Rubeus,然后显示文件夹的内容。
pipeline {
agent any
environment {
PROJECT_NAME = "Rubeus"
}
stages {
stage('Checkout') {
steps {
git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
}
}
stage('Echo') {
steps {
bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
}
}
}
}
单击保存,然后单击立即构建。然后我们可以点击“控制台输出”来向我们展示 Jenkins 正在做什么。这应该会显示下面的信息,它将显示我们 Rubeus 目录的根目录——证明我们可以通过代码克隆存储库!
现在我们可以证明我们可以实际运行一个作业并且它会执行代码,让我们试着自动化一点。我们目前正在拉取 repo 并运行dir
,让我们尝试将这段代码实际编译成可执行文件。
由于我们之前配置了 MSBuild,我们现在可以从管道中引用它——无需一直记住路径!
首先,MSBuild 有一个相当复杂的命令行结构,所以我首先在命令行中使用它,然后再将它移植到 Jenkins。Rubeus 使用 Microsoft 不再正式支持的 .NET v4.0 对此没有帮助,因此我无法找到二进制文件的合法下载。因此,我使用了 .NET v4.8,这在兼容性方面对我们来说不是最佳选择,但我们可以随时改变它!
在对 MSBuild 和各种命令行选项进行大量试验和错误之后,我的最终命令是:
"C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" /p:Configuration=Release "/p:Platform=Any CPU" /maxcpucount:2 /nodeReuse:false /p :TargetFrameworkMoniker=".NETFramework,Version=v4.8" Rubeus.sln
我们现在从 MSBuild 收到一条成功消息!
现在让我们更改jenkinsfile
一下,以便它使用 MSBuild 编译该工具。我们将把早期的 MSBuild 命令包装在一个新阶段,以帮助我们的项目保持良好和模块化:
stage('Compile') {
steps {
bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}"
}
}
我还在一个临时阶段添加了打印出Rubeus\bin\Release
文件夹的内容。这帮助我测试了它是否真的编译了可执行文件,并在我调试管道时为我节省了几次点击。
通过这些添加,我们jenkinsfile
现在看起来像下面的代码。您可以看到我添加了更多环境变量,这将有助于我将此代码重用于其他存储库。
pipeline {
agent any
environment {
PROJECT_NAME = "Rubeus"
PROJECT_FILE_PATH = "Rubeus.sln"
CONFIG = 'Release'
PLATFORM = 'Any CPU'
}
stages {
stage('Checkout') {
steps {
git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
}
}
stage('Echo') {
steps {
bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
}
}
stage('Compile') {
steps {
bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}"
}
}
stage('Echo Post Compilation') {
steps {
bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}"""
}
}
}
}
我们可以通过将二进制文件上传到 VirusTotal 来评估我们的进度。虽然我不会在现场测试中这样做,但它可以方便地评估该管道的工作情况。我们将在本文末尾再次对此进行测试,但目前我们的二进制文件仅被 41 家供应商检测到——即使它完全没有被混淆。
我们现在有一个很好的基础来构建,因为我们可以提取最新版本的 Rubeus 并只需单击一个按钮就可以编译它!我们现在的目标是从 Rubeus 可执行文件中删除一些众所周知的字符串。这将是我们混淆可执行文件的第一步。
为此,我们将使用共享库来捆绑我们将重用的代码示例。例如,这将是更改默认 GUID、删除评论等内容。从概念上讲,这与在编程时使用函数非常相似。
首先,我们将通过创建如下所示的文件夹结构来创建共享库。我的图书馆基于这篇博文。
- obfuscation-lib/
--> vars/
--> someFunction.groovy
我的someFunction.groovy
文件的代码是:
def call(String name = 'User') {
echo "Welcome, ${name}."
}
令人讨厌的是,我们不能在 Jenkins 中包含一个到共享库的本地路径,因为它希望我们从 Git 加载它。有一个很好的解决方法,我们可以使用file://
协议处理程序加载本地 Git 存储库,如此处所述。
为了为此准备我的库,我创建了一个新的 git 存储库并将我的代码提交给它。您必须记住在每次更改库后提交您的代码!
然后我们将转到Manage Jenkins -> Configure System -> Global Pipeline Libraries
,并添加我们的库。
我们可以在这里选择一个名字,我会选择obfuscation-lib
,然后我们将项目存储库设置为指向我们新创建的 git 存储库的位置。
回到我们的管道中jenkinsfile
,我们现在必须使用我们刚刚设置的名称导入这个库(在上图的顶部)。我们使用以下代码导入它:
@Libary('LIBRARY_NAME')_
不要忘记括号后面的下划线,否则它将不起作用!
总而言之,通过我们闪亮的新库,这为我们提供了以下非常基本的管道。虽然您不必在我们的函数调用中使用变量,但我想确保它能够正常工作。
@Library('obfuscation-lib')_pipeline {
agent any
environment {
SOME_VAR = "SOME_VALUE"
}
stages{
stage('Library Test') {
steps{
someFunction "${SOME_VAR}"
}
}
}
}
如下所示,它会打印出我们的变量。
总而言之,让我们使用我们的obfuscation-lib
库来混淆目标存储库中有用的东西。为此,我们将构建一个非常基本的字符串替换函数。我们将使用它来替换任何已知会引发 EDR/AV 警报的短语。一个基本的例子是替换任何提及“mimikatz”。
首先,让我们获取 Jenkins 工作区的路径。理想情况下,我们将这样做,而不必为每个函数调用手动指定它。幸运的是,我们可以${WORKSPACE}
在我们的共享库中使用来获得这条路径。我们现在可以更新我们的库,它将打印目录。
def call(String name = 'User') {
echo "Welcome, ${name}. Workspace is ${WORKSPACE}"
}
提交我们的更改并重新运行管道,我们得到以下信息:
从这里开始,我们将使用这篇文章中的一些代码来创建一个简单的查找和替换工具。
//Heavily adapted from http://www.ensode.net/roller/dheffelfinger/entry/groovy_script_to_find_and
def call(String extension = '*.cs', String findText = '', String replaceText = '') {
//Navigate to the current workspace
def currentDir = new File("${WORKSPACE}"); def backupFile;
def fileText;
currentDir.eachFileRecurse({
file ->
for (ext in exts){
if (file.name.endsWith(extension)) {
fileText = file.text;
backupFile = new File(file.path + ".bak");
backupFile.write(fileText);
fileText = fileText.replaceAll(findText, replaceText)
file.write(fileText);
}
}
})
}
我们现在将在管道中添加另一个阶段,称为“ Obfuscate
”。我们将尝试混淆版本号以证明我们的功能有效。此外,我将修改“ Echo Post Compilation
”步骤以改为运行 Rubeus,以便我们可以检查版本号是否更改。这给我们留下了下面这两个新阶段。
stage('Obfuscate') {
steps {
replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
}
}stage('Execute Post Compilation') {
steps {
bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
}
}
运行此程序后,出现与“ expected to call java.io.File.eachFileRecurse but wound up catching org.jenkinsci.plugins.workflow.cps.CpsClosure2.call error
”相关的错误。这在此处进行了解释,但基本上我们需要添加@NonCPS
到自定义函数的顶部。
我们现在得到以下结果jenkinsfile
:
@Library('obfuscation-lib')_pipeline {
agent any
environment {
PROJECT_NAME = "Rubeus"
PROJECT_FILE_PATH = "Rubeus.sln"
CONFIG = 'Release'
PLATFORM = 'Any CPU'
}
stages {
stage('Checkout') {
steps {
git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
}
}
stage('Echo') {
steps {
bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
}
}
stage('Obfuscate') {
steps {
replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
}
}
stage('Compile') {
steps {
bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}"
}
}
stage('Execute Post Compilation') {
steps {
bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
}
}
}
}
然后是我们的自定义函数:
//Heavily adapted from from http://www.ensode.net/roller/dheffelfinger/entry/groovy_script_to_find_and
@NonCPS
def call(String extension = '.cs', String findText = '', String replaceText = '') {
//Navigate to the current workspace
def currentDir = new File("${WORKSPACE}");
def fileText; currentDir.eachFileRecurse({
file ->
if (file.name.endsWith(extension)) {
fileText = file.text;
fileText = fileText.replaceAll(findText, replaceText)
file.write(fileText);
}
})
}
现在,当我们运行管道时,我们可以看到我们已经修改了 Rubeus 向控制台输出的版本号:
我们可以进一步扩展这个函数来创建一个基本函数,它为 C# 项目执行一些常见的 OPSEC 考虑。这里可以内置很多不同的检查,但我们将专注于两个主要的检查来证明这一点:
正如我们从AssemblyInfo.cs 文件中看到的,Rubeus 使用658c8b7f-3664-4a95-9572-a3e5871dfc06
.
这将提示我们正在使用 Rubeus 的任何分析师,正如我们在谷歌搜索 GUID 中看到的那样:
我们将首先使用正则表达式来逃避这一点。在开发这些 Java 正则表达式时,我发现https://www.freeformatter.com/java-regex-tester.html是一个很好的资源,它可以避免一遍又一遍地重新运行管道!为了让您不必编写 Java 正则表达式,以下是我的代码:
@NonCPS
def call() {
//Replace the default GUID & assembly info
sanitiseAssemblyInfo();
}@NonCPS
def sanitiseAssemblyInfo(){
def assemblyInfoFile = new File("${WORKSPACE}\\${PROJECT_NAME}\\Properties\\AssemblyInfo.cs");
def assemblyInfoText = assemblyInfoFile.text;
//Replace the default GUID (e.g. "[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")]")
def newGUID = "[assembly: Guid(\"${UUID.randomUUID().toString()}\")]"
assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sGuid.*/, newGUID)
}
提交更改并运行管道后,我们可以看到AssemblyInfo.cs
文件已被修改,并且我们有一个新的 GUID。
然后我们可以扩展我们的函数来清除所有的汇编值,只留下一个版本号。这遵循与上述函数非常相似的模式:
@NonCPS
def call() {
//Replace the default GUID & assembly info
sanitiseAssemblyInfo();
}@NonCPS
def sanitiseAssemblyInfo(){
def assemblyInfoFile = new File("${WORKSPACE}\\${PROJECT_NAME}\\Properties\\AssemblyInfo.cs");
def assemblyInfoText = assemblyInfoFile.text;
//Replace the default GUID (e.g. "[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")]")
def newGUID = "[assembly: Guid(\"${UUID.randomUUID().toString()}\")]"
assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sGuid.*/, newGUID)
//Replace any entry beginning with "[assembly: Assembly", removing the value within the brackets.
//I.e. [assembly: AssemblyTitle("Rubeus")] ==> [assembly: AssemblyTitle("")]
//See https://stackoverflow.com/a/38296697 for more info
assemblyInfoText = assemblyInfoText.replaceAll(/(\[assembly:\sAssembly.*\(\").*/, '$1\")]')
//Finally, we will set the AssemblyVersion value to be 1.0.0.0 just to make it look a bit more legit
assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sAssemblyVersion.*/, "[assembly: AssemblyVersion(\"1.0.0.0\")]")
//And write it all to the file :)
assemblyInfoFile.write(assemblyInfoText);
}
现在如果我们查看AssemblyInfo.cs
文件,我们可以看到已经成功剥离了程序集信息。
把这一切放在一起,我们有我们的最终jenkinsfile
:
@Library('obfuscation-lib')_pipeline {
agent any
environment {
PROJECT_NAME = "Rubeus"
PROJECT_FILE_PATH = "Rubeus.sln"
CONFIG = 'Release'
PLATFORM = 'Any CPU'
}
stages {
stage('Checkout') {
steps {
git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
}
}
stage('Obfuscate') {
steps {
replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
cSharpBasicOpsec()
}
}
stage('Compile') {
steps {
bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}"
}
}
stage('Execute Post Compilation') {
steps {
bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
}
}
}
}
除了这些功能之外,我还添加了另一个查找和替换以删除 Rubeus 的默认帮助文本。编译这个项目后,我们现在可以看到只有 32 个供应商检测到了代码——这意味着我们已经击败了其中的 9 个!
从这一点来看,你可以通过很多不同的方式来完成这个项目。Harmj0y 在他的演讲中提到了一些,但我实施的一些更容易的项目是:
Rubeus
命名空间是众所周知的,所以改变它是我的首要任务之一。
这在项目中很容易看到:
使用我们的offensive-lib
库,我创建了一个新函数来删除任何已知易于检测的函数。如前所述,我使用它来删除 Rubeus 中默认的帮助文本函数“ ShowLogo
”和“ ShowUsage
”。
现在,我选择只用一个新行替换该函数,尽管如果我们需要保留功能,可以用 C# 代码替换它。
通过使用 RastaMouse 的ThreatCheck项目,我们可以让我们的管道根据 AMSI 签名自动检查自身。我们将只运行检查并手动检查输出,但对于生产用途,我们可能会将其作为测试来实现——这样 AMSI 检测到的任何代码都不会被编译以供使用。
无需查看构建的输出,我们可以使用插件和 Slack WebHooks 将数据直接发送给我们!
这只是将 Jenkins 用于 OffSecOps 的第一步,但希望它展示了此类系统的潜在用途。
我计划的一些后续步骤包括实施更多的 GitHub 项目,以及运行多个管道来自动构建我的红队工具集。