Commit a6f8d27e authored by Max Dymond's avatar Max Dymond Committed by Daniel Stenberg
Browse files

test1451: add SMB support to the testbed

Add test 1451 which does some very basic SMB testing using the impacket
SMB server.

Closes #1630
parent f1609155
Loading
Loading
Loading
Loading
+56 −0
Original line number Diff line number Diff line
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  Project                     ___| | | |  _ \| |
#                             / __| | | | |_) | |
#                            | (__| |_| |  _ <| |___
#                             \___|\___/|_| \_\_____|
#
# Copyright (C) 2017, Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.haxx.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
"""Module for extracting test data from the test data folder"""

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import os
import xml.etree.ElementTree as ET
import logging

log = logging.getLogger(__name__)


class TestData(object):
    def __init__(self, data_folder):
        self.data_folder = data_folder

    def get_test_data(self, test_number):
        # Create the test file name
        filename = os.path.join(self.data_folder,
                                "test{0}".format(test_number))

        # The user should handle the exception from failing to find the file.
        tree = ET.parse(filename)

        # We need the <reply><data> text.
        reply = tree.find("reply")
        data = reply.find("data")

        # Return the text contents of the data
        return data.text


if __name__ == '__main__':
    td = TestData("./data")
    data = td.get_test_data(1)
    print(data)
+1 −1
Original line number Diff line number Diff line
@@ -154,7 +154,7 @@ test1416 test1417 test1418 test1419 test1420 test1421 test1422 test1423 \
test1424 test1425 test1426 \
test1428 test1429 test1430 test1431 test1432 test1433 test1434 test1435 \
test1436 test1437 test1438 test1439 test1440 test1441 test1442 test1443 \
test1444 test1445 test1446                            test1450 \
test1444 test1445 test1446                            test1450 test1451 \
\
test1500 test1501 test1502 test1503 test1504 test1505 test1506 test1507 \
test1508 test1509 test1510 test1511 test1512 test1513 test1514 test1515 \

tests/data/test1451

0 → 100644
+36 −0
Original line number Diff line number Diff line
<testcase>
<info>
<keywords>
SMB
</keywords>
</info>

#
# Server-side
<reply>
<data>Basic SMB test complete</data>
</reply>

#
# Client-side
<client>
<server>
smb
</server>
<features>
smb
</features>
 <name>
Basic SMB request
 </name>
 <command>
-u 'curltest:curltest' smb://%HOSTIP:%SMBPORT/TESTS/1451
</command>
</client>

#
# Verify data after the test has been "shot"
<verify>
<stdout>Basic SMB test complete</stdout>
</verify>
</testcase>
+259 −507
Original line number Diff line number Diff line
@@ -38,8 +38,7 @@ import errno
import sys
import random
import shutil
import string
from binascii import unhexlify, hexlify
from binascii import hexlify

# For signing
from impacket import smb, nmb, ntlm, uuid, LOG
@@ -4167,250 +4166,3 @@ smb.SMB.TRANS_TRANSACT_NMPIPE :self.__smbTransHandler.transactNamedPipe
# For windows platforms, opening a directory is not an option, so we set a void FD
VOID_FILE_DESCRIPTOR = -1
PIPE_FILE_DESCRIPTOR = -2

######################################################################
# HELPER CLASSES
######################################################################

from impacket.dcerpc.v5.rpcrt import DCERPCServer
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5.srvs import NetrShareEnum, NetrShareEnumResponse, SHARE_INFO_1, NetrServerGetInfo, NetrServerGetInfoResponse, NetrShareGetInfo, NetrShareGetInfoResponse
from impacket.dcerpc.v5.wkst import NetrWkstaGetInfo, NetrWkstaGetInfoResponse
from impacket.system_errors import ERROR_INVALID_LEVEL

