github.com/google/osv-scalibr@v0.4.1/detector/cve/untested/cve202338408/cve202338408.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package cve202338408 implements a detector for CVE-2023-38408. 16 package cve202338408 17 18 import ( 19 "bufio" 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "regexp" 27 "strings" 28 29 "github.com/google/osv-scalibr/detector" 30 "github.com/google/osv-scalibr/extractor" 31 scalibrfs "github.com/google/osv-scalibr/fs" 32 "github.com/google/osv-scalibr/inventory" 33 "github.com/google/osv-scalibr/log" 34 "github.com/google/osv-scalibr/packageindex" 35 "github.com/google/osv-scalibr/plugin" 36 "github.com/google/osv-scalibr/semantic" 37 38 osvpb "github.com/ossf/osv-schema/bindings/go/osvschema" 39 structpb "google.golang.org/protobuf/types/known/structpb" 40 ) 41 42 const ( 43 // Name of the detector. 44 Name = "cve/cve-2023-38408" 45 ) 46 47 var ( 48 // Regex matching the "ssh -A" command. 49 sshRegex = regexp.MustCompile(`ssh (.* )?-\w*A`) 50 // Regex matching the OpenSSH version. 51 openSSHVersionRegex = regexp.MustCompile(`OpenSSH_([^,]+),`) 52 // Regex matching the "forwardagent yes" line in ssh config. 53 forwardAgentRegex = regexp.MustCompile(`^forwardagent\s+yes`) 54 ) 55 56 // Detector is a SCALIBR Detector for CVE-2023-38408. 57 type Detector struct{} 58 59 // New returns a detector. 60 func New() detector.Detector { 61 return &Detector{} 62 } 63 64 // Name of the detector. 65 func (Detector) Name() string { return Name } 66 67 // Version of the detector. 68 func (Detector) Version() int { return 0 } 69 70 // Requirements of the detector. 71 func (Detector) Requirements() *plugin.Capabilities { 72 return &plugin.Capabilities{DirectFS: true, RunningSystem: true, OS: plugin.OSLinux} 73 } 74 75 // RequiredExtractors returns an empty list as there are no dependencies. 76 func (Detector) RequiredExtractors() []string { return []string{} } 77 78 // DetectedFinding returns generic vulnerability information about what is detected. 79 func (d Detector) DetectedFinding() inventory.Finding { 80 return d.findingForPackage(nil, nil) 81 } 82 83 func (Detector) findingForPackage(dbSpecific *structpb.Struct, pkg *extractor.Package) inventory.Finding { 84 return inventory.Finding{PackageVulns: []*inventory.PackageVuln{{ 85 Package: pkg, 86 Vulnerability: &osvpb.Vulnerability{ 87 Id: "CVE-2023-38408", 88 Summary: "CVE-2023-38408", 89 Details: "CVE-2023-38408", 90 Affected: []*osvpb.Affected{{ 91 Package: &osvpb.Package{ 92 Name: "openssh", 93 }, 94 Severity: []*osvpb.Severity{{ 95 Type: osvpb.Severity_CVSS_V3, 96 Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 97 }}, 98 Ranges: []*osvpb.Range{{ 99 Type: osvpb.Range_ECOSYSTEM, 100 Events: []*osvpb.Event{{Fixed: "9.3.p2"}}, 101 }}, 102 }}, 103 DatabaseSpecific: dbSpecific, 104 }, 105 }}} 106 } 107 108 func isVersionWithinRange(openSSHVersion string, lower string, upper string) (bool, error) { 109 lessEq, err1 := versionLessEqual(lower, openSSHVersion) 110 greaterEq, err2 := versionLessEqual(openSSHVersion, upper) 111 112 return lessEq && greaterEq, errors.Join(err1, err2) 113 } 114 115 // Scan checks for the presence of the OpenSSH CVE-2023-38408 vulnerability on the filesystem. 116 func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) { 117 // 1. OpenSSH between and 5.5 and 9.3p1 (inclusive) 118 openSSHVersion := getOpenSSHVersion(ctx) 119 if openSSHVersion == "" { 120 log.Debugf("No OpenSSH version found") 121 return inventory.Finding{}, nil 122 } 123 isVulnVersion, err := isVersionWithinRange(openSSHVersion, "5.5", "9.3p1") 124 125 if err != nil { 126 return inventory.Finding{}, err 127 } 128 129 if !isVulnVersion { 130 log.Debugf("Version %q not vuln", openSSHVersion) 131 return inventory.Finding{}, nil 132 } 133 log.Debugf("Found OpenSSH in range 5.5 to 9.3p1 (inclusive): %v", openSSHVersion) 134 135 // 2. Check ssh config 136 var configsWithForward []fileLocations 137 for _, path := range findSSHConfigs() { 138 ls := sshConfigContainsForward(path) 139 log.Debugf("ssh config: %q %v", path, ls) 140 if len(ls) > 0 { 141 configsWithForward = append(configsWithForward, fileLocations{Path: path, LineNumbers: ls}) 142 log.Debugf("Found ForwardConfig in %s in line(s): %v", path, ls) 143 } 144 } 145 146 // 3. Socket present 147 socketFiles, err := filepath.Glob("/tmp/ssh-*/agent.*") 148 if err != nil { 149 // The only possible returned error is ErrBadPattern, when pattern is malformed 150 return inventory.Finding{}, fmt.Errorf("filepath.Glob(\"/tmp/ssh-*/agent.*\"): %w", err) 151 } 152 socketExists := len(socketFiles) > 0 153 if socketExists { 154 log.Debugf("Found Socket at: %v", socketFiles) 155 } 156 157 // 4. check bash history 158 var historyLocations []fileLocations 159 for _, path := range findHistoryFiles() { 160 ls := findString(path, sshRegex) 161 log.Debugf("history file: %q %v", path, ls) 162 if len(ls) > 0 { 163 historyLocations = append(historyLocations, fileLocations{Path: path, LineNumbers: ls}) 164 log.Debugf("Found \"ssh .*-A\" in history file %s in line(s): %v", path, ls) 165 } 166 } 167 168 var locations []string 169 for _, l := range configsWithForward { 170 locations = append(locations, l.Path) 171 } 172 for _, l := range historyLocations { 173 locations = append(locations, l.Path) 174 } 175 locations = append(locations, socketFiles...) 176 177 dbSpecific := &structpb.Struct{ 178 Fields: map[string]*structpb.Value{ 179 "extra": {Kind: &structpb.Value_StringValue{StringValue: buildExtra(isVulnVersion, configsWithForward, socketFiles, historyLocations, locations)}}, 180 }, 181 } 182 // TODO: b/421456154 - Add package information to the finding. 183 return d.findingForPackage(dbSpecific, nil), nil 184 } 185 186 func getOpenSSHVersion(ctx context.Context) string { 187 cmd := exec.CommandContext(ctx, "ssh", "-V") 188 out, err := cmd.CombinedOutput() 189 log.Debugf("ssh -V stdout: %s", string(out)) 190 if err != nil { 191 log.Errorf("Command \"ssh -V\": %v", err) 192 return "" 193 } 194 195 matches := openSSHVersionRegex.FindStringSubmatch(string(out)) 196 if len(matches) >= 2 { 197 return matches[1] 198 } 199 return "" 200 } 201 202 func buildExtra(isVulnVersion bool, configsWithForward []fileLocations, socketFiles []string, historyLocations []fileLocations, targetLocations []string) string { 203 list := []bool{isVulnVersion, len(configsWithForward) > 0, len(socketFiles) > 0, len(historyLocations) > 0} 204 var slist []string 205 for _, l := range list { 206 if l { 207 slist = append(slist, "1") 208 } else { 209 slist = append(slist, "0") 210 } 211 } 212 return strings.Join(slist, ":") + "\nLocations:\n" + strings.Join(targetLocations, ",") 213 } 214 215 func fileExists(path string) bool { 216 _, err := os.Stat(path) 217 return !os.IsNotExist(err) 218 } 219 220 func findSSHConfigs() []string { 221 var r []string 222 223 if fileExists("/root/.ssh/config") { 224 r = append(r, "/root/.ssh/config") 225 } 226 227 matches, err := filepath.Glob("/home/*/.ssh/config") 228 if err != nil { 229 log.Errorf("filepath.Glob(\"/home/*/.ssh/config\"): %v", err) 230 } else { 231 r = append(r, matches...) 232 } 233 234 if fileExists("/etc/ssh/ssh_config") { 235 r = append(r, "/etc/ssh/ssh_config") 236 } 237 238 return r 239 } 240 241 // sshConfigContainsForward returns the line number (0 indexed) of all "ForwardAgent yes" found. 242 func sshConfigContainsForward(path string) []int { 243 f, err := os.Open(path) 244 if err != nil { 245 log.Warnf("sshConfigContainsForward(%q): %v", path, err) 246 return nil 247 } 248 defer f.Close() 249 250 scanner := bufio.NewScanner(f) 251 var r []int 252 i := -1 253 for scanner.Scan() { 254 i++ 255 line := scanner.Text() 256 line = strings.TrimSpace(line) 257 if strings.HasPrefix(line, "#") { 258 continue 259 } 260 if forwardAgentRegex.MatchString(strings.ToLower(line)) { 261 r = append(r, i) 262 } 263 } 264 265 return r 266 } 267 268 type fileLocations struct { 269 Path string 270 LineNumbers []int 271 } 272 273 func versionLessEqual(lower, upper string) (bool, error) { 274 // Version format looks like this: 3.7.1p2, 3.7, 3.2.3, 2.9p2 275 r, err := semantic.MustParse(lower, "Packagist").CompareStr(upper) 276 277 return r <= 0, err 278 } 279 280 func findHistoryFiles() []string { 281 pHistory, err := filepath.Glob("/home/*/.*history") 282 if err != nil { 283 log.Errorf("filepath.Glob(\"/home/*/.*history\"): %v", err) 284 } 285 pHistfile, err := filepath.Glob("/home/*/.histfile") 286 if err != nil { 287 log.Errorf("filepath.Glob(\"/home/*/.histfile\"): %v", err) 288 } 289 pRootHistory, err := filepath.Glob("/root/.*history") 290 if err != nil { 291 log.Errorf("filepath.Glob(\"/root/.*history\"): %v", err) 292 } 293 pRootHistfile, err := filepath.Glob("/root/.histfile") 294 if err != nil { 295 log.Errorf("filepath.Glob(\"/root/.histfile\"): %v", err) 296 } 297 return append(append(append(pHistory, pHistfile...), pRootHistory...), pRootHistfile...) 298 } 299 300 func findString(path string, re *regexp.Regexp) []int { 301 f, err := os.Open(path) 302 if err != nil { 303 log.Warnf("findString(%q, %v): %v", path, re, err) 304 return nil 305 } 306 defer f.Close() 307 308 scanner := bufio.NewScanner(f) 309 var r []int 310 i := -1 311 for scanner.Scan() { 312 i++ 313 line := scanner.Text() 314 line = strings.TrimSpace(line) 315 if re.MatchString(line) { 316 r = append(r, i) 317 } 318 } 319 320 return r 321 }