Class: Rex::Zip::Jar
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
-
#manifest ⇒ Object
Returns the value of attribute manifest.
Attributes inherited from Archive
Instance Method Summary collapse
-
#add_files(files, path, base_dir = "") ⇒ Object
Add multiple files from an array.
-
#build_manifest(opts = {}) ⇒ Object
Create a MANIFEST.MF file based on the current Archive#entries.
-
#length ⇒ Object
Length of the compressed blob.
-
#sign(key, cert, ca_certs = nil) ⇒ Object
Add a signature to this jar given a
key
and acert
. - #to_s ⇒ Object
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
#manifest ⇒ Object
Returns the value of attribute manifest.
16 17 18 |
# File 'lib/rex/zip/jar.rb', line 16 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
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/rex/zip/jar.rb', line 108 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).
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/rex/zip/jar.rb', line 36 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 |
#length ⇒ Object
Length of the compressed blob
69 70 71 |
# File 'lib/rex/zip/jar.rb', line 69 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.
Links
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
152 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 |
# File 'lib/rex/zip/jar.rb', line 152 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_s ⇒ Object
62 63 64 |
# File 'lib/rex/zip/jar.rb', line 62 def to_s pack end |