class WKSTServer(DCERPCServer):
    def __init__(self):
        DCERPCServer.__init__(self)
        self.wkssvcCallBacks = {
            0: self.NetrWkstaGetInfo,
        }
        self.addCallbacks(('6BFFD098-A112-3610-9833-46C3F87E345A', '1.0'),'\\PIPE\\wkssvc', self.wkssvcCallBacks)

    def NetrWkstaGetInfo(self,data):
        request = NetrWkstaGetInfo(data)
        self.log("NetrWkstaGetInfo Level: %d" % request['Level'])

        answer = NetrWkstaGetInfoResponse()

        if request['Level'] not in (100, 101):
            answer['ErrorCode'] = ERROR_INVALID_LEVEL
            return answer

        answer['WkstaInfo']['tag'] = request['Level']

        if request['Level'] == 100:
            # Windows. Decimal value 500.
            answer['WkstaInfo']['WkstaInfo100']['wki100_platform_id'] = 0x000001F4
            answer['WkstaInfo']['WkstaInfo100']['wki100_computername'] = NULL
            answer['WkstaInfo']['WkstaInfo100']['wki100_langroup'] = NULL
            answer['WkstaInfo']['WkstaInfo100']['wki100_ver_major'] = 5
            answer['WkstaInfo']['WkstaInfo100']['wki100_ver_minor'] = 0
        else:
            # Windows. Decimal value 500.
            answer['WkstaInfo']['WkstaInfo101']['wki101_platform_id'] = 0x000001F4
            answer['WkstaInfo']['WkstaInfo101']['wki101_computername'] = NULL
            answer['WkstaInfo']['WkstaInfo101']['wki101_langroup'] = NULL
            answer['WkstaInfo']['WkstaInfo101']['wki101_ver_major'] = 5
            answer['WkstaInfo']['WkstaInfo101']['wki101_ver_minor'] = 0
            answer['WkstaInfo']['WkstaInfo101']['wki101_lanroot'] = NULL

        return answer

class SRVSServer(DCERPCServer):
    def __init__(self):
        DCERPCServer.__init__(self)

        self._shares = {}
        self.__serverConfig = None
        self.__logFile = None

        self.srvsvcCallBacks = {
            15: self.NetrShareEnum,
            16: self.NetrShareGetInfo,
            21: self.NetrServerGetInfo,
        }

        self.addCallbacks(('4B324FC8-1670-01D3-1278-5A47BF6EE188', '3.0'),'\\PIPE\\srvsvc', self.srvsvcCallBacks)

    def setServerConfig(self, config):
        self.__serverConfig = config

    def processConfigFile(self, configFile=None):
       if configFile is not None:
           self.__serverConfig = ConfigParser.ConfigParser()
           self.__serverConfig.read(configFile)
       sections = self.__serverConfig.sections()
       # Let's check the log file
       self.__logFile      = self.__serverConfig.get('global','log_file')
       if self.__logFile != 'None':
            logging.basicConfig(filename = self.__logFile, 
                             level = logging.DEBUG, 
                             format="%(asctime)s: %(levelname)s: %(message)s", 
                             datefmt = '%m/%d/%Y %I:%M:%S %p')

       # Remove the global one
       del(sections[sections.index('global')])
       self._shares = {}
       for i in sections:
           self._shares[i] = dict(self.__serverConfig.items(i))

    def NetrShareGetInfo(self,data):
       request = NetrShareGetInfo(data)
       self.log("NetrGetShareInfo Level: %d" % request['Level'])

       s = request['NetName'][:-1].upper()
       answer = NetrShareGetInfoResponse()
       if self._shares.has_key(s):
           share  = self._shares[s]

           answer['InfoStruct']['tag'] = 1
           answer['InfoStruct']['ShareInfo1']['shi1_netname']= s+'\x00'
           answer['InfoStruct']['ShareInfo1']['shi1_type']   = share['share type']
           answer['InfoStruct']['ShareInfo1']['shi1_remark'] = share['comment']+'\x00' 
           answer['ErrorCode'] = 0
       else:
           answer['InfoStruct']['tag'] = 1
           answer['InfoStruct']['ShareInfo1']= NULL
           answer['ErrorCode'] = 0x0906 #WERR_NET_NAME_NOT_FOUND

       return answer

    def NetrServerGetInfo(self,data):
       request = NetrServerGetInfo(data)
       self.log("NetrServerGetInfo Level: %d" % request['Level'])
       answer = NetrServerGetInfoResponse()
       answer['InfoStruct']['tag'] = 101
       # PLATFORM_ID_NT = 500
       answer['InfoStruct']['ServerInfo101']['sv101_platform_id'] = 500
       answer['InfoStruct']['ServerInfo101']['sv101_name'] = request['ServerName']
       # Windows 7 = 6.1
       answer['InfoStruct']['ServerInfo101']['sv101_version_major'] = 6
       answer['InfoStruct']['ServerInfo101']['sv101_version_minor'] = 1
       # Workstation = 1
       answer['InfoStruct']['ServerInfo101']['sv101_type'] = 1
       answer['InfoStruct']['ServerInfo101']['sv101_comment'] = NULL
       answer['ErrorCode'] = 0
       return answer

    def NetrShareEnum(self, data):
       request = NetrShareEnum(data)
       self.log("NetrShareEnum Level: %d" % request['InfoStruct']['Level'])
       shareEnum = NetrShareEnumResponse()
       shareEnum['InfoStruct']['Level'] = 1
       shareEnum['InfoStruct']['ShareInfo']['tag'] = 1
       shareEnum['TotalEntries'] = len(self._shares)
       shareEnum['InfoStruct']['ShareInfo']['Level1']['EntriesRead'] = len(self._shares)
       shareEnum['ErrorCode'] = 0

       for i in self._shares:
           shareInfo = SHARE_INFO_1()
           shareInfo['shi1_netname'] = i+'\x00'
           shareInfo['shi1_type'] = self._shares[i]['share type']
           shareInfo['shi1_remark'] = self._shares[i]['comment']+'\x00'
           shareEnum['InfoStruct']['ShareInfo']['Level1']['Buffer'].append(shareInfo)

       return shareEnum

