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_file, #initialize, #inspect, #pack, #save_to, #set_comment

Constructor Details

This class inherits a constructor from Rex::Zip::Archive

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

Instance Method Details

#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


109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/rex/zip/jar.rb', line 109

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

#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).



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rex/zip/jar.rb', line 37

def build_manifest(opts={})
	main_class = 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 << "\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



70
71
72
# File 'lib/rex/zip/jar.rb', line 70

def length
	pack.length
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:



153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
# File 'lib/rex/zip/jar.rb', line 153

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



63
64
65
# File 'lib/rex/zip/jar.rb', line 63

def to_s
	pack
end