github.com/google/osv-scalibr@v0.4.1/detector/cve/untested/cve202233891/cve202233891.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 cve202233891 implements a detector for CVE-2022-33891.
    16  // To test, install a vulnerable pyspark version: python3 -m pip install pyspark==3.2.1
    17  // Run the spark-shell: spark-shell --conf spark.acls.enable=true
    18  // If spark-shell crashes with an error, change your java version to an old one: sudo update-alternatives --config java (JAVA 11 works)
    19  // Run this detector.
    20  package cve202233891
    21  
    22  import (
    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  type sparkUIPackageNames struct {
    48  	packageType      string
    49  	name             string
    50  	affectedVersions []string
    51  }
    52  
    53  const (
    54  	// Name of the detector.
    55  	Name = "cve/cve-2022-33891"
    56  
    57  	defaultTimeout = 5 * time.Second
    58  )
    59  
    60  var (
    61  	seededRand        = rand.New(rand.NewSource(time.Now().UnixNano()))
    62  	sparkServersPorts = []int{4040, 8080}
    63  	sparkUIPackages   = []sparkUIPackageNames{
    64  		{
    65  			packageType: "pypi",
    66  			name:        "pyspark",
    67  			affectedVersions: []string{
    68  				"3.0.0",
    69  				"3.0.1",
    70  				"3.0.2",
    71  				"3.0.3",
    72  				"3.1.1",
    73  				"3.1.2",
    74  				"3.2.0",
    75  				"3.2.1",
    76  			},
    77  		},
    78  	}
    79  )
    80  
    81  // Detector is a SCALIBR Detector for CVE-2022-33891.
    82  type Detector struct{}
    83  
    84  // New returns a detector.
    85  func New() detector.Detector {
    86  	return &Detector{}
    87  }
    88  
    89  // Name of the detector.
    90  func (Detector) Name() string { return Name }
    91  
    92  // Version of the detector.
    93  func (Detector) Version() int { return 0 }
    94  
    95  // Requirements of the detector.
    96  func (Detector) Requirements() *plugin.Capabilities {
    97  	return &plugin.Capabilities{OS: plugin.OSLinux, DirectFS: true, RunningSystem: true}
    98  }
    99  
   100  // RequiredExtractors returns  the list of OS package extractors needed to detect
   101  // the presence of the pyspark package in various OSes.
   102  func (Detector) RequiredExtractors() []string {
   103  	return []string{wheelegg.Name}
   104  }
   105  
   106  // DetectedFinding returns generic vulnerability information about what is detected.
   107  func (d Detector) DetectedFinding() inventory.Finding {
   108  	return d.findingForPackage(nil, nil)
   109  }
   110  
   111  func (Detector) findingForPackage(dbSpecific *structpb.Struct, pkg *extractor.Package) inventory.Finding {
   112  	pySparkPkg := &extractor.Package{
   113  		Name:     "pyspark",
   114  		PURLType: "pypi",
   115  	}
   116  	return inventory.Finding{PackageVulns: []*inventory.PackageVuln{{
   117  		Package: pkg,
   118  		Vulnerability: &osvpb.Vulnerability{
   119  			Id:      "CVE-2022-33891",
   120  			Summary: "CVE-2022-33891",
   121  			Details: "CVE-2022-33891",
   122  			Affected: inventory.PackageToAffected(pySparkPkg, "3.2.2", &osvpb.Severity{
   123  				Type:  osvpb.Severity_CVSS_V3,
   124  				Score: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
   125  			}),
   126  			DatabaseSpecific: dbSpecific,
   127  		},
   128  	}}}
   129  }
   130  
   131  // Scan scans for the vulnerability, doh!
   132  func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) {
   133  	sparkUIVersion, pkg, affectedVersions := findApacheSparkUIPackage(px)
   134  	if sparkUIVersion == "" {
   135  		log.Debugf("No Apache Spark UI version found")
   136  		return inventory.Finding{}, nil
   137  	}
   138  
   139  	isVulnVersion := false
   140  	for _, affected := range affectedVersions {
   141  		if sparkUIVersion == affected {
   142  			isVulnVersion = true
   143  		}
   144  	}
   145  
   146  	if !isVulnVersion {
   147  		log.Infof("Version %q not vuln", sparkUIVersion)
   148  		return inventory.Finding{}, nil
   149  	}
   150  	log.Infof("Found Potentially vulnerable Apache Spark UI version %v", sparkUIVersion)
   151  
   152  	vulnerable := false
   153  	for _, port := range sparkServersPorts {
   154  		randFilePath := "/tmp/" + randomString(16)
   155  		testCmd := "touch%20" + randFilePath
   156  		retCode := sparkUIHTTPQuery(ctx, "127.0.0.1", port, testCmd)
   157  		// We expect to receive a 403 error
   158  		if retCode != 403 {
   159  			log.Infof("Version %q not vuln (HTTP query didn't return 403: %v)", sparkUIVersion, retCode)
   160  			continue
   161  		}
   162  
   163  		if fileExists(scanRoot.FS, randFilePath) {
   164  			err := os.Remove(randFilePath)
   165  			if err != nil {
   166  				log.Infof("Error when removing file %v: %v", randFilePath, err)
   167  			}
   168  			log.Infof("File %v found, this server is vulnerable. Removing the file now", randFilePath)
   169  			vulnerable = true
   170  			break
   171  		}
   172  		log.Infof("Version %q not vuln (Temp file not found)", sparkUIVersion)
   173  	}
   174  	if !vulnerable {
   175  		return inventory.Finding{}, nil
   176  	}
   177  
   178  	dbSpecific := &structpb.Struct{
   179  		Fields: map[string]*structpb.Value{
   180  			"extra": {Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("%s %s %s", pkg.Name, pkg.Version, strings.Join(pkg.Locations, ", "))}},
   181  		},
   182  	}
   183  	return d.findingForPackage(dbSpecific, pkg), nil
   184  }
   185  
   186  func sparkUIHTTPQuery(ctx context.Context, sparkDomain string, sparkPort int, cmdExec string) int {
   187  	client := &http.Client{Timeout: defaultTimeout}
   188  
   189  	targetURL := fmt.Sprintf("http://%s/?doAs=`%s`", net.JoinHostPort(sparkDomain, strconv.Itoa(sparkPort)), cmdExec)
   190  	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
   191  	req.Header.Add("Accept", "*/*")
   192  	resp, err := client.Do(req)
   193  
   194  	if err != nil {
   195  		log.Infof("Error when sending request %s to the server", targetURL)
   196  		return 0
   197  	}
   198  
   199  	defer resp.Body.Close()
   200  
   201  	return resp.StatusCode
   202  }
   203  
   204  func findApacheSparkUIPackage(px *packageindex.PackageIndex) (string, *extractor.Package, []string) {
   205  	for _, r := range sparkUIPackages {
   206  		pkg := px.GetSpecific(r.name, r.packageType)
   207  		for _, p := range pkg {
   208  			return p.Version, p, r.affectedVersions
   209  		}
   210  	}
   211  	return "", nil, []string{}
   212  }
   213  
   214  func fileExists(filesys scalibrfs.FS, path string) bool {
   215  	_, err := fs.Stat(filesys, path)
   216  	return !os.IsNotExist(err)
   217  }
   218  
   219  func randomString(length int) string {
   220  	charSet := "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
   221  	b := make([]byte, length)
   222  	for i := range b {
   223  		b[i] = charSet[seededRand.Intn(len(charSet)-1)]
   224  	}
   225  	return string(b)
   226  }