Talos Vulnerability Report

TALOS-2025-2164

Tenda AC6 V5.0 /goform/getproductInfo information disclosure vulnerability

August 20, 2025
CVE Number

CVE-2025-24496

SUMMARY

An information disclosure vulnerability exists in the /goform/getproductInfo functionality of Tenda AC6 V5.0 V02.03.01.110. Specially crafted network packets can lead to a disclosure of sensitive information. An attacker can send packets to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Tenda AC6 V5.0 V02.03.01.110

PRODUCT URLS

AC6 V5.0 - https://www.tendacn.com/product/ac6v5.html

CVSSv3 SCORE

7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CWE

CWE-288 - Authentication Bypass Using an Alternate Path or Channel

DETAILS

The Tenda AC1200 AC6 is an IPv6 smart wifi router that supports multiple configuration types for home connectivity options. Extremely popular and affordable in online sellers, the Tenda AC1200 AC6 sees large usage in the home-networking space.

As common to most home routers, the main management interface for the Tenda AC6 AC1200 is via the web portal on TCP port 80. In the setup httpd_mainloop function, a series of callbacks is registered to a set of URLs, and these callbacks are accessed in series if they match on the URL of a given HTTP request:

int32_t httpd_mainloop()
// [...] 
800b2724        setup_web_server(port: 80, retries: 5)
800b2744        websUrlHandlerDefine(urlPrefix: &NULLSTR, webDir: nullptr, arg: 0, handler: http_auth_control_function, flags: 1)  // [1]
800b2764        websUrlHandlerDefine(urlPrefix: "/cgi-bin", webDir: nullptr, arg: 0, handler: cgi_bin_http_handler, flags: 0)      // [2]
800b2780        int32_t $s1 = 0
800b277c        do_log3(fmt: "-----------%s------------%d-----…", "initWebs", 0x10e)
800b279c        websUrlHandlerDefine(urlPrefix: "/goform", webDir: nullptr, arg: 0, handler: websFormHandler, flags: 0)            // [3]
800b27bc        websUrlHandlerDefine(urlPrefix: &NULLSTR, webDir: nullptr, arg: 0, handler: websHandler, flags: 2)
800b27c4        setup_goform_callbacks()           // [4]
800b27cc        setup_ate_cb()
800b27ec        websUrlHandlerDefine(urlPrefix: &slash, webDir: nullptr, arg: 0, handler: minimal_resp_cb, flags: 0)

To start, the authentication function is registered at [1], which verifies that either a correct password POST parameter is given, or that the ecos_pw cookie matches to a logged in admin. Since this registration occurs with a NULL urlPrefix, it is hit on every HTTP request.
Next up at [2], we can see a callback registered for the /cgi-bin directory which will only hit if our HTTP request’s URL starts with /cgi-bin. For the current vulnerability, we only particularly care about the authentication callback at [1], the /goform callback at [3], and then the particular functions that handle each particular goform which are registered within [4]. Let us first look briefly at the http_auth_control_function:

800c1814    int32_t http_auth_control_function(webs_t wp, char* uri)
// [...]
800c1968        if (strnstr(s1: uri, needle: "/public", len: 7) != 0 && 
                    wp->host != 0 && 
                    strnstr(s1: uri, needle: "/favicon.ico", len: 0xc) != 0 && 
                    strnstr(s1: uri, needle: "/goform/getproductInfo", len: 0x16) != 0) { // [5]
                }
800c25ac        return 0

Our requested URL is pointed to by the uri parameter, and within the line at [5], we clearly see a curious conditional. Assuming we’re requesting the /goform/getproductInfo URL, we completely bypass the entire authentication process. So, returning back to what we can do with this special URL, we first have to understand what occurs in the /goform handler in the websFormHandler function that was registered at [3]:

800b6368    int32_t websFormHandler(webs_t wp, char* urlPref, char* webDir, int32_t arg, char* url, char* path, char* query)

800b639c        data_81ee130c += 1
800b6398        void var_110
800b6398        strncpyclr(dst: &var_110, src: path, len: 0xfe)
800b63a4        char path_1[0xff]
800b63a4        char* slash = strchr(&path_1, '/')
800b63a4        
800b63ac        if (slash != 0)
800b63d4            char* second_slash = strchr(&slash[1], '/')
800b63d4            
800b63dc            if (second_slash != 0)
800b63e0                *second_slash = 0
800b63e0            
800b63ec            struct sym_t* $v0_2 = symLookup(0xffffffff, inpstr: &slash[1]) // [6]
800b63ec            
800b63f4            if ($v0_2 != 0)
800b63f8                int32_t $t8_1 = $v0_2->name.__bitfieldc.__offset(0x4).d
800b63f8                
800b641c                if ($t8_1 != 0)
800b6428                    $t8_1(wp, &slash[1], query) // [7]
800b63f4            else
800b640c                send_http_error_resp(httpreq: wp, 0xc8, errmsg: "Form %s is not defined", &slash[1])
800b63ac        else
800b63c0            send_http_error_resp(httpreq: wp, 0xc8, errmsg: "Missing form name", arg)
800b63c0        
800b6440        return 1