class SimpleSMBServer:
    """
    SimpleSMBServer class - Implements a simple, customizable SMB Server

    :param string listenAddress: the address you want the server to listen on
    :param integer listenPort: the port number you want the server to listen on
    :param string configFile: a file with all the servers' configuration. If no file specified, this class will create the basic parameters needed to run. You will need to add your shares manually tho. See addShare() method
    """
    def __init__(self, listenAddress = '0.0.0.0', listenPort=445, configFile=''):
        if configFile != '':
            self.__server = SMBSERVER((listenAddress,listenPort))
            self.__server.processConfigFile(configFile)
            self.__smbConfig = None
        else:
            # Here we write a mini config for the server
            self.__smbConfig = ConfigParser.ConfigParser()
            self.__smbConfig.add_section('global')
            self.__smbConfig.set('global','server_name',''.join([random.choice(string.letters) for _ in range(8)]))
            self.__smbConfig.set('global','server_os',''.join([random.choice(string.letters) for _ in range(8)])
)
            self.__smbConfig.set('global','server_domain',''.join([random.choice(string.letters) for _ in range(8)])
)
            self.__smbConfig.set('global','log_file','None')
            self.__smbConfig.set('global','rpc_apis','yes')
            self.__smbConfig.set('global','credentials_file','')
            self.__smbConfig.set('global', 'challenge', "A"*8)

            # IPC always needed
            self.__smbConfig.add_section('IPC$')
            self.__smbConfig.set('IPC$','comment','')
            self.__smbConfig.set('IPC$','read only','yes')
            self.__smbConfig.set('IPC$','share type','3')
            self.__smbConfig.set('IPC$','path','')
            self.__server = SMBSERVER((listenAddress,listenPort), config_parser = self.__smbConfig)
            self.__server.processConfigFile()

        # Now we have to register the MS-SRVS server. This specially important for 
        # Windows 7+ and Mavericks clients since they WONT (specially OSX) 
        # ask for shares using MS-RAP.

        self.__srvsServer = SRVSServer()
        self.__srvsServer.daemon = True
        self.__wkstServer = WKSTServer()
        self.__wkstServer.daemon = True
        self.__server.registerNamedPipe('srvsvc',('127.0.0.1',self.__srvsServer.getListenPort()))
        self.__server.registerNamedPipe('wkssvc',('127.0.0.1',self.__wkstServer.getListenPort()))

    def start(self):
        self.__srvsServer.start()
        self.__wkstServer.start()
        self.__server.serve_forever()

    def registerNamedPipe(self, pipeName, address):
        return self.__server.registerNamedPipe(pipeName, address)

    def unregisterNamedPipe(self, pipeName):
        return self.__server.unregisterNamedPipe(pipeName)

    def getRegisteredNamedPipes(self):
        return self.__server.getRegisteredNamedPipes()

    def addShare(self, shareName, sharePath, shareComment='', shareType = 0, readOnly = 'no'):
        self.__smbConfig.add_section(shareName)
        self.__smbConfig.set(shareName, 'comment', shareComment)
        self.__smbConfig.set(shareName, 'read only', readOnly)
        self.__smbConfig.set(shareName, 'share type', shareType)
        self.__smbConfig.set(shareName, 'path', sharePath)
        self.__server.setServerConfig(self.__smbConfig)
        self.__srvsServer.setServerConfig(self.__smbConfig)
        self.__server.processConfigFile()
        self.__srvsServer.processConfigFile()

    def removeShare(self, shareName):
        self.__smbConfig.remove_section(shareName)
        self.__server.setServerConfig(self.__smbConfig)
        self.__srvsServer.setServerConfig(self.__smbConfig)
        self.__server.processConfigFile()
        self.__srvsServer.processConfigFile()

    def setSMBChallenge(self, challenge):
        if challenge != '':
            self.__smbConfig.set('global', 'challenge', unhexlify(challenge))
            self.__server.setServerConfig(self.__smbConfig)
            self.__server.processConfigFile()
        
    def setLogFile(self, logFile):
        self.__smbConfig.set('global','log_file',logFile)
        self.__server.setServerConfig(self.__smbConfig)
        self.__server.processConfigFile()

    def setCredentialsFile(self, logFile):
        self.__smbConfig.set('global','credentials_file',logFile)
        self.__server.setServerConfig(self.__smbConfig)
        self.__server.processConfigFile()

    def setSMB2Support(self, value):
        if value is True:
            self.__smbConfig.set("global", "SMB2Support", "True")
        else:
            self.__smbConfig.set("global", "SMB2Support", "False")
        self.__server.setServerConfig(self.__smbConfig)
        self.__server.processConfigFile()
