ldap.py 10.3 KB
Newer Older
1
# -*- coding: utf8 -*-
2
from flask_login import AnonymousUserMixin
3

4
from .interface import BUIhandler, BUIuser, BUIloader
5
from ...utils import __
6

7 8
import ssl

9
try:
10
    from ldap3 import Server, Connection, Tls, ALL, RESTARTABLE, AUTO_BIND_TLS_BEFORE_BIND, AUTO_BIND_NONE, SIMPLE
11
except ImportError:
12
    raise ImportError('Unable to load \'ldap3\' module')
13

Benjamin "Ziirish" SANS's avatar
pep8  
Benjamin "Ziirish" SANS committed
14

15
class LdapLoader(BUIloader):
16 17
    """The :class:`burpui.misc.auth.ldap.LdapLoader` handles searching for and
    binding as a :class:`burpui.misc.auth.ldap.LdapUser` user.
18
    """
19
    section = name = 'LDAP:AUTH'
20

21
    def __init__(self, app=None, handler=None):
22 23
        """:func:`burpui.misc.auth.ldap.LdapLoader.__init__` establishes a
        connection to the LDAP server.
24 25

        :param app: Instance of the app we are running in
26
        :type app: :class:`burpui.engines.server.BUIServer`
27
        """
28
        self.app = app
29
        conf = self.app.conf
30
        handler.name = self.name
31
        defaults = {
32
            'LDAP:AUTH': {
33 34 35 36 37 38 39 40 41 42 43
                'host': 'localhost',
                'port': None,
                'encryption': None,
                'binddn': None,
                'bindpw': None,
                'filter': None,
                'base': None,
                'searchattr': 'uid',
                'validate': 'none',
                'cafile': None,
            }
44 45 46 47 48 49 50 51 52 53 54 55 56
        }
        mapping = {
            'host': 'host',
            'port': 'port',
            'encryption': 'encryption',
            'filt': 'filter',
            'base': 'base',
            'attr': 'searchattr',
            'binddn': 'binddn',
            'bindpw': 'bindpw',
            'validate': 'validate',
            'cafile': 'cafile'
        }
57 58 59 60 61 62 63 64
        conf.update_defaults(defaults)
        # Maybe the handler argument is None, maybe the 'priority'
        # option is missing. We don't care.
        try:
            handler.priority = conf.safe_get(
                'priority',
                'integer',
                section=self.section
65
            ) or handler.priority
66 67
        except:
            pass
68
        for (opt, key) in mapping.items():
69
            setattr(self, opt, conf.safe_get(key, 'force_string', section=self.section))
70

71 72 73 74
        if self.validate and self.validate.lower() in ['none', 'optional', 'required']:
            self.validate = getattr(ssl, 'CERT_{}'.format(self.validate.upper()))
        else:
            self.validate = None
75
        self.version = ssl.OP_NO_SSLv3
76
        self.users = []
77
        self.tls = None
78
        self.ssl = False
79 80
        self.auto_bind = AUTO_BIND_NONE
        if self.encryption == 'ssl':
81
            self.ssl = True
82 83 84
        elif self.encryption == 'tls':
            self.tls = Tls(local_certificate_file=self.cafile, validate=self.validate, version=self.version)
            self.auto_bind = AUTO_BIND_TLS_BEFORE_BIND
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
85 86 87 88
        if self.port:
            try:
                self.port = int(self.port)
            except ValueError:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
89
                self.logger.error('LDAP port must be a valid integer')
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
90
                self.port = None
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
91 92 93 94 95 96 97 98 99
        self.logger.info('LDAP host: {0}'.format(self.host))
        self.logger.info('LDAP port: {0}'.format(self.port))
        self.logger.info('LDAP encryption: {0}'.format(self.encryption))
        self.logger.info('LDAP filter: {0}'.format(self.filt))
        self.logger.info('LDAP base: {0}'.format(self.base))
        self.logger.info('LDAP search attr: {0}'.format(self.attr))
        self.logger.info('LDAP binddn: {0}'.format(self.binddn))
        self.logger.info('LDAP bindpw: {0}'.format('*****' if self.bindpw else 'None'))
        self.logger.info('TLS object: {0}'.format(self.tls))
