↑ ↑ ↓ ↓ ← → ← → B A Start

Время, деньги, инфобез

За окном опять сугробы 2020 год, а у меня $999 и желание прокачаться в информационной безопасности. Похоже что это отличное время чтобы инвестировать время и деньги в обучение. Именно так я и подумал когда покупал курс Penetration Testing with Kali Linux (OSCP).

OSCP (Offensive Security Certified Professional) это самый известный экзамен по информационной безопасности.

Про OSCP я много раз читал и только волшебный пинок локдауна заставил меня купить этот курс. Он отличается от других сертификаций по информационной безопасности неоправданно сложным экзаменом. Ребята из Offensive Security бегают по интернету и пристально следит за тем чтобы никакие подсказки или готовые решения не были доступны публично. И похоже им это успешно удается делать более 10 лет подряд.

Минимальная стоимость курса $999. За эти деньги ты получишь «стартер кит молодого пентестера», который включает в себя:

  • Книжку на 850 страниц
  • 17+ часов видео лекций
  • 30 дней доступа к лаборатории
  • 1 попытка сдачи экзамена

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

Небольшая ремарка. Уже в процессе обучения я решил прочитать блоги людей которые проходили курс, и оказалось что большая часть из них подошли к этому курсу основательно, готовясь к нему за полгода / год. А для тренировки перед OSCP покупали доступы к HTB и VulnHub, я же ничего этого не делал. Курс был для меня черной коробкой.

После оплаты мне скинули ссылки на кастомизированный образ Kali (чем конкретно он отличается от обычного я не понял), а также архив для проверки VPN соединения, который действует 3 дня. За эти дни нужно было скачать VMware, поставить туда этот Kali и проверить свое подключение к VPN. Говорят раньше все это присылали в день начала занятий, теперь присылают заранее.

Вообще в 2020 году курс изрядно обновили и как они сами пишут, увеличили в 2 раза размер материалов (раньше книжка была всего 400 страниц), а также на 30% число машин в лаборатории. А еще добавили новые типы атак, вроде атаки на Active Directory или лекций по PowerShell Empire (репозиторий которого уже закрыт владельцем).

Курс состоит из 3х частей

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

Вторая часть курса это время работы в лаборатории. Что это значит? Ты получаешь доступ в закрытый интранет где располагается 4 виртуальных сети связанных между собой через шлюзы. На картинке из официального гайда можно посмотреть как эти сети связаны между собой. Кроме того в этой сети развернуто два полноценных Active Directory домена по 5 машин в каждом. Это, кстати, интересный момент, его я опишу чуть ниже.

Изначально доступ есть только в Student Network (она же Public) но по мере захвата машин можно будет попасть и в другие сети. В сети 66 хостов на каждом из из которых лежит файл proof.txt, который необходимо сдать в панели управления. Принцип чем-то похож на CTF, с той лишь разницей, что сам файл не так важен как понимание уязвимости через которую можно получить права администратора.

Последняя часть программы это экзамен. О его сложности я прочитал только когда имел на руках дату начала обучения. Целые форумы посвящены плачам людей которые его пытаются сдать. Читать подобные пассажи строго не рекомендую.

День Х

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

До 2020 года книжка и видео дублировали друг друга, но сейчас это не так. Некоторые вектора атак показанные в видео не раскрываются в книге, а дополнительных заданий из книги нет в видео. Я выбрал путь стримера, смотреть в консоль на экране и слушать закадровый голос было проще чем читать все 850 страниц.

Смотрел я по 2-3 часа в день и потому мне потребовалась целая неделя для просмотра всех 17 часов от начала и до конца, попутно листая PDF.

Лабы

После теории у меня осталось 23 дня за которые я планировал получить доступ на все машины (ха-ха, как наивен я был тогда). Попав в лабораторную сеть у меня не было ничего кроме адреса подсети /24, в которой расположены какие-то уязвимые хосты. Но именно так и построено обучение. В теории рассказали возможные вектора атак, а дальше все зависит от студента.

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

Подсказок как ломать нет в принципе. Можно пойти на закрытый форум, только для студентов, но это отдельный цирк. Обсуждать можно все, но давать прямые подсказки запрещено. А потому местные посетители упражняются в аллегориях, например вместо упоминания smb пишут brazilian dance, а вместо dirtycow используют well-know exploit.

На форумах ходит поверье о 4 машинах (pain, sufferance, humble, gh0st) которые не обязательно ломать так как они какие-то особенно сложные. Забавно, что узнал я об этом много позже, после получения доступа к каждой из них. По мне, так это обычные ничем не примечательные машины которые встречаются на любом CTF соревновании.

Спустя 3 недели я закрыл всего 23 машины. В этот момент у меня заканчивался 30-дневный доступ в лабораторию и я купил дополнительные 30 дней (покупать дополнительные дни можно бесконечно). За следующие 30 дней я закрыл все 66 машины получив доступ ко всем подсетям.

