|
37 | 37 | from impacket import LOG |
38 | 38 | from impacket.ldap.ldapasn1 import Filter, Control, SimplePagedResultsControl, ResultCode, Scope, DerefAliases, Operation, \ |
39 | 39 | KNOWN_CONTROLS, CONTROL_PAGEDRESULTS, NOTIFICATION_DISCONNECT, KNOWN_NOTIFICATIONS, BindRequest, SearchRequest, \ |
40 | | - SearchResultDone, LDAPMessage |
| 40 | + SearchResultDone, LDAPMessage, AddRequest, ModifyRequest, ModifyDNRequest, DelRequest |
41 | 41 | from impacket.ntlm import getNTLMSSPType1, getNTLMSSPType3, VERSION, hmac_md5, NTLMAuthChallenge |
42 | 42 | from impacket.spnego import SPNEGO_NegTokenInit, SPNEGO_NegTokenResp, SPNEGOCipher, TypesMech |
43 | 43 |
|
|
68 | 68 | RE_EX_ATTRIBUTE_1 = re.compile(r'^%s%s?%s?$' % (ATTRIBUTE, DN, MATCHING_RULE), re.I) |
69 | 69 | RE_EX_ATTRIBUTE_2 = re.compile(r'^(){0}%s?%s$' % (DN, MATCHING_RULE), re.I) |
70 | 70 |
|
| 71 | +# LDAP modify operations |
| 72 | +MODIFY_ADD = 0 |
| 73 | +MODIFY_DELETE = 1 |
| 74 | +MODIFY_REPLACE = 2 |
| 75 | +MODIFY_INCREMENT = 3 |
71 | 76 |
|
72 | 77 | class LDAPConnection: |
73 | 78 | def __init__(self, url, baseDN='', dstIp=None, signing=True): |
@@ -546,6 +551,126 @@ def _handleControls(self, requestControls, responseControls): |
546 | 551 | pass |
547 | 552 | return done |
548 | 553 |
|
| 554 | + def add(self, dn, objectClass, attributes=None, controls=None): |
| 555 | + """ |
| 556 | + Add an entry to the LDAP directory. |
| 557 | +
|
| 558 | + :param dn: Distinguished Name of the entry to add |
| 559 | + :param objectClass: Tuple or list of object classes for the entry |
| 560 | + :param attributes: Dictionary of attributes for the entry |
| 561 | + :param controls: LDAP controls to include in the request |
| 562 | + :return: True if the entry was added successfully, else raises LDAPSessionError |
| 563 | + """ |
| 564 | + addRequest = AddRequest() |
| 565 | + addRequest['entry'] = dn |
| 566 | + addRequest['attributes'][0]['type'] = 'objectClass' |
| 567 | + addRequest['attributes'][0]['vals'].setComponents(*objectClass) |
| 568 | + |
| 569 | + index = 1 |
| 570 | + for key, value in attributes.items(): |
| 571 | + addRequest['attributes'][index]['type'] = key |
| 572 | + if isinstance(value, list): |
| 573 | + addRequest['attributes'][index]['vals'].setComponents(str(val) if isinstance(val, int) else val for val in value) |
| 574 | + else: |
| 575 | + addRequest['attributes'][index]['vals'].setComponents(str(value) if isinstance(value, int) else value) |
| 576 | + index += 1 |
| 577 | + |
| 578 | + response = self.sendReceive(addRequest, controls)[0]['protocolOp'] |
| 579 | + if response['addResponse']['resultCode'] != ResultCode('success'): |
| 580 | + raise LDAPSessionError( |
| 581 | + error=int(response['addResponse']['resultCode']), |
| 582 | + errorString=f"Error in addRequest -> {response['addResponse']['resultCode'].prettyPrint()}: {response['addResponse']['diagnosticMessage']}" |
| 583 | + ) |
| 584 | + return True |
| 585 | + |
| 586 | + def modify(self, dn, modifications, controls=None): |
| 587 | + """ |
| 588 | + Modify an entry in the LDAP directory. |
| 589 | + RFC 4511 Section 4.6 |
| 590 | +
|
| 591 | + :param dn: Distinguished Name of the entry to modify |
| 592 | + :param modifications: Dictionary of modifications of form {attribute: [(operation, values), ...]}. |
| 593 | + Operation: 0 - add, 1 - delete, 2 - replace, 3 - increment. |
| 594 | + Values: single value or list of values. |
| 595 | + :param controls: LDAP controls to include in the request |
| 596 | + :return: True if the entry was modified successfully, else raises LDAPSessionError |
| 597 | + """ |
| 598 | + modifyRequest = ModifyRequest() |
| 599 | + modifyRequest['object'] = dn |
| 600 | + |
| 601 | + # idx keeps track of the current change index |
| 602 | + idx = 0 |
| 603 | + for attr, ops in modifications.items(): |
| 604 | + for op in ops: |
| 605 | + # op should be a tuple (operation, values) |
| 606 | + if op[0] not in [MODIFY_ADD, MODIFY_DELETE, MODIFY_REPLACE, MODIFY_INCREMENT]: |
| 607 | + raise LDAPSessionError(errorString=f"Invalid modification operation '{op[0]}' for attribute '{attr}'") |
| 608 | + modifyRequest['changes'][idx]['operation'] = op[0] # operation code |
| 609 | + modifyRequest['changes'][idx]['modification']['type'] = attr |
| 610 | + |
| 611 | + # prepare changed values. Integer values need to be converted to string |
| 612 | + vals = [] |
| 613 | + if isinstance(op[1], list): |
| 614 | + vals.extend([str(val) if isinstance(val, int) else val for val in op[1]]) |
| 615 | + else: |
| 616 | + vals.append(str(op[1]) if isinstance(op[1], int) else op[1]) |
| 617 | + modifyRequest['changes'][idx]['modification']['vals'].setComponents(*vals) |
| 618 | + idx += 1 |
| 619 | + |
| 620 | + response = self.sendReceive(modifyRequest, controls)[0]['protocolOp'] |
| 621 | + if response['modifyResponse']['resultCode'] != ResultCode('success'): |
| 622 | + raise LDAPSessionError( |
| 623 | + error=int(response['modifyResponse']['resultCode']), |
| 624 | + errorString=f"Error in modifyRequest -> {response['modifyResponse']['resultCode'].prettyPrint()}: {response['modifyResponse']['diagnosticMessage']}" |
| 625 | + ) |
| 626 | + return True |
| 627 | + |
| 628 | + def modify_dn(self, dn, newrdn, deleteoldrdn=True, newSuperior=None, controls=None): |
| 629 | + """ |
| 630 | + Modify the Distinguished Name of an entry in the LDAP directory. |
| 631 | + RFC 4511 Section 4.9 |
| 632 | +
|
| 633 | + :param dn: Current Distinguished Name of the entry |
| 634 | + :param newrdn: New Relative Distinguished Name for the entry |
| 635 | + :param deleteoldrdn: Whether to delete the old RDN from the entry |
| 636 | + :param newSuperior: New superior DN if moving the entry to a different container |
| 637 | + :param controls: LDAP controls to include in the request |
| 638 | + :return: True if the DN was modified successfully, else raises LDAPSessionError |
| 639 | + """ |
| 640 | + modifyDNRequest = ModifyDNRequest() |
| 641 | + modifyDNRequest['entry'] = dn |
| 642 | + modifyDNRequest['newrdn'] = newrdn |
| 643 | + modifyDNRequest['deleteoldrdn'] = deleteoldrdn |
| 644 | + if newSuperior is not None: |
| 645 | + modifyDNRequest['newSuperior'] = newSuperior |
| 646 | + |
| 647 | + response = self.sendReceive(modifyDNRequest, controls)[0]['protocolOp'] |
| 648 | + if response['modDNResponse']['resultCode'] != ResultCode('success'): |
| 649 | + raise LDAPSessionError( |
| 650 | + error=int(response['modDNResponse']['resultCode']), |
| 651 | + errorString=f"Error in modifyDNRequest -> {response['modDNResponse']['resultCode'].prettyPrint()}: {response['modDNResponse']['diagnosticMessage']}" |
| 652 | + ) |
| 653 | + return True |
| 654 | + |
| 655 | + def delete(self, dn, controls=None): |
| 656 | + """ |
| 657 | + Delete an entry from the LDAP directory. |
| 658 | + RFC 4511 Section 4.8 |
| 659 | +
|
| 660 | + :param dn: Distinguished Name of the entry to delete |
| 661 | + :param controls: LDAP controls to include in the request |
| 662 | + :return: True if the entry was deleted successfully, else raises LDAPSessionError |
| 663 | + """ |
| 664 | + deleteRequest = DelRequest(dn) |
| 665 | + |
| 666 | + response = self.sendReceive(deleteRequest, controls)[0]['protocolOp'] |
| 667 | + if response['delResponse']['resultCode'] != ResultCode('success'): |
| 668 | + raise LDAPSessionError( |
| 669 | + error=int(response['delResponse']['resultCode']), |
| 670 | + errorString=f"Error in deleteRequest -> {response['delResponse']['resultCode'].prettyPrint()}: {response['delResponse']['diagnosticMessage']}" |
| 671 | + ) |
| 672 | + return True |
| 673 | + |
549 | 674 | def close(self): |
550 | 675 | if self._socket is not None: |
551 | 676 | self._socket.close() |
|
0 commit comments