|
Message-ID: <20201013122912.GA32635@f195.suse.de> Date: Tue, 13 Oct 2020 14:29:12 +0200 From: Matthias Gerstner <mgerstner@...e.de> To: oss-security@...ts.openwall.com Subject: kdeconnect: CVE-2020-26164: multiple security issues in kdeconnectd network daemon Hello list, following is a security review report concerning kdeconnect [1]. [1]: https://community.kde.org/KDEConnect # 1) Introduction The SUSE security team noticed that a new network service service `kdeconnectd` was active by default in openSUSE Leap 15.2 listening on TCP and UDP port 1716. `kdeconnectd` is started automatically in the context of any KDE session and runs with the privileges of the logged in user. `kdeconnectd` talks to an Android smartphone app. The use cases are, among others: - sharing the PC clipboard with the smartphone - controlling the PC from the smartphone (running commands, controlling input) I conducted an in-depth source code review to make sure that this new application doesn't introduce remote security issues in default installations. I looked into kdeconnect version 20.08.0. Any source code references stated in this report relate to this version of the code base. I only looked into the ethernet networking logic rooted in the source code class `LanLinkProvider`. There is also a `BluetoothLinkProvider` that might be affected by similar or additional issues as discussed in this report. Unfortunately at the moment only the single CVE mentioned in the subject has been assigned by upstream cumulatively for all the issues mentioned in this report. The upstream fixes meantioned in this report and in the upstream security advisory [2] are available in the upstream version kdeconnect 20.08.2. [2]: https://kde.org/info/security/advisory-20201002-1.txt # 2) The `kdeconnect` Network Protocol This section is only for readers that aren't already familiar with the basics of the kdeconnect network protocol. Others may skip over to section 3. `kdeconnectd` operates both on UDP port 1716 and on TCP port 1716 (possibly also other TCP port numbers, see section 2.b for details) as a client and as a server. The UDP port is used to send and receive broadcasts announcing kdeconnect enabled nodes in the network segment. The TCP port is used to establish individual node-to-node connections over which the actual application logic is carried out. As data exchange format JSON data structures are used. ## a) The UDP Broadcast Protocol Upon startup `kdeconnectd` sends out a single cleartext broadcast datagram on UDP port 1716 containing a JSON datastructure of type 'kdeconnect.identity' resembling this one: ``` { 'body': {'deviceId': '_bd5ad7ad_43d1_434b_ae3a_5b5af224a513_', 'deviceName': 'user@...t', 'deviceType': 'desktop', 'incomingCapabilities': ['kdeconnect.lock', 'kdeconnect.mousepad.request', <...> 'kdeconnect.mpris'], 'outgoingCapabilities': ['kdeconnect.notification.action', <...> 'kdeconnect.mpris'], 'protocolVersion': 7, 'tcpPort': 1716}, 'id': '1599051054076', 'type': 'kdeconnect.identity' } ``` This broadcast message also contains a tcp port number (by default 1716) announcing at which TCP port `kdeconnectd` is reachable for the main application protocol. When `kdeconnectd` receives a broadcast like this from another node on the network it actively attempts to connect to this node on the announced TCP port (lanlinkprovider.cpp:236). ## b) The TCP Application Protocol `kdeconnectd` listens for incoming TCP connections on port 1716. In case this port is already in use, the daemon attempts to find a higher free port number in the range [1716, 1764], which is probably also the reason why the actual port number used is announced in the UDP broadcast messages. Furthermore the daemon actively connects to the TCP ports announced by other devices in the network via UDP broadcasts. The initiator of the TCP connection (client side) initially sends a cleartext message containing a JSON datastructure of type 'kdeconnect.identity', just like the one transmitted via UDP broadcasts as described in the previous section. Afterwards an SSL connection is established (lanlinkprovider.cpp:393 ff for the server side, lanlinkprovider.cpp:282 ff for the client side). The SSL connection is based on untrusted, self-signed certificates used on both ends. For yet unknown peers `kdeconnectd` ignores any SSL validation errors and establishes the SSL connection nonetheless. These connections are treated as untrusted by `kdeconnectd`. Only trusted devices can access the actual kdeconnect features of the PC host. To establish trust, a pairing process can be triggered from either side of the connection. On the PC side untrusted devices are displayed in a list of the kdeconnect graphical widget. Each device is identified by a free form string ('deviceName' as used in the 'kdeconnect.identity' message used in UDP broadcasts). A user can actively trigger a pairing request for any of the listed untrusted devices. Then `kdeconnectd` will send out a 'kdeconnect.pair' message to the peer device that looks like this: ``` {'body': {'pair': True}, 'id': '1599053939750', 'type': 'kdeconnect.pair'} ``` There is no further communication or authentication required, the device will be treated as trusted by `kdeconnectd` from this point on forward. A remote device can also actively trigger a pairing request by sending a message of type 'kdeconnect.pair' to the host PC. In this case the trust is established via an interactive graphical popup that is displayed in the PC user's session that asks the user whether to authorize or reject the pairing request. The free form identification string ('deviceName') will be displayed in the popup. Once the user clicks on "Accept" the device is treated as trusted from this point on forward. `kdeconnectd` will store the self-signed SSL certificate of trusted devices in a local database (landevicelink.cpp:182). When a device using the same 'deviceId' is encountered later on, then the SSL connection is required to verify against this locally stored certificate (lanlinkprovider.cpp:427, lanlinkprovider.cpp:293). If this verification succeeds then the device will be treated as trusted right away. ## 3) Security Issues For reproducing some of the issues the accompanying script `kdeconnect.py` can be used. ### a) Information Leak of Username and Hostname The 'kdeconnect.identity' message is always transmitted in cleartext either in UDP broadcast messages or as initial TCP message before the SSL handshake is started. The default 'deviceName' field of this message used by `kdeconnectd` is of the form `<user>@<host>`, which leaks information about the logged in user and the local hostname of the machine. This functionality can be used to: - enumerate all machines in the network segment that have active KDE sessions - enumerate the usernames of all these sessions - the knowledge of the username and hostname could be used to attack other network services like SSH or to use them in social engineering attacks #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/b279c52101d3f7cc30a26086d58de0b5f1c547fa ### b) Use after Free in LanLinkProvider::connectError() In lanlinkprovider.cpp:255 an explicit deletion of the socket object is performed, if an outgoing TCP connection failed: ``` delete socket; ``` The correct way would be to call `QObject::deleteLater()` to cause deferred deletion. This causes a race condition, because `QSslSocket::connectToHost()` might not yet have finished running when the socket object is already deleted in LanLinkProvider::connectError(). This can result in a segmentation fault. As usual with "use after free" issues, further unspecified impact is possible. #### Reproducer ``` # this should crash any reachable `kdeconnectd` in the network segment pretty quickly # specify only a specific host IP instead of 255.255.255.255 if you want to # avoid affecting the whole network segment. $ ./kdeconnect.py --send-bcast 255.255.255.255 --deviceid some_new_device --broadcast-DoS ``` #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/d35b88c1b25fe13715f9170f18674d476ca9acdc ### c) 100 % CPU loop reachable via SocketLineReader After an SSL session is established on a TCP connection, the TCP input data processing is handed over to a type `SocketLineReader`, a member of the `LanDeviceLink` type (lanlinkprovider.cpp:537). `SocketLineReader::dataReceived()` contains a bug when a message without newline termination is sent by the peer: ``` while (m_socket->canReadLine()) { // ... } //If we still have things to read from the socket, call dataReceived again //We do this manually because we do not trust readyRead to be emitted again //So we call this method again just in case. if (m_socket->bytesAvailable() > 0) { QMetaObject::invokeMethod(this, "dataReceived", Qt::QueuedConnection); return; } ``` This causes an infinite signal processing loop resulting in 100 % CPU load in `kdeconnectd`. To trigger this, the peer only needs to send some TCP data without a terminating newline character. This only works after the initial 'kdenetwork.identity' packet has been exchanged and after the SSL handshake has completed. #### Reproducer ``` # this will cause the remote `kdeconnectd` to end up with 100 % CPU load. # add a suitable kdeconnectd remote IP address here REMOTE_IP=a.b.c.d $ ./kdeconnect.py --connect-tcp $REMOTE_IP --send-unterminated-ssl-message ``` #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/721ba9faafb79aac73973410ee1dd3624ded97a5 ### d) Lack of DoS Protection Measures The `kdeconnectd` code doesn't contain any kind of protection measures against denial of service attacks: #### i) No Upper Limit on Message Sizes There is no upper limit on the size of messages received over TCP, the `LanLinkProvider` simply tries to read a newline terminated message (lanlinkprovider.cpp:383). The underyling QTcpSocket implementation infinitely reads incoming data and appends it to its internal buffer (a maximum buffer size is not specified). Thus if an unauthenticated TCP peer is sending a infinitely long message without a newline byte appearing, the `kdeconnectd` will continously allocate memory. ##### Reproducer ``` # this will cause the remote `kdeconnectd` to end up with 100 % CPU and # an increasingly high amount of memory allocation, until an out of memory # situation occurs. # add a suitable kdeconnectd remote IP address here $ REMOTE_IP=a.b.c.d $ ./kdeconnect.py --connect-tcp $REMOTE_IP --send-overlong-tcp-message ``` ##### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/b496e66899e5bc9547b6537a7f44ab44dd0aaf38 #### ii) No Upper Limit on Parallel TCP Connections There is no limit on the number of TCP connections accepted in parallel and no timeout for unresponsive TCP connections. Therefore unauthenticated clients of `kdeconnectd` can simply open TCP connections without transmitting any data. This will cause file descriptor exhaustion and also 100 % CPU load in `kdeconnectd`. ##### Reproducer ``` # When this is finished with creating > 1000 TCP connections you should see # a lot of open socket file descriptors on the host running `kdeconnectd` # user $ ls -l /proc/`pidof kdeconnectd`/fd # # you should also see high CPU load caused by kdeconnectd # add a suitable kdeconnectd remote IP address here $ REMOTE_IP=a.b.c.d $ ./kdeconnect.py --perform-tcp-connect-DoS $REMOTE_IP ``` ##### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/ae58b9dec49c809b85b5404cee17946116f8a706 https://invent.kde.org/network/kdeconnect-kde/-/commit/5310eae85dbdf92fba30375238a2481f2e34943e #### iii) No Limit on Processed UDP Broadcasts Also on the UDP broadcast side there are no limits to processing incoming 'kdeconnect.identity' messages. By using different 'deviceId' values `kdeconnectd` will make a unique entry in an internal QMap data structure (red-black tree based) for each broadcast message received and attempt an outgoing TCP connect. Sending a high amount of broadcast messages will cause high CPU load, high memory allocation, high amount of TCP traffic and will finally end in an out of memory situation on the host running `kdeconnectd`. ##### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/66c768aa9e7fba30b119c8b801efd49ed1270b0a #### iv) Possible Amplification Attack `kdeconnectd` actively attempts to establish a TCP connection upon reception of a UDP broadcast (see section 2.a). This can be used as an amplification vector to perform a dedicated DoS attack against some other host in the same network segment. If a larger number of hosts running `kdeconnectd` are available in the network then a single attacker can send a broadcast message with a forged IP sender address (IP spoofing). All hosts receiving this broadcast that run `kdeconnectd` will then attempt to connect to the forged IP address, causing high load for this IP address. If repeated quickly this can be used to overload the target host and/or the network segment. ### e) Possibility to Trigger Arbitrary Outgoing TCP Connections in `kdeconnectd` `kdeconnectd` actively attempts to connect to the TCP ports advertised in UDP broadcast messages (as explained in section 2.a). This is a bit of a peculiar behaviour with a high degree of freedom for attackers. Since the UDP broadcast message is unauthenticated, IP address spoofing can be applied. An unauthenticated remote attacker can cause `kdeconnectd` to connect to an arbitrary IP address in the same network segment on an arbitrary TCP port. `kdeconnectd` will then send the 'kdeconnect.identity' message to this ip/port. It is difficult to say what the security implications of this are. The following items come to mind: - it can confuse other network services on other hosts, possibly create log messages there. These other hosts may not even be reachable by the attacker due to firewall rules. - by observing the resulting network traffic an attacker might get knowledge about the existence of other hosts or network services on other hosts if the host running `kdeconnectd` is treated differently by involved firewalls than the attacker. - if mechanisms like `fail2ban` are used on other hosts in the network then it might be possible to add the host running `kdeconnectd` to a blacklist on some other machine, by hitting a maximum (connections / per time) limit. ##### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/85b691e40f525e22ca5cc4ebe79c361d71d7dc05 ### f) Pairing DoS The successfully established pairing of a legitimate device can be interrupted by another unauthorized party. To do this the attacker only needs to connect to `kdeconnectd` using the same 'deviceId' as the victim uses. This information is readily available, because it is broadcasted in cleartext by the legitimate device upon startup. Now when the attacker presents an arbitrary mismatching SSL certificate, the `kdeconnectd` will unpair the victim's device unconditionally (lanlinkprovider.cpp:352). #### Reproducer - In one shell perform a successful pairing against a host running `kdeconnectd`: ``` # add a suitable kdeconnectd remote IP address here REMOTE_IP=a.b.c.d $ ./kdeconnect.py --connect-tcp $REMOTE_IP --deviceid legit_device [...] Do you want to (re-)pair? (y/n) y ``` - Accept the pairing request on the host running `kdeconnectd` by clicking "Accept" in the popup dialog. After this you should receive a pairing confirmation message in the shell: ``` {'body': {'pair': True}, 'id': '1599139331920', 'type': 'kdeconnect.pair'} ``` - Now in a second shell emulate an attacking party: ``` $ ./kdeconnect.py --connect-tcp $REMOTE_IP --deviceid legit_device --localcert fakecerts/certificate.pem --privkey fakecerts/privateKey.pem ``` After this you should receive an unpair message in the first shell started above: ``` {'body': {'pair': False}, 'id': '1599139445990', 'type': 'kdeconnect.pair'} ``` The victim's device will no longer be paired, any active applications within `kdeconnectd` will be interrupted, upon next connection attempt a new pairing will become necessary. #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/48180b46552d40729a36b7431e97bbe2b5379306 ### g) SSL Validation Checks are not Applied During Initial Connection `kdeconnectd` requires the peer SSL certificate's CN (common name) in the Subject field to match the 'deviceId' transmitted in the 'kdeconnect.identity' message (lanlinkprovider.cpp:479). The reason for this requirement is unclear to me since both, the peer's SSL certificate as well as the peer's deviceId are under attacker control. When an unpaired device (see pairing process described in section 2.b) connects to `kdeconnectd` then all SSL verification errors are ignored (lanlinkprovider.cpp:301,435). An unpaired device can then initiate the pairing process. If it succeeds and the user accepts the pairing then the same TCP connection and same SSL session will continue to be used with the peer now considered to be trusted. No SSL validation will be performed. Only during a follow-up connection attempt by the device will SSL validation errors be detected. This might also affect the cipher requirements setup in `LanLinkProvider::configureSslSocket()`. If this is the case then a weak SSL connection might be possible for the initial session of a trusted device. I did not look very deep into this. Some tests I did acting as a QSslSocket client towards `kdeconnectd` showed that the selection of ciphers set in `QSslSocket::setCiphers()` has no influence at all on the SSL handshake and resulting cipher selection. But I might have made a mistake or misunderstood something about the related Qt API. Recommendation: After successful pairing a new SSL session should be established to verify the peer's certificate. Certain SSL errors should also be handled for untrusted devices (pretty much all errors except the SelfSignedCertificate error). #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/f183b5447bad47655c21af87214579f03bf3a163 ### h) Pairing Hijacking is Possible If a second SSL session is established using the 'deviceId' of an already connected device, then `kdeconnectd` will replace the existing SSL session by the new SSL session (lanlinkprovider.cpp:535). As described in section 3.f, the 'deviceId' is readily available from cleartext broadcast messages sent by kdeconnect devices. This situation allows an attacker to 'hijack' an ongoing pairing request. This works the following way: - legitimate device A connects via TCP to a host running `kdeconnectd`. The SSL session is established. - device A sends a pairing request message 'kdeconnect.pair'. - the host running `kdeconnectd` will present a popup to the interactive user asking for confirmation for the pairing request. The user will usually require at least a couple of seconds to confirm this request. - malicious device B connects with the same deviceId as device A but using a different self-signed SSL certificate. The existing SSL session from legitimate device A will replaced by the new SSL session from the malicious device. - the user accepts the pairing request - `kdeconnectd` will now enter the SSL certificate of the malicious device into its local database and mark the malicious device as trusted. The interactive user will see no signs of a second device being present. `kdeconnectd` will immediately send out sensitive data like the host's clipboard contents to the malicious device. - The malicious device should now also be able to run arbitrary commands on the host running `kdeconnectd` (but I did not test this). #### Reproducer - i) First start a pairing request in shell A (emulate the legitimate device): ``` # add a suitable kdeconnectd remote IP address here REMOTE_IP=a.b.c.d $ ./kdeconnect.py --connect-tcp $REMOTE_IP --deviceid legit_device [...] Do you want to (re-)pair? (y/n) y ``` - ii) You should now see a popup in the hosts KDE session asking for confirmation. Do *not* accept the pairing yet. Instead connect in shell B (emulate the malicious device) without request a pairing: ``` $ ./kdeconnect.py --connect-tcp $REMOTE_IP --deviceid legit_device --localcert fakecerts/certificate.pem --privkey fakecerts/privateKey.pem Do you want to (re-)pair? (y/n) n ``` - iii) Now you need to confirm the original pairing request triggered in shell A in the host's popup window. These steps have to be performed in less than 30 seconds, because there is a pairing timeout of 30 seconds implemented in `kdeconnectd`. If performed correctly then the connection in shell A should be terminated with: ``` error occured The remote host closed the connection ``` while in shell B, without sending out a pairing request, a pairing confirmation and clipboard contents (among other information) should appear: ``` {'body': {'pair': True}, 'id': '1599141347210', 'type': 'kdeconnect.pair'} [...] {'body': {'content': 'secret clipboard content', 'timestamp': 0}, 'id': '1599141347461', 'type': 'kdeconnect.clipboard.connect'} ``` #### Upstream Fix https://invent.kde.org/network/kdeconnect-kde/-/commit/48180b46552d40729a36b7431e97bbe2b5379306 # 4) Timeline - 2020-09-07: I've sent a full report to <security@....org> - 2020-10-01: After various discussions upstream asked me to review their patches, which I did. - 2020-10-02: The upstream security advisory [2] was published, the fixed version 20.08.2 was released. -- Matthias Gerstner <matthias.gerstner@...e.de> Dipl.-Wirtsch.-Inf. (FH), Security Engineer https://www.suse.com/security Phone: +49 911 740 53 290 GPG Key ID: 0x14C405C971923553 SUSE Software Solutions Germany GmbH HRB 36809, AG Nürnberg Geschäftsführer: Felix Imendörffer Download attachment "kdeconnect.tar.bz2" of type "application/x-bzip2" (11903 bytes) Download attachment "signature.asc" of type "application/pgp-signature" (834 bytes)
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.