# frozen_string_literal: true # # Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # Ronin is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ronin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ronin. If not, see . # require 'ronin/cli/command' require 'ronin/core/cli/logging' require 'ronin/support/network/ssl' require 'command_kit/options/verbose' module Ronin class CLI module Commands # # A `netcat` clone written in Ruby using the [async-io] gem. # # [async-io]: https://github.com/socketry/async-io#readme # # ## Usage # # [options] [--tcp | --udp | --ssl | --tls] {HOST PORT | -l [HOST] PORT | --unix PATH} # # ## Options # # -v, --verbose Enables verbose output # --tcp Uses the TCP protocol # --udp Uses the UDP protocol # -U, --unix PATH Uses the UNIX socket protocol # -l, --listen Listens for incoming connections # -s, --source HOST Source address to bind to # -p, --source-port PORT Source port to bind to # -b, --buffer-size INT Buffer size to use (Default: 4096) # -x, --hexdump Hexdumps each message that is received # --ssl Enables SSL mode # --tls Enables TLS mode # --ssl-version 1|1.1|1.2 Specifies the required SSL version # --ssl-cert FILE Specifies the SSL certificate file # --ssl-key FILE Specifies the SSL key file # --ssl-verify none|peer|fail-if-no-peer-cert|client-once|true|false # SSL verification mode # --ssl-ca-bundle PATH Path to the file or directory of CA certificates # -h, --help Print help information # # ## Arguments # # [HOST] The host to connect to or listen on # [POST] The port to connect to # class Netcat < Command include Core::CLI::Logging include CommandKit::Options::Verbose usage '[options] [--tcp | --udp | --ssl | --tls] {HOST PORT | -l [HOST] PORT | --unix PATH}' option :tcp, desc: 'Uses the TCP protocol' do @protocol = :tcp end option :udp, desc: 'Uses the UDP protocol' do @protocol = :udp end option :unix, short: '-U', value: { type: String, usage: 'PATH' }, desc: 'Uses the UNIX socket protocol' do @protocol = :unix end option :listen, short: '-l', desc: 'Listens for incoming connections' do @mode = :listen end option :source, short: '-s', value: { type: String, usage: 'HOST' }, desc: 'Source address to bind to' option :source_port, short: '-p', value: { type: Integer, usage: 'PORT' }, desc: 'Source port to bind to' option :buffer_size, short: '-b', value: { type: Integer, default: 4096 }, desc: 'Buffer size to use' option :hexdump, short: '-x', desc: 'Hexdumps each message that is received' option :ssl, desc: 'Enables SSL mode' option :tls, desc: 'Enables TLS mode' do options[:ssl_version] = 1.2 end option :ssl_version, value: { type: Support::Network::SSL::VERSIONS.keys }, desc: 'Specifies the required SSL version' option :ssl_cert, value: { type: String, usage: 'FILE' }, desc: 'Specifies the SSL certificate file' option :ssl_key, value: { type: String, usage: 'FILE' }, desc: 'Specifies the SSL key file' option :ssl_verify, value: { type: Support::Network::SSL::VERIFY.transform_keys { |key| key.to_s.tr('_','-') } }, desc: 'SSL verification mode' option :ssl_ca_bundle, value: { type: String, usage: 'PATH' }, desc: 'Path to the file or directory of CA certificates' argument :host, required: false, desc: 'The host to connect to or listen on' argument :post, required: false, desc: 'The port to connect to' description 'A netcat clone command' man_page 'ronin-netcat.1' # The protocol to use. # # @return [:tcp, :udp, :unix] attr_reader :protocol # Whether to connect or listen for connections. # # @return [:connect, :listen] attr_reader :mode # # Initializes the command. # # @param [Hash{Symbol => Object}] kwargs # Additional keyword arguments. # def initialize(**kwargs) super(**kwargs) @protocol = :tcp @mode = :connect end # # Runs the `ronin netcat` command. # # @param [Array] args # Additional command-line arguments. # def run(*args) if options[:hexdump] require 'hexdump' @hexdump = Hexdump::Hexdump.new end case @mode when :connect @host, @port = *args unless @host print_error "host argument required" exit(-1) end unless @port print_error "port argument required" exit(-1) end if options[:verbose] if @protocol == :unix log_info "Connecting to #{options[:unix]} ..." else log_info "Connecting to #{@host}:#{@port} ..." end end load_async client_loop when :listen case args.length when 0 @port = options.fetch(:port,0) @host = nil when 1 @port = args[0].to_i @host = nil when 2 @host = args[0] @port = args[1].to_i end if options[:verbose] if @protocol == :unix log_info "Listening on #{options[:unix]} ..." else if @host log_info "Listening #{@host}:#{@port} ..." else log_info "Listening port #{@port} ..." end end end load_async server_loop end end # # Loads the async-io library. # def load_async require 'async/notification' require 'async/io' require 'async/io/stream' end # # Creates an SSL context. # # @return [Ronin::Support::Network::SSL] # def ssl_context Support::Network::SSL.context( version: options[:ssl_version], verify: options[:ssl_verify], key_file: options[:ssl_key], cert_file: options[:ssl_cert], ca_bundle: options[:ssl_ca_bundle] ) end # # Creates the async endpoint object. # # @return [Async::IO::Endpoint] # def async_endpoint case @protocol when :tcp then Async::IO::Endpoint.tcp(@host,@port) when :udp then Async::IO::Endpoint.udp(@host,@port) when :unix then Async::IO::Endpoint.unix(options[:unix]) when :ssl Async::IO::Endpoint.ssl(@host,@port, hostname: @host, ssl_context: ssl_context) end end # # Creates the async stdin stream. # # @return [Async::IO::Stream] # def async_stdin Async::IO::Stream.new(Async::IO::Generic.new(self.stdin)) end # # The client event loop. # def client_loop finished = Async::Notification.new endpoint = async_endpoint stdin = async_stdin buffer_size = options[:buffer_size] Async do |task| socket = begin endpoint.connect rescue StandardError => error print_error(error.message) exit(1) end stream = Async::IO::Stream.new(socket) begin client = task.async do while (data = stream.read_partial(buffer_size)) print_data(data) end rescue EOFError # ignore EOFError ensure finished.signal end user = task.async do while (data = stdin.read_partial(buffer_size)) socket.write(data) end rescue EOFError # ignore EOFError ensure finished.signal end finished.wait ensure client.stop user.stop socket.close end end end # # The server event loop. # def server_loop finished = Async::Notification.new endpoint = async_endpoint stdin = async_stdin clients = [] buffer_size = options[:buffer_size] Async do |task| endpoint.accept do |socket| if options[:verbose] log_info "Client #{socket} connected" end clients << socket stream = Async::IO::Stream.new(socket) begin while (data = stream.read_partial(buffer_size)) print_data(data) end rescue EOFError # ignore EOFError end clients.delete(socket) if options[:verbose] log_warn "Client #{socket} disconnected" end end task.async do while (data = stdin.read_partial(buffer_size)) clients.each { |client| client.write(data) } end rescue EOFError # ignore EOFError ensure finished.signal end finished.wait rescue StandardError => error print_error(error.message) exit(1) ensure clients.each(&:close) end end # # Prints or hexdumps data to stdout. # # @param [String] data # The data to print or hexdump. # def print_data(data) if @hexdump @hexdump.hexdump(data) else print(data) end end end end end end