266 lines
6.3 KiB
Go
266 lines
6.3 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
filesystem.go
|
|
|
|
In-memory fake filesystem for the SSH shell environment
|
|
|
|
Presents a realistic Ubuntu server filesystem with populated
|
|
/etc, /proc, /var, /home directories. Each file has plausible
|
|
contents that engaged attackers would expect to see after
|
|
compromising a Linux host.
|
|
*/
|
|
|
|
package sshd
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type fileEntry struct {
|
|
name string
|
|
isDir bool
|
|
content string
|
|
mode string
|
|
}
|
|
|
|
type FakeFS struct {
|
|
hostname string
|
|
files map[string]*fileEntry
|
|
}
|
|
|
|
func NewFakeFS(hostname string) *FakeFS {
|
|
fs := &FakeFS{
|
|
hostname: hostname,
|
|
files: make(map[string]*fileEntry),
|
|
}
|
|
fs.populate()
|
|
return fs
|
|
}
|
|
|
|
func (fs *FakeFS) populate() {
|
|
dirs := []string{
|
|
"/", "/bin", "/boot", "/dev", "/etc",
|
|
"/etc/ssh", "/etc/cron.d", "/etc/default",
|
|
"/home", "/home/admin", "/lib", "/lib64",
|
|
"/media", "/mnt", "/opt", "/proc",
|
|
"/root", "/run", "/sbin", "/srv",
|
|
"/sys", "/tmp", "/usr", "/usr/bin",
|
|
"/usr/lib", "/usr/local", "/usr/sbin",
|
|
"/var", "/var/log", "/var/lib", "/var/tmp",
|
|
"/var/run", "/var/spool",
|
|
}
|
|
|
|
for _, d := range dirs {
|
|
fs.files[d] = &fileEntry{
|
|
name: d,
|
|
isDir: true,
|
|
mode: "drwxr-xr-x",
|
|
}
|
|
}
|
|
|
|
fs.files["/etc/hostname"] = &fileEntry{
|
|
name: "hostname",
|
|
content: fs.hostname + "\n",
|
|
mode: "-rw-r--r--",
|
|
}
|
|
|
|
fs.files["/etc/passwd"] = &fileEntry{
|
|
name: "passwd",
|
|
content: "root:x:0:0:root:/root:/bin/bash\n" +
|
|
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n" +
|
|
"bin:x:2:2:bin:/bin:/usr/sbin/nologin\n" +
|
|
"sys:x:3:3:sys:/dev:/usr/sbin/nologin\n" +
|
|
"sync:x:4:65534:sync:/bin:/bin/sync\n" +
|
|
"games:x:5:60:games:/usr/games:/usr/sbin/nologin\n" +
|
|
"man:x:6:12:man:/var/cache/man:/usr/sbin/nologin\n" +
|
|
"lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\n" +
|
|
"mail:x:8:8:mail:/var/mail:/usr/sbin/nologin\n" +
|
|
"news:x:9:9:news:/var/spool/news:/usr/sbin/nologin\n" +
|
|
"uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\n" +
|
|
"proxy:x:13:13:proxy:/bin:/usr/sbin/nologin\n" +
|
|
"www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\n" +
|
|
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n" +
|
|
"sshd:x:110:65534::/run/sshd:/usr/sbin/nologin\n" +
|
|
"admin:x:1000:1000:admin:/home/admin:/bin/bash\n",
|
|
mode: "-rw-r--r--",
|
|
}
|
|
|
|
fs.files["/etc/shadow"] = &fileEntry{
|
|
name: "shadow",
|
|
content: "",
|
|
mode: "-rw-r-----",
|
|
}
|
|
|
|
fs.files["/etc/os-release"] = &fileEntry{
|
|
name: "os-release",
|
|
content: "PRETTY_NAME=\"Ubuntu 22.04.4 LTS\"\n" +
|
|
"NAME=\"Ubuntu\"\n" +
|
|
"VERSION_ID=\"22.04\"\n" +
|
|
"VERSION=\"22.04.4 LTS (Jammy Jellyfish)\"\n" +
|
|
"VERSION_CODENAME=jammy\n" +
|
|
"ID=ubuntu\n" +
|
|
"ID_LIKE=debian\n",
|
|
mode: "-rw-r--r--",
|
|
}
|
|
|
|
fs.files["/etc/issue"] = &fileEntry{
|
|
name: "issue",
|
|
content: "Ubuntu 22.04.4 LTS \\n \\l\n\n",
|
|
mode: "-rw-r--r--",
|
|
}
|
|
|
|
fs.files["/proc/version"] = &fileEntry{
|
|
name: "version",
|
|
content: "Linux version 5.15.0-105-generic " +
|
|
"(buildd@lcy02-amd64-032) " +
|
|
"(gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, " +
|
|
"GNU ld (GNU Binutils for Ubuntu) 2.38) " +
|
|
"#115-Ubuntu SMP Mon Apr 15 09:52:04 UTC 2024\n",
|
|
mode: "-r--r--r--",
|
|
}
|
|
|
|
fs.files["/proc/cpuinfo"] = &fileEntry{
|
|
name: "cpuinfo",
|
|
content: "processor\t: 0\n" +
|
|
"vendor_id\t: GenuineIntel\n" +
|
|
"cpu family\t: 6\n" +
|
|
"model\t\t: 85\n" +
|
|
"model name\t: Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz\n" +
|
|
"stepping\t: 4\n" +
|
|
"microcode\t: 0x2007006\n" +
|
|
"cpu MHz\t\t: 2500.000\n" +
|
|
"cache size\t: 33792 KB\n" +
|
|
"physical id\t: 0\n" +
|
|
"siblings\t: 2\n" +
|
|
"core id\t\t: 0\n" +
|
|
"cpu cores\t: 2\n" +
|
|
"bogomips\t: 5000.00\n" +
|
|
"flags\t\t: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor\n\n",
|
|
mode: "-r--r--r--",
|
|
}
|
|
|
|
fs.files["/proc/meminfo"] = &fileEntry{
|
|
name: "meminfo",
|
|
content: "MemTotal: 4028440 kB\n" +
|
|
"MemFree: 1245680 kB\n" +
|
|
"MemAvailable: 2876340 kB\n" +
|
|
"Buffers: 186420 kB\n" +
|
|
"Cached: 1580240 kB\n" +
|
|
"SwapCached: 0 kB\n" +
|
|
"SwapTotal: 2097148 kB\n" +
|
|
"SwapFree: 2097148 kB\n",
|
|
mode: "-r--r--r--",
|
|
}
|
|
|
|
fs.files["/etc/ssh/sshd_config"] = &fileEntry{
|
|
name: "sshd_config",
|
|
content: "Port 22\n" +
|
|
"PermitRootLogin yes\n" +
|
|
"PubkeyAuthentication yes\n" +
|
|
"PasswordAuthentication yes\n" +
|
|
"UsePAM yes\n" +
|
|
"X11Forwarding yes\n" +
|
|
"PrintMotd no\n" +
|
|
"AcceptEnv LANG LC_*\n" +
|
|
"Subsystem\tsftp\t/usr/lib/openssh/sftp-server\n",
|
|
mode: "-rw-r--r--",
|
|
}
|
|
}
|
|
|
|
func (fs *FakeFS) ReadFile(path string) (string, bool) {
|
|
entry, exists := fs.files[path]
|
|
if !exists || entry.isDir {
|
|
return "", false
|
|
}
|
|
return entry.content, true
|
|
}
|
|
|
|
func (fs *FakeFS) IsDir(path string) bool {
|
|
entry, exists := fs.files[path]
|
|
return exists && entry.isDir
|
|
}
|
|
|
|
func (fs *FakeFS) Exists(path string) bool {
|
|
_, exists := fs.files[path]
|
|
return exists
|
|
}
|
|
|
|
func (fs *FakeFS) ListDir(path string) string {
|
|
if !fs.IsDir(path) {
|
|
return ""
|
|
}
|
|
|
|
prefix := path
|
|
if prefix != "/" {
|
|
prefix += "/"
|
|
}
|
|
|
|
type namedEntry struct {
|
|
name string
|
|
formatted string
|
|
}
|
|
|
|
var collected []namedEntry
|
|
seen := make(map[string]bool)
|
|
|
|
for p, entry := range fs.files {
|
|
if p == path {
|
|
continue
|
|
}
|
|
|
|
if !strings.HasPrefix(p, prefix) {
|
|
continue
|
|
}
|
|
|
|
rest := strings.TrimPrefix(p, prefix)
|
|
parts := strings.SplitN(rest, "/", 2)
|
|
name := parts[0]
|
|
|
|
if seen[name] || len(parts) != 1 {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
|
|
var line string
|
|
if entry.isDir {
|
|
line = fmt.Sprintf(
|
|
"%s 2 root root 4096 %s %s",
|
|
entry.mode,
|
|
time.Now().AddDate(0, -1, 0).Format("Jan 2 15:04"),
|
|
name,
|
|
)
|
|
} else {
|
|
line = fmt.Sprintf(
|
|
"%s 1 root root %4d %s %s",
|
|
entry.mode,
|
|
len(entry.content),
|
|
time.Now().AddDate(0, 0, -7).Format("Jan 2 15:04"),
|
|
name,
|
|
)
|
|
}
|
|
collected = append(collected, namedEntry{name: name, formatted: line})
|
|
}
|
|
|
|
if len(collected) == 0 {
|
|
return ""
|
|
}
|
|
|
|
sort.Slice(collected, func(i, j int) bool {
|
|
return collected[i].name < collected[j].name
|
|
})
|
|
|
|
entries := make([]string, len(collected))
|
|
for i, ne := range collected {
|
|
entries[i] = ne.formatted
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"total %d\n%s\n",
|
|
len(entries)*4,
|
|
strings.Join(entries, "\n"),
|
|
)
|
|
}
|