|
| 1 | +# -*- coding: binary -*- |
| 2 | +# |
| 3 | + |
| 4 | +module Rex |
| 5 | +module Parser |
| 6 | + |
| 7 | +# This is a parser for the Windows Group Policy Preferences file |
| 8 | +# format. It's used by modules/post/windows/gather/credentials/gpp.rb |
| 9 | +# and uses REXML (as opposed to Nokogiri) for its XML parsing. |
| 10 | +# See: http://msdn.microsoft.com/en-gb/library/cc232587.aspx |
| 11 | +class GPP |
| 12 | + require 'rex' |
| 13 | + require 'rexml/document' |
| 14 | + |
| 15 | + def self.parse(data) |
| 16 | + if data.nil? |
| 17 | + return [] |
| 18 | + end |
| 19 | + |
| 20 | + xml = REXML::Document.new(data).root |
| 21 | + results = [] |
| 22 | + |
| 23 | + unless xml and xml.elements and xml.elements.to_a("//Properties") |
| 24 | + return [] |
| 25 | + end |
| 26 | + |
| 27 | + xml.elements.to_a("//Properties").each do |node| |
| 28 | + epassword = node.attributes['cpassword'] |
| 29 | + next if epassword.to_s.empty? |
| 30 | + password = self.decrypt(epassword) |
| 31 | + |
| 32 | + user = node.attributes['runAs'] if node.attributes['runAs'] |
| 33 | + user = node.attributes['accountName'] if node.attributes['accountName'] |
| 34 | + user = node.attributes['username'] if node.attributes['username'] |
| 35 | + user = node.attributes['userName'] if node.attributes['userName'] |
| 36 | + user = node.attributes['newName'] unless node.attributes['newName'].nil? || node.attributes['newName'].empty? |
| 37 | + changed = node.parent.attributes['changed'] |
| 38 | + |
| 39 | + # Printers and Shares |
| 40 | + path = node.attributes['path'] |
| 41 | + |
| 42 | + # Datasources |
| 43 | + dsn = node.attributes['dsn'] |
| 44 | + driver = node.attributes['driver'] |
| 45 | + |
| 46 | + # Tasks |
| 47 | + app_name = node.attributes['appName'] |
| 48 | + |
| 49 | + # Services |
| 50 | + service = node.attributes['serviceName'] |
| 51 | + |
| 52 | + # Groups |
| 53 | + expires = node.attributes['expires'] |
| 54 | + never_expires = node.attributes['neverExpires'] |
| 55 | + disabled = node.attributes['acctDisabled'] |
| 56 | + |
| 57 | + result = { |
| 58 | + :USER => user, |
| 59 | + :PASS => password, |
| 60 | + :CHANGED => changed |
| 61 | + } |
| 62 | + |
| 63 | + result.merge!({ :EXPIRES => expires }) unless expires.nil? || expires.empty? |
| 64 | + result.merge!({ :NEVER_EXPIRES => never_expires.to_i }) unless never_expires.nil? || never_expires.empty? |
| 65 | + result.merge!({ :DISABLED => disabled.to_i }) unless disabled.nil? || disabled.empty? |
| 66 | + result.merge!({ :PATH => path }) unless path.nil? || path.empty? |
| 67 | + result.merge!({ :DATASOURCE => dsn }) unless dsn.nil? || dsn.empty? |
| 68 | + result.merge!({ :DRIVER => driver }) unless driver.nil? || driver.empty? |
| 69 | + result.merge!({ :TASK => app_name }) unless app_name.nil? || app_name.empty? |
| 70 | + result.merge!({ :SERVICE => service }) unless service.nil? || service.empty? |
| 71 | + |
| 72 | + attributes = [] |
| 73 | + node.elements.each('//Attributes//Attribute') do |dsn_attribute| |
| 74 | + attributes << { |
| 75 | + :A_NAME => dsn_attribute.attributes['name'], |
| 76 | + :A_VALUE => dsn_attribute.attributes['value'] |
| 77 | + } |
| 78 | + end |
| 79 | + |
| 80 | + result.merge!({ :ATTRIBUTES => attributes }) unless attributes.empty? |
| 81 | + |
| 82 | + results << result |
| 83 | + end |
| 84 | + |
| 85 | + results |
| 86 | + end |
| 87 | + |
| 88 | + def self.create_tables(results, filetype, domain=nil, dc=nil) |
| 89 | + tables = [] |
| 90 | + results.each do |result| |
| 91 | + table = Rex::Ui::Text::Table.new( |
| 92 | + 'Header' => 'Group Policy Credential Info', |
| 93 | + 'Indent' => 1, |
| 94 | + 'SortIndex' => -1, |
| 95 | + 'Columns' => |
| 96 | + [ |
| 97 | + 'Name', |
| 98 | + 'Value', |
| 99 | + ] |
| 100 | + ) |
| 101 | + |
| 102 | + table << ["TYPE", filetype] |
| 103 | + table << ["USERNAME", result[:USER]] |
| 104 | + table << ["PASSWORD", result[:PASS]] |
| 105 | + table << ["DOMAIN CONTROLLER", dc] unless dc.nil? || dc.empty? |
| 106 | + table << ["DOMAIN", domain] unless domain.nil? || domain.empty? |
| 107 | + table << ["CHANGED", result[:CHANGED]] |
| 108 | + table << ["EXPIRES", result[:EXPIRES]] unless result[:EXPIRES].nil? || result[:EXPIRES].empty? |
| 109 | + table << ["NEVER_EXPIRES?", result[:NEVER_EXPIRES]] unless result[:NEVER_EXPIRES].nil? |
| 110 | + table << ["DISABLED", result[:DISABLED]] unless result[:DISABLED].nil? |
| 111 | + table << ["PATH", result[:PATH]] unless result[:PATH].nil? || result[:PATH].empty? |
| 112 | + table << ["DATASOURCE", result[:DSN]] unless result[:DSN].nil? || result[:DSN].empty? |
| 113 | + table << ["DRIVER", result[:DRIVER]] unless result[:DRIVER].nil? || result[:DRIVER].empty? |
| 114 | + table << ["TASK", result[:TASK]] unless result[:TASK].nil? || result[:TASK].empty? |
| 115 | + table << ["SERVICE", result[:SERVICE]] unless result[:SERVICE].nil? || result[:SERVICE].empty? |
| 116 | + |
| 117 | + unless result[:ATTRIBUTES].nil? || result[:ATTRIBUTES].empty? |
| 118 | + result[:ATTRIBUTES].each do |dsn_attribute| |
| 119 | + table << ["ATTRIBUTE", "#{dsn_attribute[:A_NAME]} - #{dsn_attribute[:A_VALUE]}"] |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + tables << table |
| 124 | + end |
| 125 | + |
| 126 | + tables |
| 127 | + end |
| 128 | + |
| 129 | + # Decrypts passwords using Microsoft's published key: |
| 130 | + # http://msdn.microsoft.com/en-us/library/cc422924.aspx |
| 131 | + def self.decrypt(encrypted_data) |
| 132 | + unless encrypted_data |
| 133 | + return "" |
| 134 | + end |
| 135 | + |
| 136 | + password = "" |
| 137 | + padding = "=" * (4 - (encrypted_data.length % 4)) |
| 138 | + epassword = "#{encrypted_data}#{padding}" |
| 139 | + decoded = Rex::Text.decode_base64(epassword) |
| 140 | + |
| 141 | + key = "\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" |
| 142 | + aes = OpenSSL::Cipher::Cipher.new("AES-256-CBC") |
| 143 | + begin |
| 144 | + aes.decrypt |
| 145 | + aes.key = key |
| 146 | + plaintext = aes.update(decoded) |
| 147 | + plaintext << aes.final |
| 148 | + password = plaintext.unpack('v*').pack('C*') # UNICODE conversion |
| 149 | + rescue OpenSSL::Cipher::CipherError => e |
| 150 | + puts "Unable to decode: \"#{encrypted_data}\" Exception: #{e}" |
| 151 | + end |
| 152 | + |
| 153 | + password |
| 154 | + end |
| 155 | + |
| 156 | +end |
| 157 | +end |
| 158 | +end |
| 159 | + |
0 commit comments