This function’s sole purpose is to look through all the registered form callbacks and to first see if that callback exists at [6], and then to call that function at [7] with our web request’s query parameters passed in as an argument. Simple enough, but now we have to see where /goform/getProductInfo has a registered callback within setup_goform_callbacks at [4]:

800c5b2c    int32_t setup_goform_callbacks()
800c5b50        set_goform_http_cb(varname: "setWAN", funcptr: generic_goform_setter)
800c5b60        set_goform_http_cb(varname: "getWAN", funcptr: generic_goform_getter)
800c5b70        set_goform_http_cb(varname: "setWifi", funcptr: generic_goform_setter)
800c5b80        set_goform_http_cb(varname: "getWifi", funcptr: generic_goform_getter)
800c5b90        set_goform_http_cb(varname: "getWifiRelay", funcptr: generic_goform_getter)
800c5ba4        set_goform_http_cb(varname: "getWifiScan", funcptr: webwrites_getWifiScan)
800c5bb4        set_goform_http_cb(varname: "setWifiRelay", funcptr: generic_goform_setter)
800c5bc4        set_goform_http_cb(varname: "getWizard", funcptr: generic_goform_getter)
800c5bd4        set_goform_http_cb(varname: "setWizard", funcptr: generic_goform_setter)
800c5be4        set_goform_http_cb(varname: "getNAT", funcptr: generic_goform_getter)
800c5bf4        set_goform_http_cb(varname: "setNAT", funcptr: generic_goform_setter)
800c5c04        set_goform_http_cb(varname: "setSysTools", funcptr: generic_goform_setter)
800c5c14        set_goform_http_cb(varname: "getSysTools", funcptr: generic_goform_getter)
800c5c24        set_goform_http_cb(varname: "sysReboot", funcptr: generic_goform_setter)
800c5c34        set_goform_http_cb(varname: "sysRestore", funcptr: generic_goform_setter)
800c5c44        set_goform_http_cb(varname: "setMacClone", funcptr: generic_goform_setter)
800c5c54        set_goform_http_cb(varname: "setHomePageInfo", funcptr: generic_goform_setter)
800c5c64        set_goform_http_cb(varname: "getStatus", funcptr: generic_goform_getter)
800c5c74        set_goform_http_cb(varname: "getQos", funcptr: generic_goform_getter)
800c5c84        set_goform_http_cb(varname: "setQos", funcptr: generic_goform_setter)
800c5c94        set_goform_http_cb(varname: "setParentControl", funcptr: generic_goform_setter)
800c5ca4        set_goform_http_cb(varname: "getParentControl", funcptr: generic_goform_getter)
800c5cb4        set_goform_http_cb(varname: "getHomePageInfo", funcptr: generic_goform_getter)
800c5cc4        set_goform_http_cb(varname: "setPowerSave", funcptr: generic_goform_setter)
800c5cd4        set_goform_http_cb(varname: "getPowerSave", funcptr: generic_goform_getter)
800c5ce8        set_goform_http_cb(varname: "setWifiWps", funcptr: webwrites_setWifiWps)
800c5cfc        set_goform_http_cb(varname: "loginOut", funcptr: webwrites_loginOut)
800c5d0c        set_goform_http_cb(varname: "setIPV6", funcptr: generic_goform_setter)
800c5d1c        set_goform_http_cb(varname: "getIPV6", funcptr: generic_goform_getter)
800c5d2c        set_goform_http_cb(varname: "getIpv6Status", funcptr: generic_goform_getter)
800c5d50        return set_goform_http_cb(varname: "getproductInfo", funcptr: generic_goform_getter) // [8]

A complete list of all available /goform/ pages is given above, and we see our /goform/getproductInfo URI registered at [8]. Interestingly, it actually uses the exact same callback as a large set of the other /goform/get* callbacks, so let us look within generic_goform_getter:

800c2a68    struct cjvar* generic_goform_getter(webs_t wp)

