Parse and collect the SUM database
UAL is a feature that can help server administrators quantify the number of unique client requests of roles and services on a local server.
The UAL only exists on Windows Server edition 2012 and above.
NOTE: Unlike other tools, Velociraptor DOES NOT use the JET API to
access the database because it has a builtin ESE parser. This means
that you do not need to repair the files using eseutil.exe
as
is commonly explained in the references below. Velociraptor should
have no trouble parsing these files on the live system.
name: Windows.Forensics.UserAccessLogs
description: |
Parse and collect the SUM database
UAL is a feature that can help server administrators quantify the
number of unique client requests of roles and services on a local
server.
The UAL only exists on Windows Server edition 2012 and above.
NOTE: Unlike other tools, Velociraptor DOES NOT use the JET API to
access the database because it has a builtin ESE parser. This means
that you **do not need to repair** the files using `eseutil.exe` as
is commonly explained in the references below. Velociraptor should
have no trouble parsing these files on the live system.
reference:
- https://advisory.kpmg.us/blog/2021/digital-forensics-incident-response.html
- https://docs.microsoft.com/en-us/windows-server/administration/user-access-logging/manage-user-access-logging
- https://www.crowdstrike.com/blog/user-access-logging-ual-overview/
export: |
LET IPProfile = '''[
["IP4", 0, [
["A", 0, "uint8"],
["B", 1, "uint8"],
["C", 2, "uint8"],
["D", 3, "uint8"],
["IP", 0, "Value", {
value: "x=> format(format='%d.%d.%d.%d', args=[x.A, x.B, x.C, x.D])"
}]
]],
["IP6", 0, [
["A", 0, "uint16be"],
["B", 2, "uint16be"],
["C", 4, "uint16be"],
["D", 6, "uint16be"],
["E", 8, "uint16be"],
["F", 10, "uint16be"],
["G", 12, "uint16be"],
["H", 14, "uint16be"],
["IP", 0, "Value", {
value: "x=> format(format='%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x', args=[x.A, x.B, x.C, x.D, x.E, x.F, x.G, x.H])"
}]
]]
]'''
-- Format the address - it can be IPv4, IPv6 or something else.
LET FormatAddress(Address) = if(condition=len(list=Address) = 4,
-- IPv4 address should be formatted in dot notation
then=parse_binary(accessor="data",
filename=Address, struct="IP4",
profile=IPProfile).IP,
else=if(condition=len(list=Address)=16,
-- IPv6 addresses are usually shortened
then=parse_binary(accessor="data",
filename=Address, struct="IP6",
profile=IPProfile).IP,
-- We dont know what kind of address it is.
else=format(format="%x", args=Address)))
-- Get the Clients table from all snapshot files.
LET SystemIdentity = SELECT OSPath FROM glob(globs=SUMGlob)
WHERE Name =~ "SystemIdentity.mdb"
-- Prepare a Role lookup to resolve the role GUID
LET RoleLookup <= memoize(key="RoleGuid", query={
SELECT * FROM foreach(row=SystemIdentity, query={
SELECT * FROM parse_ese(file=OSPath, table="ROLE_IDS")
WHERE log(message="RoleGuid " + RoleGuid)
})
})
parameters:
- name: SUMGlob
type: glob
default: C:/Windows/System32/LogFiles/Sum/*
description: A glob to file all SUM ESE databases on the system.
- name: AlsoUpload
type: bool
description: If set we also upload the raw files.
sources:
- name: SystemIdentity
description: Parse the SystemIdentity database.
query: |
SELECT * FROM foreach(row=SystemIdentity, query={
SELECT *, OSPath AS _OSPath
FROM parse_ese(file=OSPath, table="SYSTEM_IDENTITY")
})
- name: Chained Databases
query: |
SELECT * FROM foreach(row=SystemIdentity, query={
SELECT *, OSPath AS _OSPath
FROM parse_ese(file=OSPath, table="CHAINED_DATABASES")
})
- name: RoleIds
query: |
SELECT * FROM foreach(row=SystemIdentity, query={
SELECT *, OSPath AS _OSPath
FROM parse_ese(file=OSPath, table="ROLE_IDS")
})
- name: Clients
description: Dump the clients database from all ESE files
query: |
LET ContentDatabases = SELECT * FROM glob(globs=SUMGlob)
WHERE Name =~ ".mdb" AND NOT Name =~ "SystemIdentity"
-- The clients table has potentially 365 columns (1 per day) so we
-- format it a bit better by putting the Day* columns in their own dict.
LET GetClients(OSPath) = SELECT *, OSPath AS _OSPath
FROM foreach(row={
SELECT to_dict(item={
SELECT _key, _value FROM items(item=_value)
WHERE NOT _key =~ "Day"
}) +
dict(Days=to_dict(item={
SELECT _key, _value FROM items(item=_value)
WHERE _key =~ "Day"
})) AS Value
FROM items(item={
SELECT *, get(item=RoleLookup, field=RoleGuid).RoleName AS RoleName,
Address AS RawAddress,
FormatAddress(Address=unhex(string=Address)) AS Address
FROM parse_ese(file=OSPath, table="CLIENTS")
})
}, column="Value")
-- Get the Clients table from all snapshot files.
SELECT * FROM foreach(row=ContentDatabases, query={
SELECT * FROM GetClients(OSPath=OSPath)
})
- name: VIRTUALMACHINES
query: |
SELECT * FROM foreach(row=ContentDatabases, query={
SELECT *, OSPath AS _OSPath
FROM parse_ese(file=OSPath, table="VIRTUALMACHINES")
})
- name: DNS
query: |
SELECT * FROM foreach(row=ContentDatabases, query={
SELECT *, OSPath AS _OSPath
FROM parse_ese(file=OSPath, table="DNS")
})
- name: Uploads
query: |
SELECT OSPath, if(condition=AlsoUpload, then=upload(file=OSPath))
FROM glob(globs=SUMGlob)