Class: Rex::Zip::Jar

Inherits:
Archive show all
Defined in:
lib/rex/zip/jar.rb

Overview

A Jar is a zip archive containing Java class files and a MANIFEST.MF listing those classes. Several variations exist based on the same idea of class files inside a zip, most notably:

  • WAR files store XML files, Java classes, JSPs and other stuff for servlet-based webservers (e.g.: Tomcat and Glassfish)

  • APK files are Android Package files

Instance Attribute Summary collapse

Attributes inherited from Archive

#entries

Instance Method Summary collapse

Methods inherited from Archive

#add_r, #inspect, #pack, #save_to, #set_comment

Constructor Details

#initializeJar

Returns a new instance of Jar


25
26
27
28
# File 'lib/rex/zip/jar.rb', line 25

def initialize
  @substitutions = {}
  super
end

Instance Attribute Details

#manifestObject

Returns the value of attribute manifest


17
18
19
# File 'lib/rex/zip/jar.rb', line 17

def manifest
  @manifest
end

#substitutionsHash

The substitutions to apply when randomizing. Randomization is designed to be used in packages and/or classes names.

Returns:

  • (Hash)

23
24
25
# File 'lib/rex/zip/jar.rb', line 23

def substitutions
  @substitutions
end

Instance Method Details

#add_file(fname, fdata = nil, xtra = nil, comment = nil) ⇒ Object

Adds a file to the JAR, randomizing the file name and the contents.

See Also:


242
243
244
# File 'lib/rex/zip/jar.rb', line 242

def add_file(fname, fdata=nil, xtra=nil, comment=nil)
  super(randomize(fname), randomize(fdata), xtra, comment)
end

#add_files(files, path, base_dir = "") ⇒ Object

Add multiple files from an array

files should be structured like so:

[
  [ "path", "to", "file1" ],
  [ "path", "to", "file2" ]
]

and path should be the location on the file system to find the files to add. base_dir will be prepended to the path inside the jar.

Example:

war = Rex::Zip::Jar.new
war.add_file("WEB-INF/", '')
war.add_file("WEB-INF/web.xml", web_xml)
war.add_file("WEB-INF/classes/", '')
files = [
  [ "servlet", "examples", "HelloWorld.class" ],
  [ "Foo.class" ],
  [ "servlet", "Bar.class" ],
]
war.add_files(files, "./class_files/", "WEB-INF/classes/")

The above code would create a jar with the following structure from files found in ./class_files/ :

+- WEB-INF/
  +- web.xml
  +- classes/
    +- Foo.class
    +- servlet/
      +- Bar.class
      +- examples/
        +- HelloWorld.class

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/rex/zip/jar.rb', line 123

def add_files(files, path, base_dir="")
  files.each do |file|
    # Add all of the subdirectories if they don't already exist
    1.upto(file.length - 1) do |idx|
      full = base_dir + file[0,idx].join("/") + "/"
      if !(entries.map{|e|e.name}.include?(full))
        add_file(full, '')
      end
    end
    # Now add the actual file, grabbing data from the filesystem
    fd = File.open(File.join( path, file ), "rb")
    data = fd.read(fd.stat.size)
    fd.close
    add_file(base_dir + file.join("/"), data)
  end
end

#add_sub(str, bad = '') ⇒ String

Adds a substitution to have into account when randomizing. Substitutions must be added immediately after #initialize.

Parameters:

  • str (String)

    String to substitute. It's designed to randomize class and/or package names.

  • bad (String) (defaults to: '')

    String containing bad characters to avoid when applying substitutions.

Returns:

  • (String)

    The substitution which will be used when randomizing.


254
255
256
257
258
259
260
# File 'lib/rex/zip/jar.rb', line 254

def add_sub(str, bad = '')
  if @substitutions.key?(str)
    return @substitutions[str]
  end

  @substitutions[str] = Rex::Text.rand_text_alpha(str.length, bad)
end

#build_manifest(opts = {}) ⇒ Object

Create a MANIFEST.MF file based on the current Archive#entries.

See download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html for some explanation of the format.

Example MANIFEST.MF

Manifest-Version: 1.0
Main-Class: metasploit.Payload

Name: metasploit.dat
SHA1-Digest: WJ7cUVYUryLKfQFmH80/ADfKmwM=

Name: metasploit/Payload.class
SHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=

