1

物联网设备常见的web服务器——uhttpd源码分析(一)

 3 years ago
source link: https://xz.aliyun.com/t/9543
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
物联网设备常见的web服务器——uhttpd源码分析(一) - 先知社区

物联网设备常见的web服务器——uhttpd源码分析(一)

知微攻防实验室 / 2021-05-13 12:38:00 / 浏览数 8148 安全技术 WEB安全 顶(0) 踩(0)

0x00 前言
uHTTPd 是一个 OpenWrt/LUCI 开发者从头编写的 Web 服务器。 它着力于实现一个稳定高效的服务器,能够满足嵌入式设备的轻量级任务需求,且能够与 OpenWrt 的配置框架 (UCI) 整合。默认情况下它被用于 OpenWrt 的 Web 管理接口 LuCI。当然,uHTTPd 也能提供一个常规 Web 服务器所需要的所有功能。
0x01 简介
讲解uhttpd的主要原因是:uhttpd是物联网设备很常见的一个web服务器,在物联网设备漏洞挖掘的过程中,最常见的漏洞都是出现在web服务器上,如果能熟悉各个开源的web服务器的开发流程,更容易理解其他厂商的开发者是如何开发自己的web服务器,那么对物联网漏洞挖掘将事倍功半。
下载地址:https://git.openwrt.org/?p=project/uhttpd.git;a=summary 点击 snapshot 即可下载
0x02 主函数main

