Router4 was the only challenge released for the sixth Real World CTF. Three teams solved it during the event. The target simulated an ASUS RT-AC68U on firmware 3.0.0.4.386.51665, with a WAN-facing lighttpd service listening on port 443 by default.
The vulnerabilities involved were reported to ASUS after the competition and assigned CVE-2024-3079 and CVE-2024-3080. Some unintended paths were also shared with the competitors so they could report those issues as well.
Stack overflows
ASUS's lighttpd build contains several buffer overflows. These are a few that turned up before and after the contest.
- A stack overflow in lighttpd cookie handling. The cookie value is concatenated with
strncpy;tmp-usedis the length of the cookie value.

- A stack overflow in
mod_aicloud_auth.soURI parsing. The string after?is taken directly and concatenated withstrncpy, with a controllable length.

- A stack overflow in
replace_str. The function does not check length and writes into a buffer withsprintf.
char *replace_str(char *st, char *orig, char *repl, char* buff)
{
char *ch;
if (!(ch = strstr(st, orig)))
return st;
strncpy(buff, st, ch-st);
buff[ch-st] = 0;
sprintf(buff+(ch-st), "%s%s", repl, ch+strlen(orig));
return buff;
}
Looking at the call chain, change_webdav_file_path calls replace_str.

In the mod_webdav.so binary, sub_7e60 receives the buffer argument.

sub_7e60 then calls replace_str. Since replace_str concatenates with sprintf and performs no bounds check, this becomes a stack overflow.

Leaking an address with SQLite
The intended solution likely required a separate libc leak. In practice, two teams—BlueWater and Kalmarunionen—brute-forced libc instead. Because the target is 32-bit, the randomness is only 4096 possibilities, so the brute force was feasible. That was a challenge-design oversight.
During firmware reversing and code audit, an SQL injection was found in mod_webdav.so. When it was reported to ASUS, it turned out to have been reported before as CVE-2023-35720.
The component reads values from HTTP headers by keyword.

For example, it extracts Keyword from a header. Around line 2186 there is a validity check; if the value is invalid, the HTTP response is 207.

The check rejects empty values and values containing a single quote. If the value passes, it is concatenated into an SQL statement.

Before concatenation, the value is URL-decoded. Encoding the single quote bypasses the check, and the decoded value reaches the SQL statement, producing SQL injection.
That still leaves the information-leak question. The component's database is SQLite3, and SQLite3 provides a way to leak an address through its tokenizer API.
The custom tokenizer registration function is fts3_tokenizer, implemented in scalarFunc in ext/fts3/fts3_tokenizer.c. It supports two forms:
SELECT fts3_tokenizer(<tokenizer-name>);
SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);
With only one argument, the function returns the sqlite3_tokenizer_module struct pointer for the named tokenizer, represented as a blob. For example:
sqlite> select hex(fts3_tokenizer('simple'));

The result is a big-endian hex memory address. The pointer refers to a sqlite3_tokenizer_module structure. The optional second argument registers a new tokenizer, for example:
sqlite> select fts3_tokenizer('mytokenizer', x'0xdeadbeefdeadbeef');
Calling select fts3_tokenizer(name) with one argument on an already registered tokenizer returns that tokenizer's memory address. In fts3.c, SQLite3 registers the built-in simple and porter tokenizers:
if( sqlite3Fts2HashInsert(pHash, "simple", 7, (void *)pSimple) ||
sqlite3Fts2HashInsert(pHash, "porter", 7, (void *)pPorter)
For simple, the registered pointer points to the static simpleTokenizerModule:
static const sqlite3_tokenizer_module simpleTokenizerModule = {
0, simpleCreate, simpleDestroy, simpleOpen, simpleClose, simpleNext,
};
From that pointer, a simple calculation yields the libsqlite3.so base address and defeats ASLR.

Combined with the SQL injection, this leaks the address.

Authentication bypass
The route check looks for /smb/, but it misses /smb. A request to /smb does not match the condition and slips past authorization.

An unintended file read
BlueWater and Kalmarunionen solved the challenge with the stack overflow plus libc brute force. Friendly Maltese Citizens took a different route.
They noticed the SQL injection could be combined with the SMB GETMUSICCLASSIFICATION method. That method includes get_album_cover_image, which loads file contents and can leak them. They used SQL injection to write the flag path into the album table, then previewed it directly:
await fetch("/RWCTF", {
"headers": {
"classify": "album",
},
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>",
"method": "GETMUSICCLASSIFICATION"
}).then(a => a.text())