800c2a9c        char* modules = nullptr
800c2a98        struct cjvar* inp = create_cj_var()
800c2a98        
800c2aa0        if (inp != 0)
800c2acc            int32_t $s5_1 = 0
800c2ad0            modules = websGetVar(wp, needle: "modules", default: &NULLSTR)  // [9]
800c2adc            label_800c2adc:
800c2adc            char* modules_1 = modules
800c2adc            
800c2ae0            while (true)
800c2ae0                if (modules_1 == 0)
800c2b5c                    char* $s0_3 = nullptr
800c2b58                    websWrite(httpreq: wp, fmt_str: "HTTP/1.1 200 OK\nContent-type: t…")
800c2b58                    
800c2b60                    if ($s5_1 != 0)
800c2b68                        char* $v0_4 = cj_get(inp)
800c2b74                        $s0_3 = $v0_4
800c2b74                        
800c2b70                        if ($v0_4 != 0)
800c2b7c                            write_out_json(wp, $v0_4)  // [10]
800c2b7c                    
800c2b88                    check_socket_dead_or_keepalive(wp, http_code: 200)
800c2b90                    inp = parsed_json_dtor(inp)
800c2b90                    
800c2ba0                    if ($s5_1 != 0 && $s0_3 != 0)
800c2ba8                        return free($s0_3)
800c2ba8                    
800c2b98                    break
800c2b98                
800c2ae8                char* $v0_1 = strtok(&modules, U",") // [11]
800c2ae8                
800c2af0                if ($v0_1 == 0)
800c2af0                    goto label_800c2adc
800c2af0                
800c2af8                int32_t $s0_1 = 0
800c2afc                struct func_list_2* const modlist = &modules_get_list // [12]
800c2afc                
800c2b04                while (true)
800c2b04                    if (modlist->idk == 0)
800c2b04                        goto label_800c2adc
800c2b04                    
800c2b0c                    char* func = modlist->func
800c2b14                    modlist = &modlist[1]
800c2b14                    
800c2b18                    if (strcmp($v0_1, func) == 0)
800c2b40                        $s5_1 = 1
800c2b3c                        modules_get_list[$s0_1].field_8(wp, inp, 0)  // [13]
800c2b48                        modules_1 = modules
800c2b44                        break
800c2b44                    
800c2b1c                    $s0_1 += 1
800c2bd8        return inp

To summarize, the generic_goform_getter will look for a modules POST parameter at [9], and then iterate over the comma separated module strings with the strtok call at [11]. For each of these modules in the list, assuming that the module name matches one of the hard coded modules found in the static struct array at [12], then then the corresponding module getter function is called at [13], and the information is then sent inside of a json at [10]. To help clarify a little, the static modules struct is partially listed below:

80346b00  struct func_list_2 modules_get_list[0x2d] = 
80346b00  {
80346b00      [0x00] = 
80346b00      {
80346b00          char* func = data_802df84c {"wanBasicCfg"}
80346b04          uint32_t idk = 0x1
80346b08          void* field_8 = get_wanBasicCfg
80346b0c      }
80346b0c      [0x01] = 
80346b0c      {
80346b0c          char* func = data_802df858 {"internetStatus"}
80346b10          uint32_t idk = 0x1
80346b14          void* field_8 = get_internetStatus
80346b18      }    // [...]

While not listed in its entirety for brevity’s sake, there are 44 different registered getters for different modules. When calling any of the /goform/get* URLs via the web interface, these module POST parameters are pre-set for us, and correspond to whatever page is being browsed, such that only the relevant information is gathered and displayed, however keen readers will notice that there’s nothing stopping us from manually requesting all of the getters manually. Since the same generic information getter is called for all /goform/get* URLs, even though we’re requesting the /goform/getproductInfo page, which normally just grabs unimportant “productInfo” strings, we can request all the modules at once without any authentication.
This allows us to dump out the plaintext Wifi password, DDNS username and password, and most other configuration information that we could want (although the admin password is missing from this), all without any authentication, resulting in an information disclosure vulnerability.

TIMELINE

2025-04-29 - Initial Vendor Contact
2025-04-30 - Vendor Disclosure
2025-05-05 - Vendor Feedback Request
2025-05-08 - Vendor Feedback Request
2025-05-12 - Vendor Feedback Request
2025-06-11 - Vendor Feedback Request
2025-07-07 - Feedback Request / Announcement Of Upcoming Release Date
2025-07-23 - Feedback Request / Announcement Of Upcoming Release Date
2025-08-19 - Announcement Of Upcoming Release Date
2025-08-20 - Public Release

Credit

Discovered by Lilith >_> of Cisco Talos.