100 101

        try:
102
            self.server = Server(host=self.host, port=self.port, use_ssl=self.ssl, get_info=ALL, tls=self.tls)
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
103
            self.logger.debug('LDAP Server = {0}'.format(str(self.server)))
104 105 106 107
            if self.binddn:
                self.ldap = Connection(self.server, user=self.binddn, password=self.bindpw, raise_exceptions=True, client_strategy=RESTARTABLE, auto_bind=self.auto_bind, authentication=SIMPLE)
            else:
                self.ldap = Connection(self.server, raise_exceptions=True, client_strategy=RESTARTABLE, auto_bind=self.auto_bind)
108
            okay = False
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
109
            with self.ldap:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
110 111
                self.logger.debug('LDAP Connection = {0}'.format(str(self.ldap)))
                self.logger.info('OK, connected to LDAP')
112
                okay = True
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
113

114 115 116 117
            if not okay:
                raise Exception('Not connected')

            self._prefetch()
118
        except Exception as e:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
119
            self.logger.error('Could not connect to LDAP: {0}'.format(str(e)))
120
            self.server = None
121 122 123
            self.ldap = None

    def __exit__(self, exc_type, exc_value, traceback):
124 125
        """:func:`burpui.misc.auth.ldap.LdapLoader.__exit__` closes the
        connection to the LDAP server.
126
        """
127
        if self.ldap and self.ldap.bound:
128
            self.ldap.unbind()
129

130
    def fetch(self, searchval=None, uniq=True):
131 132
        """:func:`burpui.misc.auth.ldap.LdapLoader.fetch` searches for a user
        object in the LDAP server.
133 134 135 136

        :param searchval: attribute value to search for
        :type searchval: str

137 138 139
        :param uniq: only return one result
        :type uniq: bool

140 141 142
        :returns: dictionary of `distinguishedName` and `commonName` attributes for the
        user if found, otherwise None.
        """
143
        try:
144
            if self.filt:
145
                query = self.filt.format(self.attr, searchval)
146
            else:
147
                query = '({0}={1})'.format(self.attr, searchval)
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
148
            self.logger.info('filter: {0} | base: {1}'.format(query, self.base))
149 150
            r = None
            with self.ldap:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
151
                self.logger.debug('LDAP Connection = {0}'.format(str(self.ldap)))
152 153 154
                self.ldap.search(self.base, query, attributes=['cn', self.attr])
                r = self.ldap.response
            if not r:
155
                raise ValueError('no results')
156
        except Exception as e:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
157
            self.logger.error('Ooops, LDAP lookup failed: {0}'.format(str(e)))
158 159
            return None

160 161 162
        if not uniq:
            return r

163
        for record in r:
164 165
            attrs = record['attributes']
            if self.attr in attrs and searchval in attrs[self.attr]:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
166
                self.logger.info('Found DN: {0}'.format(record['dn']))
167
                return {'dn': record['dn'], 'cn': attrs['cn'][0]}
168

169 170 171 172 173 174 175 176
    def _prefetch(self):
        """Prefetch all users that match the filter/base"""
        self.users = []
        results = self.fetch('*', False) or []
        for record in results:
            attrs = record['attributes']
            if self.attr in attrs:
                self.users.append(attrs[self.attr][0])
177
        self.logger.debug(self.users)
178

179
    def check(self, dn=None, passwd=None):
180 181
        """:func:`burpui.misc.auth.ldap.LdapLoader.check` authenticates a user
        against the LDAP server.
182

183
        :param dn: canonical `dn` of the user to authenticate as
184 185 186 187 188 189 190
        :type dn: str

        :param passwd: password of the user to authenticate as
        :type passwd: str

        :returns: True if bind was successful, otherwise False
        """
191
        try:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
192 193
            with Connection(self.server, user='{0}'.format(dn), password=passwd, raise_exceptions=True, auto_bind=self.auto_bind, authentication=SIMPLE) as con:
                self.logger.debug('LDAP Connection = {0}'.format(str(con)))
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
194
                self.logger.info('Bound as user: {0}'.format(dn))
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
195
                return con.bind()
196
        except Exception as e:
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
197
            self.logger.error('Failed to authenticate user: {0}, {1}'.format(dn, str(e)))
198

Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
199
        self.logger.error('Bind as \'{0}\' failed'.format(dn))
Benjamin "Ziirish" SANS's avatar
Benjamin "Ziirish" SANS committed
200
        return False
201 202


203
class UserHandler(BUIhandler):
204 205
    __doc__ = __('Connects to a LDAP database to authenticate users. Handles '
                 'searching for and binding as.')
206 207
    priority = 50

208 209
    preload_users = False

210 211
    """The :class:`burpui.misc.auth.ldap.UserHandler` class maintains a list of
    ``Burp-UI`` users.
212
    """
213
    def __init__(self, app=None):
Benjamin "Ziirish" SANS's avatar
doc  
Benjamin "Ziirish" SANS committed
214 215 216 217
        """:func:`burpui.misc.auth.ldap.UserHandler.__init__` creates the
        handler instance

        :param app: Instance of the app we are running in
218
        :type app: :class:`burpui.engines.server.BUIServer`
Benjamin "Ziirish" SANS's avatar
doc  
Benjamin "Ziirish" SANS committed
219
        """
220
        self.ldap = LdapLoader(app, self)
221 222 223
        self.users = {}

    def user(self, name=None):
Benjamin "Ziirish" SANS's avatar
doc  
Benjamin "Ziirish" SANS committed
224
        """See :func:`burpui.misc.auth.interface.BUIhandler.user`"""
225 226
        if name not in self.users:
            self.users[name] = LdapUser(self.ldap, name)
227 228
        ret = self.users[name]
        if not ret.active:
229
            return AnonymousUserMixin()
230
        return ret
231

232 233 234 235
    @property
    def loader(self):
        return self.ldap

236

Benjamin "Ziirish" SANS's avatar
doc  
Benjamin "Ziirish" SANS committed
237
class LdapUser(BUIuser):
238 239
    """The :class:`burpui.misc.auth.ldap.LdapUser` class generates a ``Burp-UI``
    user from a user object found in the LDAP server.
240
    """
241
    def __init__(self, ldap=None, name=None):
242 243
        """:func:`burpui.misc.auth.ldap.LdapUser.__init__` function finds a user
        in the LDAP server and stores the DN of the user if found.
244 245 246 247 248 249 250

        :param ldap: an ``LdapLoader`` instance
        :type ldap: :class:`burpui.misc.auth.ldap.LdapLoader`

        :param name: login name of the user to find in the LDAP server
        :param type: str
        """
251
        self.active = False
252
        self.authenticated = False
253 254
        self.ldap = ldap
        self.name = name
255
        self.backend = self.ldap.name
256

257
        found = self.ldap.fetch(name)
258

259 260
        if found:
            self.id = found['dn']
261
            self.active = True
262

263
    def login(self, passwd=None):
264 265
        """:func:`burpui.misc.auth.ldap.LdapUser.login` function finds a user in
        the LDAP server and authenticates that user using an LDAP bind.
266 267 268 269

        :param passwd: password to bind to the LDAP server with
        :type passwd: str

270 271 272
        :returns: True if found and bind was successful;
                  False if found but bind failed;
                  otherwise de-activates the user and returns False
273
        """
274
        if self.ldap.fetch(self.name):
275 276
            self.authenticated = self.ldap.check(self.id, passwd)
            return self.authenticated
277
        else:
278
            self.authenticated = False
279
            self.active = False
280
            return False
281

282
    def get_id(self):
283
        """:func:`burpui.misc.auth.ldap.LdapUser.get_id` function
284 285 286 287

        :returns: login name of the user
        """
        return self.name