srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/jslibscan/main.go285
1 files changed, 285 insertions, 0 deletions
diff --git a/plugins/jslibscan/main.go b/plugins/jslibscan/main.go
new file mode 100644
index 0000000..0bbcc94
--- /dev/null
+++ b/plugins/jslibscan/main.go
@@ -0,0 +1,285 @@
+// Command jslibscan is a reference mitmux plugin - a Retire.js-style
+// passive scanner for known-vulnerable JavaScript library versions.
+// Unlike authcheck and paramminer, this one never sends anything: it
+// only reads response bodies already captured by ordinary proxying and
+// checks any JS library version string it can find against a small
+// built-in table of known-bad ranges, tagging a match "jslibscan:hit".
+// Demonstrating a purely passive plugin (subscribe, inspect, tag - no
+// repeat calls at all) alongside the two active ones is deliberate: it's
+// the simplest possible plugin shape, and the one least likely to
+// surprise anyone running it against traffic they can't afford to probe.
+//
+// The built-in table is a small, illustrative starting set (five
+// libraries, one well-known vulnerable-version threshold each) - NOT a
+// maintained vulnerability feed. Real Retire.js pulls from a
+// continuously updated JSON database with dozens of libraries and many
+// more precise version ranges; replicating that here would mean
+// committing to keep it current, which this reference plugin doesn't.
+// Extending libraries is adding one entry to the libraries slice below.
+//
+// Speaks the wire protocol directly, no internal/ipc import - see
+// plugins/authcheck's package doc for why, and PLUGINS.md for the
+// protocol.
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+type request struct {
+ Type string `json:"type"`
+ ID int64 `json:"id,omitempty"`
+ TagPlugin string `json:"tag_plugin,omitempty"`
+ Tag string `json:"tag,omitempty"`
+ TagData string `json:"tag_data,omitempty"`
+}
+
+type summary struct {
+ ID int64 `json:"id"`
+ Source string `json:"source"`
+}
+
+type entryDetail struct {
+ summary
+ ResponseRaw []byte `json:"response_raw"`
+}
+
+type response struct {
+ Type string `json:"type"`
+ New *summary `json:"new,omitempty"`
+ Detail *entryDetail `json:"detail,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type client struct {
+ conn net.Conn
+ enc *json.Encoder
+ dec *json.Decoder
+}
+
+func dial(path string) (*client, error) {
+ conn, err := net.Dial("unix", path)
+ if err != nil {
+ return nil, err
+ }
+ return &client{conn: conn, enc: json.NewEncoder(conn), dec: json.NewDecoder(conn)}, nil
+}
+
+func (c *client) call(req request) (response, error) {
+ if err := c.enc.Encode(req); err != nil {
+ return response{}, err
+ }
+ var resp response
+ if err := c.dec.Decode(&resp); err != nil {
+ return response{}, err
+ }
+ if resp.Type == "error" {
+ return response{}, fmt.Errorf("%s", resp.Error)
+ }
+ return resp, nil
+}
+
+func defaultSocketPath() string {
+ if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" {
+ return filepath.Join(rt, "mitmux.sock")
+ }
+ cfg, err := os.UserConfigDir()
+ if err != nil {
+ return "mitmux.sock"
+ }
+ return filepath.Join(cfg, "mitmux", "mitmux.sock")
+}
+
+// library is one entry in the built-in illustrative table: match finds
+// a version string for the library (its first capture group is the
+// dotted version number, e.g. "3.4.1"), and any version strictly below
+// fixedIn is flagged.
+type library struct {
+ name string
+ match *regexp.Regexp
+ fixedIn [3]int
+ advice string
+}
+
+// The 0-to-15-character non-greedy gap between a library's name and its
+// version digits (rather than a fixed one-character separator) is
+// deliberate - confirmed directly, not assumed: real-world version
+// strings show up as "jQuery v1.8.3" (space-then-"v", two separator
+// characters, not one), "jquery-3.4.1.min.js" (filename form), and
+// "jquery.min.js?v=1.11.0" (cache-busting query string) alike. Kept
+// bounded and non-greedy rather than wide open, so it can't walk past
+// the library's own version into an unrelated number elsewhere on the
+// page.
+var libraries = []library{
+ {
+ name: "jQuery",
+ match: regexp.MustCompile(`(?i)jquery.{0,15}?(\d+\.\d+\.\d+)`),
+ fixedIn: [3]int{3, 5, 0},
+ advice: "known cross-site scripting vulnerability in HTML parsing/prefiltering fixed in 3.5.0",
+ },
+ {
+ name: "Lodash",
+ match: regexp.MustCompile(`(?i)lodash.{0,15}?(\d+\.\d+\.\d+)`),
+ fixedIn: [3]int{4, 17, 21},
+ advice: "known prototype pollution / command injection vulnerabilities fixed in 4.17.21",
+ },
+ {
+ name: "Handlebars",
+ match: regexp.MustCompile(`(?i)handlebars.{0,15}?(\d+\.\d+\.\d+)`),
+ fixedIn: [3]int{4, 7, 7},
+ advice: "known prototype pollution vulnerability fixed in 4.7.7",
+ },
+ {
+ name: "Moment.js",
+ match: regexp.MustCompile(`(?i)moment(?:\.js)?.{0,15}?(\d+\.\d+\.\d+)`),
+ fixedIn: [3]int{2, 29, 4},
+ advice: "known path traversal / ReDoS vulnerabilities fixed in 2.29.4",
+ },
+ {
+ name: "AngularJS",
+ match: regexp.MustCompile(`(?i)angular(?:js|\.js)?.{0,15}?(1\.\d+\.\d+)`),
+ fixedIn: [3]int{1, 8, 3},
+ advice: "AngularJS 1.x reached end of life; known sandbox-escape/XSS issues, no fixes past 1.8.3",
+ },
+}
+
+type hit struct {
+ Library string `json:"library"`
+ VersionFound string `json:"version_found"`
+ FirstFixedIn string `json:"first_fixed_in"`
+ Advisory string `json:"advisory"`
+}
+
+func main() {
+ socketPath := flag.String("socket", "", "daemon control socket path (default: same as mitmux itself)")
+ pluginName := flag.String("name", "jslibscan", "name this plugin tags entries as")
+ flag.Parse()
+
+ path := *socketPath
+ if path == "" {
+ path = defaultSocketPath()
+ }
+
+ actor, err := dial(path)
+ if err != nil {
+ log.Fatalf("dial %s: %v", path, err)
+ }
+ defer actor.conn.Close()
+
+ subConn, err := net.Dial("unix", path)
+ if err != nil {
+ log.Fatalf("dial %s (subscribe): %v", path, err)
+ }
+ defer subConn.Close()
+ if err := json.NewEncoder(subConn).Encode(request{Type: "subscribe"}); err != nil {
+ log.Fatalf("subscribe: %v", err)
+ }
+
+ log.Printf("jslibscan: watching live traffic on %s", path)
+ dec := json.NewDecoder(subConn)
+ for {
+ var resp response
+ if err := dec.Decode(&resp); err != nil {
+ log.Fatalf("subscribe feed closed: %v", err)
+ }
+ if resp.Type != "new" || resp.New == nil {
+ continue
+ }
+ // Not a correctness concern here the way it is for authcheck/
+ // paramminer (this plugin never calls repeat, so there's no
+ // loop risk) - just avoids redundant work rescanning response
+ // bodies that are probably near-identical to an original
+ // request's, which is what most of a wordlist-driven probe's
+ // own resends look like.
+ if resp.New.Source != "proxy" {
+ continue
+ }
+ if err := scanEntry(actor, *pluginName, resp.New.ID); err != nil {
+ log.Printf("entry #%d: %v", resp.New.ID, err)
+ }
+ }
+}
+
+func scanEntry(c *client, pluginName string, id int64) error {
+ resp, err := c.call(request{Type: "get", ID: id})
+ if err != nil {
+ return fmt.Errorf("get: %w", err)
+ }
+ if resp.Detail == nil || len(resp.Detail.ResponseRaw) == 0 {
+ return nil
+ }
+ body := string(resp.Detail.ResponseRaw)
+
+ var hits []hit
+ for _, lib := range libraries {
+ m := lib.match.FindStringSubmatch(body)
+ if m == nil {
+ continue
+ }
+ found, ok := parseVersion(m[1])
+ if !ok || !isBelow(found, lib.fixedIn) {
+ continue
+ }
+ hits = append(hits, hit{
+ Library: lib.name,
+ VersionFound: m[1],
+ FirstFixedIn: joinVersion(lib.fixedIn),
+ Advisory: lib.advice,
+ })
+ }
+ if len(hits) == 0 {
+ return nil
+ }
+
+ data, err := json.Marshal(hits)
+ if err != nil {
+ return fmt.Errorf("marshal hits: %w", err)
+ }
+ if _, err := c.call(request{Type: "tag_entry", ID: id, TagPlugin: pluginName, Tag: "jslibscan:hit", TagData: string(data)}); err != nil {
+ return fmt.Errorf("tag_entry: %w", err)
+ }
+ names := make([]string, len(hits))
+ for i, h := range hits {
+ names[i] = fmt.Sprintf("%s %s", h.Library, h.VersionFound)
+ }
+ log.Printf("#%d -> outdated JS librar(y/ies): %s", id, strings.Join(names, ", "))
+ return nil
+}
+
+func parseVersion(s string) ([3]int, bool) {
+ parts := strings.SplitN(s, ".", 3)
+ if len(parts) != 3 {
+ return [3]int{}, false
+ }
+ var v [3]int
+ for i, p := range parts {
+ n, err := strconv.Atoi(p)
+ if err != nil {
+ return [3]int{}, false
+ }
+ v[i] = n
+ }
+ return v, true
+}
+
+func isBelow(v, fixedIn [3]int) bool {
+ for i := 0; i < 3; i++ {
+ if v[i] != fixedIn[i] {
+ return v[i] < fixedIn[i]
+ }
+ }
+ return false // exactly equal to the fixed version - not vulnerable
+}
+
+func joinVersion(v [3]int) string {
+ return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2])
+}