mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 22:40:54 -07:00
modules/recon/joomla-config-exposure.yaml flags an exposed configuration.php backup through the JConfig class paired with the password property, so a generic php class is not reported, then extracts the database password. modules/recon/drupal-config-exposure.yaml flags an exposed settings.php backup through the databases array paired with a literal password value, so an array that lacks the marker or resolves the password from the environment is not reported, then extracts the password. modules/recon/magento-config-exposure.yaml flags an exposed app/etc/env.php backup through the crypt or mode marker paired with a literal key or password value, so a generic return array or a cloud placeholder is not reported, then extracts the crypt key. internal/modules/cms_config_exposure_test.go drives the three modules end to end through ExecuteHTTPModule and asserts the leak alongside the near misses a strict review wants pinned: a config missing its password, a generic php class, an array without the databases marker, a databases array with no password, an env indirection password, a return array without a magento marker, a magento config with no credential, a cloud placeholder key, an html tutorial for each file, a plain 200 and a 404. verify: go test ./internal/modules, each matcher and extractor proven to bite (break -> red, restore -> green).
206 lines
8.5 KiB
Go
206 lines
8.5 KiB
Go
package modules_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dropalldatabases/sif/internal/modules"
|
|
)
|
|
|
|
func runCMSCfgModule(t *testing.T, file string, status int, body string) *modules.Result {
|
|
t.Helper()
|
|
def, err := modules.ParseYAMLModule(file)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", file, err)
|
|
}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
|
Timeout: 5 * time.Second,
|
|
Threads: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("execute %s: %v", file, err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func cmsCfgExtract(res *modules.Result, key string) string {
|
|
for _, f := range res.Findings {
|
|
if v := f.Extracted[key]; v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestCMSConfigExposureModules(t *testing.T) {
|
|
const joomla = "../../modules/recon/joomla-config-exposure.yaml"
|
|
const drupal = "../../modules/recon/drupal-config-exposure.yaml"
|
|
const magento = "../../modules/recon/magento-config-exposure.yaml"
|
|
|
|
joomlaConfig := "<?php\nclass JConfig {\n\tpublic $offline = '0';\n" +
|
|
"\tpublic $host = 'localhost';\n\tpublic $user = 'joomla_user';\n" +
|
|
"\tpublic $password = 'S3cretJoomlaPass';\n\tpublic $db = 'joomla_db';\n" +
|
|
"\tpublic $dbprefix = 'jos_';\n\tpublic $secret = 'AbCdEfGhIjKlMnOp';\n}\n"
|
|
|
|
drupalConfig := "<?php\n$databases['default']['default'] = array (\n" +
|
|
" 'database' => 'drupal_db',\n 'username' => 'drupal_user',\n" +
|
|
" 'password' => 'S3cretDrupalPass',\n 'host' => 'localhost',\n" +
|
|
" 'driver' => 'mysql',\n);\n$settings['hash_salt'] = 'longrandomhashsalt';\n"
|
|
|
|
magentoConfig := "<?php\nreturn [\n 'backend' => ['frontName' => 'admin_x7y'],\n" +
|
|
" 'crypt' => ['key' => 'a1b2c3d4e5f6g7h8'],\n 'db' => [\n" +
|
|
" 'connection' => ['default' => [\n 'host' => 'localhost',\n" +
|
|
" 'dbname' => 'magento',\n 'username' => 'magento_user',\n" +
|
|
" 'password' => 'S3cretMagentoPass',\n ]],\n ],\n 'MAGE_MODE' => 'production',\n];\n"
|
|
|
|
t.Run("an exposed joomla configuration leaks the password", func(t *testing.T) {
|
|
res := runCMSCfgModule(t, joomla, 200, joomlaConfig)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a joomla finding")
|
|
}
|
|
if v := cmsCfgExtract(res, "joomla_password"); v != "S3cretJoomlaPass" {
|
|
t.Errorf("joomla_password=%q, want S3cretJoomlaPass", v)
|
|
}
|
|
})
|
|
|
|
t.Run("an exposed drupal settings leaks the password", func(t *testing.T) {
|
|
res := runCMSCfgModule(t, drupal, 200, drupalConfig)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a drupal finding")
|
|
}
|
|
if v := cmsCfgExtract(res, "drupal_password"); v != "S3cretDrupalPass" {
|
|
t.Errorf("drupal_password=%q, want S3cretDrupalPass", v)
|
|
}
|
|
})
|
|
|
|
t.Run("an exposed magento env leaks the crypt key", func(t *testing.T) {
|
|
res := runCMSCfgModule(t, magento, 200, magentoConfig)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a magento finding")
|
|
}
|
|
if v := cmsCfgExtract(res, "magento_crypt_key"); v != "a1b2c3d4e5f6g7h8" {
|
|
t.Errorf("magento_crypt_key=%q, want a1b2c3d4e5f6g7h8", v)
|
|
}
|
|
})
|
|
|
|
t.Run("a joomla config missing the password is not flagged", func(t *testing.T) {
|
|
body := "<?php\nclass JConfig {\n\tpublic $host = 'localhost';\n" +
|
|
"\tpublic $db = 'joomla_db';\n\tpublic $dbprefix = 'jos_';\n}\n"
|
|
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a config without a password should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a php class with a public password but no jconfig is not joomla", func(t *testing.T) {
|
|
body := "<?php\nclass MyAuth {\n\tpublic $password = 'changeme';\n" +
|
|
"\tpublic $username = 'admin';\n}\n"
|
|
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a generic class should not match joomla, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a php array with a password but no databases is not drupal", func(t *testing.T) {
|
|
body := "<?php\n$config = array('password' => 'x', 'host' => 'y');\n"
|
|
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a generic array should not match drupal, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a drupal databases array without a password is not flagged", func(t *testing.T) {
|
|
body := "<?php\n$databases['default']['default'] = array (\n" +
|
|
" 'database' => 'drupal_db',\n 'host' => 'localhost',\n 'driver' => 'mysql',\n);\n"
|
|
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a databases array without a password should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a php return array with a password but no magento markers is not flagged", func(t *testing.T) {
|
|
body := "<?php\nreturn ['db' => ['password' => 'secret', 'host' => 'localhost']];\n"
|
|
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a generic return array should not match magento, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a magento config without a credential is not flagged", func(t *testing.T) {
|
|
body := "<?php\nreturn ['MAGE_MODE' => 'production', 'db' => ['host' => 'localhost']];\n"
|
|
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a magento config without a credential should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("an html page demonstrating a joomla config is not a leak", func(t *testing.T) {
|
|
body := "<!DOCTYPE html><html><body><pre>class JConfig { public $password = 'x'; public $db = 'y'; }</pre></body></html>"
|
|
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("an html joomla tutorial should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a drupal settings using env indirection is not a literal password leak", func(t *testing.T) {
|
|
body := "<?php\n$databases['default']['default'] = array (\n" +
|
|
" 'database' => 'drupal_db',\n 'username' => 'drupal_user',\n" +
|
|
" 'password' => getenv('DB_PASS'),\n 'host' => 'localhost',\n);\n"
|
|
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("env indirection should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a magento env with a cloud placeholder key is not a literal leak", func(t *testing.T) {
|
|
body := "<?php\nreturn ['crypt' => ['key' => '#env.CRYPT_KEY#'], 'MAGE_MODE' => 'production'];\n"
|
|
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("a cloud placeholder should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a magento env with a placeholder key but a literal password is flagged not mis-extracted", func(t *testing.T) {
|
|
body := "<?php\nreturn ['crypt' => ['key' => '#env.CRYPT_KEY#'],\n" +
|
|
" 'db' => ['connection' => ['default' => ['password' => 'RealDbPass']]],\n" +
|
|
" 'MAGE_MODE' => 'production'];\n"
|
|
res := runCMSCfgModule(t, magento, 200, body)
|
|
if len(res.Findings) == 0 {
|
|
t.Fatal("expected a magento finding on the literal password")
|
|
}
|
|
if v := cmsCfgExtract(res, "magento_crypt_key"); v == "#env.CRYPT_KEY#" {
|
|
t.Errorf("extractor surfaced the placeholder %q as the crypt key", v)
|
|
}
|
|
})
|
|
|
|
t.Run("an html page demonstrating a drupal config is not a leak", func(t *testing.T) {
|
|
body := "<!DOCTYPE html><html><body><pre>$databases['default']['default'] = array('password' => 'x');</pre></body></html>"
|
|
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("an html drupal tutorial should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("an html page demonstrating a magento config is not a leak", func(t *testing.T) {
|
|
body := "<!DOCTYPE html><html><body><pre>'crypt' => ['key' => 'x'], 'MAGE_MODE' => 'production'</pre></body></html>"
|
|
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
|
|
t.Errorf("an html magento tutorial should not match, got %d findings", len(res.Findings))
|
|
}
|
|
})
|
|
|
|
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
|
for _, file := range []string{joomla, drupal, magento} {
|
|
if res := runCMSCfgModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
|
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("a 404 is not a leak", func(t *testing.T) {
|
|
for _, file := range []string{joomla, drupal, magento} {
|
|
if res := runCMSCfgModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
|
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
|
}
|
|
}
|
|
})
|
|
}
|