Nodejs реализует службу проникновения во внутреннюю сеть

Node.js

Вам может быть сложно найти в Интернете статью, объясняющую проникновение в интранет с уровня кода, я искал и не смог, поэтому я сделал эту статью.

1. Интранет-прокси

Давайте посмотрим на бумаги, как добиться сервисного агента в локальной сети? Потому что это очень просто, так, прямо в коде.

const net = require('net')

const proxy = net.createServer(socket => {
  const localServe = new net.Socket()
  localServe.connect(5502, '192.168.31.130') // 局域网内的服务端口及ip。

  socket.pipe(localServe).pipe(socket)
})

proxy.listen(80)

Это очень простой серверный агент, код простой и понятный.Если у вас есть какие-либо вопросы, считается, что труба (PIPE), просто. Socket — это полнодуплексный поток, который представляет собой поток данных с возможностью чтения и записи. В коде, когда сокет получает данные клиента, он записывает данные в локальный сервер.Когда локальный сервер имеет данные, он записывает данные в сокет, и сокет отправляется клиенту.

2. Проникновение в интранет

Локальный сетевой прокси прост, а проникновение в интранет не так просто, однако это основной код, требующий значительной логической обработки. Перед конкретной реализацией разберемся с проникновением в интранет.

Что такое проникновение в интранет?

Проще говоря, это клиент общедоступной сети, который может получать доступ к службам в локальной сети. Например, локально запущенная служба. Как клиент общедоступной сети может узнать локальную подачу? Здесь необходимо использовать общедоступный сетевой сервер. Так как же общедоступный сетевой сервер узнает о локальной службе? Это требует установления сокетного соединения между локальным сервером и сервером.

четыре роли

Из приведенного выше описания мы выявляем четыре роли.

  1. Клиент общедоступной сети, мы называем его client.
  2. Сервер общедоступной сети из-за его роли прокси называется proxyServe.
  3. Локальная служба с именем localServe.
  4. Сокет между локалом и сервером длинно подключен, это мост между proxyServe и localServe, он отвечает за передачу данных, мы называем его bridge.

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

bridge

Из раздела о четырех ролях мы знаем, что мост — это сокетное соединение с proxyServe, и это передача данных.

const net = require('net')

const proxyServe = '10.253.107.245'

const bridge = new net.Socket()
bridge.connect(80, proxyServe, _ => {
  bridge.write('GET /regester?key=sq HTTP/1.1\r\n\r\n')
})

bridge.on('data', data => {
  const localServer = new net.Socket()
  localServer.connect(8088, 'localhost', _ => {
    localServer.write(data)
    localServer.on('data', res => bridge.write(res))
  })
})

Код четкий и читаемый, даже запоминающийся. Введите сетевую библиотеку, объявите общедоступный сетевой адрес, создайте мост и подключите мост к proxyServe. После успеха зарегистрируйте локальную службу на proxyServe. Затем мост прослушивает данные. Когда поступает запрос, он создает соединение с локальной службой.После успеха данные запроса отправляются на localServe, прослушиваются данные ответа и записываются поток ответа на мост.

Остальное объяснять нечего, ведь это всего лишь пример кода. Однако в примере кода есть раздел /regester?key=sq, этот ключ играет большую роль, здесь key=sq. Затем, когда клиент роли обращается к локальному сервису через прокси-сервис, необходимо добавить этот ключ в путь, чтобы proxyServe мог соответствовать мосту и, таким образом, соответствовать localServe.

Например: lcoalServe это:http://localhost:8088, rpoxyServe этоexample.com, зарегистрированный ключ sq. Затем для доступа к localServe через prxoyServe нужно написать следующее:example.com/sq. Зачем так писать? Конечно, это всего лишь определение.После того, как вы разберетесь с кодом в этой статье, вы сможете изменить такое соглашение.

Что ж, позвольте мне увидеть следующий код ключа:

proxyServe

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

Блок кода 1: createServe

Основная функция этого блока - создать прокси-сервис, установить сокетное соединение с клиентом и мостом, сокет прослушивает запросы данных и выполняет логическую обработку в функции обратного вызова. Конкретный код выглядит следующим образом:

const net = require('net')

const bridges = {} // 当有bridge建立socket连接时,缓存在这里
const clients = {} // 当有client建立socket连接时,缓存在这里,具体数据结构看源代码

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url
    
    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

Взгляните на логику кода в мониторинге данных:

  1. Преобразуйте данные запроса в строку.
  2. Найдите URL-адрес от запроса и закончите запрос напрямую, если URL не найден.
  3. Судя по URL, является ли это бриджем, если да, то регистрируйте бридж, иначе считайте это клиентским запросом.
  4. Проверьте, есть ли в клиентском запросе зарегистрированный мост — помните, это прокси-сервис, и если зарегистрированного моста нет, запрос считается недействительным.
  5. Кэшировать этот запрос.
  6. Затем отправьте запрос на мостик.

