diff --git a/laps-client/README.md b/laps-client/README.md index ccf039c..2dfd953 100644 --- a/laps-client/README.md +++ b/laps-client/README.md @@ -1,5 +1,5 @@ # LAPS4LINUX Client -The management client enables administrators to view the current (decrypted) local admin passwords. It can be used from command line or as graphical application. +The management client enables administrators to easily view the current (decrypted) local admin passwords and the Bitlocker recovery key too. It can be used from command line or as graphical application. ### Graphical User Interface (GUI) ![screenshot](../.github/screenshot.png) @@ -61,7 +61,10 @@ You can create a preset config file `/etc/laps-client.json` which will be loaded - `use-starttls`: Boolean which indicates wheter to use StartTLS on unencrypted LDAP connections (requires valid server certificate). - `username`: The username for LDAP simple binds. For Microsoft AD, you need to append the domain (`user@example.com`). For OpenLDAP, you need to enter your user DN (`dn=user,dc=example,dc=com`). - `use-kerberos`: Boolean which indicates wheter to use Kerberos for LDAP bind before falling back to simple bind. - - `ldap-attributes`: A dict of LDAP attributes to display. Dict key is the display name and the corresponding value is the LDAP attribute name. The dict value can also be a list of strings. Then, the first non-empty LDAP attribute will be displayed. + - `ldap-attributes`: A dict of LDAP attributes to display. + - Dict key is the display name and the corresponding value is the LDAP attribute name. + - The dict value can also be a list of strings. Then, the first non-empty LDAP attribute will be displayed. This is useful when migrating to Native LAPS - you can display the new attribute value if exists, otherwise the old attribute value of Legacy LAPS is shown. + - When appending `sub:` to the dict value (= LDAP attribute name), the sub-enrties of the computer object are searched. This is useful for querying the Bitlocker recovery key (`sub:msFVE-RecoveryPassword`). Make sure that you have permission to view the Bitlocker keys! - `ldap-attribute-password`: The LDAP attribute name which contains the admin password. The client will try to decrypt this value (in case of Native LAPS) and use it for Remmina connections. Can also be a list of strings. - `ldap-attribute-password-expiry`: The LDAP attribute name which contains the admin password expiration date. The client will write the updated expiration date into this attribute. Can also be a list of strings. - `ldap-attribute-password-history`: The LDAP attribute name which contains the admin password history. The client will try to decrypt this value (in case of Native LAPS) and use it to display the password history. Can also be a list of strings. diff --git a/laps-client/laps-client-settings.json.example b/laps-client/laps-client-settings.json.example index 1682737..0b4b397 100644 --- a/laps-client/laps-client-settings.json.example +++ b/laps-client/laps-client-settings.json.example @@ -36,6 +36,7 @@ "ldap-attributes": { "Operating System": "operatingSystem", "Last Logon Timestamp": "lastLogonTimestamp", + "Bitlocker Recovery Key": "sub:msFVE-RecoveryPassword", "Administrator Password": [ "msLAPS-EncryptedPassword", "msLAPS-Password", diff --git a/laps-client/laps_client/laps_cli.py b/laps-client/laps_client/laps_cli.py index b1c73e7..8c9ded8 100755 --- a/laps-client/laps_client/laps_cli.py +++ b/laps-client/laps_client/laps_cli.py @@ -157,57 +157,66 @@ def queryAttributes(self): ) # display result for entry in self.connection.entries: - # evaluate attributes of interest - for title, attribute in self.GetAttributesAsDict().items(): - value = None - if(isinstance(attribute, list)): - for _attribute in attribute: - # use first non-empty attribute - if(str(_attribute) in entry and entry[str(_attribute)]): - value = entry[str(_attribute)] - attribute = str(_attribute) - break - elif(str(attribute) in entry): - value = entry[str(attribute)] - - # handle non-existing attributes - if(value == None): - self.pushResult(str(title), '') - - # if this is the password attribute -> try to parse Native LAPS format - elif(len(value) > 0 and - (str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword)) - ): - password, username, timestamp = self.parseLapsValue(value.values[0]) - if(not username or not password): - self.pushResult(str(title), password) - else: - self.pushResult(str(title), password+' ('+username+') ('+timestamp+')') - - # if this is the encrypted password history attribute -> try to parse Native LAPS format - elif(len(value) > 0 and - (str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory)) - ): - for _value in value.values: - password, username, timestamp = self.parseLapsValue(_value) + # we are looking at the main computer object + if(entry.entry_dn == self.tmpDn): + # evaluate attributes of interest + for title, attribute in self.GetAttributesAsDict().items(): + if(attribute[:4] == 'sub:'): continue + value = None + if(isinstance(attribute, list)): + for _attribute in attribute: + # use first non-empty attribute + if(str(_attribute) in entry and entry[str(_attribute)]): + value = entry[str(_attribute)] + attribute = str(_attribute) + break + elif(str(attribute) in entry): + value = entry[str(attribute)] + + # handle non-existing attributes + if(value == None): + self.pushResult(str(title), '') + + # if this is the password attribute -> try to parse Native LAPS format + elif(len(value) > 0 and + (str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword)) + ): + password, username, timestamp = self.parseLapsValue(value.values[0]) if(not username or not password): self.pushResult(str(title), password) else: self.pushResult(str(title), password+' ('+username+') ('+timestamp+')') - # if this is the expiry date attribute -> format date - elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)): - try: - self.pushResult(str(title), str(value)+' ('+str(filetime_to_dt( int(str(value)) ))+')') - except Exception as e: - eprint('Error:', str(e)) + # if this is the encrypted password history attribute -> try to parse Native LAPS format + elif(len(value) > 0 and + (str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory)) + ): + for _value in value.values: + password, username, timestamp = self.parseLapsValue(_value) + if(not username or not password): + self.pushResult(str(title), password) + else: + self.pushResult(str(title), password+' ('+username+') ('+timestamp+')') + + # if this is the expiry date attribute -> format date + elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)): + try: + self.pushResult(str(title), str(value)+' ('+str(filetime_to_dt( int(str(value)) ))+')') + except Exception as e: + eprint('Error:', str(e)) + self.pushResult(str(title), str(value)) + + # display raw value + else: self.pushResult(str(title), str(value)) - # display raw value - else: - self.pushResult(str(title), str(value)) - - return + # we are looking at a sub-item of the computer object, e.g. a BitLocker recovery key + else: + for title, attribute in self.GetAttributesAsDict().items(): + if(attribute[:4] != 'sub:'): continue + subattribute = str(attribute[4:]) + if(subattribute in entry): + self.pushResult(str(title), str(entry[subattribute])) dpapiCache = dpapi_ng.KeyCache() def decryptPassword(self, blob): diff --git a/laps-client/laps_client/laps_gui.py b/laps-client/laps_client/laps_gui.py index 802f87c..78b4853 100755 --- a/laps-client/laps_client/laps_gui.py +++ b/laps-client/laps_client/laps_gui.py @@ -626,63 +626,73 @@ def queryAttributes(self): ) # display result for entry in self.connection.entries: - self.btnSetExpirationTime.setEnabled(True) - self.btnSearchComputer.setEnabled(True) - - # evaluate attributes of interest - for title, attribute in self.GetAttributesAsDict().items(): - textBox = self.refLdapAttributesTextBoxes[str(title)] - value = None - if(isinstance(attribute, list)): - for _attribute in attribute: - # use first non-empty attribute - if(str(_attribute) in entry and entry[str(_attribute)]): - value = entry[str(_attribute)] - attribute = str(_attribute) - break - elif(str(attribute) in entry): - value = entry[str(attribute)] - - # handle non-existing attributes - if(value == None): - self.updateTextboxText(textBox, '') - - # if this is the password attribute -> try to parse Native LAPS format - elif(len(value) > 0 and - (str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword)) - ): - password, username, timestamp = self.parseLapsValue(value.values[0]) - self.updateTextboxText(textBox, str(password)) - if(username and password): - self.cfgConnectUsername = username - textBox.setToolTip(username+', '+timestamp) - - # if this is the encrypted password history attribute -> try to parse Native LAPS format - elif(len(value) > 0 and - (str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory)) - ): - lines = [] - for _value in value.values: - password, username, timestamp = self.parseLapsValue(_value) - if(not username or not password): - lines.append(str(password)) - else: - lines.append(password+' '+username+' '+timestamp) - self.updateTextboxText(textBox, "\n".join(lines)) - - # if this is the expiry date attribute -> format date - elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)): - try: - self.updateTextboxText(textBox, str(filetime_to_dt( int(str(value)) )) ) - except Exception as e: - print(str(e)) + # we are looking at the main computer object + if(entry.entry_dn == self.tmpDn): + self.btnSetExpirationTime.setEnabled(True) + self.btnSearchComputer.setEnabled(True) + + # evaluate attributes of interest + for title, attribute in self.GetAttributesAsDict().items(): + if(attribute[:4] == 'sub:'): continue + textBox = self.refLdapAttributesTextBoxes[str(title)] + value = None + if(isinstance(attribute, list)): + for _attribute in attribute: + # use first non-empty attribute + if(str(_attribute) in entry and entry[str(_attribute)]): + value = entry[str(_attribute)] + attribute = str(_attribute) + break + elif(str(attribute) in entry): + value = entry[str(attribute)] + + # handle non-existing attributes + if(value == None): + self.updateTextboxText(textBox, '') + + # if this is the password attribute -> try to parse Native LAPS format + elif(len(value) > 0 and + (str(attribute) == self.cfgLdapAttributePassword or (isinstance(self.cfgLdapAttributePassword, list) and str(attribute) in self.cfgLdapAttributePassword)) + ): + password, username, timestamp = self.parseLapsValue(value.values[0]) + self.updateTextboxText(textBox, str(password)) + if(username and password): + self.cfgConnectUsername = username + textBox.setToolTip(username+', '+timestamp) + + # if this is the encrypted password history attribute -> try to parse Native LAPS format + elif(len(value) > 0 and + (str(attribute) == self.cfgLdapAttributePasswordHistory or (isinstance(self.cfgLdapAttributePasswordHistory, list) and str(attribute) in self.cfgLdapAttributePasswordHistory)) + ): + lines = [] + for _value in value.values: + password, username, timestamp = self.parseLapsValue(_value) + if(not username or not password): + lines.append(str(password)) + else: + lines.append(password+' '+username+' '+timestamp) + self.updateTextboxText(textBox, "\n".join(lines)) + + # if this is the expiry date attribute -> format date + elif(str(attribute) == self.cfgLdapAttributePasswordExpiry or (isinstance(self.cfgLdapAttributePasswordExpiry, list) and str(attribute) in self.cfgLdapAttributePasswordExpiry)): + try: + self.updateTextboxText(textBox, str(filetime_to_dt( int(str(value)) )) ) + except Exception as e: + print(str(e)) + self.updateTextboxText(textBox, str(value)) + + # display raw value + else: self.updateTextboxText(textBox, str(value)) - # display raw value - else: - self.updateTextboxText(textBox, str(value)) - - return + # we are looking at a sub-item of the computer object, e.g. a BitLocker recovery key + else: + for title, attribute in self.GetAttributesAsDict().items(): + textBox = self.refLdapAttributesTextBoxes[str(title)] + if(attribute[:4] != 'sub:'): continue + subattribute = str(attribute[4:]) + if(subattribute in entry): + self.updateTextboxText(textBox, str(entry[subattribute])) def updateTextboxText(self, textBox, text): if(isinstance(textBox, QPlainTextEdit)):