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-used is the length of the cookie value.

Cookie handling overflow

  • A stack overflow in mod_aicloud_auth.so URI parsing. The string after ? is taken directly and concatenated with strncpy, with a controllable length.

URI parser overflow

  • A stack overflow in replace_str. The function does not check length and writes into a buffer with sprintf.
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.

Call chain to replace_str

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

sub_7e60 receives buffer

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

replace_str call inside sub_7e60

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.

Header keyword lookup

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.

Validity check returning 207

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

SQL concatenation

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'));

Hex tokenizer pointer

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.

libsqlite3 base calculation

Combined with the SQL injection, this leaks the address.

Leaked address through SQL injection

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.

Route check for /smb/

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())