File: //usr/syno/sbin/synopyntlmd
#!/usr/bin/python2
import os
import select
import socket
import errno
import base64
import time
import argparse
import subprocess
from struct import unpack
from threading import Lock
from binascii import hexlify
from smb.smb_constants import *
from smb.smb_structs import *
from smb.smb2_structs import *
from smb.base import SMB, NotConnectedError, NotReadyError, SMBTimeout
from smb import ntlm, securityblob, smb_structs
class SYNOIWAConnection(SMB):
def __init__(self):
SMB.__init__(self, '', '', '', '', '', True, SMB.SIGN_WHEN_REQUIRED, True)
self.sock = None
self.is_busy = False
def onAuthOK(self):
'''
copy from SMBConnection
'''
self.auth_result = True
def onAuthFailed(self):
'''
copy from SMBConnection
'''
self.auth_result = False
def write(self, data):
'''
copy from SMBConnection
'''
assert self.sock
data_len = len(data)
total_sent = 0
while total_sent < data_len:
sent = self.sock.send(data[total_sent:])
if sent == 0:
raise NotConnectedError('Server disconnected')
total_sent = total_sent + sent
def close(self):
'''
copy from SMBConnection
'''
if self.sock:
self.sock.close()
self.sock = None
def _pollForNetBIOSPacket(self, timeout):
'''
copy from SMBConnection
'''
expiry_time = time.time() + timeout
read_len = 4
data = ''
while read_len > 0:
try:
if expiry_time < time.time():
raise SMBTimeout
ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout)
if not ready:
raise SMBTimeout
d = self.sock.recv(read_len)
if len(d) == 0:
raise NotConnectedError
data = data + d
read_len -= len(d)
except select.error, ex:
if isinstance(ex, types.TupleType):
if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN:
raise ex
else:
raise ex
type_, flags, length = unpack('>BBH', data)
if flags & 0x01:
length = length | 0x10000
read_len = length
while read_len > 0:
try:
if expiry_time < time.time():
raise SMBTimeout
ready, _, _ = select.select([ self.sock.fileno() ], [ ], [ ], timeout)
if not ready:
raise SMBTimeout
d = self.sock.recv(read_len)
if len(d) == 0:
raise NotConnectedError
data = data + d
read_len -= len(d)
except select.error, ex:
if isinstance(ex, types.TupleType):
if ex[0] != errno.EINTR and ex[0] != errno.EAGAIN:
raise ex
else:
raise ex
self.feedData(data)
def connect(self, ip, port = 445, sock_family = socket.AF_INET, timeout = 60):
if self.sock:
self.sock.close()
self.auth_result = None
self.sock = socket.socket(sock_family)
self.sock.settimeout(timeout)
self.sock.connect(( ip, port ))
self.is_busy = True
return self.sock
def negotiate(self, ntlm_data, timeout = 60):
self.ntlm_data = ntlm_data
self.onNMBSessionOK()
self._pollForNetBIOSPacket(timeout)
self._pollForNetBIOSPacket(timeout)
result, ntlm_token = securityblob.decodeChallengeSecurityBlob(self.smb_message.payload.security_blob)
return ntlm_token
def authenticate(self, ntlm_data, timeout = 60):
if smb_structs.SUPPORT_SMB2:
self.authenticate_SMB2(ntlm_data)
else:
self.authenticate_SMB1(ntlm_data)
self._pollForNetBIOSPacket(timeout)
self.is_busy = False
return self.auth_result
def authenticate_SMB1(self, ntlm_data):
assert self.smb_message.hasExtendedSecurity
if self.smb_message.uid and not self.uid:
self.uid = self.smb_message.uid
blob = securityblob.generateAuthSecurityBlob(ntlm_data)
self._sendSMBMessage(SMBMessage(ComSessionSetupAndxRequest__WithSecurityExtension(0, blob)))
if self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_REQUIRE:
self.log.info('Server requires all SMB messages to be signed')
self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER)
elif self.security_mode & NEGOTIATE_SECURITY_SIGNATURES_ENABLE:
self.log.info('Server supports SMB signing')
self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED)
else:
self.is_signing_active = False
if self.is_signing_active:
self.log.info('SMB signing activated. All SMB messages will be signed.')
self.signing_session_key = ntlm_data[-16:]
if self.capabilities & CAP_EXTENDED_SECURITY:
self.signing_challenge_response = None
else:
self.signing_challenge_response = blob
else:
self.log.info('SMB signing deactivated. SMB messages will NOT be signed.')
def authenticate_SMB2(self, ntlm_data):
blob = securityblob.generateAuthSecurityBlob(ntlm_data)
self._sendSMBMessage(SMB2Message(SMB2SessionSetupRequest(blob)))
if self.security_mode & SMB2_NEGOTIATE_SIGNING_REQUIRED:
self.log.info('Server requires all SMB messages to be signed')
self.is_signing_active = (self.sign_options != SMB.SIGN_NEVER)
elif self.security_mode & SMB2_NEGOTIATE_SIGNING_ENABLED:
self.log.info('Server supports SMB signing')
self.is_signing_active = (self.sign_options == SMB.SIGN_WHEN_SUPPORTED)
else:
self.is_signing_active = False
if self.is_signing_active:
self.log.info('SMB signing activated. All SMB messages will be signed.')
self.signing_session_key = (ntlm_data[-16:] + '\0'*16)[:16]
if self.capabilities & CAP_EXTENDED_SECURITY:
self.signing_challenge_response = None
else:
self.signing_challenge_response = blob
else:
self.log.info('SMB signing deactivated. SMB messages will NOT be signed.')
def _handleNegotiateResponse_SMB1(self, message):
if message.uid and not self.uid:
self.uid = message.uid
if message.hasExtendedSecurity or message.payload.supportsExtendedSecurity:
ntlm_data = self.ntlm_data
blob = securityblob.generateNegotiateSecurityBlob(ntlm_data)
self._sendSMBMessage(SMBMessage(ComSessionSetupAndxRequest__WithSecurityExtension(message.payload.session_key, blob)))
def _handleNegotiateResponse_SMB2(self, message):
ntlm_data = self.ntlm_data
blob = securityblob.generateNegotiateSecurityBlob(ntlm_data)
self._sendSMBMessage(SMB2Message(SMB2SessionSetupRequest(blob)))
def _handleSessionChallenge_SMB1(self, message, ntlm_token):
pass
def _handleSessionChallenge_SMB2(self, message, ntlm_token):
pass
class CacheConnections:
def __init__(self):
self._mutex = Lock()
self._cache = {}
def __len__(self):
return len(self._cache)
def remove(self, id):
self._mutex.acquire()
(proxy, ts) = self._cache.get(id, (None,None))
if proxy:
proxy.close()
del self._cache[id]
self._mutex.release()
def add(self, id, proxy):
self._mutex.acquire()
self._cache[id] = ( proxy, int(time.time()) )
self._mutex.release()
def cleanTimeout(self):
now = int(time.time())
self._mutex.acquire()
for id, conn in self._cache.items():
if conn[1]+60<now:
conn[0].close()
del self._cache[id]
self._mutex.release()
def cleanAll(self):
now = int(time.time())
self._mutex.acquire()
for id, conn in self._cache.items():
conn[0].close()
del self._cache[id]
self._mutex.release()
def has_key(self,id):
return self._cache.has_key(id)
def get_proxy(self, id):
self._mutex.acquire()
proxy = self._cache[id][0]
self._mutex.release()
return proxy
cache = CacheConnections()
def decode_http_authorization_header(auth):
ah = auth.split(' ')
if len(ah) == 2:
b64 = base64.b64decode(ah[1])
if ah[0] == 'NTLM':
return ('NTLM', b64)
return False
def ntlm_message_type(msg):
if not msg.startswith('NTLMSSP\x00') or len(msg)<12:
raise RuntimeError('Not a valid NTLM message: "{}"'.format(hexlify(msg)))
msg_type = unpack('<I', msg[8:8+4])[0]
if msg_type not in (1,2,3):
raise RuntimeError('Incorrect NTLM message Type: {}'.format(msg_type))
return msg_type
def parse_ntlm_authenticate(msg):
NTLMSSP_NEGOTIATE_UNICODE = 0x00000001
idx = 28
length, offset = unpack('<HxxI', msg[idx:idx+8])
domain = msg[offset:offset+length]
idx += 8
length, offset = unpack('<HxxI', msg[idx:idx+8])
username = msg[offset:offset+length]
idx += 24
flags = unpack('<I', msg[idx:idx+4])[0]
if flags & NTLMSSP_NEGOTIATE_UNICODE:
domain = str(domain.decode('utf-16-le'))
username = str(username.decode('utf-16-le'))
return username, domain
def get_connected_dc():
p = subprocess.Popen('/bin/wbinfo -P', stdout=subprocess.PIPE, shell=True)
output, err = p.communicate()
p_status = p.wait()
if err:
raise RuntimeError('wbinfo error: ' + err)
arr = output.split('"')
if len(arr) != 3:
raise RuntimeError('wbinfo format wrong: ' + output)
return arr[1]
def process(cid, auth):
try:
ah_data = decode_http_authorization_header(auth)
except:
ah_data = False
if not ah_data:
return -1, 'No NTLM authenticate header'
try:
ntlm_version = ntlm_message_type(ah_data[1])
if ntlm_version != 1 and ntlm_version != 3:
raise RuntimeError('TYPE {} message in client request.(183)'.format(ntlm_version))
if ntlm_version == 1:
cache.cleanTimeout()
proxy = SYNOIWAConnection()
proxy.connect(get_connected_dc())
ntlm_token = proxy.negotiate(ah_data[1])
cache.add(cid, proxy)
return cid, 'NTLM ' + base64.b64encode(ntlm_token)
if ntlm_version == 3:
if not cache.has_key(cid):
raise RuntimeError('TYPE 3 error: Proxy timeout.(id={}) >60s'.format(cid))
user, domain = parse_ntlm_authenticate(ah_data[1])
if not domain:
raise RuntimeError('user {}\\{} invalid.'.format(domain, user))
proxy = cache.get_proxy(cid)
if not proxy.authenticate(ah_data[1]):
raise RuntimeError('TYPE 3 error: authenticate failed.')
cache.remove(cid)
return 0, domain + '\\' + user
except Exception as e:
if cache.has_key(cid):
cache.remove(cid)
return -1, str(e)
def run():
socket_file = '/var/run/synocgid/synopyntlm.htua.ntlm.socket'
try:
os.mkdir('/var/run/synocgid', 0755);
os.remove(socket_file)
except OSError:
pass
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(socket_file)
sock.listen(5)
while True:
conn, addr = sock.accept()
data = conn.recv(4096)
if not data:
continue
arr = data.split(',')
if os.system('/bin/wbinfo -p &> /dev/null') != 0:
message = 'Ping to winbindd failed'
elif len(arr) == 2:
result, message = process(arr[0], arr[1])
else:
result = -1
message = 'Format error:' + data
conn.send(str(result) + ',' + message)
conn.close()
try:
os.remove(socket_file)
except OSError:
pass
cache.cleanAll()
def debug(cid, auth):
result, message = process(cid, auth)
print(str(result) + ',' + message)
def synontlm(args):
if args.auth:
debug(5566, args.auth)
else:
run()
parser = argparse.ArgumentParser(description='Process NTLM negotiation.')
parser.add_argument('-a', '--auth', default=False)
args = parser.parse_args()
synontlm(args)