+159 −2
Original line number Diff line number Diff line
@@ -146,6 +146,8 @@ my $HTTPPIPEPORT; # HTTP pipelining port
my $HTTPUNIXPATH;        # HTTP server Unix domain socket path
my $HTTP2PORT;           # HTTP/2 server port
my $DICTPORT;            # DICT server port
my $SMBPORT;             # SMB server port
my $SMBSPORT;            # SMBS server port

my $srcdir = $ENV{'srcdir'} || '.';
my $CURL="../src/curl".exe_ext(); # what curl executable to run on the tests
@@ -380,7 +382,7 @@ sub init_serverpidfile_hash {
    }
  }
  for my $proto (('tftp', 'sftp', 'socks', 'ssh', 'rtsp', 'gopher', 'httptls',
                  'dict')) {
                  'dict', 'smb', 'smbs')) {
    for my $ipvnum ((4, 6)) {
      for my $idnum ((1, 2)) {
        my $serv = servername_id($proto, $ipvnum, $idnum);
@@ -1120,6 +1122,67 @@ sub verifysocks {
    return $pid;
}

#######################################################################
# Verify that the server that runs on $ip, $port is our server.  This also
# implies that we can speak with it, as there might be occasions when the
# server runs fine but we cannot talk to it ("Failed to connect to ::1: Can't
# assign requested address")
#
sub verifysmb {
    my ($proto, $ipvnum, $idnum, $ip, $port) = @_;
    my $server = servername_id($proto, $ipvnum, $idnum);
    my $pid = 0;
    my $time=time();
    my $extra="";

    my $verifylog = "$LOGDIR/".
        servername_canon($proto, $ipvnum, $idnum) .'_verify.log';
    unlink($verifylog) if(-f $verifylog);

    my $flags = "--max-time $server_response_maxtime ";
    $flags .= "--silent ";
    $flags .= "--verbose ";
    $flags .= "--globoff ";
    $flags .= "-u 'curltest:curltest' ";
    $flags .= $extra;
    $flags .= "\"$proto://$ip:$port/SERVER/verifiedserver\"";

    my $cmd = "$VCURL $flags 2>$verifylog";

    # check if this is our server running on this port:
    logmsg "RUN: $cmd\n" if($verbose);
    my @data = runclientoutput($cmd);

    my $res = $? >> 8; # rotate the result
    if($res & 128) {
        logmsg "RUN: curl command died with a coredump\n";
        return -1;
    }

    foreach my $line (@data) {
        if($line =~ /WE ROOLZ: (\d+)/) {
            # this is our test server with a known pid!
            $pid = 0+$1;
            last;
        }
    }
    if($pid <= 0 && @data && $data[0]) {
        # this is not a known server
        logmsg "RUN: Unknown server on our $server port: $port\n";
        return 0;
    }
    # we can/should use the time it took to verify the server as a measure
    # on how fast/slow this host is.
    my $took = int(0.5+time()-$time);

    if($verbose) {
        logmsg "RUN: Verifying our test $server server took $took seconds\n";
    }
    $ftpchecktime = $took>=1?$took:1; # make sure it never is below 1

    return $pid;
}

#######################################################################
# Verify that the server that runs on $ip, $port is our server.
# Retry over several seconds before giving up.  The ssh server in
@@ -1146,7 +1209,8 @@ my %protofunc = ('http' => \&verifyhttp,
                 'socks' => \&verifysocks,
                 'gopher' => \&verifyhttp,
                 'httptls' => \&verifyhttptls,
                 'dict' => \&verifyftp);
                 'dict' => \&verifyftp,
                 'smb' => \&verifysmb);

sub verifyserver {
    my ($proto, $ipvnum, $idnum, $ip, $port) = @_;
@@ -2243,6 +2307,83 @@ sub rundictserver {
    return ($dictpid, $pid2);
}

#######################################################################
# start the SMB server
#
sub runsmbserver {
    my ($verbose, $alt, $port) = @_;
    my $proto = "smb";
    my $ip = $HOSTIP;
    my $ipvnum = 4;
    my $idnum = 1;
    my $server;
    my $srvrname;
    my $pidfile;
    my $logfile;
    my $flags = "";

    if($alt eq "ipv6") {
        # No IPv6
    }

    $server = servername_id($proto, $ipvnum, $idnum);

    $pidfile = $serverpidfile{$server};

    # don't retry if the server doesn't work
    if ($doesntrun{$pidfile}) {
        return (0,0);
    }

    my $pid = processexists($pidfile);
    if($pid > 0) {
        stopserver($server, "$pid");
    }
    unlink($pidfile) if(-f $pidfile);

    $srvrname = servername_str($proto, $ipvnum, $idnum);

    $logfile = server_logfilename($LOGDIR, $proto, $ipvnum, $idnum);

    $flags .= "--verbose 1 " if($debugprotocol);
    $flags .= "--pidfile \"$pidfile\" --logfile \"$logfile\" ";
    $flags .= "--id $idnum " if($idnum > 1);
    $flags .= "--port $port --srcdir \"$srcdir\"";

    my $cmd = "$srcdir/smbserver.py $flags";
    my ($smbpid, $pid2) = startnew($cmd, $pidfile, 15, 0);

    if($smbpid <= 0 || !pidexists($smbpid)) {
        # it is NOT alive
        logmsg "RUN: failed to start the $srvrname server\n";
        stopserver($server, "$pid2");
        displaylogs($testnumcheck);
        $doesntrun{$pidfile} = 1;
        return (0,0);
    }

    # Server is up. Verify that we can speak to it.
    my $pid3 = verifyserver($proto, $ipvnum, $idnum, $ip, $port);
    if(!$pid3) {
        logmsg "RUN: $srvrname server failed verification\n";
        # failed to talk to it properly. Kill the server and return failure
        stopserver($server, "$smbpid $pid2");
        displaylogs($testnumcheck);
        $doesntrun{$pidfile} = 1;
        return (0,0);
    }
    $pid2 = $pid3;

    if($verbose) {
        logmsg "RUN: $srvrname server is now running PID $smbpid\n";
    }

    sleep(1);

    return ($smbpid, $pid2);
}


#######################################################################
# Single shot http and gopher server responsiveness test. This should only
# be used to verify that a server present in %run hash is still functional
@@ -2843,6 +2984,9 @@ sub subVariables {

  $$thing =~ s/%DICTPORT/$DICTPORT/g;

  $$thing =~ s/%SMBPORT/$SMBPORT/g;
  $$thing =~ s/%SMBSPORT/$SMBSPORT/g;

  # server Unix domain socket paths

  $$thing =~ s/%HTTPUNIXPATH/$HTTPUNIXPATH/g;
@@ -4695,6 +4839,17 @@ sub startservers {
                $run{'dict'}="$pid $pid2";
            }
        }
        elsif($what eq "smb") {
            if(!$run{'smb'}) {
                ($pid, $pid2) = runsmbserver($verbose, "", $SMBPORT);
                if($pid <= 0) {
                    return "failed starting SMB server";
                }
                logmsg sprintf ("* pid SMB => %d %d\n", $pid, $pid2)
                    if($verbose);
                $run{'dict'}="$pid $pid2";
            }
        }
        elsif($what eq "none") {
            logmsg "* starts no server\n" if ($verbose);
        }
@@ -5155,6 +5310,8 @@ $HTTPPROXYPORT = $base++; # HTTP proxy port, when using CONNECT
$HTTPPIPEPORT    = $base++; # HTTP pipelining port
$HTTP2PORT       = $base++; # HTTP/2 port
$DICTPORT        = $base++; # DICT port
$SMBPORT         = $base++; # SMB port
$SMBSPORT        = $base++; # SMBS port
$HTTPUNIXPATH    = 'http.sock'; # HTTP server Unix domain socket path

#######################################################################
Loading