• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Разбор Redis CVE-2023-28425 с использованием chatGPT в качестве помощника

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
Apr 2, 2023
Оригинал -> https://tin-z.github.io/redis/cve/chatgpt/2023/04/02/redis-cve2023.html

Введение

В этом блоге я покажу вам, как я изучал исходный код redis, чтобы написать простой PoC для CVE-2023-28425. Кроме того, я дам несколько советов о том, как читать исходный код проекта с открытым исходным кодом, чтобы найти уязвимости для известных ошибок. Наконец, я покажу вам, как я регулярно использую chatGPT для поиска дополнительной информации о цели.

redis

Redis - это хранилище структур данных в памяти с открытым исходным кодом, которое широко используется в качестве базы данных, кэша и брокера сообщений. Впервые он был выпущен в 2009 году и с тех пор стал одной из самых популярных баз данных NoSQL, благодаря своей высокой производительности, масштабируемости и гибкости. В целом, Redis - это мощное и универсальное хранилище данных, которое нашло широкое применение в самых разных приложениях и отраслях, от социальных сетей и электронной коммерции до финансов и здравоохранения.
CVE-2023-28425

Как описано в рекомендации по безопасности, аутентифицированный пользователь может использовать команду MSETNX, чтобы вызвать утверждение во время выполнения и завершение процесса сервера Redis.


t1.jpg


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

  • затронутые версии >= 7.0.8
  • патч применяется с версии 7.0.10
  • уязвимость просто запустить, и только аутентифицированные пользователи могут ее запустить
  • после срабатывания уязвимости она будет обнаружена утверждением во время выполнения, что приведет к отключению серверов redis (DoS).

сбор информации

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

  • Поиск CVE на twitter/dorks/github/gitlab
  • Ищите ключевые слова в разделах github/gitlab issues и commits, в данном случае "MSETNX" может быть хорошим кандидатом.
  • Следите за социальной активностью багхантера

На этом этапе я нашел: заметки о внутреннем устройстве redis, CVE-2022-31144 POC.

проверка кода

Этот этап подразделяется на:
  • Сравнить патченную версию с непропатченной версией, которая имеет ближайший номер версии к патченной, используя git diff.
  • Grep, выполнить поиск по ключевым словам
  • Используйте инструмент навигации по коду, для больших проектов используйте eclipse, в остальных случаях лучше vim+cscope (ссылка)
  • Попросите chatGPT стать вашим помощником

Давайте выполним первый подшаг:

Код:
git diff 7.0.9 7.0.10

Попробуем поискать ключевое слово "MSETNX", и после изучения diff обнаружим, что был добавлен тестовый случай для "MSETNX с несуществующими ключами - один и тот же ключ дважды", возможно, это и есть вульна.

t2.jpg



Мы спрашиваем о команде MSETNX и уязвимости chatGPT. Чтобы получить положительные результаты, нам нужно попросить chatgpt помочь нам. Я делаю это так: прежде чем задать вопрос, связанный с уязвимостями, я добавляю следующее предложение: "Как <роль> <прилагательное-роль>, вы - мой помощник.", причем <роль> может быть, например, "аудитор кода безопасности, исследователь безопасности, исследователь уязвимостей", а <прилагательное> - "опытный, эксперт" и так далее. Вы можете комбинировать роли.

Теперь вопрос к chatGPT не может быть простым "как вызвать CVE-2023-...", (или это не так). Потому что, во-первых, он скажет вам, что ничего не знает после Sept 2021 (как мы и полагаем), а во-вторых, вопрос может быть помечен как чувствительный, и ответ будет ограничивающим или манипулирующим (например, расскажите мне анекдот про мужчину и расскажите мне анекдот про женщину).

Я ухожу от этого следующим образом: "Я анализирую проект <имя проекта>, который размещен на github по адресу <url>, и обнаружил следующую уязвимость: "<vulnerability-description>". Можете ли вы объяснить это лучше и привести пример?".

Используется последний вопрос:

Как эксперт по аудиту кода безопасности и опытный охотник за багами, вы являетесь моим помощник. Я анализирую проект redis, который размещен на github здесь https://github.com/redis/redis, и я обнаружил следующую уязвимость "Аутентифицированные пользователи могут использовать команду MSETNX для запуска утверждения во время выполнения и завершение процесса сервера Redis.", не могли бы вы объяснить лучше и, возможно. Можете привести пример?

t3.jpg


Создадим тестовую среду

Код:
# ubuntu 20.04 docker image
git clone https://github.com/redis/redis
cd redis
git checkout 7.0.9
export CFLAGS="-g"
make
cd src

# run redis server
#gdb -ex "run" --args ./redis-server --port 7777
./redis-server --port 7777

# run client
./redis-cli -h 127.0.0.1 -p 7777

Давайте попробуем poc, предоставленный chatgpt.

t4.jpg


Давайте попробуем тестовый пример, который был добавлен в пропатченную версию. Круто, мы нашли vuln.

t5.jpg



t6.jpg


Поиск уязвимостей (?)

Скомпилируйте исходный код с флагом "-g", затем после запуска аварийного завершения программы проверьте кадр стека вызывающей функции в gdb с помощью команд up <#frame> и down <#frame>. В нашем случае мы делаем up 4, а затем даем команду context для обновления вида консоли gdb.

t7.jpg



t8.jpg



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

Код:
# append this to bashrc or only for the current bash session
# Cscope config
export CSCOPE_EDITOR=`which vim`
alias cscope_cpp="find . -iname '*.cpp' -o -iname '*.c' -o -iname '*.h' -o -iname '*.hpp' -o -iname '*.cc' > cscope.files"
alias cscope_java="find . -iname '*.java' > cscope.files"
alias cscope_py="find . -iname '*.py' > cscope.files"
alias cscope_all="find . -iname '*.cpp' -o -iname '*.c' -o -iname '*.h' -o -iname '*.hpp' -o -iname '*.cc' -o -iname '*.java' -o -iname '*.py' > cscope.files"
alias cscope_database="cscope -q -R -b -i cscope.files"
alias cscope_Clean='rm cscope.in.out cscope.po.out cscope.files cscope.out'

cd redis
cscope_cpp
cscope_database
cscope -d

Мы ищем в разделе «Найти это глобальное определение», используйте «TAB» для перехода от / к разделам запроса.

t9.jpg


Итак, вернемся к dbAddфункция. Мы заключаем, что утверждение происходит, потому что de != NULL true.

Код:
/* Add the key to the DB. It's up to the caller to increment the reference
 * counter of the value if needed.
 *
 * The program is aborted if the key already exists. */
void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    dictEntry *de = dictAddRaw(db->dict, copy, NULL);
    serverAssertWithInfo(NULL, key, de != NULL);
    dictSetVal(db->dict, de, val);
    signalKeyAsReady(db, key, val->type);
    if (server.cluster_enabled) slotToKeyAddEntry(de, db);
    notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
}

Давайте проверим функцию dictAddRaw. С помощью gdb мы обнаружим, что если ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) является истиной, что означает, что ключ уже присутствует в словаре и поэтому возвращается NULL, что вызовет условие assert, описанное ранее.

Код:
/* Low level add or find:
 * This function adds the entry but instead of setting a value returns the
 * dictEntry structure to the user, that will make sure to fill the value
 * field as they wish.
 *
 * This function is also directly exposed to the user API to be called
 * mainly in order to store non-pointers inside the hash value, example:
 *
 * entry = dictAddRaw(dict,mykey,NULL);
 * if (entry != NULL) dictSetSignedIntegerVal(entry,1000);
 *
 * Return values:
 *
 * If key already exists NULL is returned, and "*existing" is populated
 * with the existing entry if existing is not NULL.
 *
 * If key was added, the hash entry is returned to be manipulated by the caller.
 */
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{
    long index;
    dictEntry *entry;
    int htidx;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    htidx = dictIsRehashing(d) ? 1 : 0;
    size_t metasize = dictMetadataSize(d);
    entry = zmalloc(sizeof(*entry) + metasize);
    if (metasize > 0) {
        memset(dictMetadata(entry), 0, metasize);
    }
    entry->next = d->ht_table[htidx][index];
    d->ht_table[htidx][index] = entry;
    d->ht_used[htidx]++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key);
    return entry;
}

По итогу

В итоге уязвимость вызвана объявлением нового ключа дважды в одной и той же выполняемой команде MSETNX. В частности, уязвимость присутствует в функции msetGenericCommand, которая вызывается при разборе команды MSETNX k1 val1 k1 val2. Поскольку nx не равно 0, и поскольку ни один из объявленных командой ключей не присутствует в БД, то происходит setkey_flags |= SETKEY_DOESNT_EXIST;. Далее мы перебираем каждый объявленный ключ и используем тот же setkey_flags, setKey(c, c->db, c->argv[j], c->argv[j + 1], setkey_flags);. Что правильно для первого объявления k1 val1, но неправильно для k1 val2, так как k1 уже был объявлен и не должен иметь бит SETKEY_DOESNT_EXIST, установленный в setkey_flags.

Код:
void msetGenericCommand(client *c, int nx) {
    int j;
    int setkey_flags = 0;

    if ((c->argc % 2) == 0) {
        addReplyErrorArity(c);
        return;
    }

    /* Handle the NX flag. The MSETNX semantic is to return zero and don't
     * set anything if at least one key already exists. */
    if (nx) {                                                               // [0]
        for (j = 1; j < c->argc; j += 2) {
            if (lookupKeyWrite(c->db,c->argv[j]) != NULL) {
                addReply(c, shared.czero);
                return;
            }
        }
        setkey_flags |= SETKEY_DOESNT_EXIST;                                // [1]
    }

    for (j = 1; j < c->argc; j += 2) {
        c->argv[j+1] = tryObjectEncoding(c->argv[j+1]);
        setKey(c, c->db, c->argv[j], c->argv[j + 1], setkey_flags);         // [2]
        notifyKeyspaceEvent(NOTIFY_STRING,"set",c->argv[j],c->db->id);
    }
    server.dirty += (c->argc-1)/2;
    addReply(c, nx ? shared.cone : shared.ok);

Diff patch:

Код:
diff --git a/src/t_string.c b/src/t_string.c
index af58d7d54..4659e1861 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -561,7 +561,6 @@ void mgetCommand(client *c) {

 void msetGenericCommand(client *c, int nx) {
     int j;
-    int setkey_flags = 0;

     if ((c->argc % 2) == 0) {
         addReplyErrorArity(c);
@@ -577,12 +576,11 @@ void msetGenericCommand(client *c, int nx) {
                 return;
             }
         }
-        setkey_flags |= SETKEY_DOESNT_EXIST;
     }

     for (j = 1; j < c->argc; j += 2) {
         c->argv[j+1] = tryObjectEncoding(c->argv[j+1]);
-        setKey(c, c->db, c->argv[j], c->argv[j + 1], setkey_flags);
+        setKey(c, c->db, c->argv[j], c->argv[j + 1], 0);
         notifyKeyspaceEvent(NOTIFY_STRING,"set",c->argv[j],c->db->id);
     }
     server.dirty += (c->argc-1)/2;

POC -> https://github.com/tin-z/Stuff_and_POCs/blob/main/etc/poc_cve-2023-28425.sh
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх