Skip to content

uhttpd: heap out-of-bounds read in uh_ubus_send_list() — missing type check on list params #36

Description

@JuliusBairaktaris

Summary

The JSON-RPC list handler iterates the params array with the raw iterator, which bounds
the walk but performs no type check, then hands each element to ubus_lookup() as a C string:

rem = blobmsg_data_len(dup);
data.verbose = true;
__blob_for_each_attr(cur, blobmsg_data(dup), rem)
        ubus_lookup(ctx, blobmsg_data(cur), uh_ubus_list_cb, &data);

For any element that is not BLOBMSG_TYPE_STRING the payload carries no NUL, and
blob_memdup() allocates exactly blob_pad_len(attr) bytes — so the strlen() inside
blob_put_string() runs off the end of the allocation.

list reaches this with no ACL check at all: uh_ubus_handle_request_object() calls
uh_ubus_allowed() only for "call".

Reproducer

POST /ubus HTTP/1.1
Host: 192.168.1.1
Content-Length: 59

{"jsonrpc":"2.0","id":1,"method":"list","params":[16843009]}

16843009 == 0x01010101, chosen so the 4-byte INT32 payload contains no zero byte and strlen()
cannot terminate inside it. [true], [{}], [[]], [1.5] behave the same.

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 5 at 0x71fe46be020c
    #0 strlen
    #1 blob_put_string           libubox/blob.h:209
    #2 ubus_lookup               ubus/libubus.c:187
    #3 uh_ubus_send_list         ubus.c:677
    #4 uh_ubus_handle_request_object ubus.c:807

0x71fe46be020c is located 0 bytes after 28-byte region [0x71fe46be01f0,0x71fe46be020c)
allocated by blob_memdup               libubox/blob.c:343

Why I am filing this publicly rather than as an advisory

I could not turn it into any consequence beyond the read, and I tried:

  • The over-read bytes are memcpy'd into the outgoing blob and sent to ubusd as
    UBUS_ATTR_OBJPATH. They are not reflected to the attacker — the lookup simply fails — so
    this is not an information-disclosure primitive.
  • blob_memdup()'s allocation is exactly 28 + 12·(n−1) for an n-element array, and the
    64 KiB POST cap allows n up to ~32000 (~384 KiB), comfortably past glibc's mmap threshold.
    To fault, the allocation must end flush with its mapping. Measuring the real layout via
    /proc/self/maps across n = 9000…20000: for mmap-backed chunks glibc places the user region
    at offset 16 and rounds the mapping up to a whole extra page (e.g. n=10920, alloc=131056,
    mapsz=135168, slack 4096). Minimum slack reachable in that range was 24 bytes, on a
    heap-backed chunk.
  • A sweep of n = 11000…13000 plus the specific boundary triples on a non-ASan build all
    returned 200 OK with the daemon alive.

So on x86-64/glibc the read lands in mapped heap and strlen() terminates almost immediately.
Scored strictly on what is demonstrated that is C:N/I:N/A:N.

Open question I could not close: musl/mallocng and 32-bit targets, which is where the OpenWrt
fleet actually lives and where the layout differs. I have no evidence either way there. If
someone demonstrates the fault on musl or 32-bit, this should be re-filed privately.

It is still a genuine memory-safety violation in an unauthenticated network parser reachable
with no ACL check, which is why it is worth fixing regardless of score.

Fix

One line, inside the loop:

if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
        continue;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions