├── LICENSE ├── README.md ├── amicontained.go ├── caps.go ├── cgroups.go ├── main.go ├── misc.go ├── mounts.go └── network.go /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 The ConMachi Authors and NCCGroup 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conmachi Container Scanner 2 | ## Is It Wrong To Pick Up Capabilities In A Container? 3 | 4 | Conmachi is a tool written in Golang intended to be used to collect information about a container environment and list potential security issues. It can be statically compiled so that it can be dropped into a container environment and run without any dependencies. 5 | 6 | ## Building 7 | 8 | Dependencies: 9 | 10 | ~~~ 11 | sudo apt-get install libpcap-dev 12 | ~~~ 13 | 14 | Conmachi is intended to be built on any LTS version of Ubuntu. In development, it has been built with Go version 1.9 and above but should compile with most versions. 15 | 16 | You can statically build the tool with the following commands: 17 | 18 | ~~~ 19 | go get github.com/nccgroup/ConMachi 20 | go get ./... 21 | cd $GOPATH/bin 22 | 23 | # Dynamically compile 24 | CGO_LDFLAGS="/usr/lib/x86_64-linux-gnu/libpcap.a" go build conmachi 25 | # OR statically compile, may cause issues with network scanning 26 | CGO_LDFLAGS="/usr/lib/x86_64-linux-gnu/libpcap.a" go build -ldflags '-w -extldflags "-static"' conmachi 27 | ~~~ 28 | 29 | ## What it checks for 30 | 31 | Conmachi scans for a large number of potential issues including: 32 | 33 | * Disabled process and user namespacing 34 | * Dangerous capabilities 35 | * Disabled Seccomp/AppArmor profiles 36 | * Devices mounted from the host 37 | 38 | It also collects information that may be useful while exploring a container including: 39 | 40 | * Kernel version 41 | * All capability sets with decoded values 42 | * Detect container solution 43 | * Sniff network interface for other hosts 44 | 45 | Currently in progress: 46 | 47 | * Scan for Kubernetes related issues 48 | * Scan for cloud provider related issues 49 | 50 | ## Flags 51 | 52 | ~~~ 53 | Usage of ./conmachi: 54 | -l string 55 | File to log to 56 | -sniff int 57 | Sniff the network for IP addresses for a given number of seconds 58 | ~~~ 59 | 60 | ## Example 61 | 62 | Let's walk through running Conmachi in a Docker container. To start, we have our working directory with a copy of Conmachi in it: 63 | 64 | ~~~ 65 | $ ls 66 | conmachi 67 | ~~~ 68 | 69 | Then let's add a Dockerfile: 70 | 71 | ~~~ 72 | FROM ubuntu 73 | 74 | WORKDIR /example 75 | ADD . /example 76 | ~~~ 77 | 78 | And build it: 79 | 80 | ~~~ 81 | $ ls 82 | conmachi Dockerfile 83 | $ docker build -t example 84 | ~~~ 85 | 86 | And run it with a scary capability: 87 | 88 | ~~~ 89 | $ docker run --rm --security-opt seccomp=unconfined --security-opt apparmor:unconfined --cap-add SYS_ADMIN -it example 90 | root@ffffffffffff:/example# ./conmachi 91 | Container has dangerous capabilities 92 | ------------------------------------ 93 | * Container has capabilities in the CapBnd set which could enable network attacks if a process runs with uid=0: CAP_NET_RAW 94 | * Container has capabilities in the CapEff which could enable network attacks at the current privilege level: CAP_NET_RAW 95 | * Container has dangerous capabilities in the CapBnd set which may allow for container escape if a process runs with uid=0: CAP_SYS_ADMIN 96 | * Container has dangerous capabilities in the CapEff set which may allow for container escape if a process runs at the current privilege level: CAP_SYS_ADMIN 97 | 98 | 99 | Container has non-default Docker capabilities 100 | --------------------------------------------- 101 | Container has non-default Docker capabilities: CAP_SYS_ADMIN 102 | 103 | Writeable Mounts 104 | ---------------- 105 | Some paths are mounted with write permission and may be filesystems mounted from the host. If this is the case, modifying the files at these paths may allow a contained process to modify files outside of the contained environment. The following paths were detected to be mounted with write permissions: 106 | --- 107 | Device: /dev/sda1 108 | Path: /etc/resolv.conf 109 | Type: ext4 110 | Opts: [rw relatime data=ordered] 111 | Freq: 0 112 | Pass: 0 113 | --- 114 | Device: /dev/sda1 115 | Path: /etc/hostname 116 | Type: ext4 117 | Opts: [rw relatime data=ordered] 118 | Freq: 0 119 | Pass: 0 120 | --- 121 | Device: /dev/sda1 122 | Path: /etc/hosts 123 | Type: ext4 124 | Opts: [rw relatime data=ordered] 125 | Freq: 0 126 | Pass: 0 127 | --- 128 | 129 | 130 | User Namespace Disabled 131 | ----------------------- 132 | User namespacing is not enabled. As a result, if a contained process is running with uid=0 it will be running as a privileged user if it gains access to resources outside of the container. 133 | 134 | Container Running Unconfined AppArmor Profile 135 | --------------------------------------------- 136 | Container is not enforcing an AppArmor profile on contained processes 137 | 138 | Seccomp is Disabled 139 | ------------------- 140 | Seccomp is disabled in container 141 | 142 | Processor Vulnerable to Hardware Attacks 143 | ---------------------------------------- 144 | The following processors have bugs which may be exploited. More information about each of the processors can be found by reading /proc/cpuinfo. 145 | 146 | Processor 0 (Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz) bugs: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf 147 | Processor 1 (Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz) bugs: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf 148 | 149 | 150 | Container Capabilities 151 | ---------------------- 152 | CapInh (00000000a82425fb): CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_CHROOT, CAP_SYS_ADMIN, CAP_MKNOD, CAP_AUDIT_WRITE, CAP_SETFCAP, CAP_MAC_OVERRIDE, CAP_MAC_ADMIN, CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, CAP_AUDIT_READ 153 | CapPrm (00000000a82425fb): CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_CHROOT, CAP_SYS_ADMIN, CAP_MKNOD, CAP_AUDIT_WRITE, CAP_SETFCAP, CAP_MAC_OVERRIDE, CAP_MAC_ADMIN, CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, CAP_AUDIT_READ 154 | CapEff (00000000a82425fb): CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_CHROOT, CAP_SYS_ADMIN, CAP_MKNOD, CAP_AUDIT_WRITE, CAP_SETFCAP, CAP_MAC_OVERRIDE, CAP_MAC_ADMIN, CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, CAP_AUDIT_READ 155 | CapBnd (00000000a82425fb): CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_FSETID, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_CHROOT, CAP_SYS_ADMIN, CAP_MKNOD, CAP_AUDIT_WRITE, CAP_SETFCAP, CAP_MAC_OVERRIDE, CAP_MAC_ADMIN, CAP_WAKE_ALARM, CAP_BLOCK_SUSPEND, CAP_AUDIT_READ 156 | CapAmb (0000000000000000): 157 | 158 | 159 | Cgroup Policy Does Not Restrict CPU Usage 160 | ----------------------------------------- 161 | Processes in the conainer are capable of using excessive amounts of CPU time 162 | 163 | Cgroup Policy Allows for Excessive Memory Usage 164 | ----------------------------------------------- 165 | Processes in the conainer are capable of using excessive amounts (>8GB) of RAM 166 | 167 | Detected Container Runtime 168 | -------------------------- 169 | Container runtime: docker 170 | 171 | Kernel Version Info 172 | ------------------- 173 | Linux version 4.4.0-134-generic (buildd@lgw01-amd64-033) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) ) #160-Ubuntu SMP Wed Aug 15 14:58:00 UTC 2018 174 | 175 | ~~~ 176 | 177 | Cool! It caught our disabled profiles, our dangerous capability, and collected a bunch of info about the environment. 178 | -------------------------------------------------------------------------------- /amicontained.go: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 The Genuinetools Authors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "syscall" 30 | "strconv" 31 | "io/ioutil" 32 | "strings" 33 | "os" 34 | ) 35 | 36 | const ( 37 | // RuntimeDocker is the string for the docker runtime. 38 | RuntimeDocker = "docker" 39 | // RuntimeSingularity is the string for the Singularity runtime 40 | RuntimeSingularity = "singularity" 41 | // RuntimeRkt is the string for the rkt runtime. 42 | RuntimeRkt = "rkt" 43 | // RuntimeNspawn is the string for the systemd-nspawn runtime. 44 | RuntimeNspawn = "systemd-nspawn" 45 | // RuntimeLXC is the string for the lxc runtime. 46 | RuntimeLXC = "lxc" 47 | // RuntimeLXCLibvirt is the string for the lxc-libvirt runtime. 48 | RuntimeLXCLibvirt = "lxc-libvirt" 49 | // RuntimeOpenVZ is the string for the openvz runtime. 50 | RuntimeOpenVZ = "openvz" 51 | // RuntimeKubernetes is the string for the kubernetes runtime. 52 | RuntimeKubernetes = "kube" 53 | // RuntimeGarden is the string for the garden runtime. 54 | RuntimeGarden = "garden" 55 | // RuntimePodman is the string for the podman runtime. 56 | RuntimePodman = "podman" 57 | 58 | uint32Max = 4294967295 59 | ) 60 | 61 | 62 | func readFile(file string) string { 63 | if !fileExists(file) { 64 | return "" 65 | } 66 | 67 | b, err := ioutil.ReadFile(file) 68 | if err != nil { 69 | return "" 70 | } 71 | return strings.TrimSpace(string(b)) 72 | } 73 | 74 | func fileExists(file string) bool { 75 | if _, err := os.Stat(file); !os.IsNotExist(err) { 76 | return true 77 | } 78 | return false 79 | } 80 | 81 | func deleteEmpty(s []string) []string { 82 | var r []string 83 | for _, str := range s { 84 | if strings.TrimSpace(str) != "" { 85 | r = append(r, strings.TrimSpace(str)) 86 | } 87 | } 88 | return r 89 | } 90 | 91 | // Taken from https://github.com/genuinetools/amicontained/blob/c0168981b856dd8a81b02ed6ed81a8d67e37cfd2/container/container.go#L48 92 | // DetectRuntime returns the container runtime the process is running in. 93 | func DetectRuntime() (string, error) { 94 | runtimes := []string{RuntimeDocker, RuntimeSingularity, RuntimeRkt, RuntimeNspawn, RuntimeLXC, RuntimeLXCLibvirt, RuntimeOpenVZ, RuntimeKubernetes, RuntimeGarden, RuntimePodman} 95 | 96 | // read the cgroups file 97 | cgroups := readFile("/proc/self/cgroup") 98 | if len(cgroups) > 0 { 99 | for _, runtime := range runtimes { 100 | if strings.Contains(cgroups, runtime) { 101 | return runtime, nil 102 | } 103 | } 104 | } 105 | 106 | if fileExists("/.singularity.d") && fileExists("/singularity") { 107 | return RuntimeSingularity, nil 108 | } 109 | 110 | // /proc/vz exists in container and outside of the container, /proc/bc only outside of the container. 111 | if fileExists("/proc/vz") && !fileExists("/proc/bc") { 112 | return RuntimeOpenVZ, nil 113 | } 114 | 115 | ctrenv := os.Getenv("container") 116 | if ctrenv != "" { 117 | for _, runtime := range runtimes { 118 | if ctrenv == runtime { 119 | return runtime, nil 120 | } 121 | } 122 | } 123 | 124 | // PID 1 might have dropped this information into a file in /run. 125 | // Read from /run/systemd/container since it is better than accessing /proc/1/environ, 126 | // which needs CAP_SYS_PTRACE 127 | f := readFile("/run/systemd/container") 128 | if len(f) > 0 { 129 | for _, runtime := range runtimes { 130 | if f == runtime { 131 | return runtime, nil 132 | } 133 | } 134 | } 135 | 136 | return "not-found", nil 137 | } 138 | 139 | // HasNamespace determines if the container is using a particular namespace or the 140 | // host namespace. 141 | // The device number of an unnamespaced /proc/1/ns/{ns} is 4 and anything else is 142 | // higher. 143 | func HasNamespace(ns string) (bool, error) { 144 | file := fmt.Sprintf("/proc/1/ns/%s", ns) 145 | 146 | // Use Lstat to not follow the symlink. 147 | var info syscall.Stat_t 148 | if err := syscall.Lstat(file, &info); err != nil { 149 | return false, &os.PathError{Op: "lstat", Path: file, Err: err} 150 | } 151 | 152 | // Get the device number. If it is higher than 4 it is in a namespace. 153 | if info.Dev > 4 { 154 | return true, nil 155 | } 156 | 157 | return false, nil 158 | } 159 | 160 | 161 | // UserMapping holds the values for a {uid,gid}_map. 162 | type UserMapping struct { 163 | ContainerID int64 164 | HostID int64 165 | Range int64 166 | } 167 | 168 | // UserNamespace determines if the container is running in a UserNamespace and returns the mappings if so. 169 | func UserNamespace() (bool, []UserMapping) { 170 | f := readFile("/proc/self/uid_map") 171 | if len(f) < 0 { 172 | // user namespace is uninitialized 173 | return true, nil 174 | } 175 | 176 | userNs, mappings, err := readUserMappings(f) 177 | if err != nil { 178 | return false, nil 179 | } 180 | 181 | return userNs, mappings 182 | } 183 | 184 | func readUserMappings(f string) (iuserNS bool, mappings []UserMapping, err error) { 185 | parts := strings.Split(f, " ") 186 | parts = deleteEmpty(parts) 187 | if len(parts) < 3 { 188 | return false, nil, nil 189 | } 190 | 191 | for i := 0; i < len(parts); i += 3 { 192 | nsu, hu, r := parts[i], parts[i+1], parts[i+2] 193 | mapping := UserMapping{} 194 | 195 | mapping.ContainerID, err = strconv.ParseInt(nsu, 10, 0) 196 | if err != nil { 197 | return false, nil, nil 198 | } 199 | mapping.HostID, err = strconv.ParseInt(hu, 10, 0) 200 | if err != nil { 201 | return false, nil, nil 202 | } 203 | mapping.Range, err = strconv.ParseInt(r, 10, 0) 204 | if err != nil { 205 | return false, nil, nil 206 | } 207 | 208 | if mapping.ContainerID == 0 && mapping.HostID == 0 && mapping.Range == uint32Max { 209 | return false, nil, nil 210 | } 211 | 212 | mappings = append(mappings, mapping) 213 | } 214 | 215 | return true, mappings, nil 216 | } 217 | -------------------------------------------------------------------------------- /caps.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "strconv" 7 | ) 8 | 9 | type Capability uint64 10 | const ( 11 | CAP_CHOWN Capability = 0 12 | CAP_DAC_OVERRIDE Capability = 1 13 | CAP_DAC_READ_SEARCH Capability = 2 14 | CAP_FOWNER Capability = 3 15 | CAP_FSETID Capability = 4 16 | CAP_KILL Capability = 5 17 | CAP_SETGID Capability = 6 18 | CAP_SETUID Capability = 7 19 | CAP_SETPCAP Capability = 8 20 | CAP_LINUX_IMMUTABLE Capability = 9 21 | CAP_NET_BIND_SERVICE Capability = 10 22 | CAP_NET_BROADCAST Capability = 11 23 | CAP_NET_ADMIN Capability = 12 24 | CAP_NET_RAW Capability = 13 25 | CAP_IPC_LOCK Capability = 14 26 | CAP_IPC_OWNER Capability = 15 27 | CAP_SYS_MODULE Capability = 16 28 | CAP_SYS_RAWIO Capability = 17 29 | CAP_SYS_CHROOT Capability = 18 30 | CAP_SYS_PTRACE Capability = 19 31 | CAP_SYS_PACCT Capability = 20 32 | CAP_SYS_ADMIN Capability = 21 33 | CAP_SYS_BOOT Capability = 22 34 | CAP_SYS_NICE Capability = 23 35 | CAP_SYS_RESOURCE Capability = 24 36 | CAP_SYS_TIME Capability = 25 37 | CAP_SYS_TTY_CONFIG Capability = 26 38 | CAP_MKNOD Capability = 27 39 | CAP_LEASE Capability = 28 40 | CAP_AUDIT_WRITE Capability = 29 41 | CAP_AUDIT_CONTROL Capability = 30 42 | CAP_SETFCAP Capability = 31 43 | CAP_MAC_OVERRIDE Capability = 32 44 | CAP_MAC_ADMIN Capability = 33 45 | CAP_SYSLOG Capability = 34 46 | CAP_WAKE_ALARM Capability = 35 47 | CAP_BLOCK_SUSPEND Capability = 36 48 | CAP_AUDIT_READ Capability = 37 49 | CAP_MAX Capability = 38 50 | ) 51 | 52 | func (capv Capability) String() string { 53 | switch capv { 54 | case CAP_CHOWN: 55 | return "CAP_CHOWN" 56 | case CAP_DAC_OVERRIDE: 57 | return "CAP_DAC_OVERRIDE" 58 | case CAP_DAC_READ_SEARCH: 59 | return "CAP_DAC_READ_SEARCH" 60 | case CAP_FOWNER: 61 | return "CAP_FOWNER" 62 | case CAP_FSETID: 63 | return "CAP_FSETID" 64 | case CAP_KILL: 65 | return "CAP_KILL" 66 | case CAP_SETGID: 67 | return "CAP_SETGID" 68 | case CAP_SETUID: 69 | return "CAP_SETUID" 70 | case CAP_SETPCAP: 71 | return "CAP_SETPCAP" 72 | case CAP_LINUX_IMMUTABLE: 73 | return "CAP_LINUX_IMMUTABLE" 74 | case CAP_NET_BIND_SERVICE: 75 | return "CAP_NET_BIND_SERVICE" 76 | case CAP_NET_BROADCAST: 77 | return "CAP_NET_BROADCAST" 78 | case CAP_NET_ADMIN: 79 | return "CAP_NET_ADMIN" 80 | case CAP_NET_RAW: 81 | return "CAP_NET_RAW" 82 | case CAP_IPC_LOCK: 83 | return "CAP_IPC_LOCK" 84 | case CAP_IPC_OWNER: 85 | return "CAP_IPC_OWNER" 86 | case CAP_SYS_MODULE: 87 | return "CAP_SYS_MODULE" 88 | case CAP_SYS_RAWIO: 89 | return "CAP_SYS_RAWIO" 90 | case CAP_SYS_CHROOT: 91 | return "CAP_SYS_CHROOT" 92 | case CAP_SYS_PTRACE: 93 | return "CAP_SYS_PTRACE" 94 | case CAP_SYS_PACCT: 95 | return "CAP_SYS_PACCT" 96 | case CAP_SYS_ADMIN: 97 | return "CAP_SYS_ADMIN" 98 | case CAP_SYS_BOOT: 99 | return "CAP_SYS_BOOT" 100 | case CAP_SYS_NICE: 101 | return "CAP_SYS_NICE" 102 | case CAP_SYS_RESOURCE: 103 | return "CAP_SYS_RESOURCE" 104 | case CAP_SYS_TIME: 105 | return "CAP_SYS_TIME" 106 | case CAP_SYS_TTY_CONFIG: 107 | return "CAP_SYS_TTY_CONFIG" 108 | case CAP_MKNOD: 109 | return "CAP_MKNOD" 110 | case CAP_LEASE: 111 | return "CAP_LEASE" 112 | case CAP_AUDIT_WRITE: 113 | return "CAP_AUDIT_WRITE" 114 | case CAP_AUDIT_CONTROL: 115 | return "CAP_AUDIT_CONTROL" 116 | case CAP_SETFCAP: 117 | return "CAP_SETFCAP" 118 | case CAP_MAC_OVERRIDE: 119 | return "CAP_MAC_OVERRIDE" 120 | case CAP_MAC_ADMIN: 121 | return "CAP_MAC_ADMIN" 122 | case CAP_SYSLOG: 123 | return "CAP_SYSLOG" 124 | case CAP_WAKE_ALARM: 125 | return "CAP_WAKE_ALARM" 126 | case CAP_BLOCK_SUSPEND: 127 | return "CAP_BLOCK_SUSPEND" 128 | case CAP_AUDIT_READ: 129 | return "CAP_AUDIT_READ" 130 | case CAP_MAX: 131 | return "CAP_MAX" 132 | default: 133 | return "UNKNOWN_CAPABILITY" 134 | } 135 | } 136 | 137 | // Capability helpers 138 | func capToIndex(capability Capability) uint64 { 139 | return uint64(capability >> 5) 140 | } 141 | 142 | func capToMask(capability Capability) uint64 { 143 | return uint64(1 << (capability & 31)) 144 | } 145 | 146 | type CapData struct { 147 | CapInh uint64 148 | CapPrm uint64 149 | CapEff uint64 150 | CapBnd uint64 151 | CapAmb uint64 152 | } 153 | 154 | func (cdat *CapData) String() string { 155 | return fmt.Sprintf("CapInh=%x\nCapPrm=%x\nCapEff=%x\nCapBnd=%x\nCapAmb=%x\n", 156 | cdat.CapInh, cdat.CapPrm, cdat.CapEff, cdat.CapBnd, cdat.CapAmb) 157 | } 158 | 159 | func CheckCap(mask uint64, capability Capability) bool { 160 | if (capToMask(capability) & mask) > 0 { 161 | return true 162 | } 163 | return false 164 | } 165 | 166 | func GetCaps(mask uint64) []Capability { 167 | result := make([]Capability, 0) 168 | var i uint64 169 | for i=0; i 0 { 274 | caplist := listCaps(badCaps) 275 | InfoLog.Printf("Found non-default Docker capabilities in CapBnd: %s", caplist) 276 | desc := fmt.Sprintf("Container has non-default Docker capabilities in CapBnd: %s", caplist) 277 | res := NewResult("Container has non-default Docker capabilities in CapBnd", desc, SEV_MEDIUM) 278 | results = append(results, res) 279 | } 280 | return results, nil 281 | } 282 | 283 | func isDangerousCap(cap Capability) bool { 284 | switch cap { 285 | case CAP_DAC_READ_SEARCH, 286 | CAP_SYS_ADMIN, 287 | CAP_SYS_RAWIO, 288 | CAP_SYS_MODULE, 289 | CAP_SYS_PTRACE: 290 | return true 291 | default: 292 | return false 293 | } 294 | } 295 | 296 | func isNetworkAttackCap(cap Capability) bool { 297 | switch cap { 298 | case CAP_NET_ADMIN, 299 | CAP_NET_RAW: 300 | return true; 301 | } 302 | return false 303 | } 304 | 305 | func ScanDangerousCaps(state *scanState) ([]*ScanResult, error) { 306 | results := make([]*ScanResult, 0) 307 | 308 | dangerousBnd := make([]Capability, 0) 309 | networkBnd := make([]Capability, 0) 310 | for _, capv := range GetCaps(state.Capabilities.CapBnd) { 311 | if isDangerousCap(capv) { 312 | dangerousBnd = append(dangerousBnd, capv) 313 | } 314 | if isNetworkAttackCap(capv) { 315 | networkBnd = append(networkBnd, capv) 316 | } 317 | } 318 | 319 | dangerousEff := make([]Capability, 0) 320 | networkEff := make([]Capability, 0) 321 | for _, capv := range GetCaps(state.Capabilities.CapEff) { 322 | if isDangerousCap(capv) { 323 | dangerousEff = append(dangerousEff, capv) 324 | } 325 | if isNetworkAttackCap(capv) { 326 | networkEff = append(networkEff, capv) 327 | } 328 | } 329 | 330 | desc := "" 331 | title := "Container has potentially dangerous capabilities" 332 | sev := SEV_INFO 333 | 334 | if len(networkBnd) > 0 { 335 | caplist := listCaps(networkBnd) 336 | desc += fmt.Sprintf("* Container has capabilities in the CapBnd set which could enable network attacks if a process runs with uid=0: %s\n", caplist) 337 | sev = SEV_MEDIUM 338 | } 339 | 340 | if len(networkEff) > 0 { 341 | caplist := listCaps(networkEff) 342 | desc += fmt.Sprintf("* Container has capabilities in the CapEff which could enable network attacks at the current privilege level: %s\n", caplist) 343 | sev = SEV_MEDIUM 344 | } 345 | 346 | if len(dangerousBnd) > 0 { 347 | caplist := listCaps(dangerousBnd) 348 | desc += fmt.Sprintf("* Container has dangerous capabilities in the CapBnd set which may allow for container escape if a process runs with uid=0: %s\n", caplist) 349 | sev = SEV_MEDIUM 350 | } 351 | 352 | if len(dangerousEff) > 0 { 353 | caplist := listCaps(dangerousEff) 354 | title = "Container has dangerous capabilities" 355 | desc += fmt.Sprintf("* Container has dangerous capabilities in the CapEff set which may allow for container escape if a process runs at the current privilege level: %s\n", caplist) 356 | sev = SEV_CRITICAL 357 | } 358 | 359 | if len(desc) > 0 { 360 | res := NewResult(title, desc, sev) 361 | results = append(results, res) 362 | } 363 | 364 | return results, nil 365 | } 366 | 367 | func ReadCaps(state *scanState) (*CapData, error) { 368 | result := &CapData{ 369 | } 370 | var parsed uint64 371 | var v string 372 | 373 | v = state.ProcStatus["CapInh"] 374 | parsed, err := strconv.ParseUint(v, 16, 64) 375 | if err != nil { 376 | return nil, fmt.Errorf("error parsing capability value: %s", v) 377 | } 378 | result.CapInh = parsed 379 | 380 | v = state.ProcStatus["CapPrm"] 381 | parsed, err = strconv.ParseUint(v, 16, 64) 382 | if err != nil { 383 | return nil, fmt.Errorf("error parsing capability value: %s", v) 384 | } 385 | result.CapPrm = parsed 386 | 387 | v = state.ProcStatus["CapEff"] 388 | parsed, err = strconv.ParseUint(v, 16, 64) 389 | if err != nil { 390 | return nil, fmt.Errorf("error parsing capability value: %s", v) 391 | } 392 | result.CapEff = parsed 393 | 394 | v = state.ProcStatus["CapBnd"] 395 | parsed, err = strconv.ParseUint(v, 16, 64) 396 | if err != nil { 397 | return nil, fmt.Errorf("error parsing capability value: %s", v) 398 | } 399 | result.CapBnd = parsed 400 | 401 | v = state.ProcStatus["CapAmb"] 402 | parsed, err = strconv.ParseUint(v, 16, 64) 403 | if err != nil { 404 | return nil, fmt.Errorf("error parsing capability value: %s", v) 405 | } 406 | result.CapAmb = parsed 407 | 408 | return result, nil 409 | } 410 | -------------------------------------------------------------------------------- /cgroups.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "io/ioutil" 6 | "fmt" 7 | "strings" 8 | "bytes" 9 | ) 10 | 11 | type deviceRule struct { 12 | deviceType string 13 | majorType int 14 | minorType int 15 | read bool 16 | write bool 17 | mknod bool 18 | } 19 | 20 | func (rule *deviceRule) String() string { 21 | var major, minor string 22 | 23 | if rule.majorType >= 0 { 24 | major = strconv.Itoa(rule.majorType) 25 | } else { 26 | major = "*" 27 | } 28 | 29 | if rule.minorType >= 0 { 30 | minor = strconv.Itoa(rule.minorType) 31 | } else { 32 | minor = "*" 33 | } 34 | 35 | types := fmt.Sprintf("%s:%s", major, minor) 36 | 37 | access := "" 38 | if rule.read { 39 | access += "r" 40 | } 41 | if rule.write { 42 | access += "w" 43 | } 44 | if rule.mknod { 45 | access += "m" 46 | } 47 | 48 | return fmt.Sprintf("%s %s %s", rule.deviceType, types, access) 49 | } 50 | 51 | type DeviceRules struct { 52 | rules []*deviceRule 53 | } 54 | 55 | func (rules *DeviceRules) String() string { 56 | s := "" 57 | for _, rule := range rules.rules { 58 | s += rule.String() + "\n" 59 | } 60 | return s 61 | } 62 | 63 | func (rules *DeviceRules) CheckDevice(devType string, majorType int, minorType int, access string) bool { 64 | for _, rule := range rules.rules { 65 | if rule.deviceType != "a" && rule.deviceType != devType { 66 | continue 67 | } 68 | if rule.majorType >= 0 && rule.majorType != majorType { 69 | continue 70 | } 71 | if rule.majorType >= 0 && rule.minorType != minorType { 72 | continue 73 | } 74 | if access == "r" && !rule.read { 75 | continue 76 | } 77 | if access == "w" && !rule.write { 78 | continue 79 | } 80 | if access == "m" && !rule.mknod { 81 | continue 82 | } 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | func (rules *DeviceRules) addRule(entry string) { 89 | fields := strings.Fields(string(entry)) 90 | if len(fields) == 0 { 91 | return 92 | } 93 | typeParts := strings.Split(fields[1], ":") 94 | var major, minor int 95 | if typeParts[0] == "*" { 96 | major = -1 97 | } else { 98 | var err error 99 | major, err = strconv.Atoi(typeParts[0]) 100 | if err != nil { 101 | major = -2 102 | } 103 | } 104 | 105 | if typeParts[1] == "*" { 106 | minor = -1 107 | } else { 108 | var err error 109 | minor, err = strconv.Atoi(typeParts[1]) 110 | if err != nil { 111 | minor = -2 112 | } 113 | } 114 | 115 | rule := &deviceRule{ 116 | deviceType: fields[0], 117 | majorType: major, 118 | minorType: minor, 119 | read: strings.Contains(fields[2], "r"), 120 | write: strings.Contains(fields[2], "w"), 121 | mknod: strings.Contains(fields[2], "m"), 122 | } 123 | rules.rules = append(rules.rules, rule) 124 | } 125 | 126 | func NewDeviceRules() (*DeviceRules) { 127 | return &DeviceRules{ 128 | rules: make([]*deviceRule, 0), 129 | } 130 | } 131 | 132 | func ReadDevices(cgroupDevicesPath string) (*DeviceRules, error) { 133 | ret := NewDeviceRules() 134 | data, err := ioutil.ReadFile(cgroupDevicesPath + "/devices.list") 135 | if err != nil { 136 | return nil, fmt.Errorf("error reading devices.list: %s", err) 137 | } 138 | lines := bytes.Split(data, []byte("\n")) 139 | for _, line := range lines { 140 | ret.addRule(string(line)) 141 | } 142 | return ret, nil 143 | } 144 | 145 | func getDangerousDevices(rules *DeviceRules) []*deviceRule { 146 | /* 147 | we're just going to assume anything in the docker default is safe and everything 148 | else is scary and should be disallowed. Maybe we'll loosen this up later 149 | c 1:5 rwm 150 | c 1:3 rwm 151 | c 1:9 rwm 152 | c 1:8 rwm 153 | c 5:0 rwm 154 | c 5:1 rwm 155 | c *:* m 156 | b *:* m 157 | c 1:7 rwm 158 | c 136:* rwm 159 | c 5:2 rwm 160 | c 10:200 rwm 161 | */ 162 | ret := make([]*deviceRule, 0) 163 | for _, rule := range rules.rules { 164 | if rule.deviceType == "a" { 165 | ret = append(ret, rule) 166 | continue 167 | } else if rule.deviceType == "c" { 168 | if rule.majorType == 1 { 169 | if rule.minorType == 3 || // /dev/null 170 | rule.minorType == 5 || // /dev/zero 171 | rule.minorType == 7 || // /dev/full 172 | rule.minorType == 8 || // /dev/random 173 | rule.minorType == 9 { // /dev/urandom 174 | continue 175 | } 176 | } else if rule.majorType == 5 { 177 | if rule.minorType == 0 || // /dev/tty 178 | rule.minorType == 1 || // /dev/console 179 | rule.minorType == 2 { // /dev/ptmx 180 | continue 181 | } 182 | } else if rule.majorType == 10 && rule.minorType == 200 { 183 | continue 184 | } else if rule.majorType == 136 { 185 | continue 186 | } 187 | } 188 | if rule.mknod && !rule.read && !rule.write { 189 | continue 190 | } 191 | ret = append(ret, rule) 192 | } 193 | 194 | return ret 195 | } 196 | 197 | func ScanCgroups(state *scanState) ([]*ScanResult, error) { 198 | // Scan for capability related issues, store capabilities in state 199 | results := make([]*ScanResult, 0) 200 | 201 | if state.CgroupDeviceRules == nil { 202 | return results, nil 203 | } 204 | 205 | // Scan for devices 206 | allowedDevices := state.CgroupDeviceRules 207 | dangerousDevices := getDangerousDevices(allowedDevices) 208 | if len(dangerousDevices) > 0 { 209 | devlist := "" 210 | for _, device := range dangerousDevices { 211 | devlist += device.String() + "\n" 212 | } 213 | desc := "The following cgroup device rules allow contianer users to access potentially dangerous devices:\n" + devlist 214 | result := NewResult("Cgroup allows access to potentially dangerous devices", desc, SEV_MEDIUM) 215 | results = append(results, result) 216 | } 217 | 218 | err := ioutil.WriteFile(state.CgroupPath+"/devices/devices.allow", 219 | []byte("a"), 0644) 220 | if err == nil { 221 | result := NewResult("Cgroups Device Settings Modifiable", "Processes in the conainer can modify the devices cgroup settings which would allow them to access potentially dangerous devices", SEV_HIGH) 222 | results = append(results, result) 223 | } 224 | 225 | if state.CgroupCPUShares == 1024 { 226 | result := NewResult("Cgroup Policy Does Not Restrict CPU Usage", "Processes in the conainer are capable of using excessive amounts of CPU time", SEV_INFO) 227 | results = append(results, result) 228 | } 229 | 230 | if state.CgroupMaxMemory > 1024*1024*1024*8 { 231 | result := NewResult("Cgroup Policy Allows for Excessive Memory Usage", "Processes in the conainer are capable of using excessive amounts (>8GB) of RAM", SEV_INFO) 232 | results = append(results, result) 233 | } 234 | 235 | return results, nil 236 | } 237 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "fmt" 5 | "flag" 6 | "os" 7 | "bytes" 8 | "strconv" 9 | "io/ioutil" 10 | "strings" 11 | "log" 12 | ) 13 | 14 | var InfoLog *log.Logger 15 | 16 | type SeverityLevel int 17 | const ( 18 | SEV_INFO SeverityLevel = iota 19 | SEV_LOW 20 | SEV_MEDIUM 21 | SEV_HIGH 22 | SEV_CRITICAL 23 | ) 24 | func (sev SeverityLevel) String() string { 25 | switch sev { 26 | case SEV_INFO: 27 | return "Informational" 28 | case SEV_LOW: 29 | return "Low" 30 | case SEV_MEDIUM: 31 | return "Medium" 32 | case SEV_HIGH: 33 | return "High" 34 | case SEV_CRITICAL: 35 | return "Critical" 36 | default: 37 | return "Unknown" 38 | } 39 | } 40 | 41 | type CmdOptions struct { 42 | LogFile string 43 | IPSniffTime int 44 | MinimumSeverity SeverityLevel 45 | KubernetesScan bool 46 | } 47 | 48 | type scanState struct { 49 | Capabilities *CapData 50 | ProcStatus map[string]string 51 | ProcCPUInfo []map[string]string 52 | CmdOpts *CmdOptions 53 | Runtime string 54 | CgroupDeviceRules *DeviceRules 55 | CgroupPath string 56 | CgroupCPUShares uint64 57 | CgroupMaxMemory uint64 58 | } 59 | 60 | func NewScanState() *scanState { 61 | return &scanState{ 62 | ProcStatus: make(map[string]string), 63 | ProcCPUInfo: make([]map[string]string, 0), 64 | } 65 | } 66 | 67 | func ParseArgs() (*CmdOptions, error) { 68 | opts := &CmdOptions{} 69 | //var MinSevStr string 70 | 71 | flag.StringVar(&opts.LogFile, "l", "", "File to log to") 72 | flag.IntVar(&opts.IPSniffTime, "sniff", 0, "Sniff the network for IP addresses for a given number of seconds") 73 | //flag.StringVar(&MinSevStr, "s", "info", "Minimum severity to include in output") 74 | //flag.BoolVar(&opts.KubernetesScan, "k", false, "Check for Kubernetes related issues") 75 | flag.Parse() 76 | 77 | /* 78 | switch strings.ToLower(MinSevStr)[0] { 79 | case 'i': 80 | opts.MinimumSeverity = SEV_INFO 81 | case 'l': 82 | opts.MinimumSeverity = SEV_LOW 83 | case 'm': 84 | opts.MinimumSeverity = SEV_MEDIUM 85 | case 'h': 86 | opts.MinimumSeverity = SEV_HIGH 87 | case 'c': 88 | opts.MinimumSeverity = SEV_CRITICAL 89 | default: 90 | return nil, fmt.Errorf("Unknown severity level: %s", MinSevStr) 91 | } 92 | */ 93 | opts.MinimumSeverity = SEV_INFO // we're just using severity for sorting for now 94 | opts.KubernetesScan = false // we'll add the flag back once it's implemented 95 | 96 | return opts, nil 97 | } 98 | 99 | func (state *scanState) ReadData() error { 100 | data, err := ioutil.ReadFile("/proc/self/status") 101 | if err != nil { 102 | InfoLog.Printf("could not open /proc/1/status: %s", err) 103 | } else { 104 | strs := bytes.Split(data, []byte("\n")) 105 | for _, s := range strs { 106 | vals := bytes.Split(s, []byte(":\t")) 107 | if len(vals) != 2 { 108 | continue 109 | } 110 | k := string(vals[0]) 111 | v := string(vals[1]) 112 | state.ProcStatus[k] = v 113 | } 114 | } 115 | 116 | data, err = ioutil.ReadFile("/proc/cpuinfo") 117 | if err != nil { 118 | InfoLog.Printf("could not open /proc/cpuinfo: %s", err) 119 | } else { 120 | strs := bytes.Split(data, []byte("\n")) 121 | workingDict := make(map[string]string) 122 | for _, s := range strs { 123 | if len(s) == 0 { 124 | state.ProcCPUInfo = append(state.ProcCPUInfo, workingDict) 125 | workingDict = make(map[string]string) 126 | continue 127 | } 128 | 129 | vals := bytes.SplitN(s, []byte(":"), 2) 130 | if len(vals) != 2 { 131 | continue 132 | } 133 | k := string(vals[0]) 134 | v := string(vals[1]) 135 | k = strings.TrimSpace(k) 136 | v = strings.TrimSpace(v) 137 | workingDict[k] = v 138 | } 139 | state.ProcCPUInfo = append(state.ProcCPUInfo, workingDict) 140 | } 141 | 142 | runtime, err := DetectRuntime() 143 | if err != nil { 144 | state.Runtime = "not-found" 145 | } else { 146 | state.Runtime = runtime 147 | } 148 | 149 | loadCaps(state) 150 | 151 | state.CgroupPath = "/sys/fs/cgroup" 152 | devrules, err := ReadDevices(state.CgroupPath + "/devices") 153 | if err == nil { 154 | state.CgroupDeviceRules = devrules 155 | } 156 | 157 | fname := state.CgroupPath + "/cpu/cpu.shares" 158 | data, err = ioutil.ReadFile(fname) 159 | if err != nil { 160 | InfoLog.Printf("error reading %s: %s\n", fname, err) 161 | } else { 162 | parsed, err := strconv.ParseUint(string(data[:len(data)-1]), 10, 64) 163 | if err != nil { 164 | InfoLog.Printf("error parsing CPU shares: %s: %s\n", data, err) 165 | } 166 | state.CgroupCPUShares = parsed 167 | } 168 | 169 | fname = state.CgroupPath + "/memory/memory.limit_in_bytes" 170 | data, err = ioutil.ReadFile(fname) 171 | if err != nil { 172 | InfoLog.Printf("error reading %s: %s\n", fname, err) 173 | } else { 174 | parsed, err := strconv.ParseUint(string(data[:len(data)-1]), 10, 64) 175 | if err != nil { 176 | InfoLog.Printf("error parsing memory in bytes: %s: %s\n", data, err) 177 | } 178 | state.CgroupMaxMemory = parsed 179 | } 180 | 181 | return nil 182 | } 183 | 184 | type ScanResult struct { 185 | Title string 186 | Description string 187 | Severity SeverityLevel 188 | } 189 | 190 | func NewResult(title string, description string, sev SeverityLevel) *ScanResult { 191 | return &ScanResult{ 192 | Title: title, 193 | Description: description, 194 | Severity: sev, 195 | } 196 | } 197 | 198 | func (result *ScanResult) RenderTerm() string { 199 | s := result.Title + "\n" 200 | for i:=0; i 0) { 245 | sniffResults, err := SniffIPs(curState, curState.CmdOpts.IPSniffTime) 246 | if err != nil { 247 | InfoLog.Printf("error sniffing network: %s\n", err) 248 | } else { 249 | results = append(results, sniffResults...) 250 | } 251 | } else { 252 | scanResults, err := NormalScan(curState) 253 | if err != nil { 254 | InfoLog.Printf("error performing scan: %s\n", err) 255 | } else { 256 | results = append(results, scanResults...) 257 | } 258 | } 259 | 260 | // Print results 261 | RenderTermResults(results) 262 | } 263 | 264 | func NormalScan(curState *scanState) ([]*ScanResult, error) { 265 | var err error 266 | err = curState.ReadData() 267 | if err != nil { 268 | fmt.Printf("error reading data: %s\n", err) 269 | os.Exit(1) 270 | } 271 | results := make([]*ScanResult, 0) 272 | 273 | // Check capabilities 274 | capresults, err := ScanCaps(curState) 275 | if err != nil { 276 | fmt.Printf("error checking caps: %s\n", err.Error()) 277 | os.Exit(1) 278 | } 279 | results = append(results, capresults...) 280 | 281 | // mounted dirs/files 282 | mountresults, err := ScanMounts(curState) 283 | if err != nil { 284 | fmt.Printf("error checking scan mounts: %s", err.Error()) 285 | os.Exit(1) 286 | } 287 | results = append(results, mountresults...) 288 | // Character/block devices 289 | // FDs 290 | // cgroups 291 | result, err := ScanCgroups(curState) 292 | if err != nil { 293 | fmt.Printf("error scanning cgroups: %s", err.Error()) 294 | os.Exit(1) 295 | } 296 | results = append(results, result...) 297 | 298 | // Misc 299 | result, err = ScanMisc(curState) 300 | if err != nil { 301 | fmt.Printf("error with misc scans: %s\n", err.Error()) 302 | os.Exit(1) 303 | } 304 | results = append(results, result...) 305 | 306 | return results, nil 307 | } 308 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | func RWFile(path string) (bool) { 10 | // Attempts to read a file and write its contents back to the file 11 | // Used to check for read/write permissions 12 | // Returns whether successful 13 | data, err := ioutil.ReadFile(path) 14 | if err == nil { 15 | err = ioutil.WriteFile(path, data, 0644) 16 | if err == nil { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | 23 | func SeccompMode(state *scanState) (string, error) { 24 | if val, ok := state.ProcStatus["Seccomp"]; ok { 25 | if val == "0" { 26 | return "disabled", nil 27 | } else if val == "1" { 28 | return "strict", nil 29 | } else if val == "2" { 30 | return "filtered", nil 31 | } 32 | } 33 | return "", fmt.Errorf("error scanning for seccomp profile") 34 | } 35 | 36 | func ScanMisc(state *scanState) ([]*ScanResult, error) { 37 | results := make([]*ScanResult, 0) 38 | var result []*ScanResult 39 | var err error 40 | 41 | // Scan Runtime 42 | result, err = ScanRuntime(state) 43 | if err != nil { 44 | InfoLog.Printf("error scanning runtime: %s", err) 45 | } else { 46 | results = append(results, result...) 47 | } 48 | 49 | // Check for user namespacing 50 | result, err = ScanUserNamespacing(state) 51 | if err != nil { 52 | InfoLog.Printf("error checking for user namespacing: %s", err) 53 | } else { 54 | results = append(results, result...) 55 | } 56 | 57 | // Check for proc namespacing 58 | result, err = ScanProcessNamespacing(state) 59 | if err != nil { 60 | InfoLog.Printf("error checking for process namespacing: %s", err) 61 | } else { 62 | results = append(results, result...) 63 | } 64 | 65 | // Check for core_pattern 66 | result, err = ScanProcVars(state) 67 | if err != nil { 68 | InfoLog.Printf("error checking for namespacing: %s", err) 69 | } else { 70 | results = append(results, result...) 71 | } 72 | 73 | // Check for kcore 74 | result, err = ScanKcore(state) 75 | if err != nil { 76 | InfoLog.Printf("error checking for kcore: %s", err) 77 | } else { 78 | results = append(results, result...) 79 | } 80 | 81 | // Check for AppArmor 82 | result, err = ScanAppArmor(state) 83 | if err != nil { 84 | InfoLog.Printf("error checking for AppArmor profile: %s", err) 85 | } else { 86 | results = append(results, result...) 87 | } 88 | 89 | // Check for seccomp 90 | result, err = ScanSeccompEnabled(state) 91 | if err != nil { 92 | InfoLog.Printf("error checking if seccomp is enabled: %s", err) 93 | } else { 94 | results = append(results, result...) 95 | } 96 | 97 | // Scan CPUInfo 98 | result, err = ScanProcCPUInfo(state) 99 | if err != nil { 100 | InfoLog.Printf("error scanning CPU info: %s", err) 101 | } else { 102 | results = append(results, result...) 103 | } 104 | 105 | // get kernel version 106 | versionresults, err := ScanVersion(state) 107 | if err != nil { 108 | InfoLog.Printf("error finding kernel version: %s", err) 109 | } else { 110 | results = append(results, versionresults...) 111 | } 112 | 113 | return results, nil 114 | } 115 | 116 | func ScanVersion(state *scanState) ([]*ScanResult, error) { 117 | results := make([]*ScanResult, 0) 118 | data, err := ioutil.ReadFile("/proc/version") 119 | if err != nil { 120 | InfoLog.Printf("could not open /proc/version: %s. Trying /proc/sys/kernel/version", err) 121 | data, err = ioutil.ReadFile("/proc/sys/kernel/version") 122 | if err != nil { 123 | InfoLog.Printf("could not open /proc/sys/kernel/version: %s, giving up on finding version info", err) 124 | return results, nil 125 | } 126 | } 127 | 128 | result := NewResult("Kernel Version Info", string(data), SEV_INFO) 129 | results = append(results, result) 130 | return results, nil 131 | } 132 | 133 | func ScanProcessNamespacing(state *scanState) ([]*ScanResult, error) { 134 | results := make([]*ScanResult, 0) 135 | hasNs, err := HasNamespace("pid") 136 | if err != nil { 137 | InfoLog.Printf("error checking pid namespacing: %s\n", err) 138 | return results, nil 139 | } 140 | if !hasNs { 141 | result := NewResult("Container not using process namespaces", "Container is not using process namespaces. This allows contained processes to interact with uncontained processes", SEV_HIGH) 142 | results = append(results, result) 143 | } 144 | return results, nil 145 | } 146 | 147 | func ScanSeccompEnabled(state *scanState) ([]*ScanResult, error) { 148 | results := make([]*ScanResult, 0) 149 | mode, _ := SeccompMode(state) 150 | if mode == "disabled" { 151 | result := NewResult("Seccomp is Disabled", "Seccomp is disabled in container", SEV_LOW) 152 | results = append(results, result) 153 | } 154 | return results, nil 155 | } 156 | 157 | func ScanProcVars(state *scanState) ([]*ScanResult, error) { 158 | // Checks if we can read/write to various /proc files 159 | results := make([]*ScanResult, 0) 160 | 161 | if RWFile("/proc/sys/kernel/core_pattern") { 162 | result := NewResult("/proc/sys/kernel/core_pattern is writable", "/proc/sys/kernel/core_pattern is writable which allows for container escape", SEV_CRITICAL) 163 | results = append(results, result) 164 | } 165 | 166 | if RWFile("/proc/sys/kernel/modprobe") { 167 | result := NewResult("/proc/sys/kernel/modprobe is writable", "/proc/sys/kernel/modprobe is writable which allows for container escape", SEV_CRITICAL) 168 | results = append(results, result) 169 | } 170 | 171 | if RWFile("/proc/sys/vm/panic_on_oom") { 172 | result := NewResult("/proc/sys/vm/panic_on_oom is writable", "/proc/sys/vm/panic_on_oom is writable which could allow a contained process to crash the host", SEV_LOW) 173 | results = append(results, result) 174 | } 175 | return results, nil 176 | } 177 | 178 | func ScanKcore(state *scanState) ([]*ScanResult, error) { 179 | results := make([]*ScanResult, 0) 180 | // Open kcore, kmem, mem RW then confirm we can actually get data out of it 181 | f, err := os.OpenFile("/proc/kcore", os.O_RDWR, 0644) 182 | if err == nil { 183 | b := make([]byte, 3) 184 | n, err := f.Read(b) 185 | if err == nil && n > 0 { 186 | f.Close() 187 | result := NewResult("/proc/kcore is writable", "/proc/kcore is writable which allows for container escape", SEV_CRITICAL) 188 | results = append(results, result) 189 | } 190 | } 191 | 192 | // kmem 193 | f, err = os.OpenFile("/proc/kmem", os.O_RDWR, 0644) 194 | if err == nil { 195 | b := make([]byte, 3) 196 | n, err := f.Read(b) 197 | if err == nil && n > 0 { 198 | f.Close() 199 | result := NewResult("/proc/kmem is writable", "/proc/kmem is writable which allows for container escape", SEV_CRITICAL) 200 | results = append(results, result) 201 | } 202 | } 203 | 204 | // mem 205 | f, err = os.OpenFile("/proc/mem", os.O_RDWR, 0644) 206 | if err == nil { 207 | b := make([]byte, 3) 208 | n, err := f.Read(b) 209 | if err == nil && n > 0 { 210 | f.Close() 211 | result := NewResult("/proc/mem is writable", "/proc/mem is writable which allows for container escape", SEV_CRITICAL) 212 | results = append(results, result) 213 | } 214 | } 215 | 216 | return results, nil 217 | } 218 | 219 | func ScanProcCPUInfo(state *scanState) ([]*ScanResult, error) { 220 | results := make([]*ScanResult, 0) 221 | bugstr := "" 222 | isFinding := false 223 | for _, p := range state.ProcCPUInfo { 224 | var ok bool 225 | var bugline string 226 | if bugline, ok = p["bugs"]; !ok { 227 | continue 228 | } 229 | 230 | var proc string 231 | if proc, ok = p["processor"]; !ok { 232 | proc = "??" 233 | } 234 | 235 | isFinding = true 236 | var mname string 237 | if mname, ok = p["model name"]; ok { 238 | bugstr += fmt.Sprintf("Processor %s (%s) bugs: %s\n", proc, mname, bugline) 239 | } else { 240 | bugstr += fmt.Sprintf("Processor %s bugs: %s\n", proc, bugline) 241 | } 242 | } 243 | if isFinding { 244 | desc := "The following processors have bugs which may be exploited. More information about each of the processors can be found by reading /proc/cpuinfo.\n\n" + bugstr 245 | result := NewResult("Processor Vulnerable to Hardware Attacks", desc, SEV_LOW) 246 | results = append(results, result) 247 | } 248 | return results, nil 249 | } 250 | 251 | func ScanRuntime(state *scanState) ([]*ScanResult, error) { 252 | results := make([]*ScanResult, 0) 253 | desc := fmt.Sprintf("Container runtime: %s", state.Runtime) 254 | result := NewResult("Detected Container Runtime", desc, SEV_INFO) 255 | results = append(results, result) 256 | return results, nil 257 | } 258 | 259 | func ScanAppArmor(state *scanState) ([]*ScanResult, error) { 260 | results := make([]*ScanResult, 0) 261 | f := readFile("/proc/self/attr/current") 262 | if f == "unconfined" || f == "" { 263 | result := NewResult("Container Running Unconfined AppArmor Profile", 264 | "Container is not enforcing an AppArmor profile on contained processes", 265 | SEV_LOW) 266 | results = append(results, result) 267 | } else { 268 | desc := fmt.Sprintf("AppArmor Profile: %s", f) 269 | result := NewResult("AppArmor Profile", desc, SEV_INFO) 270 | results = append(results, result) 271 | } 272 | return results, nil 273 | } 274 | 275 | func ScanUserNamespacing(state *scanState) ([]*ScanResult, error) { 276 | results := make([]*ScanResult, 0) 277 | isNamespaced, mappings := UserNamespace() 278 | if isNamespaced { 279 | var desc string 280 | if mappings == nil { 281 | desc = "User namespacing enabled but not initialized therefore there are no mappings." 282 | } else { 283 | desc = "User namespacing enabled. The following mappings were detected:\n" 284 | for _, mapping := range mappings { 285 | desc += fmt.Sprintf("\nContainer -> %d / Host -> %d / Range -> %d", mapping.ContainerID, mapping.HostID, mapping.Range) 286 | } 287 | } 288 | result := NewResult("User Namespace Mappings", desc, SEV_INFO) 289 | results = append(results, result) 290 | } else { 291 | result := NewResult("User Namespace Disabled", 292 | "User namespacing is not enabled. As a result, if a contained process is running with uid=0 it will be running as a privileged user if it gains access to resources outside of the container.", 293 | SEV_MEDIUM) 294 | results = append(results, result) 295 | } 296 | return results, nil 297 | } 298 | -------------------------------------------------------------------------------- /mounts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strings" 7 | "strconv" 8 | "bytes" 9 | ) 10 | 11 | 12 | type MountPoint struct { 13 | Device string 14 | Path string 15 | Type string 16 | Opts []string 17 | Freq int 18 | Pass int 19 | } 20 | 21 | const ( 22 | procMountsPath = "/proc/mounts" 23 | expectedNumFieldsPerLine = 6 24 | ) 25 | 26 | var flagMountTypes []string = []string{"adfs", "affs", "autofs", "cifs", "coda", "coherent", "cramfs", /*"debugfs",*/ /*"devpts",*/ "efs", "ext", "ext2", "ext3", "ext4", "hfs", "hfsplus", "hpfs", "iso9660", "jfs", "minix", "msdos", "ncpfs", "nfs", "nfs4", "ntfs", /*"proc",*/ "qnx4", "ramfs", "reiserfs", "romfs", "squashfs", "smbfs", "sysv", /*"tmpfs",*/ "ubifs", "udf", "ufs", "umsdos", "usbfs", "vfat", "xenix", "xfs", "xiafs"} // taken from mount manpage 27 | 28 | func listProcMounts() ([]MountPoint, error) { 29 | content, err := ioutil.ReadFile(procMountsPath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return parseProcMounts(content) 34 | } 35 | 36 | func parseProcMounts(content []byte) ([]MountPoint, error) { 37 | out := []MountPoint{} 38 | lines := strings.Split(string(content), "\n") 39 | for _, line := range lines { 40 | if line == "" { 41 | // the last split() item is empty string following the last \n 42 | continue 43 | } 44 | fields := strings.Fields(line) 45 | if len(fields) != expectedNumFieldsPerLine { 46 | return nil, fmt.Errorf("wrong number of fields (expected %d, got %d): %s", expectedNumFieldsPerLine, len(fields), line) 47 | } 48 | 49 | mp := MountPoint{ 50 | Device: fields[0], 51 | Path: fields[1], 52 | Type: fields[2], 53 | Opts: strings.Split(fields[3], ","), 54 | } 55 | 56 | freq, err := strconv.Atoi(fields[4]) 57 | if err != nil { 58 | return nil, err 59 | } 60 | mp.Freq = freq 61 | 62 | pass, err := strconv.Atoi(fields[5]) 63 | if err != nil { 64 | return nil, err 65 | } 66 | mp.Pass = pass 67 | 68 | out = append(out, mp) 69 | } 70 | return out, nil 71 | } 72 | 73 | func ShouldIgnoreMount(mountPoint MountPoint) bool { 74 | for _, mtype := range flagMountTypes { 75 | if mountPoint.Type == mtype { 76 | return false 77 | } 78 | } 79 | return true 80 | } 81 | 82 | func AddToOutput(mountPoint MountPoint, outputBuffer *bytes.Buffer) (error) { 83 | 84 | _, err := outputBuffer.WriteString(fmt.Sprintf("Device: %s\n", mountPoint.Device)) 85 | if err != nil { 86 | return fmt.Errorf("Error writing to buffer: %s", err) 87 | } 88 | _, err = outputBuffer.WriteString(fmt.Sprintf("Path: %s\n", mountPoint.Path)) 89 | if err != nil { 90 | return fmt.Errorf("Error writing to buffer: %s", err) 91 | } 92 | _, err = outputBuffer.WriteString(fmt.Sprintf("Type: %s\n", mountPoint.Type)) 93 | if err != nil { 94 | return fmt.Errorf("Error writing to buffer: %s", err) 95 | } 96 | _, err = outputBuffer.WriteString(fmt.Sprintf("Opts: %v\n", mountPoint.Opts)) 97 | if err != nil { 98 | return fmt.Errorf("Error writing to buffer: %s", err) 99 | } 100 | _, err = outputBuffer.WriteString(fmt.Sprintf("Freq: %d\n", mountPoint.Freq)) 101 | if err != nil { 102 | return fmt.Errorf("Error writing to buffer: %s", err) 103 | } 104 | _, err = outputBuffer.WriteString(fmt.Sprintf("Pass: %d\n", mountPoint.Pass)) 105 | if err != nil { 106 | return fmt.Errorf("Error writing to buffer: %s", err) 107 | } 108 | _, err = outputBuffer.WriteString("---\n") 109 | if err != nil { 110 | return fmt.Errorf("Error writing to buffer: %s", err) 111 | } 112 | return nil 113 | } 114 | 115 | func CreateDescriptionFromMountPointList(mountPointList *[]MountPoint, outputBuffer *bytes.Buffer) (error){ 116 | 117 | var numVulnerableMountPoints = 0 118 | for _, mountPointIter := range *mountPointList { 119 | AddToOutput(mountPointIter, outputBuffer) 120 | numVulnerableMountPoints += 1 121 | } 122 | if numVulnerableMountPoints == 0 { 123 | return fmt.Errorf("No vulnerable mountpoints available to create description from.") 124 | } 125 | return nil 126 | } 127 | 128 | 129 | func ScanMounts(state *scanState) ([]*ScanResult, error) { 130 | results := make([]*ScanResult, 0) 131 | mountPoints, err := listProcMounts() 132 | if err != nil { 133 | InfoLog.Printf("error parsing mount info, bailing on scanning mounts: %s", err) 134 | return results, nil 135 | } 136 | 137 | writeable := []MountPoint{} 138 | readable := []MountPoint{} 139 | 140 | for _, mountPointIter := range mountPoints { 141 | if ShouldIgnoreMount(mountPointIter) { 142 | continue 143 | } 144 | 145 | if mountPointIter.Opts[0] == "rw" { 146 | writeable = append(writeable, mountPointIter) 147 | } 148 | if mountPointIter.Opts[0] == "ro" { 149 | readable = append(readable, mountPointIter) 150 | } 151 | } 152 | 153 | var output bytes.Buffer 154 | 155 | if (len(writeable) > 0) { 156 | err = CreateDescriptionFromMountPointList(&writeable, &output) 157 | if err == nil { 158 | desc := "Some paths are mounted with write permission and may be filesystems mounted from the host. If this is the case, modifying the files at these paths may allow a contained process to modify files outside of the contained environment. The following paths were detected to be mounted with write permissions:\n---\n" 159 | desc += output.String() 160 | ret := NewResult("Writeable Mounts", desc, SEV_MEDIUM) 161 | results = append(results, ret) 162 | output.Reset() 163 | } 164 | } 165 | 166 | if (len(readable) > 0) { 167 | CreateDescriptionFromMountPointList(&readable, &output) 168 | if err == nil { 169 | desc := "Some paths are mounted with read permission and may be filesystems mounted from the host. If this is the case, reading the files at the following paths may allow a contained process to read files from the host. The following paths were detected to be mounted with read permissions:\n---\n" 170 | desc += output.String() 171 | ret := NewResult("Readable Mounts", desc, SEV_LOW) 172 | results = append(results, ret) 173 | //OUTPUT.RESET() HERE IF YOU'RE GOING TO ADD MORE RESULTS 174 | } 175 | } 176 | 177 | return results, nil 178 | } 179 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "net" 5 | "strings" 6 | "time" 7 | "fmt" 8 | "github.com/google/gopacket" 9 | "github.com/google/gopacket/pcap" 10 | ) 11 | 12 | import "C" 13 | 14 | func SniffIPs(state *scanState, sniffTime int) ([]*ScanResult, error) { 15 | results := make([]*ScanResult, 0) 16 | allInterfaces, err := net.Interfaces() 17 | if err != nil { 18 | InfoLog.Printf("error getting interfaces: %s\n", err) 19 | InfoLog.Println("skipping network scans") 20 | return results, nil 21 | } 22 | interfaces := make([]net.Interface, 0) 23 | InfoLog.Println("Container has following interfaces:") 24 | for _, i := range allInterfaces { 25 | InfoLog.Println(i) 26 | if i.Flags & net.FlagLoopback == 0 { 27 | interfaces = append(interfaces, i) 28 | } else { 29 | InfoLog.Println("loopback interface, not using in scans") 30 | } 31 | } 32 | InfoLog.Println(interfaces) 33 | 34 | 35 | // Set up the timer 36 | InfoLog.Println("Sniffing for packets...") 37 | packetsSniffed := 0 38 | timeout := make(chan bool, 1) 39 | go func() { 40 | maxtime := sniffTime 41 | lastPrintLen := 0 42 | for i:=0; i 0 { 103 | desc += fmt.Sprintf("IPs discovered on %s:\n", k) 104 | desc += strings.Join(ipList, "\n") 105 | desc += "\n" 106 | } else { 107 | desc += fmt.Sprintf("No packets sniffed on %s\n", k) 108 | } 109 | } 110 | 111 | result := NewResult("Sniffed IP Addresses", desc, SEV_INFO) 112 | results = append(results, result) 113 | return results, nil 114 | } 115 | 116 | --------------------------------------------------------------------------------