HEX
Server: Apache/2.2.34 (Unix) mod_fastcgi/mod_fastcgi-SNAP-0910052141
System: Linux Kou-Etsu-Dou 4.4.59+ #25556 SMP PREEMPT Thu Mar 4 18:03:46 CST 2021 x86_64
User: hosam (1026)
PHP: 7.2.29
Disabled: NONE
Upload Files
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)