Исходя из тредов на форуме можно сделать вывод, что типичный студент закрывает около 40 машин, так как взлом машин через SSH туннелирование это развлечение не для всех.

Экзамен

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

Экзамен занимает полные 48 часов, из которых первые 24 часа дается на взлом 5 серверов, а вторые 24 часа на написание отчета о том как был получен доступ к каждому из них.

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

В день экзамена, за 15 минут до начала, на почту падает ссылка для проведения сессии подтверждения личности (я использовал загранпаспорт). После это приходит уже новое письмо с доступами для подключение к VPN и ссылка на экзаменационную панель управления.

Для успешной сдачи экзамена нужно набрать 75 очков. Для этого есть 5 машин, они каждый раз разные, но тип каждой из них всегда одинаковый:

  • 1 машина на написание эксплойта с переполнением буфера за 25 очков
  • 1 комплексная машина со множеством ложных векторов атаки за 25 очков
  • 2 обычные машины по 20 очков
  • 1 совсем простая за 10

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

Так как экзамен это рулетка, то и соотношение Windows/Linux может быть любым. Мне попались 3 машины на Linux и 2 на Windows.

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

Затем, чтобы расслабится, я получил доступ к машине за 10 очков, часто ее называют low-hanging fruit, для атаки требовалось просто найти один публичный эксплойт, который дает доступ к учетной записи с правами администратора.

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

После этих упражнений у меня было 60 очков в кармане. Оставшееся от экзамена время я потратил на одновременную атаку на 2 оставшиеся машины. На одной из них я быстро получил непривилегированного пользователя, а вот с эскалацией привилегий вышло сложнее. На второй же кроме LFI не получалось прощупать ничего. Ближе к вечеру я получил привилегированный доступ на первой из машин, а на вторую решил просто забить и лечь спать.

Я читал о смельчаках которые не спали по 24 часа и все равно проваливали экзамен. Примыкать к их числу я не хотел. Проснувшись за 3 часа до окончания экзамена проверил хватает ли мне скриншотов и логов для заполнения отчета. А после этого попытался еще раз копнуть оставшуюся машину, но так ничего и не вышло.

Отчет

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

Написание такого отчета может отпугнуть неподготовленного русскоязычного обывателя. Но и у англоязычных студентов это вызывало приступы паники. Некоторые «счастливчики» писали отчеты по 200+ страниц. Сказу скажу, что мой отчет содержал всего 68 и это с учетом того, что я вставлял по одному скриншоту на лист.

Вообще боятся этого смысла нет, писать эссе там точно не надо. Достаточно взять шаблон и вставлять в нужные места скриншоты с одним предложением что-же он значит. Использовать стандартный шаблон тоже не обязательно. Для своего отчета я использовал альтернативный вариант в Markdown.

После заполнения отчета его нужно было запаковать в 7z и отправить на специальной странице для отчетов. До 2020 надо было еще и паролем шифровать, но теперь отказались от этой практики.

Результат

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

На этом заканчиваю свой рассказ, а о каких-то технических хаках, которые могут помочь на этом курсе, напишу в отдельной заметке. Надеюсь что к этому времени DHL уже пришлет мне мой сертификат. А на вопрос стоит ли сдавать этот экзамен ответ однозначный — да, если есть много свободного времени и лишние $999.

Как отказаться от RSS в 2020 году

— Я решил отказаться от RSS…

— Ок, бумер

За несколько лет получение и потребление контента изменилось. Twitter и Telegram заняли первое место среди каналов доставки информации, а RSS как был так и остался чем-то маргинальным.

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

Идея трансляции RSS в Telegram занимала мои мысли давно, а беглый поиск по Github’у давал десятки готовых скриптов. Но во всех этих скриптах был фатальный недостаток - их надо было заворачивать в какие-то контейнеры, куда-то ставить, а потом еще и поддерживать. К счастью Вастрик написал лонгрид о движении NoCode после прочтения которого я понял, что время пришло. Если авторы контента не идут в Telegram, то их можно туда принести.

Первым делом зарегистрировал себе бесплатный аккаунт в Integromat, это такой IFTTT для нормальных людей. Затем создал бота в Telegram и открыл по одному каналу на каждую из RSS лент. Можно было бы все ленты доставлять в один канал, но я не пытался из мессенджера сделать RSS ридер.

Осталось собрать вместе части конструктора. Для этого я взял готовый рецепт, вставил адрес первой RSS ленты и канал доставки, а также поставил запуск каждый 15 минут. После этого клонировал получившийся сценарий меняя настройки. Через пару минут все каналы ожили и стали доставлять обновления из RSS, а я удалил все ленты из Feedly и закрыл аккаунт навсегда.