int main(int argc, char **argv) 
{ 
        struct alias *alias; 
        bool nofork = false; 
        char *port; 
        int opt, ch; 
        int cur_fd; 
        int bound = 0; 
#ifdef HAVE_TLS 
        int n_tls = 0; 
        const char *tls_key = NULL, *tls_crt = NULL, *tls_ciphers = NULL; 
#endif 
#ifdef HAVE_LUA 
        const char *lua_prefix = NULL, *lua_handler = NULL; 
#endif 
    // 如果没有LUA插件,才会执行 
        BUILD_BUG_ON(sizeof(uh_buf) < PATH_MAX); 

        uh_dispatch_add(&cgi_dispatch); // 添加cgi_dispatch到dispatch_handlers链表中,后续解析cgi或者lua用 
        init_defaults_pre(); //初始化默认参数,如:cgi的前缀名为 /cgi-bin 
    /*设置信号屏蔽*/ 
        signal(SIGPIPE, SIG_IGN); 
    /*用户输入的参数*/ 
        while ((ch = getopt(argc, argv, "A:aC:c:Dd:E:e:fh:H:I:i:K:k:L:l:m:N:n:P:p:qRr:Ss:T:t:U:u:Xx:y:")) != -1) { 
                switch(ch) { 
#ifdef HAVE_TLS 
                case 'C': 
                        tls_crt = optarg; 
                        break; 

                case 'K': 
                        tls_key = optarg; 
                        break; 

                case 'P': 
                        tls_ciphers = optarg; 
                        break; 

                case 'q': 
                        conf.tls_redirect = 1; 
                        break; 

                case 's': 
                        n_tls++; 
                        /* fall through */ 
#else 
                case 'C': 
                case 'K': 
                case 'P': 
                case 'q': 
                case 's': 
                        fprintf(stderr, "uhttpd: TLS support not compiled, " 
                                        "ignoring -%c\n", ch); 
                        break; 
#endif 
                case 'p': 
                        optarg = strdup(optarg); 
                        bound += add_listener_arg(optarg, (ch == 's')); 
                        break; 

                case 'h': 
                        if (!realpath(optarg, uh_buf)) { 
                                fprintf(stderr, "Error: Invalid directory %s: %s\n", 
                                                optarg, strerror(errno)); 
                                exit(1); 
                        } 
                        conf.docroot = strdup(uh_buf); 
                        break; 

                case 'H': 
                        if (uh_handler_add(optarg)) { 
                                fprintf(stderr, "Error: Failed to load handler script %s\n", 
                                        optarg); 
                                exit(1); 
                        } 
                        break; 

                case 'E': 
                        if (optarg[0] != '/') { 
                                fprintf(stderr, "Error: Invalid error handler: %s\n", 
                                                optarg); 
                                exit(1); 
                        } 
                        conf.error_handler = optarg; 
                        break; 

                case 'I': 
                        if (optarg[0] == '/') { 
                                fprintf(stderr, "Error: Invalid index page: %s\n", 
                                                optarg); 
                                exit(1); 
                        } 
                        uh_index_add(optarg); 
                        break; 

                case 'S': 
                        conf.no_symlinks = 1; 
                        break; 

                case 'D': 
                        conf.no_dirlists = 1; 
                        break; 

                case 'R': 
                        conf.rfc1918_filter = 1; 
                        break; 

                case 'n': 
                        conf.max_script_requests = atoi(optarg); 
                        break; 

                case 'N': 
                        conf.max_connections = atoi(optarg); 
                        break; 
                /*cgi文件路径前缀*/ 
                case 'x': 
                        fixup_prefix(optarg); 
                        conf.cgi_prefix = optarg; 
                        break; 

                case 'y': 
                        alias = calloc(1, sizeof(*alias)); 
                        if (!alias) { 
                                fprintf(stderr, "Error: failed to allocate alias\n"); 
                                exit(1); 
                        } 
                        alias->alias = strdup(optarg); 
                        alias->path = strchr(alias->alias, '='); 
                        if (alias->path) 
                                *alias->path++ = 0; 
                        list_add(&alias->list, &conf.cgi_alias); 
                        break; 

                case 'i': 
                        optarg = strdup(optarg); 
                        port = strchr(optarg, '='); 
                        if (optarg[0] != '.' || !port) { 
                                fprintf(stderr, "Error: Invalid interpreter: %s\n", 
                                                optarg); 
                                exit(1); 
                        } 

                        *port++ = 0; 
                        uh_interpreter_add(optarg, port); 
                        break; 

                case 't': 
                        conf.script_timeout = atoi(optarg); 
                        break; 

                case 'T': 
                        conf.network_timeout = atoi(optarg); 
                        break; 

                case 'k': 
                        conf.http_keepalive = atoi(optarg); 
                        break; 

                case 'A': 
                        conf.tcp_keepalive = atoi(optarg); 
                        break; 

                case 'f': 
                        nofork = 1; 
                        break; 

                case 'd': 
                        optarg = strdup(optarg); 
                        port = alloca(strlen(optarg) + 1); 
                        if (!port) 
                                return -1; 

                        /* "decode" plus to space to retain compat */ 
                        for (opt = 0; optarg[opt]; opt++) 
                                if (optarg[opt] == '+') 
                                        optarg[opt] = ' '; 

                        /* opt now contains strlen(optarg) -- no need to re-scan */ 
                        if (uh_urldecode(port, opt, optarg, opt) < 0) { 
                                fprintf(stderr, "uhttpd: invalid encoding\n"); 
                                return -1; 
                        } 

                        printf("%s", port); 
                        return 0; 
                        break; 

                /* basic auth realm */ 
                case 'r': 
                        conf.realm = optarg; 
                        break; 

                /* md5 crypt */ 
                case 'm': 
                        printf("%s\n", crypt(optarg, "$1$")); 
                        return 0; 
                        break; 

                /* config file */ 
                case 'c': 
                        conf.file = optarg; 
                        break; 

#ifdef HAVE_LUA 
                case 'l': 
                case 'L': 
                        if (ch == 'l') { 
                                if (lua_prefix) 
                                        fprintf(stderr, "uhttpd: Ignoring previous -%c %s\n", 
                                                ch, lua_prefix); 

                                lua_prefix = optarg; 
                        } 
                        else { 
                                if (lua_handler) 
                                        fprintf(stderr, "uhttpd: Ignoring previous -%c %s\n", 
                                                ch, lua_handler); 

                                lua_handler = optarg; 
                        } 

                        if (lua_prefix && lua_handler) { 
                                add_lua_prefix(lua_prefix, lua_handler); 
                                lua_prefix = NULL; 
                                lua_handler = NULL; 
                        } 

                        break; 
#else 
                case 'l': 
                case 'L': 
                        fprintf(stderr, "uhttpd: Lua support not compiled, " 
                                        "ignoring -%c\n", ch); 
                        break; 
#endif 
#ifdef HAVE_UBUS 
                case 'a': 
                        conf.ubus_noauth = 1; 
                        break; 

                case 'u': 
                        conf.ubus_prefix = optarg; 
                        break; 

                case 'U': 
                        conf.ubus_socket = optarg; 
                        break; 

                case 'X': 
                        conf.ubus_cors = 1; 
                        break; 

                case 'e': 
                        conf.events_retry = atoi(optarg); 
                        break; 
#else 
                case 'a': 
                case 'u': 
                case 'U': 
                case 'X': 
                case 'e': 
                        fprintf(stderr, "uhttpd: UBUS support not compiled, " 
                                        "ignoring -%c\n", ch); 
                        break; 
#endif 
                default: 
                        return usage(argv[0]); 
                } 
        } 
        /*配置文件解析*/ 
        uh_config_parse(); 

        if (!conf.docroot) { 
                if (!realpath(".", uh_buf)) { //uh_buf为当前工作目录的绝对路径指针 
                        fprintf(stderr, "Error: Unable to determine work dir\n"); 
                        return 1; 
                } 
                conf.docroot = strdup(uh_buf); //docroot字段保存了当前工作路径 
        } 
        /*初始化默认主页和cgi绝对路径*/ 
        init_defaults_post(); 

        if (!bound) { //如果没有监听端口成功,则报错退出 
                fprintf(stderr, "Error: No sockets bound, unable to continue\n"); 
                return 1; 
        } 

#ifdef HAVE_TLS // 如果有TLS插件才会执行,也就是https 
        if (n_tls) { 
                if (!tls_crt || !tls_key) {//没有公匙或者没有私匙,则报错退出 
                        fprintf(stderr, "Please specify a certificate and " 
                                        "a key file to enable SSL support\n"); 
                        return 1; 
                } 

                if (uh_tls_init(tls_key, tls_crt, tls_ciphers))//初始化加密 
                    return 1; 
        } 
#endif 

#ifdef HAVE_LUA // 有LUA插件才会执行 
        if (lua_handler || lua_prefix) { 
                fprintf(stderr, "Need handler and prefix to enable Lua support\n"); 
                return 1; 
        } 
    /*uh_plugin_init会初始化lua的插件,也就是会执行uhttpd_lua.so中的初始化函数,然后再判断是否存在lua文件,类似执行 lua luci 命令,使用lua执行自己创建的文件,如果执行错误,或者缺少某些文件则不能继续执行*/ 
        if (!list_empty(&conf.lua_prefix) && uh_plugin_init("uhttpd_lua.so")) 
                return 1; 
#endif 
#ifdef HAVE_UBUS // 有UBUS插件才会执行 
    /*原理和LUA差不多*/ 
        if (conf.ubus_prefix && uh_plugin_init("uhttpd_ubus.so")) 
                return 1; 
#endif 
        /* 加了-f 则 nofork==1,这样则不会通过fork创建子进程*/ 
        /* fork (if not disabled) */ 
        if (!nofork) { 
                switch (fork()) { 
                case -1: 
                        perror("fork()"); 
                        exit(1); 

                case 0: 
                        /* daemon setup */ 
                        if (chdir("/")) 
                                perror("chdir()"); 

                        cur_fd = open("/dev/null", O_WRONLY); 
                        if (cur_fd > 0) { 
                                dup2(cur_fd, 0); 
                                dup2(cur_fd, 1); 
                                dup2(cur_fd, 2); 
                        } 

                        break; 

                default: 
                        exit(0); 
                } 
        } 
    /*运行服务器的主要函数*/ 
        return run_server(); 
}

0x03 signal函数
位置:main-->signal

signal(SIGPIPE, SIG_IGN);
根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出)。简单的理解就是在发送或接受数据时,可能会产生一个SIGPIPE信号,可能会导致进程退出,但是我们却不想进程退出,为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号(屏蔽)处理函数。
参考:
1. https://blog.csdn.net/sysleo/article/details/95984946
2. https://www.runoob.com/cprogramming/c-function-signal.html
3. https://my.oschina.net/u/2252538/blog/2993724

0x04 uh_config_parse函数
位置:main-->uh_config_parse

static void uh_config_parse(void) 
{ 
        const char *path = conf.file; 
        FILE *c; 
        char line[512]; 
        char *col1; 
        char *col2; 
        char *eol; 
        /*如果conf.file没有赋值,则默认为/etc/httpd.conf*/ 
        if (!path) 
                path = "/etc/httpd.conf"; 

        c = fopen(path, "r"); 
        if (!c) 
                return; 

        memset(line, 0, sizeof(line)); 
        /*配置文件的逐步解析*/ 
        while (fgets(line, sizeof(line) - 1, c)) { 
                if ((line[0] == '/') && (strchr(line, ':') != NULL)) { 
                        if (!(col1 = strchr(line, ':')) || (*col1++ = 0) || 
                                !(col2 = strchr(col1, ':')) || (*col2++ = 0) || 
                                !(eol = strchr(col2, '\n')) || (*eol++  = 0)) 
                                continue; 

                        uh_auth_add(line, col1, col2); 
                } else if (!strncmp(line, "I:", 2)) { 
                        if (!(col1 = strchr(line, ':')) || (*col1++ = 0) || 
                                !(eol = strchr(col1, '\n')) || (*eol++  = 0)) 
                                continue; 

                        uh_index_add(strdup(col1)); 
                } else if (!strncmp(line, "E404:", 5)) { 
                        if (!(col1 = strchr(line, ':')) || (*col1++ = 0) || 
                                !(eol = strchr(col1, '\n')) || (*eol++  = 0)) 
                                continue; 

                        conf.error_handler = strdup(col1); 
                } 
                else if ((line[0] == '*') && (strchr(line, ':') != NULL)) { 
                        if (!(col1 = strchr(line, '*')) || (*col1++ = 0) || 
                                !(col2 = strchr(col1, ':')) || (*col2++ = 0) || 
                                !(eol = strchr(col2, '\n')) || (*eol++  = 0)) 
                                continue; 

                        uh_interpreter_add(col1, col2); 
                } 
        } 

        fclose(c); 
}

