Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CVE-2019-19945: uhttpd, OpenWrt 18.06.5 to 18.06.6

OpenWrt 18.06.6 updates uhttpd to 2019-12-22, with three upstream commits (OpenWrt commit b901563):

  • 5f9ae57, “client: fix invalid data access through invalid content-length values”: the fix. Jan-Niklas Sohn found that a POST to a CGI script with Transfer-Encoding: chunked and a Content-Length above 2^31 reads as a negative length: client_parse_header stores it, and client_poll_post_data passes it on to ustream_consume (advisory 2020-01-13-1).
  • 6b03f96, “ubus: increase maximum ubus request size to 64KB”.
  • 91fcac3, “uhttpd: Fix multiple format string problems”.

OpenWrt runs sstrip on what it ships, so only the segments of the file are left.

Get the binaries

armvirt before 21.02 ships an empty rootfs.cpio.gz, so the files come from the tar of the root file system:

$ for v in 18.06.5 18.06.6; do
    mkdir -p $v
    curl -s https://downloads.openwrt.org/releases/$v/targets/armvirt/64/openwrt-$v-armvirt-64-default-rootfs.tar.gz \
      | tar -xz -C $v ./usr/sbin/uhttpd
  done

Compare

$ hereticus --format markdown 18.06.5/usr/sbin/uhttpd 18.06.6/usr/sbin/uhttpd
FUNCTIONCHANGESCORE+/-/~TOUCHEDMATCHED BY
sub_4076f8 -> sub_4076fcchanged0.916/1/01 branch, 1 jumpsimilar
sub_4040fcchanged0.973/2/01 branch, 2 memsimilar
sub_404288 -> sub_404290changed0.993/1/01 branchsimilar
sub_404bfc -> sub_404c08changed0.971/2/02 memsimilar
sub_404ca4 -> sub_404cacchanged0.981/2/02 memsimilar

228 matched (222 unchanged, 6 changed), 0 gone, 0 new

sub_4040fc is client_poll_post_data, which calls ustream_consume, and sub_404288 is client_parse_header, which reads "chunked": the fix is on rows 2 and 3. The other rows hold the two other commits, or code that moved with them; this report does not tell them apart. The build for ARMv7 reads the same, with the fix on rows 2 and 3 of 5. The MIPS16e build for malta, from the initramfs of its kernel, has the fix on rows 1 and 2 of 6. Its code is not PIC, so the literal pool after each function holds the absolute addresses it loads, which move with each release; the matching reads them as data.

Read the fix

$ hereticus --view il --diff sub_404288 18.06.5/usr/sbin/uhttpd 18.06.6/usr/sbin/uhttpd
...
+ w1 = load.8 [x1]
+ if (w1 != 0) goto $+0x8
+ if ((w0 & 0x80000000) == 0) goto $-0x80
  x2 = &page
  w1 = 0x190
  x2 = &"Bad Request"
...

The new test of bit 31 refuses a length that reads as negative: the code after it answers 400 Bad Request.