А теперь к плохим новостям. Через 5 дней Integromat радостно сообщил, что я по потратил весь бесплатный лимит. Ведь каждый сценарий при 15-минутных запусках будет тратить по ~2880 операций в месяц. Чтобы не заводить новый аккаунт заплатил за базовый план, а для настроек выбрал запуск рецептов раз в час, таким образом снизив число запусков до 720 в месяц.

Кибер настольный календарь

Типичный день в энтерпрайзе чуть менее, чем полностью состоит из встреч. В офисе об этих встречах напоминает не только Microsoft Exchange но и члены команды.

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

Raspberry Pi

Для этого взял Raspberry Pi Zero W, прицепил экран и написал скрипт который раз в 5 минут заходит на корпоративный сервер Exchange, достанет ближайшую встречу и выводит на экран ее название и человекопонятное описание когда она начнется.

Raspberry Pi

Календарь я хотел сделать приближенным к аналоговым предметам, а потому взял E Ink экран, который не светится в темноте и не раздражает глаза.

Waveshare 2.13inch e-Paper HAT я купил год назад на Amazon и он все это время лежал без дела. Китайцы из Waveshare делают экраны совместимые с Raspberry Pi и любезно предоставляют готовый SDK на Python.

Raspberry Pi

Итоговый проект и инструкция по установке выложил на github и подробно рассматривать не буду. Если кратко, то функция next_meeting достает ближайшую встречу, а функция get_image формирует изображение, которое выводится на экран.

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

Как работает встраивание ошибок в Go

Обработка ошибок в Go концептуально прозрачна но вызывает массу вопросов и негодования у людей переходящих на Go с ООП языков. И первое с чем такие люди сталкиваются это отсутствие вложенности ошибок. Чтобы как-то смягчить эту боль в заднице Dave Cheney написал библиотеку для оборачивания ошибок и реализуя некое подобие наследования.

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

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

А если оборачиваем ошибку используя вызов fmt.Errorf("%w", err), то возвращаемая ошибка получит уже другую структуру, содержащую указатель на оборачиваемую ошибку и метод Unwrap() для доступа к ней.

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

С такими ошибками можно делать ровно два действия, сравнить со значением и с типом (не совсем). Понять как это работает проще всего на примере. Возьмем абстрактный микросервис, которых ходит в другой микросервис через RPC клиент по http.

Handler → Middleware → RPC Client → HTTP Client

Ошибка может произойти как на уровне http клиента, так и в постобработке полученного ответа. При этом логирование ошибок происходит на уровне handler’а, а остальные уровни только оборачивают ошибки и пробрасывают наверх. Хватит теории, давай код.

package main

import (
	"fmt"
	"net/http"
)

type httpError struct {
	Code    int
	Message string
}

