Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
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.