Skip to content

Commit

Permalink
option to display Bitlocker keys
Browse files Browse the repository at this point in the history
  • Loading branch information
schorschii committed Aug 6, 2024
1 parent 1ae51ae commit c9f4ea7
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 101 deletions.
7 changes: 5 additions & 2 deletions laps-client/README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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 (`[email protected]`). 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.
Expand Down
1 change: 1 addition & 0 deletions laps-client/laps-client-settings.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
97 changes: 53 additions & 44 deletions laps-client/laps_client/laps_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
120 changes: 65 additions & 55 deletions laps-client/laps_client/laps_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down

0 comments on commit c9f4ea7

Please sign in to comment.