The SHA1-Digest lines are optional unless the jar is signed (see #sign).


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rex/zip/jar.rb', line 48

def build_manifest(opts={})
  main_class = (opts[:main_class] ? randomize(opts[:main_class]) : nil)
  app_name = (opts[:app_name] ? randomize(opts[:main_class]) : nil)
  existing_manifest = nil

  @manifest =  "Manifest-Version: 1.0\r\n"
  @manifest << "Main-Class: #{main_class}\r\n" if main_class
  @manifest << "Application-Name: #{app_name}\r\n" if app_name
  @manifest << "Permissions: all-permissions\r\n"
  @manifest << "\r\n"
  @entries.each { |e|
    next if e.name =~ %r|/$|
    if e.name == "META-INF/MANIFEST.MF"
      existing_manifest = e
      next
    end
    #next unless e.name =~ /\.class$/
    @manifest << "Name: #{e.name}\r\n"
    #@manifest << "SHA1-Digest: #{Digest::SHA1.base64digest(e.data)}\r\n"
    @manifest << "\r\n"
  }
  if existing_manifest
    existing_manifest.data = @manifest
  else
    add_file("META-INF/", '')
    add_file("META-INF/MANIFEST.MF", @manifest)
  end
end

#lengthObject

Length of the compressed blob


84
85
86
# File 'lib/rex/zip/jar.rb', line 84

def length
  pack.length
end

#randomize(str) ⇒ String

Randomizes an input by applying the `substitutions` available.

Parameters:

  • str (String)

    String to randomize.

Returns:

  • (String)

    The input `str` with all the possible `substitutions` applied.


267
268
269
270
271
272
273
274
275
276
277
# File 'lib/rex/zip/jar.rb', line 267

def randomize(str)
  return str if str.nil?

  random = str

  @substitutions.each do |orig, subs|
    random = str.gsub(orig, subs)
  end

  random
end

#sign(key, cert, ca_certs = nil) ⇒ Object

Add a signature to this jar given a key and a cert. cert should be an instance of OpenSSL::X509::Certificate and key is expected to be an instance of one of OpenSSL::PKey::DSA or OpenSSL::PKey::RSA.

This method aims to create signature files compatible with the jarsigner tool destributed with the JDK and any JVM should accept the resulting jar.

Signature contents

Modifies the META-INF/MANIFEST.MF entry adding SHA1-Digest attributes in each Name section. The signature consists of two files, a .SF and a .DSA (or .RSA if signing with an RSA key). The .SF file is similar to the manifest with Name sections but the SHA1-Digest is not optional. The difference is in what gets hashed for the SHA1-Digest line – in the manifest, it is the file's contents, in the .SF, it is the file's section in the manifest (including trailing newline!). The .DSA/.RSA file is a PKCS7 signature of the .SF file contents.

A short description of the format: download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Signed%20JAR%20File

Some info on importing a private key into a keystore which is not directly supported by keytool for some unfathomable reason www.agentbob.info/agentbob/79-AB.html

Raises:


167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/rex/zip/jar.rb', line 167

def sign(key, cert, ca_certs=nil)
  m = self.entries.find { |e| e.name == "META-INF/MANIFEST.MF" }
  raise RuntimeError.new("Jar has no manifest") unless m

  ca_certs ||= [ cert ]

  new_manifest = ''
  sigdata =  "Signature-Version: 1.0\r\n"
  sigdata << "Created-By: 1.6.0_18 (Sun Microsystems Inc.)\r\n"
  sigdata << "\r\n"

  # Grab the sections of the manifest
  files = m.data.split(/\r?\n\r?\n/)
  if files[0] =~ /Manifest-Version/
    # keep the header as is
    new_manifest << files[0]
    new_manifest << "\r\n\r\n"
    files = files[1,files.length]
  end

  # The file sections should now look like this:
  #  "Name: metasploit/Payload.class\r\nSHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=\r\n\r\n"
  files.each do |f|
    next unless f =~ /Name: (.*)/
    name = $1
    e = self.entries.find { |e| e.name == name }
    if e
      digest = OpenSSL::Digest::SHA1.digest(e.data)
      manifest_section =  "Name: #{name}\r\n"
      manifest_section << "SHA1-Digest: #{[digest].pack('m').strip}\r\n"
      manifest_section << "\r\n"

      manifest_digest = OpenSSL::Digest::SHA1.digest(manifest_section)

      sigdata << "Name: #{name}\r\n"
      sigdata << "SHA1-Digest: #{[manifest_digest].pack('m')}\r\n"
      new_manifest << manifest_section
    end
  end

  # Now overwrite with the new manifest
  m.data = new_manifest

  flags = 0
  flags |= OpenSSL::PKCS7::BINARY
  flags |= OpenSSL::PKCS7::DETACHED
  # SMIME and ATTRs are technically valid in the signature but they
  # both screw up the java verifier, so don't include them.
  flags |= OpenSSL::PKCS7::NOSMIMECAP
  flags |= OpenSSL::PKCS7::NOATTR

  signature = OpenSSL::PKCS7.sign(cert, key, sigdata, ca_certs, flags)
  sigalg = case key
    when OpenSSL::PKey::RSA; "RSA"
    when OpenSSL::PKey::DSA; "DSA"
    # Don't really know what to do if it's not DSA or RSA.  Can
    # OpenSSL::PKCS7 actually sign stuff with it in that case?
    # Regardless, the java spec says signatures can only be RSA,
    # DSA, or PGP, so just assume it's PGP and hope for the best
    else; "PGP"
    end

  # SIGNFILE is the default name in documentation.  MYKEY is probably
  # more common, though because that's what keytool defaults to.  We
  # can probably randomize this with no ill effects.
  add_file("META-INF/SIGNFILE.SF", sigdata)
  add_file("META-INF/SIGNFILE.#{sigalg}", signature.to_der)

  return true
end

#to_sObject


77
78
79
# File 'lib/rex/zip/jar.rb', line 77

def to_s
  pack
end