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