NOTE: Requires velociraptor 0.7.1 or higher. – Alternatively, import the artifact dependency Linux.Sys.Groups manually into your installation.
This artifacts checks a number of files and directories to verify whether they have the expected owner, group owner and mode. A file with an incorrect owner may allow attackers to modify important system files. Similarly, incorrect mode, like word-writable configuration or passwd/shadow files may also be signs of serious misconfiguration or signs of malicious activity.
The parameter FilesToInspect contains lines of globs to search for. Each line may specify expected user, expected group, expected file mode and expected directory mode. It is very important that the order of the lines is in order of increasing specificity. For example:
/etc/*,root,root,644,755
/etc/?shadow*,,shadow,640,
Here, every file in the directory /etc is expected to be owned by root:root and have file permissions set to 644, and group permissions set to 755. The files under /etc matching “?shadow*” are still expected to be owned by root, since the override for user is empty, but the group is no longer expected to be “root”, but “shadow”. File permissions should be “640” instead of “644”. Note that this is an example and will most likely return hits, since a number of files in /etc have different owners and modes.
“User” may either be an integer (UID) or a string (username). “Group” may be either an integer (GID) or a string (group name). The names for all UIDs and GIDs are looked up and displayed along with their IDs in the result.
Modes may be specified in either octal numbers or strings.
Modes specified in octal numbers, e.g. 755, 640, 1777, are matched using a regular expression, so that both “0640”, “640” and “100640” matches “100640”. An implicit anchor, ‘$’, is used to match against the end of the octal mode string.
When using mode strings (NumericMode unchecked), modes take the format “-rwx-r-x-r-x”. Regex comparison is used, and an implicit ‘$’ anchor is inserted at the end of the string. String modes allows for verifying only certain bits of permissions, like ensuring that only the owner has write access, no one has permission to execute, but read access is not important: “r.-.–.–”. Or ensuring that SUID/GUID is not set. For finding files specifically with SUID set, look at Linux.Sys.SUID.
Mixing both formats is not supported and will result in unexpected results.
This artifact can also be used to look for all files owned by root with world-writable permissions, for instance. Uncheck NumericMode, add a glob, select “root” as owner and enter any invalid permission string in UserMode. This will return every file owned by root. In the notebook, add something like “WHERE Mode=~‘w.$’” to the query. The User field may also be empty, essentially returning every file in the glob as long as the UserMode field contains an invalid value. This turns this artifact into a file finder-like tool with metadata like username and group names for further processing.
The following columns are returned:
Note that the artifacts used to look up usernames and group names use the
files /etc/passwd and /etc/group. You will have to modify this artifact to
use getent passwd
/getent group
to use NSS and get users and groups from
Active Directory etc.
The provided default values in FilesToInspect is an example only.
name: Linux.Detection.IncorrectPermissions
author: Andreas Misje – @misje
description: |
NOTE: Requires velociraptor 0.7.1 or higher. – Alternatively, import the
artifact dependency Linux.Sys.Groups manually into your installation.
This artifacts checks a number of files and directories to verify whether
they have the expected owner, group owner and mode. A file with an incorrect
owner may allow attackers to modify important system files. Similarly,
incorrect mode, like word-writable configuration or passwd/shadow files may
also be signs of serious misconfiguration or signs of malicious activity.
The parameter FilesToInspect contains lines of globs to search for. Each
line may specify expected user, expected group, expected file mode and
expected directory mode. It is very important that the order of the lines
is in order of increasing specificity. For example:
```csv
/etc/*,root,root,644,755
/etc/?shadow*,,shadow,640,
```
Here, every file in the directory /etc is expected to be owned by root:root
and have file permissions set to 644, and group permissions set to 755.
The files under /etc matching "?shadow*" are still expected to be owned by
root, since the override for user is empty, but the group is no longer
expected to be "root", but "shadow". File permissions should be "640"
instead of "644". Note that this is an example and will most likely return
hits, since a number of files in /etc have different owners and modes.
"User" may either be an integer (UID) or a string (username). "Group"
may be either an integer (GID) or a string (group name). The names for all
UIDs and GIDs are looked up and displayed along with their IDs in the result.
Modes may be specified in either octal numbers or strings.
Modes specified in octal numbers, e.g. 755, 640, 1777, are matched
using a regular expression, so that both "0640", "640" and "100640" matches
"100640". An implicit anchor, '$', is used to match against the end of the
octal mode string.
When using mode strings (NumericMode unchecked), modes take the format
"-rwx-r-x-r-x". Regex comparison is used, and an implicit '$' anchor
is inserted at the end of the string. String modes allows for verifying only
certain bits of permissions, like ensuring that only the owner has write
access, no one has permission to execute, but read access is not important:
"r.-.--.--". Or ensuring that SUID/GUID is not set. For finding files
specifically with SUID set, look at Linux.Sys.SUID.
Mixing both formats is not supported and will result in unexpected results.
This artifact can also be used to look for all files owned by root with
world-writable permissions, for instance. Uncheck NumericMode, add a glob,
select "root" as owner and enter any invalid permission string in UserMode.
This will return every file owned by root. In the notebook, add something
like "WHERE Mode=~'w.$'" to the query. The User field may also be empty,
essentially returning every file in the glob as long as the UserMode field
contains an invalid value. This turns this artifact into a file finder-like
tool with metadata like username and group names for further processing.
The following columns are returned:
- OSPath
- IsDir
- UID
- User
- EUser (expected user from FilesToInspect)
- GID
- Group
- EGroup (expected group from FilesToInspect)
- Mode (file/directory mode/permissions)
- EMode (expected file/directory mode from FilesToInspect)
- Mismatch (a comma-separated string of one or several of "uid", "gid" and "mode")
- Mtime
- Ctime
- Atime
Note that the artifacts used to look up usernames and group names use the
files /etc/passwd and /etc/group. You will have to modify this artifact to
use `getent passwd`/`getent group` to use NSS and get users and groups from
Active Directory etc.
The provided default values in FilesToInspect is an example only.
parameters:
- name: FilesToInspect
type: csv
default: |
Globs,User,Group,FileMode,DirMode
/etc/passwd?,root,root,644,
/etc/?shadow?,root,shadow,640,
/etc/group?,root,root,,
description: The files to investigate. The default is just an example.
- name: NumericMode
type: bool
default: true
description: |
Whether modes should be interpreted, compared and presented as octal
numbers (e.g. 640) rather than strings (e.g. -rw-rw-r--)
- name: IncludeDirs
type: bool
description: Include directories
- name: FollowLinks
type: bool
description: Inlcude all symlinks, even though they may interfere with the results
precondition: |
SELECT OS From info() WHERE OS = 'linux'
sources:
- name: Discrepancies
query: |
/* Passwd/group will be looked up a lot. Make it efficient: */
LET Users <= memoize(query={
SELECT int(int=Uid) AS UID, User FROM Artifact.Linux.Sys.Users()
}, key='UID')
LET Groups <= memoize(query={
SELECT GID, Group FROM Artifact.Linux.Sys.Groups()
}, key='GID')
LET FindUser(uid) = get(item=Users, field=uid).User
LET FindGroup(gid) = get(item=Groups, field=gid).Group
LET StrIf(str) = if(condition=str, then=str(str=str))
LET Files = SELECT OSPath, IsDir, ModeString,
/* If the globs are specified in the correct order, picking the
last item will get a correct override behaviour: */
/* Filter out all empty strings and keep integers: */
filter(list=enumerate(items=User), regex='.', condition='x=>true')[-1] AS EUser,
filter(list=enumerate(items=Group), regex='.', condition='x=>true')[-1] AS EGroup,
filter(list=enumerate(items=FileMode), regex='.', condition='x=>true')[-1] AS FMode,
filter(list=enumerate(items=DirMode), regex='.', condition='x=>true')[-1] AS DMode
/* globs() can of course take a list of globs, like FilesToInspect.Globs,
but by using that approach, we would no longer be able to tie
User, Group etc. to the individual globs: */
FROM foreach(row={
SELECT Globs AS _Globs, * FROM FilesToInspect
}, query={
SELECT OSPath, IsDir, User, Group, FileMode, DirMode,
Mode.String AS ModeString
FROM glob(globs=_Globs)
WHERE (IncludeDirs OR NOT IsDir) AND (NOT IsLink OR FollowLinks)
})
GROUP BY OSPath
LET FilesInfo = SELECT * FROM foreach(row=Files, async=true, query={
SELECT OSPath, Sys.Uid AS UID, Sys.Gid AS GID, if(condition=NumericMode,
then=format(format='%o', args=[Sys.Mode]), else=ModeString)
AS Mode, Mtime, Atime, Ctime, EUser, EGroup, log(message='%v, %v, %v', args=[Sys.Mode, ModeString, FMode]), StrIf(str=FMode) AS FMode,
StrIf(str=DMode) AS DMode, IsDir
FROM stat(filename=OSPath)
})
GROUP BY OSPath
LET _UIDFiltered = SELECT OSPath, IsDir,
UID,
EUser,
GID, FindGroup(gid=GID) AS Group,
null AS EGroup,
'uid' AS Mismatch,
Mode,
null AS EMode,
Mtime, Atime, Ctime,
get(item=Users, field=UID) AS _u
FROM FilesInfo
WHERE EUser AND _u.UID=UID AND
/* User can be either an ID or a string: */
if(condition=EUser=~'^\\d+$', then=EUser!=UID,
else=EUser!=_u.User)
LET _GIDFiltered = SELECT OSPath, IsDir,
UID, FindUser(uid=UID) AS User,
EUser,
GID,
EGroup,
'gid' AS Mismatch,
Mode,
null AS EMode,
Mtime, Atime, Ctime,
get(item=Groups, field=GID) AS _g
FROM FilesInfo
WHERE EGroup AND _g.GID=GID AND
if(condition=EGroup=~'^\\d+$', then=EGroup!=GID,
else=EGroup!=_g.Group)
LET _FileModeFiltered = SELECT OSPath, IsDir,
UID, FindUser(uid=UID) AS User,
EUser,
GID, FindGroup(gid=GID) AS Group,
EGroup,
'mode' AS Mismatch,
Mode,
FMode AS EMode,
Mtime, Atime, Ctime
FROM FilesInfo
WHERE NOT IsDir AND FMode AND NOT Mode=~FMode+'$'
LET _DirModeFiltered = SELECT OSPath, IsDir,
UID, FindUser(uid=UID) AS User,
EUser,
GID, FindGroup(gid=GID) AS Group,
EGroup,
'mode' AS Mismatch,
Mode,
DMode AS EMode,
Mtime, Atime, Ctime
FROM FilesInfo
WHERE IsDir AND DMode AND NOT Mode=~DMode+'$'
SELECT OSPath, IsDir, UID, User, EUser, GID, Group, EGroup, Mode, EMode,
join(array=enumerate(items=Mismatch), sep=',') AS Mismatch,
Mtime, Atime, Ctime
FROM chain(a={
SELECT OSPath, IsDir, UID, _u.User AS User, EUser,
GID, Group, EGroup, Mode, EMode,
join(array=enumerate(items=Mismatch), sep=',') AS Mismatch,
Mtime, Atime, Ctime
FROM _UIDFiltered
GROUP BY OSPath
}, b={
SELECT OSPath, IsDir, UID, User, EUser, GID, _g.Group AS Group,
EGroup, Mode, EMode,
join(array=enumerate(items=Mismatch), sep=',') AS Mismatch,
Mtime, Atime, Ctime
FROM _GIDFiltered
GROUP BY OSPath
}, c={
SELECT OSPath, IsDir, UID, User, EUser, GID, Group, EGroup, Mode, EMode,
join(array=enumerate(items=Mismatch), sep=',') AS Mismatch,
Mtime, Atime, Ctime
FROM _FileModeFiltered
GROUP BY OSPath
}, d={
SELECT OSPath, IsDir, UID, User, EUser, GID, Group, EGroup, Mode, EMode,
join(array=enumerate(items=Mismatch), sep=',') AS Mismatch,
Mtime, Atime, Ctime
FROM _DirModeFiltered
GROUP BY OSPath
})
GROUP BY OSPath
ORDER BY OSPath