func (e *httpError) Error() string {
	return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

type rpcError struct {
	Method string
	err    error
}

func (e *rpcError) Error() string {
	return fmt.Sprintf("%s error: %s", e.Method, e.err.Error())
}

func (e *rpcError) Unwrap() error {
	return e.err
}

func main() {
	httpErr := &httpError{
		Code:    http.StatusInternalServerError,
		Message: http.StatusText(http.StatusInternalServerError),
	}
	rpcErr := &rpcError{
		Method: "getEnterpriseResourceV1",
		err:    httpErr,
	}
	middlewareErr := fmt.Errorf("middleware: %w", rpcErr)
	handlerErr := fmt.Errorf("handler: %w", middlewareErr)

	fmt.Printf("%q\n", handlerErr)
	// handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error
}

Тут происходит имитация ошибки на уровне http клиента с последующим оборачиванием в rpc ошибку, а потом в middleware и handler. Конечная ошибка выглядит как набор вложенных друг в друга структур.

fmt.wrapError{
    msg: "handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error",
    err: &fmt.wrapError{
        msg: "middleware: getEnterpriseResourceV1 error: 500: Internal Server Error",
        err: &rpcError{
            Method: "getEnterpriseResourceV1",
            err: &httpError{
                Code:    500,
                Message: "Internal Server Error",
            },
        },
    },
}

Имея такую матрешку попробуем выяснить какая ошибка была изначально и какие этапы оборачивания она прошла. Для этого воспользуемся функцией сравнения ошибок по значению errors.Is(err, target error). Это функция рекурсивно сравнивает значение ошибки с target вызывая .Unwrap() для продвижения вглубь матрешки.

errors.Is(handlerErr, handlerErr)
// true т.к эквивалент ==
errors.Is(handlerErr, middlewareErr)
// true т.к handlerErr.Unwrap() == middlewareErr
errors.Is(handlerErr, rpcErr)
// true т.к handlerErr.Unwrap().Unwrap() == rpcErr
errors.Is(handlerErr, httpErr)
// true т.к handlerErr.Unwrap().Unwrap().Unwrap() == httpErr

errors.Is(handlerErr, fmt.Errorf("generic")) // false
errors.Is(handlerErr, &unknownError{})       // false
errors.Is(handlerErr, &rpcError{})           // false т.к совпадает тип, но не значение
errors.Is(handlerErr, &httpError{})          // false т.к совпадает тип, но не значение

Выглядит логично и удобно, но в реальном мире это не будет работать. Переменные вложенных ошибок будут в другом скоупе и их невозможно будет достать чтобы сравнить. Правда можно добавить немного магии, реализовав метод Is(target error) у httpError.

func (e *httpError) Is(target error) bool {
	return reflect.ValueOf(target).Elem().Type() == reflect.TypeOf(httpError{})
}

В таком случае сработает проверка по-типу даже если значения не будут совпадать.

errors.Is(handlerErr, &httpError{})
// true

Но это хак, т.к для проверки по типу в Go добавили вторую функцию errors.As(err error, target interface{}). Она работает аналогично функции errors.Is, рекурсивно обходя матрешку, но сравнивает не значения, а тип target и вызывая .Unwrap() для продвижения вглубь. Если же совпадение найдено, но значения типа совпавшей ошибки копируется в target.

var s *rpcError
if errors.As(handlerErr, &s) {
    fmt.Printf("handlerErr as rpcError: %s\n", s.Method)
}
// handlerErr as rpcError: getEnterpriseResourceV1

var h *httpError
if errors.As(handlerErr, &h) {
    fmt.Printf("handlerErr as httpError: %d\n", h.Code)
}
// handlerErr as httpError: 500

var e error
if errors.As(handlerErr, &e) {
    fmt.Printf("handlerErr as error: %s\n", e)
}
// handlerErr as error: handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error

var u *unknownError
if errors.As(handlerErr, &u) {
    fmt.Printf("handlerErr as unknownError: %v\n", u)
}
// не выведет ничего

У такого подхода один очевидный плюс, разработчики убили сразу двух зайцев, и сравнение по типу и распаковка ошибки. Но есть и минусы, в ситуации когда нужно просто понять что за тип ошибки нет варианта использовать конструкцию switch, как это можно сделать используя пакет pkg/errors.

В целом новый механизм работы с ошибками решает часть болей и потенциально помогает отказаться от еще одной внешней зависимости в проекте.

Luma

Нашел на /r/proceduralgeneration пост в котором некто написал фильтр для изображений, который превращает исходное изображение в условно абстрактное. Код был написан на Lua, я же решил переписать предложенный метод на Go. Во-первых я никогда не писал что-то связанное с обработкой изображений на гошке, а во-вторых почему бы и нет.

package main

import (
	"image"
	"image/color"
	"image/jpeg"
	"os"
	"sort"
)

func main() {
	file, err := os.Open("in.jpg")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig)

	img, _, err := image.Decode(file)
	if err != nil {
		panic(err)
	}

	rec := img.Bounds()
	cimg := image.NewRGBA(rec)

	m := make([]color.Color, rec.Dy())
	for x := 0; x < rec.Dx(); x++ {
		for y := 0; y < rec.Dy(); y++ {
			m[y] = img.At(x, y)
		}

		sort.SliceStable(m, func(i, j int) bool {
			return luma(m[i]) < luma(m[j])
		})

		for y := range m {
			cimg.Set(x, y, m[y])
		}
	}

	out, err := os.Create("out.jpg")
	if err != nil {
		panic(err)
	}

	err = jpeg.Encode(out, cimg, &jpeg.Options{Quality: 80})
	if err != nil {
		panic(err)
	}
}

func luma(c color.Color) float32 {
	r, g, b, _ := c.RGBA()

	return 0.2126*float32(r>>8) +
		0.7152*float32(g>>8) +
		0.0722*float32(b>>8)
}

TL;DR Что тут происходит? Берем пиксельную сетку изображения, делим на столбцы и в каждом из столбцов сортируем пиксели по относительной яркости.

Для вычисления относительной яркости нужно взять каждый пиксель, разложить на RGB цвета и умножить их на коэффициенты. Почему именно такие коэффициенты лучше почитать в статье на хабре. Результат выходит приличный, для изображений содержащих большой однородный задний фон.

Luma

Второй интересный момент момент это то, как в Go реализованы возвращаемые значения цветов. Все цвета хранятся в uint32 в диапазоне от 0 до 0xFFFF, что позволяет хранить эффективные значения для 16-битного диапазона. Как обычно, самый исчерпывающий ответ о том как это работает можно найти на SO, где же еще. Но так как формула относительной яркости работает с 8-битными значениями, то конвертируем 16-битное представление обратно в 8 бит используя битовый сдвиг вправо.