Class: SSH_Utils

Inherits:
Object
  • Object
show all
Defined in:
lib/audit/lib/ssh_utils.rb

Overview

require ssh-keygen command line program

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ SSH_Utils

Returns a new instance of SSH_Utils.



9
10
11
12
13
14
15
# File 'lib/audit/lib/ssh_utils.rb', line 9

def initialize(options)
  if options[:logger] then
    @logger = options[:logger]
  else
    @logger = Logger.new(STDOUT)
  end
end

Instance Method Details

#convert_to_ssh(openssh_key) ⇒ Object

Get the SSH key in standard SSH format from the OpenSSH format

Parameters:

  • openssh_key

    Key in OpenSSH format as string

Returns:

  • Key in SSH (RFC) format as string



132
133
134
135
136
137
138
139
140
# File 'lib/audit/lib/ssh_utils.rb', line 132

def convert_to_ssh(openssh_key)
  tempfile = Tempfile.new('convert_')
  tempfile << openssh_key; tempfile.flush()
  
  ssh_key = `ssh-keygen -e -f #{tempfile.path()}`
  tempfile.close!()
  
  return ssh_key
end

#enable_root_login(host, username, ssh_options, public_key) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/audit/lib/ssh_utils.rb', line 152

def (host, username, ssh_options, public_key)
  ssh_options = {:paranoid => false}.merge(ssh_options)
  root_prefix = get_root_prefix(host, username, ssh_options)
  return if root_prefix.nil?()
  
  Net::SSH.start(host, username, ssh_options) do|conn|
    conn.exec!(root_prefix + "sh -c 'grep -v PermitRootLogin /etc/ssh/sshd_config > /tmp/sshd_config; echo \"PermitRootLogin without-password\" >> /tmp/sshd_config; mv /tmp/sshd_config /etc/ssh/sshd_config'")
    conn.exec!(root_prefix + "kill -SIGHUP $( ps -A -o pid -o cmd | grep /usr/sbin/sshd | grep -v grep | awk '{print $1}')")
  end
  
  Net::SSH.start(host, username, ssh_options) do|conn|
    conn.exec!(root_prefix + "sh -c 'echo \"#{public_key}\" > ${HOME}/.ssh/authorized_keys'") 
  end
end

#find_root_method(host, username, ssh_options) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/audit/lib/ssh_utils.rb', line 95

def find_root_method(host, username, ssh_options)
  ssh_options = {:paranoid => false}.merge(ssh_options)
  Net::SSH.start(host, username, ssh_options) do|conn|
    output = conn.exec!("id --user 2>/dev/null")
    return :ALREADY_ROOT if output.strip() == "0"
    
    output = conn.exec!("sudo -n id --user 2>/dev/null")
    return :SUDO if output && output.strip() == "0"
  end
  
  return :NONE
end

#get_public_key(private_key, key_name = "") ⇒ Object



125
126
127
# File 'lib/audit/lib/ssh_utils.rb', line 125

def get_public_key(private_key, key_name = "")
  return (`ssh-keygen -y -f #{private_key}`.strip() + " " + key_name).strip()
end

#get_root_prefix(host, username, ssh_options) ⇒ Object



109
110
111
112
113
114
115
116
117
118
# File 'lib/audit/lib/ssh_utils.rb', line 109

def get_root_prefix(host, username, ssh_options)
  root_method = find_root_method(host, username, ssh_options)
  
  case root_method
  when :NONE then return nil
  when :SUDO then return "sudo -H -n "
  when :ALREADY_ROOT then return ""
  else return nil
  end
end

#guess_username(host, ssh_options, possible_usernames, timeout = 5) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/audit/lib/ssh_utils.rb', line 28

def guess_username(host, ssh_options, possible_usernames, timeout = 5)
  ssh_options = {:paranoid => false}.merge(ssh_options)
  possible_usernames.each do|username|
    begin
      timeout(timeout) do
        Net::SSH.start(host, username, ssh_options) do|conn|
          output = conn.exec!("id --user --name")
          if output.strip() == username then
            @logger.info {"Found user name '#{username}'\n"}
            return username
          else
            @logger.debug {"User name '#{username}' logs in but does not execute commands\n"} 
          end
        end
      end
    rescue Net::SSH::AuthenticationFailed => ex
      @logger.debug {"Authentication with user name '#{username}' failed\n"}
    rescue Timeout::Error => err
      @logger.debug {"Authentication with user name '#{username}' timed out\n"}
    end
  end
  
  return nil
end

#parse_nmap_ssh_keydata(keydata) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/audit/lib/ssh_utils.rb', line 255

def parse_nmap_ssh_keydata(keydata)
  line_type = :fingerprint_line
  current_key = nil
  keys = []
  keydata.each_line do|line|
    if line_type == :fingerprint_line then
      match = /^([0-9]+) ([0-9a-f:]+) (\([A-Z0-9]+\))$/.match(line)
      if match && !current_key.nil?() then
        #something is desynchronized, skip the key
        puts "Unexpected input at ssh host key data: '#{keydata}'"
        break
      elsif match && current_key.nil?() then
        current_key = { :length => match[1], :fingerprint => match[2], :type => match[3] }
      else
        puts "Unexpected line at ssh host key data, expected fingerprint data: '#{keydata}'"
        break
      end
      line_type = :key_line
    else
      match = /^([a-z0-9_-]+) ([A-Za-z0-9+\/=]+)$/.match(line)
      if match && current_key.nil?() then
        puts "Unexpected line at ssh host key data, expected key data: '#{keydata}'"
        break
      elsif match && !current_key.nil?() then
        current_key[:ssh_type] = match[1]
        current_key[:key] = match[2]
        keys << current_key
        current_key = nil
      else
        puts "Unexpected input at ssh host key data, expected key data: '#{keydata}'"
        break
      end
      line_type = :fingerprint_line
    end
  end
  return keys