0x05 init_defaults_post函数
位置:main-->init_defaults_post

static void init_defaults_post(void) 
{ 
    /*将这四个文件设置为默认主页*/ 
        uh_index_add("index.html"); 
        uh_index_add("index.htm"); 
        uh_index_add("default.html"); 
        uh_index_add("default.htm"); 
        /*如果设置有cgi前缀,则将当前工作路径加上cgi的工作路径,默认情况(默认情况cgi是/cgi-bin)如:/xxx/xxx/xxx/cgi-bin(绝对路径加 /cgi-bin)*/ 
        if (conf.cgi_prefix) { 
                char *str = malloc(strlen(conf.docroot) + strlen(conf.cgi_prefix) + 1); 

                strcpy(str, conf.docroot); 
                strcat(str, conf.cgi_prefix); 
                conf.cgi_docroot_path = str; 
                conf.cgi_prefix_len = strlen(conf.cgi_prefix); 
        }; 
}

0x00 uh_index_add函数
位置:main-->init_defaults_post-->uh_index_add

void uh_index_add(const char *filename) 
{ 
        struct index_file *idx; 

        idx = calloc(1, sizeof(*idx)); 
        idx->name = filename; 
        list_add_tail(&idx->list, &index_files);//将传进来的默认主页添加到index_files双向列表中 
}

0x06 uh_plugin_init函数
位置:main-->uh_plugin_init

/*通过so库初始化插件*/ 
int uh_plugin_init(const char *name) 
{ 
        struct uhttpd_plugin *p; 
        const char *sym; 
        void *dlh; 
    /* 路径默认:/usr/lib/ + name 如果路径不对的话,则会报错*/ 
        dlh = dlopen(name, RTLD_LAZY | RTLD_GLOBAL); 
        if (!dlh) { 
                fprintf(stderr, "Could not open plugin %s: %s\n", name, dlerror()); 
                return -ENOENT; 
        } 

        sym = "uhttpd_plugin"; 
        p = dlsym(dlh, sym); 
        if (!p) { 
                fprintf(stderr, "Could not find symbol '%s' in plugin '%s'\n", sym, name); 
                return -ENOENT; 
        } 

        list_add(&p->list, &plugins); 
        return p->init(&ops, &conf);//比如ubus:p->init == uh_ubus_plugin_init 
}

0x00 dlopen函数
位置:main-->uh_plugin_init-->dlopen

dlh = dlopen(name, RTLD_LAZY | RTLD_GLOBAL);
相当于打开so动态链接库,并返回一个句柄,如果打开失败则会返回NULL(本人只出现过没有该文件,八成就是没有在/usr/lib/路径下创建该动态链接库),以供后续调用该动态链接库中的函数,或其他操作。
参考:
https://man7.org/linux/man-pages/man3/dlopen.3.html
https://blog.csdn.net/teleger/article/details/80857900

0x01 dlsym函数
位置:main-->uh_plugin_init-->dlsym

sym = "uhttpd_plugin";
p = dlsym(dlh, sym);
dlsym是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的地址,不但可以获取函数地址,也可以获取变量地址。uhttpd则是获取一个初始化的函数。如果初始化失败,则会返回NULL。
参考:
https://baike.baidu.com/item/dlsym/6603915?fr=aladdinsym = "uhttpd_plugin";
p = dlsym(dlh, sym);
dlsym是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的地址,不但可以获取函数地址,也可以获取变量地址。uhttpd则是获取一个初始化的函数。如果初始化失败,则会返回NULL。
参考:
https://baike.baidu.com/item/dlsym/6603915?fr=aladdin

0x07 run_server函数
位置:main-->run_server

未完待续...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK