Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. 

2# 

3# This software is provided under under a slightly modified version 

4# of the Apache Software License. See the accompanying LICENSE file 

5# for more information. 

6# 

7# Authors: Alberto Solino (@agsolino) 

8# Kacper Nowak (@kacpern) 

9# 

10# Description: 

11# RFC 4511 Minimalistic implementation. We don't need much functionality yet 

12# If we need more complex use cases we might opt to use a third party implementation 

13# Keep in mind the APIs are still unstable, might require to re-write your scripts 

14# as we change them. 

15# Adding [MS-ADTS] specific functionality 

16# 

17# ToDo: 

18# [x] Implement Paging Search, especially important for big requests 

19# 

20 

21import os 

22import re 

23import socket 

24from binascii import unhexlify 

25import random 

26 

27from pyasn1.codec.ber import encoder, decoder 

28from pyasn1.error import SubstrateUnderrunError 

29from pyasn1.type.univ import noValue 

30 

31from impacket import LOG 

32from impacket.ldap.ldapasn1 import Filter, Control, SimplePagedResultsControl, ResultCode, Scope, DerefAliases, Operation, \ 

33 KNOWN_CONTROLS, CONTROL_PAGEDRESULTS, NOTIFICATION_DISCONNECT, KNOWN_NOTIFICATIONS, BindRequest, SearchRequest, \ 

34 SearchResultDone, LDAPMessage 

35from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3 

36from impacket.spnego import SPNEGO_NegTokenInit, TypesMech 

37 

38try: 

39 import OpenSSL 

40 from OpenSSL import SSL, crypto 

41except: 

42 LOG.critical("pyOpenSSL is not installed, can't continue") 

43 raise 

44 

45__all__ = [ 

46 'LDAPConnection', 'LDAPFilterSyntaxError', 'LDAPFilterInvalidException', 'LDAPSessionError', 'LDAPSearchError', 

47 'Control', 'SimplePagedResultsControl', 'ResultCode', 'Scope', 'DerefAliases', 'Operation', 

48 'CONTROL_PAGEDRESULTS', 'KNOWN_CONTROLS', 'NOTIFICATION_DISCONNECT', 'KNOWN_NOTIFICATIONS', 

49] 

50 

51# https://tools.ietf.org/search/rfc4515#section-3 

52DESCRIPTION = r'(?:[a-z][a-z0-9\-]*)' 

53NUMERIC_OID = r'(?:(?:\d|[1-9]\d+)(?:\.(?:\d|[1-9]\d+))*)' 

54OID = r'(?:%s|%s)' % (DESCRIPTION, NUMERIC_OID) 

55OPTIONS = r'(?:(?:;[a-z0-9\-]+)*)' 

56ATTRIBUTE = r'(%s%s)' % (OID, OPTIONS) 

57DN = r'(:dn)' 

58MATCHING_RULE = r'(?::(%s))' % OID 

59 

60RE_OPERATOR = re.compile(r'([:<>~]?=)') 

61RE_ATTRIBUTE = re.compile(r'^%s$' % ATTRIBUTE, re.I) 

62RE_EX_ATTRIBUTE_1 = re.compile(r'^%s%s?%s?$' % (ATTRIBUTE, DN, MATCHING_RULE), re.I) 

63RE_EX_ATTRIBUTE_2 = re.compile(r'^(){0}%s?%s$' % (DN, MATCHING_RULE), re.I) 

64 

65 

66class LDAPConnection: 

67 def __init__(self, url, baseDN='', dstIp=None): 

68 """ 

69 LDAPConnection class 

70 

71 :param string url: 

72 :param string baseDN: 

73 :param string dstIp: 

74 

75 :return: a LDAP instance, if not raises a LDAPSessionError exception 

76 """ 

77 self._SSL = False 

78 self._dstPort = 0 

79 self._dstHost = 0 

80 self._socket = None 

81 self._baseDN = baseDN 

82 self._dstIp = dstIp 

83 

84 if url.startswith('ldap://'): 84 ↛ 88line 84 didn't jump to line 88, because the condition on line 84 was never false

85 self._dstPort = 389 

86 self._SSL = False 

87 self._dstHost = url[7:] 

88 elif url.startswith('ldaps://'): 

89 self._dstPort = 636 

90 self._SSL = True 

91 self._dstHost = url[8:] 

92 elif url.startswith('gc://'): 

93 self._dstPort = 3268 

94 self._SSL = False 

95 self._dstHost = url[5:] 

96 else: 

97 raise LDAPSessionError(errorString="Unknown URL prefix: '%s'" % url) 

98 

99 # Try to connect 

100 if self._dstIp is not None: 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true

101 targetHost = self._dstIp 

102 else: 

103 targetHost = self._dstHost 

104 

105 LOG.debug('Connecting to %s, port %d, SSL %s' % (targetHost, self._dstPort, self._SSL)) 

106 try: 

107 af, socktype, proto, _, sa = socket.getaddrinfo(targetHost, self._dstPort, 0, socket.SOCK_STREAM)[0] 

108 self._socket = socket.socket(af, socktype, proto) 

109 except socket.error as e: 

110 raise socket.error('Connection error (%s:%d)' % (targetHost, 88), e) 

111 

112 if self._SSL is False: 112 ↛ 116line 112 didn't jump to line 116, because the condition on line 112 was never false

113 self._socket.connect(sa) 

114 else: 

115 # Switching to TLS now 

116 ctx = SSL.Context(SSL.TLSv1_METHOD) 

117 # ctx.set_cipher_list('RC4') 

118 self._socket = SSL.Connection(ctx, self._socket) 

119 self._socket.connect(sa) 

120 self._socket.do_handshake() 

121 

122 def kerberosLogin(self, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, 

123 TGS=None, useCache=True): 

124 """ 

125 logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported. 

126 

127 :param string user: username 

128 :param string password: password for the user 

129 :param string domain: domain where the account is valid for (required) 

130 :param string lmhash: LMHASH used to authenticate using hashes (password is not used) 

131 :param string nthash: NTHASH used to authenticate using hashes (password is not used) 

132 :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication 

133 :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho) 

134 :param struct TGT: If there's a TGT available, send the structure here and it will be used 

135 :param struct TGS: same for TGS. See smb3.py for the format 

136 :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False 

137 

138 :return: True, raises a LDAPSessionError if error. 

139 """ 

140 

141 if lmhash != '' or nthash != '': 

142 if len(lmhash) % 2: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true

143 lmhash = '0' + lmhash 

144 if len(nthash) % 2: 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true

145 nthash = '0' + nthash 

146 try: # just in case they were converted already 

147 lmhash = unhexlify(lmhash) 

148 nthash = unhexlify(nthash) 

149 except TypeError: 

150 pass 

151 

152 # Importing down here so pyasn1 is not required if kerberos is not used. 

153 from impacket.krb5.ccache import CCache 

154 from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set 

155 from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS 

156 from impacket.krb5 import constants 

157 from impacket.krb5.types import Principal, KerberosTime, Ticket 

158 import datetime 

159 

160 if TGT is not None or TGS is not None: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true

161 useCache = False 

162 

163 if useCache: 163 ↛ 200line 163 didn't jump to line 200, because the condition on line 163 was never false

164 try: 

165 ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) 

166 except: 

167 # No cache present 

168 pass 

169 else: 

170 # retrieve domain information from CCache file if needed 

171 if domain == '': 

172 domain = ccache.principal.realm['data'].decode('utf-8') 

173 LOG.debug('Domain retrieved from CCache: %s' % domain) 

174 

175 LOG.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME')) 

176 principal = 'ldap/%s@%s' % (self._dstHost.upper(), domain.upper()) 

177 creds = ccache.getCredential(principal) 

178 if creds is None: 

179 # Let's try for the TGT and go from there 

180 principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper()) 

181 creds = ccache.getCredential(principal) 

182 if creds is not None: 

183 TGT = creds.toTGT() 

184 LOG.debug('Using TGT from cache') 

185 else: 

186 LOG.debug('No valid credentials found in cache') 

187 else: 

188 TGS = creds.toTGS(principal) 

189 LOG.debug('Using TGS from cache') 

190 

191 # retrieve user information from CCache file if needed 

192 if user == '' and creds is not None: 

193 user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8') 

194 LOG.debug('Username retrieved from CCache: %s' % user) 

195 elif user == '' and len(ccache.principal.components) > 0: 

196 user = ccache.principal.components[0]['data'].decode('utf-8') 

197 LOG.debug('Username retrieved from CCache: %s' % user) 

198 

199 # First of all, we need to get a TGT for the user 

200 userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value) 

201 if TGT is None: 201 ↛ 206line 201 didn't jump to line 206, because the condition on line 201 was never false

202 if TGS is None: 202 ↛ 210line 202 didn't jump to line 210, because the condition on line 202 was never false

203 tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, 

204 aesKey, kdcHost) 

205 else: 

206 tgt = TGT['KDC_REP'] 

207 cipher = TGT['cipher'] 

208 sessionKey = TGT['sessionKey'] 

209 

210 if TGS is None: 210 ↛ 215line 210 didn't jump to line 215, because the condition on line 210 was never false

211 serverName = Principal('ldap/%s' % self._dstHost, type=constants.PrincipalNameType.NT_SRV_INST.value) 

212 tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, 

213 sessionKey) 

214 else: 

215 tgs = TGS['KDC_REP'] 

216 cipher = TGS['cipher'] 

217 sessionKey = TGS['sessionKey'] 

218 

219 # Let's build a NegTokenInit with a Kerberos REQ_AP 

220 

221 blob = SPNEGO_NegTokenInit() 

222 

223 # Kerberos 

224 blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']] 

225 

226 # Let's extract the ticket from the TGS 

227 tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0] 

228 ticket = Ticket() 

229 ticket.from_asn1(tgs['ticket']) 

230 

231 # Now let's build the AP_REQ 

232 apReq = AP_REQ() 

233 apReq['pvno'] = 5 

234 apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value) 

235 

236 opts = [] 

237 apReq['ap-options'] = constants.encodeFlags(opts) 

238 seq_set(apReq, 'ticket', ticket.to_asn1) 

239 

240 authenticator = Authenticator() 

241 authenticator['authenticator-vno'] = 5 

242 authenticator['crealm'] = domain 

243 seq_set(authenticator, 'cname', userName.components_to_asn1) 

244 now = datetime.datetime.utcnow() 

245 

246 authenticator['cusec'] = now.microsecond 

247 authenticator['ctime'] = KerberosTime.to_asn1(now) 

248 

249 encodedAuthenticator = encoder.encode(authenticator) 

250 

251 # Key Usage 11 

252 # AP-REQ Authenticator (includes application authenticator 

253 # subkey), encrypted with the application session key 

254 # (Section 5.5.1) 

255 encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None) 

256 

257 apReq['authenticator'] = noValue 

258 apReq['authenticator']['etype'] = cipher.enctype 

259 apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator 

260 

261 blob['MechToken'] = encoder.encode(apReq) 

262 

263 # Done with the Kerberos saga, now let's get into LDAP 

264 

265 bindRequest = BindRequest() 

266 bindRequest['version'] = 3 

267 bindRequest['name'] = user 

268 bindRequest['authentication']['sasl']['mechanism'] = 'GSS-SPNEGO' 

269 bindRequest['authentication']['sasl']['credentials'] = blob.getData() 

270 

271 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

272 

273 if response['bindResponse']['resultCode'] != ResultCode('success'): 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true

274 raise LDAPSessionError( 

275 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(), 

276 response['bindResponse']['diagnosticMessage']) 

277 ) 

278 

279 return True 

280 

281 def login(self, user='', password='', domain='', lmhash='', nthash='', authenticationChoice='sicilyNegotiate'): 

282 """ 

283 logins into the target system 

284 

285 :param string user: username 

286 :param string password: password for the user 

287 :param string domain: domain where the account is valid for 

288 :param string lmhash: LMHASH used to authenticate using hashes (password is not used) 

289 :param string nthash: NTHASH used to authenticate using hashes (password is not used) 

290 :param string authenticationChoice: type of authentication protocol to use (default NTLM) 

291 

292 :return: True, raises a LDAPSessionError if error. 

293 """ 

294 bindRequest = BindRequest() 

295 bindRequest['version'] = 3 

296 

297 if authenticationChoice == 'simple': 297 ↛ 298line 297 didn't jump to line 298, because the condition on line 297 was never true

298 if '.' in domain: 

299 bindRequest['name'] = user + '@' + domain 

300 elif domain: 

301 bindRequest['name'] = domain + '\\' + user 

302 else: 

303 bindRequest['name'] = user 

304 bindRequest['authentication']['simple'] = password 

305 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

306 elif authenticationChoice == 'sicilyPackageDiscovery': 

307 bindRequest['name'] = user 

308 bindRequest['authentication']['sicilyPackageDiscovery'] = '' 

309 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

310 elif authenticationChoice == 'sicilyNegotiate': 310 ↛ 338line 310 didn't jump to line 338, because the condition on line 310 was never false

311 # Deal with NTLM Authentication 

312 if lmhash != '' or nthash != '': 

313 if len(lmhash) % 2: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true

314 lmhash = '0' + lmhash 

315 if len(nthash) % 2: 315 ↛ 316line 315 didn't jump to line 316, because the condition on line 315 was never true

316 nthash = '0' + nthash 

317 try: # just in case they were converted already 

318 lmhash = unhexlify(lmhash) 

319 nthash = unhexlify(nthash) 

320 except TypeError: 

321 pass 

322 

323 bindRequest['name'] = user 

324 

325 # NTLM Negotiate 

326 negotiate = getNTLMSSPType1('', domain) 

327 bindRequest['authentication']['sicilyNegotiate'] = negotiate.getData() 

328 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

329 

330 # NTLM Challenge 

331 type2 = response['bindResponse']['matchedDN'] 

332 

333 # NTLM Auth 

334 type3, exportedSessionKey = getNTLMSSPType3(negotiate, bytes(type2), user, password, domain, lmhash, nthash) 

335 bindRequest['authentication']['sicilyResponse'] = type3.getData() 

336 response = self.sendReceive(bindRequest)[0]['protocolOp'] 

337 else: 

338 raise LDAPSessionError(errorString="Unknown authenticationChoice: '%s'" % authenticationChoice) 

339 

340 if response['bindResponse']['resultCode'] != ResultCode('success'): 340 ↛ 341line 340 didn't jump to line 341, because the condition on line 340 was never true

341 raise LDAPSessionError( 

342 errorString='Error in bindRequest -> %s: %s' % (response['bindResponse']['resultCode'].prettyPrint(), 

343 response['bindResponse']['diagnosticMessage']) 

344 ) 

345 

346 return True 

347 

348 def search(self, searchBase=None, scope=None, derefAliases=None, sizeLimit=0, timeLimit=0, typesOnly=False, 

349 searchFilter='(objectClass=*)', attributes=None, searchControls=None, perRecordCallback=None): 

350 if searchBase is None: 350 ↛ 352line 350 didn't jump to line 352, because the condition on line 350 was never false

351 searchBase = self._baseDN 

352 if scope is None: 352 ↛ 354line 352 didn't jump to line 354, because the condition on line 352 was never false

353 scope = Scope('wholeSubtree') 

354 if derefAliases is None: 354 ↛ 356line 354 didn't jump to line 356, because the condition on line 354 was never false

355 derefAliases = DerefAliases('neverDerefAliases') 

356 if attributes is None: 356 ↛ 357line 356 didn't jump to line 357, because the condition on line 356 was never true

357 attributes = [] 

358 

359 searchRequest = SearchRequest() 

360 searchRequest['baseObject'] = searchBase 

361 searchRequest['scope'] = scope 

362 searchRequest['derefAliases'] = derefAliases 

363 searchRequest['sizeLimit'] = sizeLimit 

364 searchRequest['timeLimit'] = timeLimit 

365 searchRequest['typesOnly'] = typesOnly 

366 searchRequest['filter'] = self._parseFilter(searchFilter) 

367 searchRequest['attributes'].setComponents(*attributes) 

368 

369 done = False 

370 answers = [] 

371 # We keep asking records until we get a SearchResultDone packet and all controls are handled 

372 while not done: 

373 response = self.sendReceive(searchRequest, searchControls) 

374 for message in response: 

375 searchResult = message['protocolOp'].getComponent() 

376 if searchResult.isSameTypeWith(SearchResultDone()): 

377 if searchResult['resultCode'] == ResultCode('success'): 377 ↛ 380line 377 didn't jump to line 380, because the condition on line 377 was never false

378 done = self._handleControls(searchControls, message['controls']) 

379 else: 

380 raise LDAPSearchError( 

381 error=int(searchResult['resultCode']), 

382 errorString='Error in searchRequest -> %s: %s' % (searchResult['resultCode'].prettyPrint(), 

383 searchResult['diagnosticMessage']), 

384 answers=answers 

385 ) 

386 else: 

387 if perRecordCallback is None: 387 ↛ 390line 387 didn't jump to line 390, because the condition on line 387 was never false

388 answers.append(searchResult) 

389 else: 

390 perRecordCallback(searchResult) 

391 

392 return answers 

393 

394 def _handleControls(self, requestControls, responseControls): 

395 done = True 

396 if requestControls is not None: 396 ↛ 397line 396 didn't jump to line 397, because the condition on line 396 was never true

397 for requestControl in requestControls: 

398 if responseControls is not None: 

399 for responseControl in responseControls: 

400 if str(requestControl['controlType']) == CONTROL_PAGEDRESULTS: 

401 if str(responseControl['controlType']) == CONTROL_PAGEDRESULTS: 

402 if hasattr(responseControl, 'getCookie') is not True: 

403 responseControl = decoder.decode(encoder.encode(responseControl), 

404 asn1Spec=KNOWN_CONTROLS[CONTROL_PAGEDRESULTS]())[0] 

405 if responseControl.getCookie(): 

406 done = False 

407 requestControl.setCookie(responseControl.getCookie()) 

408 break 

409 else: 

410 # handle different controls here 

411 pass 

412 return done 

413 

414 def close(self): 

415 if self._socket is not None: 

416 self._socket.close() 

417 

418 def send(self, request, controls=None): 

419 message = LDAPMessage() 

420 message['messageID'] = random.randrange(1, 2147483647) 

421 message['protocolOp'].setComponentByType(request.getTagSet(), request) 

422 if controls is not None: 422 ↛ 423line 422 didn't jump to line 423, because the condition on line 422 was never true

423 message['controls'].setComponents(*controls) 

424 

425 data = encoder.encode(message) 

426 

427 return self._socket.sendall(data) 

428 

429 def recv(self): 

430 REQUEST_SIZE = 8192 

431 data = b'' 

432 done = False 

433 while not done: 

434 recvData = self._socket.recv(REQUEST_SIZE) 

435 if len(recvData) < REQUEST_SIZE: 435 ↛ 437line 435 didn't jump to line 437, because the condition on line 435 was never false

436 done = True 

437 data += recvData 

438 

439 response = [] 

440 while len(data) > 0: 

441 try: 

442 message, remaining = decoder.decode(data, asn1Spec=LDAPMessage()) 

443 except SubstrateUnderrunError: 

444 # We need more data 

445 remaining = data + self._socket.recv(REQUEST_SIZE) 

446 else: 

447 if message['messageID'] == 0: # unsolicited notification 447 ↛ 448line 447 didn't jump to line 448, because the condition on line 447 was never true

448 name = message['protocolOp']['extendedResp']['responseName'] or message['responseName'] 

449 notification = KNOWN_NOTIFICATIONS.get(name, "Unsolicited Notification '%s'" % name) 

450 if name == NOTIFICATION_DISCONNECT: # Server has disconnected 

451 self.close() 

452 raise LDAPSessionError( 

453 error=int(message['protocolOp']['extendedResp']['resultCode']), 

454 errorString='%s -> %s: %s' % (notification, 

455 message['protocolOp']['extendedResp']['resultCode'].prettyPrint(), 

456 message['protocolOp']['extendedResp']['diagnosticMessage']) 

457 ) 

