August 25, 2026
Two Zero-Days in the DrayTek Vigor2962 Router
Introduction

By David
9 min read
Introduction
Around six months ago, I started researching the DrayTek Vigor2962 router, focusing on its web management interface, internal services, and the underlying DrayOS components.
At the time of my research, the latest firmware I tested was 4.4.5. The out-of-bounds write described in this article was identified in that version and was subsequently fixed in a later release. The format-string vulnerability was also identified during my research and remained present in 4.4.6.1, before being fixed in 4.4.6.2.
The current Vigor2962 firmware listed by DrayTek is 4.4.6.2, released on August 17, 2026.
This research focuses on two different memory-safety issues discovered during my analysis:
- An out-of-bounds write in the user-management functionality, which can also result in disclosure of a user's password through the web interface.
- A format-string vulnerability in the online-user logging functionality.
Both issues require authenticated administrative access, so they should not be interpreted as unauthenticated remote compromise vulnerabilities.
Research Environment
The research was performed against the DrayTek Vigor2962 firmware and its underlying ARM64 software components.
The main binary of interest is:
/draytek/drayapp/soho2962.bin/draytek/drayapp/soho2962.binOne of the challenges during analysis was that newer firmware versions ship this binary stripped of useful symbol information. Recovering function names and understanding the program structure therefore became an important part of the research process.
For firmware analysis, I followed the general methodology described in the research by cctrl, particularly the firmware decryption, filesystem extraction, symbol recovery, and QEMU emulation techniques. The referenced research covers these techniques for DrayTek firmware, including the use of older firmware containing symbols and Rizzo for transferring symbol information into a stripped binary.
Reference:
DrayTek CVE-2024–41592 Stack_Overflow Replicate — Medium
Firmware Decryption and Filesystem Extraction
The first challenge was obtaining a useful filesystem from the firmware image.
The firmware image itself is encrypted, so simply running Binwalk against it does not immediately provide a complete filesystem.
The approach I used was based on locating the firmware decryption functionality in an older, unencrypted firmware image and then using that information to decrypt the newer firmware.
The referenced research follows essentially the same approach: locate the firmware decryption script/program in the GPL source tree and use it to recover the encrypted firmware contents.
The GPL source code was obtained from DrayTek's public GPL server:
After decrypting and extracting the firmware, I was able to obtain the filesystem and the relevant DrayOS components for further analysis.
The older firmware contains the filesystem and utilities used by DrayOS itself, including the chacha20 binary:
/draytek/drayv2962/chacha20/draytek/drayv2962/chacha20The relevant decryption logic can be found in the following script:
/*decryption code
./Vigor2962_431_GPL_release/drayrt-release-gpl/output/rootfs/draytek/drayapp/runcommand/fw_upload
*/
#!/bin/sh
decrypt_firmware(){
input_file=$1
decrypt_data_sect="/tmp/decrypt_data_sect"
data_sect="/tmp/data_sect"
head_sect="/tmp/head_sect"
tail -c +$((256 + 64 + 64 + 1)) $input_file > $data_sect
head -c $((256 + 64 + 64)) $input_file > $head_sect
enc_signature=$(head -c $((320 + 36)) $head_sect | tail -c 4)
if [ "$enc_signature" == "0204" ]; then
echo "Firmware is encypt, start to decrypt."
nonce=$(head -c $((320 + 48)) $head_sect | tail -c 12)
/draytek/drayv2962/chacha20 $data_sect $decrypt_data_sect $nonce
cat $head_sect $decrypt_data_sect > ${input_file}_dec_file || abort "Unable to decrypt"
rm $decrypt_data_sect
fi
rm $data_sect $head_sect
}
decrypt_firmware $1/*decryption code
./Vigor2962_431_GPL_release/drayrt-release-gpl/output/rootfs/draytek/drayapp/runcommand/fw_upload
*/
#!/bin/sh
decrypt_firmware(){
input_file=$1
decrypt_data_sect="/tmp/decrypt_data_sect"
data_sect="/tmp/data_sect"
head_sect="/tmp/head_sect"
tail -c +$((256 + 64 + 64 + 1)) $input_file > $data_sect
head -c $((256 + 64 + 64)) $input_file > $head_sect
enc_signature=$(head -c $((320 + 36)) $head_sect | tail -c 4)
if [ "$enc_signature" == "0204" ]; then
echo "Firmware is encypt, start to decrypt."
nonce=$(head -c $((320 + 48)) $head_sect | tail -c 12)
/draytek/drayv2962/chacha20 $data_sect $decrypt_data_sect $nonce
cat $head_sect $decrypt_data_sect > ${input_file}_dec_file || abort "Unable to decrypt"
rm $decrypt_data_sect
fi
rm $data_sect $head_sect
}
decrypt_firmware $1Recovering Symbols
Another important part of the research was symbol recovery.
The latest soho2962.bin I analyzed was stripped, which makes static analysis considerably more difficult. Instead of analyzing the binary entirely through anonymous functions such as:
sub_40xxxxxx()sub_40xxxxxx()I wanted to recover the original function names wherever possible.
The approach described in the referenced DrayTek research is to locate an older firmware binary containing symbols and use it as a reference. Rizzo can compare the binaries and recover matching functions and symbols.
I used the same general technique:
- Obtain a suitable older firmware.
- Locate a
sohobinary containing symbols. - Load the symbolic binary into IDA.
- Export the symbols using
rizzo.py. - Load the resulting Rizzo symbol database into the stripped target binary.
- Continue analysis using the recovered function names.
Although I initially used IDA for the symbol-recovery step, my primary reverse-engineering tool is Binary Ninja. Therefore, after recovering the symbols with Rizzo, I exported the recovered information and imported it into Binary Ninja.
This made the subsequent analysis significantly easier because the important functions could be analyzed using meaningful names rather than anonymous addresses.
QEMU Emulation
Because the Vigor2962 is an ARM64-based platform, reproducing the vulnerable environment on real hardware is not always convenient.
I therefore built an emulated environment using QEMU.
The initial setup was again based on the approach described in the referenced research.
Rather than using a generic upstream QEMU build, I obtained the QEMU source from DrayTek's GPL release and built it locally. This provided better compatibility with the platform-specific modifications used by the Vigor2962 firmware.
network script:
#!/bin/bash
#iflan=ens38
#ifwan=ens39
mylanip="192.168.1.2"
brctl delbr br-lan 2> /dev/null
brctl delbr br-wan 2> /dev/null
ip link add br-lan type bridge
ip tuntap add qemu-lan mode tap
#brctl addif br-lan $iflan
brctl addif br-lan qemu-lan
#ip addr flush dev $iflan
ifconfig br-lan $mylanip
ifconfig br-lan up
ifconfig qemu-lan up
#ifconfig $iflan up
ip link add br-wan type bridge
ip tuntap add qemu-wan mode tap
#brctl addif br-wan $ifwan
brctl addif br-wan qemu-wan
#ip addr flush dev $ifwan
ifconfig br-lan $mylanip
ifconfig br-wan up
ifconfig qemu-wan up
#ifconfig $ifwan up
brctl show
#for speed test
ethtool -K $iflan gro off
ethtool -K $iflan gso off
ethtool -K $ifwan gro off
ethtool -K $ifwan gso off
ethtool -K qemu-lan gro off
ethtool -K qemu-lan gso off
ethtool -K qemu-wan gro off
ethtool -K qemu-wan gso off
#for telnet from linux to drayos 192.168.1.1
ethtool -K br-lan tx off#!/bin/bash
#iflan=ens38
#ifwan=ens39
mylanip="192.168.1.2"
brctl delbr br-lan 2> /dev/null
brctl delbr br-wan 2> /dev/null
ip link add br-lan type bridge
ip tuntap add qemu-lan mode tap
#brctl addif br-lan $iflan
brctl addif br-lan qemu-lan
#ip addr flush dev $iflan
ifconfig br-lan $mylanip
ifconfig br-lan up
ifconfig qemu-lan up
#ifconfig $iflan up
ip link add br-wan type bridge
ip tuntap add qemu-wan mode tap
#brctl addif br-wan $ifwan
brctl addif br-wan qemu-wan
#ip addr flush dev $ifwan
ifconfig br-lan $mylanip
ifconfig br-wan up
ifconfig qemu-wan up
#ifconfig $ifwan up
brctl show
#for speed test
ethtool -K $iflan gro off
ethtool -K $iflan gso off
ethtool -K $ifwan gro off
ethtool -K $ifwan gso off
ethtool -K qemu-lan gro off
ethtool -K qemu-lan gso off
ethtool -K qemu-wan gro off
ethtool -K qemu-wan gso off
#for telnet from linux to drayos 192.168.1.1
ethtool -K br-lan tx offprepare environment:
#!/bin/bash
# 1. do "fw_setenv purelinux 1" first , then reboot
# 2. do setup_qemu_linux.sh (default P3 as WAN, P4 as LAN, for both 1Gbps connection only)
# 3. remember to recover to normal mode by "fw_setenv purelinux 0"
rangen() {
printf "%02x" `shuf -i 1-255 -n 1`
}
wan_mac(){
idx=$1
printf "%02x\n" $((0x${C}+0x$idx)) | tail -c 3 # 3 = 2 digit + 1 terminating character
}
A=$(rangen); B=$(rangen); C=$(rangen);
LAN_MAC="00:1d:aa:${A}:${B}:${C}"
mkfifo serial0
platform_path="./platform"
echo "x86" > $platform_path
enable_kvm_path="./enable_kvm"
echo "kvm" > $enable_kvm_path
cfg_path="./magic_file"
echo "GCI_SKIP" > gci_magic
uffs_flash="./v2962_ram_flash.bin"
echo "0" > memsize
SHM_SIZE=16777216
route add default gw 192.168.1.1
dd if=/dev/zero of=v2962_ram_flash.bin bs=512 count=100000
./qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -dtb DrayTek -m 512 \
-kernel ./soho2962.bin $serial_option \
-nographic $gdb_serial_option $gdb_remote_option -name debug-threads=on \
-device virtio-net-pci,netdev=network-lan,mac=${LAN_MAC} \
-netdev tap,id=network-lan,ifname=qemu-lan,script=no,downscript=no \
-device virtio-net-pci,netdev=network-wan,mac=00:1d:aa:${A}:${B}:$(wan_mac 1)\
-netdev tap,id=network-wan,ifname=qemu-wan,script=no,downscript=no \
-device virtio-serial-pci -chardev pipe,id=ch0,path=serial0 \
-device virtserialport,chardev=ch0,name=serial0 \
-monitor telnet:127.0.0.1:7777,server,nowait \
-device loader,file=gci_magic,addr=0x4de0000 \
-device loader,file=$uffs_flash,addr=0x00be0000 \
-device loader,file=$cfg_path,addr=0x260000 \
-device loader,file=$platform_path,addr=0x25fff0 \
-device loader,file=$enable_kvm_path,addr=0x25ffe0 \
-device loader,file=memsize,addr=0x25ff67 \
-device ivshmem-plain,memdev=hostmem \
-object memory-backend-file,size=${SHM_SIZE},share,mem-path=/dev/shm/ivshmem,id=hostmem \#!/bin/bash
# 1. do "fw_setenv purelinux 1" first , then reboot
# 2. do setup_qemu_linux.sh (default P3 as WAN, P4 as LAN, for both 1Gbps connection only)
# 3. remember to recover to normal mode by "fw_setenv purelinux 0"
rangen() {
printf "%02x" `shuf -i 1-255 -n 1`
}
wan_mac(){
idx=$1
printf "%02x\n" $((0x${C}+0x$idx)) | tail -c 3 # 3 = 2 digit + 1 terminating character
}
A=$(rangen); B=$(rangen); C=$(rangen);
LAN_MAC="00:1d:aa:${A}:${B}:${C}"
mkfifo serial0
platform_path="./platform"
echo "x86" > $platform_path
enable_kvm_path="./enable_kvm"
echo "kvm" > $enable_kvm_path
cfg_path="./magic_file"
echo "GCI_SKIP" > gci_magic
uffs_flash="./v2962_ram_flash.bin"
echo "0" > memsize
SHM_SIZE=16777216
route add default gw 192.168.1.1
dd if=/dev/zero of=v2962_ram_flash.bin bs=512 count=100000
./qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -dtb DrayTek -m 512 \
-kernel ./soho2962.bin $serial_option \
-nographic $gdb_serial_option $gdb_remote_option -name debug-threads=on \
-device virtio-net-pci,netdev=network-lan,mac=${LAN_MAC} \
-netdev tap,id=network-lan,ifname=qemu-lan,script=no,downscript=no \
-device virtio-net-pci,netdev=network-wan,mac=00:1d:aa:${A}:${B}:$(wan_mac 1)\
-netdev tap,id=network-wan,ifname=qemu-wan,script=no,downscript=no \
-device virtio-serial-pci -chardev pipe,id=ch0,path=serial0 \
-device virtserialport,chardev=ch0,name=serial0 \
-monitor telnet:127.0.0.1:7777,server,nowait \
-device loader,file=gci_magic,addr=0x4de0000 \
-device loader,file=$uffs_flash,addr=0x00be0000 \
-device loader,file=$cfg_path,addr=0x260000 \
-device loader,file=$platform_path,addr=0x25fff0 \
-device loader,file=$enable_kvm_path,addr=0x25ffe0 \
-device loader,file=memsize,addr=0x25ff67 \
-device ivshmem-plain,memdev=hostmem \
-object memory-backend-file,size=${SHM_SIZE},share,mem-path=/dev/shm/ivshmem,id=hostmem \Vulnerability #1 — Out-of-Bounds Write in User Management
Overview
The first vulnerability is an out-of-bounds write in the user-management functionality.
The vulnerable functionality is reached when creating a user account.
The web interface normally validates the username and password length and restricts them to a maximum of 32 bytes. However, this restriction is enforced by the client-side/web-interface request handling and can be bypassed by modifying the request directly.
Root Cause #1
Triggering the Vulnerability #1
By intercepting the HTTP request with a proxy such as Burp Suite, the username and password fields can be modified after the web UI has performed its normal validation.
This allows an input substantially larger than the intended maximum to reach the backend.
The original report demonstrates that after creating such an account, the user-management page begins exhibiting abnormal behavior.
After creating the oversized user entry, the user list no longer behaves normally.
An interesting observation was that the response contains an unknown character. Replacing that character allows the user list to be displayed again.
More importantly, examination of the resulting memory state showed that the problem was not simply a rendering bug; but we can fix it.
There were originally two default user accounts. After creating only one additional account with an oversized value, memory belonging to the user structure was being overwritten.
The GDB outpu identifies the beginning of the newly created username structure at approximately:
0x486718900x48671890and observed a relationship between the fields:
password = username + 132
next account = password + 168password = username + 132
next account = password + 168This suggested that the user records are stored in a relatively simple, contiguous structure in memory.
This observation was important because it transformed what initially looked like a malformed user-management response into a clear memory-corruption primitive.
Password Disclosure
The out-of-bounds write can also lead to password disclosure due to the layout of the user structures in memory.
By providing a sufficiently long username, the end of the supplied data can overwrite the beginning of the next user's password field. When the router later generates the user list, it interprets this overwritten memory as part of the previous user's username and displays it in the web panel.
As a result, the next user's password can appear as part of the previous user's username, allowing the password to be leaked through the user-management page.
Vulnerability #2 — Format String in Online User Logging
The second vulnerability is a format-string vulnerability in the functionality responsible for logging online users.
The attack again requires Authenticated administrative access
The interesting part of this vulnerability is that the attacker can control the username field that eventually reaches a formatting function.
Root Cause #2
During analysis of the relevant code path, I found that the username is incorporated into a string using a format operation.
The important function is:
TR069WebChangeNotifyTR069WebChangeNotifywhich eventually calls:
vscnprintf()vscnprintf()a wrapper around the standard vsnprintf() functionality.
the string directly passed to vscnprintf!
Before triggering we should enable "User-Based" and "http" options for target user:
Creating the Malicious Username
To reach the vulnerable code path, the relevant user-management options first need to be enabled.
A new user account is then created through the User Profile page.
finally we should see it under user profiles:
Triggering the Vulnerability #2
After the modified user is created, the username does not immediately trigger the vulnerable formatting operation.
The user first needs to appear in the router's online-user list.
During testing, I found that the newly created account could not be logged into through the web interface, but it could be used through the router's Telnet service.
Once logged in, the account appears under:
User Management
└── User Online StatusUser Management
└── User Online Status
The report documents this behavior and the use of Telnet to make the account appear in the online-user list.
From the User Online Status page, selecting the relevant user causes another request to be generated.
The request contains fields associated with the online-user operation.
The important observation is that two fields can be modified:
username
webchangeusername
webchangeThe username field is set to the URL-encoded format string:
%25x%25xwhile:
webchange=0webchange=0is changed to:
webchange=1webchange=1Now lets check it in GDB and hit breakpoints
At this point the controlled format string reaches the vulnerable formatting path.
Conclusion
During my research into the DrayTek Vigor2962, I identified two vulnerabilities in its user-management functionality: an out-of-bounds write that can lead to password disclosure, and a format-string vulnerability in the User Online Status functionality.
Both vulnerabilities require authenticated administrative access. The issues have since been addressed in newer firmware, with 4.4.6.2 being the current version.
This research also highlights how firmware extraction, symbol recovery, emulation, and dynamic debugging can be combined to analyze embedded devices.
Timeline
- ~February–March 2026 — Vulnerabilities Discovered
- 5 August 2026 — Vulnerabilities Reported to DrayTek
- 18 August 2026— DrayTek Update
- 25 August 2026 — Public disclosure