11from __future__ import annotations
22
3+ import stat
34from typing import TYPE_CHECKING
45
56from dissect .util .ts import from_unix
89from dissect .target .filesystem import FilesystemEntry , LayerFilesystemEntry
910from dissect .target .helpers .record import TargetRecordDescriptor
1011from dissect .target .plugin import Plugin , arg , export
12+ from dissect .target .plugins .filesystem .unix .capability import parse_entry as parse_capability_entry
1113
1214if TYPE_CHECKING :
1315 from collections .abc import Iterator
2729 ("uint32" , "mode" ),
2830 ("uint32" , "uid" ),
2931 ("uint32" , "gid" ),
30- ("string[]" , "fstypes" ),
32+ ("boolean" , "is_suid" ),
33+ ("string[]" , "attr" ),
34+ ("string[]" , "fs_types" ),
3135 ],
3236)
3337
3438
35- class WalkFSPlugin (Plugin ):
39+ class WalkFsPlugin (Plugin ):
3640 """Filesystem agnostic walkfs plugin."""
3741
3842 def check_compatible (self ) -> None :
@@ -41,8 +45,39 @@ def check_compatible(self) -> None:
4145
4246 @export (record = FilesystemRecord )
4347 @arg ("--walkfs-path" , default = "/" , help = "path to recursively walk" )
44- def walkfs (self , walkfs_path : str = "/" ) -> Iterator [FilesystemRecord ]:
45- """Walk a target's filesystem and return all filesystem entries."""
48+ @arg ("--capability" , action = "store_true" , help = "output capability records" )
49+ def walkfs (self , walkfs_path : str = "/" , capability : bool = False ) -> Iterator [FilesystemRecord ]:
50+ """Walk a target's filesystem and return all filesystem entries.
51+
52+ References:
53+ - https://man7.org/linux/man-pages/man2/lstat.2.html
54+ - https://man7.org/linux/man-pages/man7/inode.7.html
55+ - https://man7.org/linux/man-pages/man7/xattr.7.html
56+ - https://man7.org/linux/man-pages/man2/execve.2.html
57+ - https://steflan-security.com/linux-privilege-escalation-suid-binaries
58+ - https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
59+
60+ Yields FilesystemRecords for every filesystem entry and CapabilityRecords if ``xattr`` security
61+ attributes were found in the filesystem entry and the ``--capability`` flag is set.
62+
63+ .. code-block:: text
64+
65+ hostname (string): The target hostname.
66+ domain (string): The target domain.
67+ mtime (datetime): modified timestamp indicates the last time the contents of a file were modified.
68+ atime (datetime): access timestamp indicates the last time a file was accessed.
69+ ctime (datetime): changed timestamp indicates the last time metadata of a file was modified.
70+ btime (datetime): birth timestamp indicates the time when a file was created.
71+ ino (varint): number of the corresponding underlying filesystem inode.
72+ path (path): path location of the entry.
73+ size (filesize): size of the file in bytes on the filesystem.
74+ mode (uint32): contains the file type and mode.
75+ uid (uint32): the user id of the owner of the entry.
76+ gid (uint32): the group id of the owner of the entry.
77+ is_suid (boolean): denotes if the entry has the set-user-id bit set.
78+ attr (string[]): list of key-value pair attributes separated by '='.
79+ fs_types (string[]): list of filesystem type(s) of the entry.
80+ """
4681
4782 path = self .target .fs .path (walkfs_path )
4883
@@ -56,45 +91,64 @@ def walkfs(self, walkfs_path: str = "/") -> Iterator[FilesystemRecord]:
5691
5792 for entry in self .target .fs .recurse (walkfs_path ):
5893 try :
59- yield generate_record (self .target , entry )
94+ yield from generate_record (self .target , entry , capability )
6095
6196 except FileNotFoundError as e : # noqa: PERF203
6297 self .target .log .warning ("File not found: %s" , entry )
6398 self .target .log .debug ("" , exc_info = e )
6499 except Exception as e :
65- self .target .log .warning ("Exception generating record for: %s" , entry )
100+ self .target .log .warning ("Exception generating walkfs record for %s : %s" , entry , e )
66101 self .target .log .debug ("" , exc_info = e )
67102 continue
68103
69104
70- def generate_record (target : Target , entry : FilesystemEntry ) -> FilesystemRecord :
71- """Generate a :class:`FilesystemRecord ` from the given :class:`FilesystemEntry`.
105+ def generate_record (target : Target , entry : FilesystemEntry , capability : bool ) -> Iterator [ FilesystemRecord ] :
106+ """Generate a :class:`WalkFsRecord ` from the given :class:`FilesystemEntry`.
72107
73108 Args:
74109 target: :class:`Target` instance
75110 entry: :class:`FilesystemEntry` instance
76111
77112 Returns:
78- Generated :class:`FilesystemRecord` for the given :class:`FilesystemEntry`.
113+ Generator of :class:`FilesystemRecord` for the given :class:`FilesystemEntry`.
79114 """
80- stat = entry .lstat ()
115+ entry_stat = entry .lstat ()
81116
82117 if isinstance (entry , LayerFilesystemEntry ):
83118 fs_types = [sub_entry .fs .__type__ for sub_entry in entry .entries ]
84119 else :
85120 fs_types = [entry .fs .__type__ ]
86121
87- return FilesystemRecord (
88- atime = from_unix (stat .st_atime ),
89- mtime = from_unix (stat .st_mtime ),
90- ctime = from_unix (stat .st_ctime ),
91- btime = from_unix (stat .st_birthtime ) if stat .st_birthtime else None ,
92- ino = stat .st_ino ,
93- path = entry .path ,
94- size = stat .st_size ,
95- mode = stat .st_mode ,
96- uid = stat .st_uid ,
97- gid = stat .st_gid ,
98- fstypes = fs_types ,
122+ fields = {
123+ "atime" : from_unix (entry_stat .st_atime ),
124+ "mtime" : from_unix (entry_stat .st_mtime ),
125+ "ctime" : from_unix (entry_stat .st_ctime ),
126+ "btime" : from_unix (entry_stat .st_birthtime ) if entry_stat .st_birthtime else None ,
127+ "ino" : entry_stat .st_ino ,
128+ "path" : entry .path ,
129+ "size" : entry_stat .st_size ,
130+ "mode" : entry_stat .st_mode ,
131+ "uid" : entry_stat .st_uid ,
132+ "gid" : entry_stat .st_gid ,
133+ "is_suid" : bool (entry_stat .st_mode & stat .S_ISUID ),
134+ "fs_types" : fs_types ,
135+ }
136+
137+ try :
138+ fields ["attr" ] = [f"{ attr .name } ={ attr .value .hex ()} " for attr in entry .lattr ()]
139+
140+ except (TypeError , AttributeError , NotImplementedError ):
141+ # Suppress lattr calls on VirtualDirectory entries, filesystems without implemented attr's and NTFS attr's.
142+ pass
143+
144+ except Exception as e :
145+ target .log .warning ("Unable to expand xattr for entry %s: %s" , entry .path , e )
146+ target .log .debug ("" , exc_info = e )
147+
148+ yield FilesystemRecord (
149+ ** fields ,
99150 _target = target ,
100151 )
152+
153+ if capability and fields .get ("attr" ):
154+ yield from parse_capability_entry (entry , target )
0 commit comments