end

#start_tunnel(host, private_key_file, local_tun_num = 0, remote_tun_num = 0, local_ip = "172.16.0.1", remote_ip = "172.16.0.2", ssh_options = {}) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/audit/lib/ssh_utils.rb', line 183

def start_tunnel(host, private_key_file, local_tun_num = 0, remote_tun_num = 0, local_ip = "172.16.0.1", remote_ip = "172.16.0.2", ssh_options = {})
  username = 'root' # tunneling only works with root user, so assume root here
  result = false
  ssh_tunnel_process = "bla" # just any value to initialize the variable and assign the scope
  
  # change ssh server configuration to permit tunneling
  Net::SSH.start(host, username, {:keys => [private_key_file], :paranoid => false}.merge(ssh_options)) do|conn|
    conn.exec!("grep -v PermitTunnel /etc/ssh/sshd_config > /tmp/sshd_config; echo \"PermitTunnel yes\" >> /tmp/sshd_config; mv /tmp/sshd_config /etc/ssh/sshd_config")
    conn.exec!("kill -SIGHUP $( ps -A -o pid -o cmd | grep /usr/sbin/sshd | grep -v grep | awk '{print $1}')")
  end
  
  @logger.debug {"Modified SSH server configuration for tunneling"}
  
  # open the tunnel and configure
  Net::SSH.start(host, username, {:keys => [private_key_file], :paranoid => false}.merge(ssh_options)) do|conn|
    
    ssh_tunnel_cmd = "ssh -NT -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=30 -w #{local_tun_num}:#{remote_tun_num} -i #{private_key_file} #{username}@#{host}"
    @logger.debug {"LOCAL: Starting tunneling SSH process: '#{ssh_tunnel_cmd}'"}
    ssh_tunnel_process = IO.popen(ssh_tunnel_cmd)
    # configure tunneling interfaces
    sleep(3) #some delay because otherwise tun devices are not configured correctly
    remote_tun_conf_cmd = "ifconfig tun#{remote_tun_num} #{local_ip} #{remote_ip} netmask 255.255.255.0 2>/dev/null 1>/dev/null"
    @logger.debug {"REMOTE: configuring tun interface: '#{remote_tun_conf_cmd}'"}
    conn.exec!(remote_tun_conf_cmd)
    
    local_tun_conf_cmd = "ifconfig tun#{local_tun_num} #{remote_ip} #{local_ip} netmask 255.255.255.0 2>/dev/null 1>/dev/null"
    @logger.debug {"LOCAL: configuring tun interface: '#{local_tun_conf_cmd}'"}
    system(local_tun_conf_cmd)
    # check that the connection works
    ping_cmd = "ping -c1 #{remote_ip} 2>/dev/null 1>/dev/null"
    @logger.debug {"LOCAL: pinging remote interface: '#{ping_cmd}'"}
    result = system(ping_cmd)
  end
  
  return {:success => result, 
          :tunnel_server_pid => ssh_tunnel_process.pid(), 
          :remote_ip => remote_ip, 
          :local_ip => local_ip, 
          :remote_tun_interface => "tun#{remote_tun_num}",
          :local_tun_interface => "tun#{local_tun_num}"}
end

#stop_tunnel(tunnel_hash) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/audit/lib/ssh_utils.rb', line 230

def stop_tunnel(tunnel_hash)
  # first try to end gently
  begin
    Process::kill('TERM', tunnel_hash[:tunnel_server_pid])
  rescue => err
  end
  #check if it really ended
  begin
    Process::kill('TERM', tunnel_hash[:tunnel_server_pid])
  rescue Errno::ESRCH => err
    return
  rescue => err
  end
  
  #if not, wait a second and then try the hard way
  sleep(1)
  begin
    Process::kill('KILL', tunnel_hash[:tunnel_server_pid])
  rescue Errno::ESRCH => err
    return
  rescue => err
  end
end

#wait_for_server(host, port = 22, interval = 20, max_retries = 5) ⇒ Object

Wait for a server to be online.

Parameters:

  • host

    The host name to poll for a service.

  • port (defaults to: 22)

    The service port to poll.

  • interval (defaults to: 20)

    The wait time between two connection tries in seconds

  • max_retries (defaults to: 5)

    The maximum number of connection retries

Returns:

  • :OK if server is reachable, :CONNECTION_REFUSED if the maximum number of retries was reached and the connection was refused, or :TIMED_OUT if either the overall timeout was reached or the maximum number of retries was reached and the connection timed out. May also return :UNKNOWN_ERROR on unknown error.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/audit/lib/ssh_utils.rb', line 61

def wait_for_server(host, port = 22, interval = 20, max_retries = 5)
  for tries in 0 ... max_retries
    begin
      sock = TCPSocket.new(host, port)
      return :OK
    rescue Errno::ECONNREFUSED => ex
      return :CONNECTION_REFUSED if tries >= max_retries - 1
      @logger.debug {"Connection timed out, apparently server has not finished booting yet ... waiting #{interval}s\n"}
      sleep interval
    rescue Errno::ETIMEDOUT => ex
      return :TIMED_OUT if tries >= max_retries - 1
      @logger.debug {"Connection timed out, apparently server has not finished booting yet ... waiting #{interval}s\n"}
      sleep interval
    rescue Errno::EHOSTUNREACH => err
      return :TIMED_OUT if tries >= max_retries - 1
      @logger.debug {"No route to host, routes have not yet been set up ... waiting #{interval}s\n"}
      sleep interval
    end
  end
  
  return :UNKNOWN_ERROR
end