Совмещая код и логическую сортировку, вы должны понимать, но у вас могут возникнуть вопросы по поводу 5, а затем разбирать их по одному.

Блок кода 2: isBridge

Чтобы определить, является ли это запросом на регистрацию моста, очень просто написать здесь, но для реального бизнеса могут быть определены более точные данные.

function isBridge (url) {
  return url.startsWith('/regester?')
}

Третий блок кода: regesterBridge

Просто, посмотрите на код и объясните:

function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}
  1. Найдите ключ моста для регистрации по URL.
  2. Кэшировать измененное соединение сокета.
  3. Удалите мониторинг данных моста — каждый сокет в кодовом блоке 1 имеет функцию обратного вызова по умолчанию для мониторинга данных, говорящую, что если ее не удалить, это вызовет последующую путаницу данных.

Четвертый блок кода: findBridge

Когда логика доходит до блока кода 4, это означает, что это уже клиентский запрос.Тогда нужно сначала найти соответствующий ему бридж.Если бриджа нет, то сначала нужно зарегистрировать бридж, а затем пользователь должен инициировать запрос клиента позже. код показывает, как показано ниже:

function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}
  1. Сопоставьте ключ моста для проксирования с URL-адресом и верните соответствующий мост и ключ, когда они будут найдены.
  2. Если не найдёте, поищите по рефереру в шапке запроса, если найдёте, верните бридж и ключ.
  3. Не можем найти, мы знаем, что этот запрос будет завершен в первом блоке кода.

Пятый блок кода: cacheClientRequest

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

Почему клиентские запросы Cache?

В текущей схеме мы хотим, чтобы запросы и ответы были попарно заказаны. Мы знаем, что сетевые передачи передаются в фрагментах. В настоящее время, если мы не контролируем сопряжение и упорядочение запросов и ответов на уровне приложений, он приведет к путанице между пакетами. В настоящее время, если в будущем есть лучший раствор, не стоит заставлять запрос и реагирование данных, которые нужно управлять на уровне приложений, а слой TCP / IP можно доверять.

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

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}
  1. Сначала мы оцениваем, есть ли уже кеш клиентских запросов под ключом, соответствующим мосту, и если да, то проталкиваем его.
  2. Если нет, то создаем объект и инициализируем в нем этот запрос.

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

Шестой блок кода: sendRequestToBridgeByKey

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

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}
  1. Выньте клиента, соответствующего ключу моста, из client.
  2. Определите, есть ли у клиента отправляемый запрос, и если да, завершите выполнение. Если нет, продолжайте.
  3. Определить, есть ли запрос под клиентом, если есть, продолжить, если нет, завершить выполнение.
  4. Возьмите первый из очереди, который содержит запрошенный сокет и буферизованный мост.
  5. Замените согласованные данные и отправьте окончательные данные запроса на мост.
  6. Прослушайте ответ данных моста.
    • получить код ответа
      • Если ответ равен 200, мы получаем длину содержимого, если она есть, мы выполняем некоторую операцию инициализации этого запроса. При настройке длины запроса указывается длина отправленного запроса.
      • Если это не 200, мы отправляем данные клиенту и завершаем запрос, удаляем мониторинг данных и рекурсивно вызываем sendRequestToBridgeByKey.
    • Если код не получен, мы считаем этот ответ не в первый раз, а затем добавляем его длину в отправленное поле.
    • Затем мы отправляем эти данные клиенту.
    • Затем оцените, совпадает ли длина ответа с длиной отправленных данных.Если это соответствует, установите статус отправки данных клиента на false, удалите мониторинг данных и рекурсивно вызовите sendRequestToBridgeByKey.

На данный момент основная логика кода закончилась.

Суммировать

Поняв этот код, вы можете расширить его, чтобы обогатить код для вашего использования. Разобравшись с этим кодом, можете ли вы придумать другие сценарии использования? Можно ли использовать эту идею в удаленном управлении?Если вы хотите управлять клиентом, вы можете найти вдохновение в этом коде.

Этот набор кода может быть сложным. Возможно, вам потребуется знать все о tcp/ip и http, а также знать некоторые ключевые заголовки запросов и некоторую ключевую информацию об ответах. Конечно, чем больше вы знаете о http, тем лучше.

Если есть что сообщить, пишите.

исходный код проксисервера

const net = require('net')

const bridges = {}
const clients = {}

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url
    
    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

function isBridge (url) {
  return url.startsWith('/regester?')
}

function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}

function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}
Категории