458 response.append(message) 

459 data = remaining 

460 

461 return response 

462 

463 def sendReceive(self, request, controls=None): 

464 self.send(request, controls) 

465 return self.recv() 

466 

467 def _parseFilter(self, filterStr): 

468 try: 

469 filterStr = filterStr.decode() 

470 except AttributeError: 

471 pass 

472 filterList = list(reversed(filterStr)) 

473 searchFilter = self._consumeCompositeFilter(filterList) 

474 if filterList: # we have not consumed the whole filter string 474 ↛ 475line 474 didn't jump to line 475, because the condition on line 474 was never true

475 raise LDAPFilterSyntaxError("unexpected token: '%s'" % filterList[-1]) 

476 return searchFilter 

477 

478 def _consumeCompositeFilter(self, filterList): 

479 try: 

480 c = filterList.pop() 

481 except IndexError: 

482 raise LDAPFilterSyntaxError('EOL while parsing search filter') 

483 if c != '(': # filter must start with a '(' 483 ↛ 484line 483 didn't jump to line 484, because the condition on line 483 was never true

484 filterList.append(c) 

485 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c) 

486 

487 try: 

488 operator = filterList.pop() 

489 except IndexError: 

490 raise LDAPFilterSyntaxError('EOL while parsing search filter') 

491 if operator not in ['!', '&', '|']: # must be simple filter in this case 491 ↛ 495line 491 didn't jump to line 495, because the condition on line 491 was never false

492 filterList.extend([operator, c]) 

493 return self._consumeSimpleFilter(filterList) 

494 

495 filters = [] 

496 while True: 

497 try: 

498 filters.append(self._consumeCompositeFilter(filterList)) 

499 except LDAPFilterSyntaxError: 

500 break 

501 

502 try: 

503 c = filterList.pop() 

504 except IndexError: 

505 raise LDAPFilterSyntaxError('EOL while parsing search filter') 

506 if c != ')': # filter must end with a ')' 

507 filterList.append(c) 

508 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c) 

509 

510 return self._compileCompositeFilter(operator, filters) 

511 

512 def _consumeSimpleFilter(self, filterList): 

513 try: 

514 c = filterList.pop() 

515 except IndexError: 

516 raise LDAPFilterSyntaxError('EOL while parsing search filter') 

517 if c != '(': # filter must start with a '(' 517 ↛ 518line 517 didn't jump to line 518, because the condition on line 517 was never true

518 filterList.append(c) 

519 raise LDAPFilterSyntaxError("unexpected token: '%s'" % c) 

520 

521 filter = [] 

522 while True: 

523 try: 

524 c = filterList.pop() 

525 except IndexError: 

526 raise LDAPFilterSyntaxError('EOL while parsing search filter') 

527 if c == ')': # we pop till we find a ')' 

528 break 

529 elif c == '(': # should be no unencoded parenthesis 529 ↛ 530line 529 didn't jump to line 530, because the condition on line 529 was never true

530 filterList.append(c) 

531 raise LDAPFilterSyntaxError("unexpected token: '('") 

532 else: 

533 filter.append(c) 

534 

535 filterStr = ''.join(filter) 

536 try: 

537 # https://tools.ietf.org/search/rfc4515#section-3 

538 attribute, operator, value = RE_OPERATOR.split(filterStr, 1) 

539 except ValueError: 

540 raise LDAPFilterInvalidException("invalid filter: '(%s)'" % filterStr) 

541 

542 return self._compileSimpleFilter(attribute, operator, value) 

543 

544 @staticmethod 

545 def _compileCompositeFilter(operator, filters): 

546 searchFilter = Filter() 

547 if operator == '!': 

548 if len(filters) != 1: 

549 raise LDAPFilterInvalidException("'not' filter must have exactly one element") 

550 searchFilter['not'].setComponents(*filters) 

551 elif operator == '&': 

552 if len(filters) == 0: 

553 raise LDAPFilterInvalidException("'and' filter must have at least one element") 

554 searchFilter['and'].setComponents(*filters) 

555 elif operator == '|': 

556 if len(filters) == 0: 

557 raise LDAPFilterInvalidException("'or' filter must have at least one element") 

558 searchFilter['or'].setComponents(*filters) 

559 

560 return searchFilter 

561 

562 @staticmethod 

563 def _compileSimpleFilter(attribute, operator, value): 

564 searchFilter = Filter() 

565 if operator == ':=': # extensibleMatch 565 ↛ 566line 565 didn't jump to line 566, because the condition on line 565 was never true

566 match = RE_EX_ATTRIBUTE_1.match(attribute) or RE_EX_ATTRIBUTE_2.match(attribute) 

567 if not match: 

568 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute) 

569 attribute, dn, matchingRule = match.groups() 

570 if attribute: 

571 searchFilter['extensibleMatch']['type'] = attribute 

572 if dn: 

573 searchFilter['extensibleMatch']['dnAttributes'] = bool(dn) 

574 if matchingRule: 

575 searchFilter['extensibleMatch']['matchingRule'] = matchingRule 

576 searchFilter['extensibleMatch']['matchValue'] = value 

577 else: 

578 if not RE_ATTRIBUTE.match(attribute): 578 ↛ 579line 578 didn't jump to line 579, because the condition on line 578 was never true

579 raise LDAPFilterInvalidException("invalid filter attribute: '%s'" % attribute) 

580 if value == '*' and operator == '=': # present 

581 searchFilter['present'] = attribute 

582 elif '*' in value and operator == '=': # substring 582 ↛ 583line 582 didn't jump to line 583, because the condition on line 582 was never true

583 assertions = value.split('*') 

584 choice = searchFilter['substrings']['substrings'].getComponentType() 

585 substrings = [] 

586 if assertions[0]: 

587 substrings.append(choice.clone().setComponentByName('initial', assertions[0])) 

588 for assertion in assertions[1:-1]: 

589 substrings.append(choice.clone().setComponentByName('any', assertion)) 

590 if assertions[-1]: 

591 substrings.append(choice.clone().setComponentByName('final', assertions[-1])) 

592 searchFilter['substrings']['type'] = attribute 

593 searchFilter['substrings']['substrings'].setComponents(*substrings) 

594 elif '*' not in value: # simple 594 ↛ 604line 594 didn't jump to line 604, because the condition on line 594 was never false

595 if operator == '=': 595 ↛ 597line 595 didn't jump to line 597, because the condition on line 595 was never false

596 searchFilter['equalityMatch'].setComponents(attribute, value) 

597 elif operator == '~=': 

598 searchFilter['approxMatch'].setComponents(attribute, value) 

599 elif operator == '>=': 

600 searchFilter['greaterOrEqual'].setComponents(attribute, value) 

601 elif operator == '<=': 

602 searchFilter['lessOrEqual'].setComponents(attribute, value) 

603 else: 

604 raise LDAPFilterInvalidException("invalid filter '(%s%s%s)'" % (attribute, operator, value)) 

605 

606 return searchFilter 

607 

608 

609class LDAPFilterSyntaxError(SyntaxError): 

610 pass 

611 

612 

613class LDAPFilterInvalidException(Exception): 

614 pass 

615 

616 

617class LDAPSessionError(Exception): 

618 """ 

619 This is the exception every client should catch 

620 """ 

621 

622 def __init__(self, error=0, packet=0, errorString=''): 

623 Exception.__init__(self) 

624 self.error = error 

625 self.packet = packet 

626 self.errorString = errorString 

627 

628 def getErrorCode(self): 

629 return self.error 

630 

631 def getErrorPacket(self): 

632 return self.packet 

633 

634 def getErrorString(self): 

635 return self.errorString 

636 

637 def __str__(self): 

638 return self.errorString 

639 

640 

641class LDAPSearchError(LDAPSessionError): 

642 def __init__(self, error=0, packet=0, errorString='', answers=None): 

643 LDAPSessionError.__init__(self, error, packet, errorString) 

644 if answers is None: 

645 answers = [] 

646 self.answers = answers 

647 

648 def getAnswers(self): 

649 return self.answers