浅析路由器WEB服务架构(一)

IoT 2年前 (2022) admin
753 0 0

点击 / 关注我们

浅析路由器WEB服务架构

在路由器设备的漏洞挖掘当中,WEB服务通常都是安全研究人员重点关注的内容之一,安全研究员拿到一个设备之后,都会去对这个设备的攻击面进行收集与分析,但可以不可以一拿到设备的固件看到文件夹的结构就知道这个路由器的攻击面呢?网上似乎也没有这种总结从路由器WEB服务架构看路由器的攻击面的文章,这些路由器的WEB架构看着都有相似的点,也有不同之处,那到底是哪里相同,哪里不同呢?作为一个WEB小萌新,对此类概念还很模糊,接下来就来探讨一下吧!

前言

学过开发的小伙伴应该都知道,一个WEB服务的开发过程当中有着这样的几个角色,前端页面和后端处理以及中间件,这些路由器的相同之处就在于此,实现的逻辑大致相同,但实现的方法又大有不同,前端大多都是用html和CSS以及JavaScript组成的,在前端当中可能会发现XSS,CSRF之类的漏洞,但想要达到RCE这些漏洞还是差点意思,所以笔者会更多的关注后端处理以及中间件的漏洞。

中间件是什么?

中间件的诞生其实也是跟WEB服务紧密相关的,在写完html,CSS等前端页面的时候,总不能只能打开html让浏览器解析吧!那么想要能够按照域名或者IP地址来访问,就需要通过中间件或者WEB框架来进行路由,用户向指定域名发出请求,通过DNS服务器转换成IP地址,然后向指定IP发送请求,请求来到中间件上,中间件根据已经配合好的规则去寻找对应的资源,最后将资源返回给用户,这就是一套完整的请求流程,中间件其实说白了就是一个服务器。

所以日常的WEB开发的模式是完全可以套用在对路由器WEB服务框架的研究上面的,就如同一个路由器里面集成了整个WEB服务架构,比如下图的中间件(WebServer)就大致等于路由器中的一些httpd等二进制程序,而后端中的php文件就等同于路由器中的一些cgi,那么整体的流程就是前端某个功能被客户所触发并发送请求,请求提交到中间件中,中间件根据URL再去寻找对应的php文件中的函数进行处理,处理完成之后将结果返回给中间件,最后返回给前端页面进行渲染或者做出相应的操作,所以一些初期的研究人员(没有做过WEB开发)包括我自己,一开始在研究漏洞的时候都很诧异,怎么又是某个路由器的cgi爆漏洞了?怎么天天都是这个cgi出漏洞呢?现在一切都明了了….人家就是处理逻辑的地方,那不就是挨审计的地方吗?

浅析路由器WEB服务架构(一)
1.png

在知道了中间件(WebServer)大致的开发思路之后,还得在往深了走,那么各大路由器厂商的路由器的开发思路以及结构是怎么样的呢?接下来将会列举几个目前比较常见的开发模式以及对应的例子。

WebServer开发模式

主流的中间件➕CGI

这种模式不多见但也不少,通常出现在思科、合勤,腾达等国外的路由器厂商,是以nginx,apache,Goahead等市面上常见的中间件作为中间转发的服务器转发请求

goahead源码分析:通信模型

src/goahead.c中MAIN函数主要是对webserver进行初始化并处理请求

MAIN(goahead, int argc, char **argv, char **envp)
{
    ...
    route = "route.txt";
    auth = "auth.txt";

    for (argind = 1; argind < argc; argind++) {
        argp = argv[argind];
        if (*argp != '-') {
            break;

        } else if (smatch(argp, "--auth") || smatch(argp, "-a")) {
            auth = argv[++argind];

#if BIT_UNIX_LIKE && !MACOSX
        } else if (smatch(argp, "--background") || smatch(argp, "-b")) {
            websSetBackground(1);
#endif

        } else if (smatch(argp, "--debugger") || smatch(argp, "-d") || smatch(argp, "-D")) {
            websSetDebug(1);

        } else if (smatch(argp, "--home")) {
            if (argind >= argc) usage();
            home = argv[++argind];
            if (chdir(home) < 0) {
                error("Can't change directory to %s", home);
                exit(-1);
            }
        } else if (smatch(argp, "--log") || smatch(argp, "-l")) {
            if (argind >= argc) usage();
            logSetPath(argv[++argind]);

        } else if (smatch(argp, "--verbose") || smatch(argp, "-v")) {
            logSetPath("stdout:2");

        } else if (smatch(argp, "--route") || smatch(argp, "-r")) {
            route = argv[++argind];

        } else if (smatch(argp, "--version") || smatch(argp, "-V")) {
            printf("%sn", BIT_VERSION);
            exit(0);

        } else if (*argp == '-' && isdigit((uchar) argp[1])) {
            lspec = sfmt("stdout:%s", &argp[1]);
            logSetPath(lspec);
            wfree(lspec);

        } else {
            usage();
        }
    }
    documents = BIT_GOAHEAD_DOCUMENTS;
    if (argc > argind) {
        documents = argv[argind++];
    }
    initPlatform();
    if (websOpen(documents, route) < 0) {
        error("Can't initialize server. Exiting.");
        return -1;
    }
    if (websLoad(auth) < 0) {
        error("Can't load %s", auth);
        return -1;
    }
    logHeader();
    if (argind < argc) {
        while (argind < argc) {
            endpoint = argv[argind++];
            if (websListen(endpoint) < 0) {
                return -1;
            }
        }
    } else {
        endpoints = sclone(BIT_GOAHEAD_LISTEN);
        for (endpoint = stok(endpoints, ", t", &tok); endpoint; endpoint = stok(NULL, ", t,", &tok)) {
#if !BIT_PACK_SSL
            if (strstr(endpoint, "https")) continue;
#endif
            if (websListen(endpoint) < 0) {
                return -1;
            }
        }
        wfree(endpoints);
    }
#if BIT_ROM && KEEP
    websAddRoute("/", "file", 0);
#endif
#if BIT_UNIX_LIKE && !MACOSX
    if (websGetBackground()) {
        if (daemon(0, 0) < 0) {
            error("Can't run as daemon");
            return -1;
        }
    }
#endif
    websServiceEvents(&finished);
    logmsg(1, "Instructed to exit");
    websClose();
    ...
    return 0;
}

下面是MAIN函数中几个比较重要的函数:

initPlatform();              // 初始化系统
websOpen(documents, route);  // 按照route文件定义URL handler
websLoad(auth);              // 加载auth文件,确立权限认证的方式
logHeader();                 // 打印os等初始化信息
websListen(endpoint);        // 初始化IO操作
websGetBackground();         // 判断是否需要采用deamon模式运行,是则切换到deamon后台运行
websServiceEvents(&finished);// socketSelect()多路IO复用对套接字的事件进行响应

主要的IO初始化从这个函数开始,socketListen绑定回调函数websAccept

PUBLIC int websListen(char *endpoint)
{
   ...
    if (listenMax >= WEBS_MAX_LISTEN) {
        error("Too many listen endpoints");
        return -1;
    }
    socketParseAddress(endpoint, &ip, &port, &secure, 80);
    if ((sid = socketListen(ip, port, websAccept, 0)) < 0) {
        error("Unable to open socket on port %d.", port);
        return -1;
    }
    sp = socketPtr(sid);
    sp->secure = secure;
    if (sp->secure) {
        if (!defaultSslPort) {
            defaultSslPort = port;
        }
    } else if (!defaultHttpPort) {
        defaultHttpPort = port;
    }
    listens[listenMax++] = sid;
    if (ip) {
        ipaddr = smatch(ip, "::") ? "[::]" : ip;
    } else {
        ipaddr = "*";
    }
    trace(2, "Started %s://%s:%d", secure ? "https" : "http", ipaddr, port);

    if (!websHostUrl) {
        if (port == 80) {
            websHostUrl = sclone(ip ? ip : websIpAddr);
        } else {
            websHostUrl = sfmt("%s:%d", ip ? ip : websIpAddr, port);
        }
    }
    if (!websIpAddrUrl) {
        if (port == 80) {
            websIpAddrUrl = sclone(websIpAddr);
        } else {
            websIpAddrUrl = sfmt("%s:%d", websIpAddr, port);
        }
    }
    wfree(ip);
    return sid;
}

监听完成之后初始化accept为连接创建一个新的句柄,里面包含一个Webs结构体,同时为它创建IO读写的功能:

PUBLIC int websAccept(int sid, char *ipaddr, int port, int listenSid)
{
    ...
    assert(sid >= 0);
    assert(ipaddr && *ipaddr);
    assert(listenSid >= 0);
    assert(port >= 0);

    if ((wid = websAlloc(sid)) < 0) {
        return -1;
    }
    wp = webs[wid];
    assert(wp);
    wp->listenSid = listenSid;
    strncpy(wp->ipaddr, ipaddr, min(sizeof(wp->ipaddr) - 1, strlen(ipaddr)));
    ...
    len = sizeof(ifAddr);
    if (getsockname(socketList[sid]->sock, (struct sockaddr*) &ifAddr, (Socklen*) &len) < 0) {
        error("Can't get sockname");
        return -1;
    }
    socketAddress((struct sockaddr*) &ifAddr, (int) len, wp->ifaddr, sizeof(wp->ifaddr), NULL);

#if BIT_GOAHEAD_LEGACY
    // 检查是不是本地发起的请求
    if (strcmp(wp->ipaddr, "127.0.0.1") == 0 || strcmp(wp->ipaddr, websIpAddr) == 0 || 
            strcmp(wp->ipaddr, websHost) == 0) {
        wp->flags |= WEBS_LOCAL;
    }
#endif

     lp = socketPtr(listenSid);
    trace(4, "New connection from %s:%d to %s:%d", ipaddr, port, wp->ifaddr, lp->port);

#if BIT_PACK_SSL
    if (lp->secure) {
        wp->flags |= WEBS_SECURE;
        trace(4, "Upgrade connection to TLS");
        if (sslUpgrade(wp) < 0) {
            error("Can't upgrade to TLS");
            return -1;
        }
    }
#endif
    assert(wp->timeout == -1);
    wp->timeout = websStartEvent(PARSE_TIMEOUT, checkTimeout, (void*) wp);
    // 创建IO读写的功能
    socketEvent(sid, SOCKET_READABLE, wp);
    return 0;
}

分配句柄和initWebs结构体

PUBLIC int websAlloc(int sid)
{
    if ((wid = wallocObject(&webs, &websMax, sizeof(Webs))) < 0) {
        return -1;
    }
    wp = webs[wid];
    assert(wp);
    initWebs(wp, 0, 0);
    wp->wid = wid;
    wp->sid = sid;
    wp->timestamp = time(0);
    return wid;
}

新句柄的Webs结构体,里面包含此次连接的相关信息

static void initWebs(Webs *wp, int flags, int reuse)
{
    // 如果wp结构体被复用,恢复wp结构体中原来的value
    if (reuse) {
        rxbuf = wp->rxbuf;      // Raw receive buffer
        wid = wp->wid;
        sid = wp->sid;
        timeout = wp->timeout;
        ssl = wp->ssl;
    } else {
        wid = sid = -1;
        timeout = -1;
        ssl = 0;
    }
    memset(wp, 0, sizeof(Webs));
    wp->flags = flags;      // Current flags
    wp->state = WEBS_BEGIN;     // Current state
    wp->wid = wid;      // Index into webs
    wp->sid = sid;      // Socket id
    wp->timeout = timeout;      // Timeout handle
    wp->docfd = -1;     // File descriptor for document being served
    wp->txLen = -1;     // Tx content length header value
    wp->rxLen = -1;     // Rx content length
    wp->code = HTTP_CODE_OK;        // Response status code
    wp->ssl = ssl;      // SSL context
#if !BIT_ROM
    wp->putfd = -1;     // File handle to write PUT data
#endif
#if BIT_GOAHEAD_CGI
    wp->cgifd = -1;     // File handle for CGI program input
#endif
#if BIT_GOAHEAD_UPLOAD
    wp->upfd = -1;      // Upload file handle
#endif
    if (!reuse) {
        wp->timeout = -1;
    }
    wp->vars = hashCreate(WEBS_HASH_INIT);
    ...
}

创建readEventwriteEvent的IO操作

static void socketEvent(int sid, int mask, void *wptr)
{
    ...
    if (! websValid(wp)) {
        return;
    }
    if (mask & SOCKET_READABLE) {
        readEvent(wp);
    } 
    if (mask & SOCKET_WRITABLE) {
        writeEvent(wp);
    } 
    if (wp->flags & WEBS_CLOSED) {
        websFree(wp);
    }
}

读取事件的处理

static void readEvent(Webs *wp)
{
    ...
    websNoteRequestActivity(wp);
    rxbuf = &wp->rxbuf;

    if (bufRoom(rxbuf) < (BIT_GOAHEAD_LIMIT_BUFFER + 1)) {
        if (!bufGrow(rxbuf, BIT_GOAHEAD_LIMIT_BUFFER + 1)) {
            websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Can't grow rxbuf");
            websPump(wp);
            return;
        }
    }
    // 读取消息队列中的数据
    if ((nbytes = websRead(wp, (char*) rxbuf->endp, BIT_GOAHEAD_LIMIT_BUFFER)) > 0) {
        wp->lastRead = nbytes;
        bufAdjustEnd(rxbuf, nbytes);
        bufAddNull(rxbuf);
    } 
    if (nbytes > 0 || wp->state > WEBS_BEGIN) {
        // 处理数据
        websPump(wp);
    }
    if (wp->flags & WEBS_CLOSED) {
        return;
    } else if (nbytes < 0 && socketEof(wp->sid)) {

        if (wp->state < WEBS_READY) {
            if (wp->state > WEBS_BEGIN) {
                websError(wp, HTTP_CODE_COMMS_ERROR, "Read error: connection lost");
                websPump(wp);
            }
            complete(wp, 0);
        }
    } else if (wp->state < WEBS_READY) {
        sp = socketPtr(wp->sid);
        socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_READABLE, socketEvent, wp);
    }
}

写事件的处理

static void writeEvent(Webs *wp)
{
    WebsBuf     *op;

    op = &wp->output;
    if (bufLen(op) > 0) {
        websFlush(wp);
    }
    if (bufLen(op) == 0 && wp->writeData) {
        (wp->writeData)(wp);
    }
    if (wp->state != WEBS_RUNNING) {
        websPump(wp);
    }
}

websPump就包含处理的五个Webs state,但WEBS_RUNNING并不会做任何操作

  • • WEBS_BEGIN

  • • WEBS_CONTENT

  • • WEBS_READY

  • • WEBS_RUNNING

  • • WEBS_COMPLETE

src/goahead.h中定义了上述几种状态:

#define WEBS_BEGIN              0           /**< Beginning state */
#define WEBS_CONTENT            1           /**< Ready for body data */
#define WEBS_READY              2           /**< Ready to route and start handler */
#define WEBS_RUNNING            3           /**< Processing request */
#define WEBS_COMPLETE           4           /**< Request complete */

websPumpsrc/http.c中,里面描述了通信过程当中的五种状态的处理方法:

PUBLIC void websPump(Webs *wp)
{
    for (canProceed = 1; canProceed; ) {
        switch (wp->state) {
        case WEBS_BEGIN:
            canProceed = parseIncoming(wp);
            break;
        case WEBS_CONTENT:
            canProceed = processContent(wp);
            break;
        case WEBS_READY:
            if (!websRunRequest(wp)) {
                // 通过route交给对应的处理程序
                websRouteRequest(wp);
                wp->state = WEBS_READY;
                canProceed = 1;
                continue;
            }
            canProceed = (wp->state != WEBS_RUNNING);
            break;
        case WEBS_RUNNING:
            return;
        case WEBS_COMPLETE:
            complete(wp, 1);
            canProceed = bufLen(&wp->rxbuf) != 0;
            break;
        }
    }
}

WEBS_BEGIN

解析http内容

static bool parseIncoming(Webs *wp)
{
    rxbuf = &wp->rxbuf;
    while (*rxbuf->servp == 'r' || *rxbuf->servp == 'n') {
        bufGetc(rxbuf);
    }
    if ((end = strstr((char*) wp->rxbuf.servp, "rnrn")) == 0) {
        if (bufLen(&wp->rxbuf) >= BIT_GOAHEAD_LIMIT_HEADER) {
            websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Header too large");
            return 1;
        }
        return 0;
    }    
    trace(3 | WEBS_RAW_MSG, "n<<< Requestn");
    c = *end;
    *end = '';
    trace(3 | WEBS_RAW_MSG, "%sn", wp->rxbuf.servp);
    *end = c;
    // 解析http首行信息
    parseFirstLine(wp);
    if (wp->state == WEBS_COMPLETE) {
        return 1;
    }
    // 解析http头部信息
    parseHeaders(wp);
    if (wp->state == WEBS_COMPLETE) {
        return 1;
    }
    wp->state = (wp->rxChunkState || wp->rxLen > 0) ? WEBS_CONTENT : WEBS_READY;

    websRouteRequest(wp);
    if (wp->state == WEBS_COMPLETE) {
        return 1;
    }
#if !BIT_ROM
#if BIT_GOAHEAD_CGI
    if (strstr(wp->path, BIT_GOAHEAD_CGI_BIN) != 0) {
        if (smatch(wp->method, "POST")) {
            wp->cgiStdin = websGetCgiCommName();
            if ((wp->cgifd = open(wp->cgiStdin, O_CREAT | O_WRONLY | O_BINARY, 0666)) < 0) {
                websError(wp, HTTP_CODE_NOT_FOUND | WEBS_CLOSE, "Can't open CGI file");
                return 1;
            }
        }
    }
#endif
    if (smatch(wp->method, "PUT")) {
        WebsStat    sbuf;
        wp->code = (stat(wp->filename, &sbuf) == 0 && sbuf.st_mode & S_IFDIR) ? HTTP_CODE_NO_CONTENT : HTTP_CODE_CREATED;
        wp->putname = websTempFile(BIT_GOAHEAD_PUT_DIR, "put");
        if ((wp->putfd = open(wp->putname, O_BINARY | O_WRONLY | O_CREAT, 0644)) < 0) {
            error("Can't create PUT filename %s", wp->putname);
            websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Can't create the put URI");
            wfree(wp->putname);
            return 1;
        }
    }
#endif
    return 1;
}

WEBS_CONTENT

static bool processContent(Webs *wp)
{
    if (!filterChunkData(wp)) {
        return 0;
    }
#if BIT_GOAHEAD_CGI && !BIT_ROM
    if (wp->cgifd >= 0 && websProcessCgiData(wp) < 0) {
        return 0;
    }
#endif
#if BIT_GOAHEAD_UPLOAD
    if ((wp->flags & WEBS_UPLOAD) && websProcessUploadData(wp) < 0) {
        return 0;
    }
#endif
#if !BIT_ROM
    if (wp->putfd >= 0 && websProcessPutData(wp) < 0) {
        return 0;
    }
#endif
    if (wp->eof) {
        wp->state = WEBS_READY;
        socketDeleteHandler(wp->sid);
        return 1;
    }
    return 0;
}

WEBS_READY

wp->route存放着此结构的路由

PUBLIC bool websRunRequest(Webs *wp)
{
    ...
    if ((route = wp->route) == 0) {
        websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no route for request");
        return 1;
    }
    if (!route->handler) {
        websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no handler for route");
        return 1;
    }
    if (!wp->filename || route->dir) {
        wfree(wp->filename);
        wp->filename = sfmt("%s%s", route->dir ? route->dir : websGetDocuments(), wp->path);
    }
    if (!(wp->flags & WEBS_VARS_ADDED)) {
        if (wp->query && *wp->query) {
            websSetQueryVars(wp);
        }
        if (wp->flags & WEBS_FORM) {
            websSetFormVars(wp);
        }
        wp->flags |= WEBS_VARS_ADDED;
    }
    wp->state = WEBS_RUNNING;
    trace(5, "Route %s calls handler %s", route->prefix, route->handler->name);

#if ME_GOAHEAD_LEGACY
    if (route->handler->flags & WEBS_LEGACY_HANDLER) {
        return (*(WebsLegacyHandlerProc) route->handler->service)(wp, route->prefix, route->dir, route->flags) == 0;
    } else
#endif
    if (!route->handler->service) {
        websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no handler service callback");
        return 1;
    }
    return (*route->handler->service)(wp);
}

websSetQueryVars主要是分割数据包中的环境变量和关键字

PUBLIC void websSetQueryVars(Webs *wp)
{
    // 分割环境变量
    if (wp->query && *wp->query) {
        wp->decodedQuery = sclone(wp->query);
        addFormVars(wp, wp->decodedQuery);
    }
}

static void addFormVars(Webs *wp, char *vars)
{
    keyword = stok(vars, "&", &tok);
    while (keyword != NULL) {
        if ((value = strchr(keyword, '=')) != NULL) {
            *value++ = '';
            websDecodeUrl(keyword, keyword, strlen(keyword));
            websDecodeUrl(value, value, strlen(value));
        } else {
            value = "";
        }
        if (*keyword) {
        // 如果已经设置了关键字,则将新值附加到已存储的内容中。
            if ((prior = websGetVar(wp, keyword, NULL)) != 0) {
                websSetVarFmt(wp, keyword, "%s %s", prior, value);
            } else {
                websSetVar(wp, keyword, value);
            }
        }
        keyword = stok(NULL, "&", &tok);
    }
}

获取到环境变量

PUBLIC void websRouteRequest(Webs *wp)
{
    ...
    safeMethod = smatch(wp->method, "POST") || smatch(wp->method, "GET") || smatch(wp->method, "HEAD");
    plen = slen(wp->path);

    /*
        Resume routine from last matched route. This permits the legacy service() callbacks to return false
        and continue routing.
     */
    if (wp->route && !(wp->flags & WEBS_REROUTE)) {
        for (= 0; i < routeCount; i++) {
            if (wp->route == routes[i]) {
                i++;
                break;
            }
        }
        if (>= routeCount) {
            i = 0;
        }
    } else {
        i = 0;
    }
    wp->route = 0;

    for (; i < routeCount; i++) {
        route = routes[i];
        assert(route->prefix && route->prefixLen > 0);

        if (plen < route->prefixLen) continue;
        len = min(route->prefixLen, plen);
        trace(5, "Examine route %s", route->prefix);
        /*
            Match route
         */
        if (strncmp(wp->path, route->prefix, len) != 0) {
            continue;
        }
        if (route->protocol && !smatch(route->protocol, wp->protocol)) {
            trace(5, "Route %s does not match protocol %s", route->prefix, wp->protocol);
            continue;
        }
        if (route->methods >= 0) {
            if (!hashLookup(route->methods, wp->method)) {
                trace(5, "Route %s does not match method %s", route->prefix, wp->method);
                continue;
            }
        } else if (!safeMethod) {
            continue;
        }
        if (route->extensions >= 0 && (wp->ext == 0 || !hashLookup(route->extensions, &wp->ext[1]))) {
            trace(5, "Route %s doesn match extension %s", route->prefix, wp->ext ? wp->ext : "");
            continue;
        }

        wp->route = route;
#if ME_GOAHEAD_AUTH
        if (route->authType && !websAuthenticate(wp)) {
            return;
        }
        if (route->abilities >= 0 && !websCan(wp, route->abilities)) {
            return;
        }
#endif
        if ((handler = route->handler) == 0) {
            continue;
        }
        if (!handler->match || (*handler->match)(wp)) {
            /* Handler matches */
            return;
        }
        wp->route = 0;
        if (wp->flags & WEBS_REROUTE) {
            wp->flags &= ~WEBS_REROUTE;
            if (++wp->routeCount >= WEBS_MAX_ROUTE) {
                break;
            }
            i = 0;
        }
    }
    if (wp->routeCount >= WEBS_MAX_ROUTE) {
        error("Route loop for %s", wp->url);
    }
    websError(wp, HTTP_CODE_NOT_FOUND, "Cannot find suitable route for request.");
    assert(wp->route == 0);
}

WEBS_COMPLETE

当处理完成之后就将wp->state置为4,然后就进入complete进行关闭连接和释放结构体

static void complete(Webs *wp, int reuse) 
{
    ...
    if (reuse && wp->flags & WEBS_KEEP_ALIVE && wp->rxRemaining == 0) {
        reuseConn(wp);
        socketCreateHandler(wp->sid, SOCKET_READABLE, socketEvent, wp);
        trace(5, "Keep connection alive");
        return;
    }
    trace(5, "Close connection");
    assert(wp->timeout >= 0);
    websCancelTimeout(wp);
    assert(wp->sid >= 0);
#if BIT_PACK_SSL
    sslFree(wp);
#endif
    socketDeleteHandler(wp->sid);
    socketCloseConnection(wp->sid);
    wp->sid = -1;
    bufFlush(&wp->rxbuf);
    wp->state = WEBS_BEGIN;
    wp->flags |= WEBS_CLOSED;
}

最后通过websServiceEvents监听事件,通过socketSelect来判断事件的响应,最后让socketProcess调用socketDoEvent

PUBLIC void websServiceEvents(int *finished)
{
    WebsTime    delay, nextEvent;

    if (finished) {
        *finished = 0;
    }
    delay = 0;
    while (!finished || !*finished) {
        if (socketSelect(-1, delay)) {
            socketProcess();
        }
#if BIT_GOAHEAD_CGI && !BIT_ROM
        delay = websCgiPoll();
#else
        delay = MAXINT;
#endif
        nextEvent = websRunEvents();
        delay = min(delay, nextEvent);
    }
}

goahead例子:Tenda W15E Router

通过对比此设备的httpd比源码多了如下几个函数:

system_init_core_dump()     // 系统初始化
websGetLogLevel()       // 获取log级别
init_debug_level(logLevel)  // 初始化调试级别

连接上路由器的shell,在/var当中的route.txt中定义了各种路由,在前面提到的websOpen(documents, route)初始化,下面链接是官方定义的路由规则

  • • Request Routing

浅析路由器WEB服务架构(一)
6.png

auth.txt中包含着后台的账号密码,如果可以未授权读取此文件就能绕过鉴权,通过websLoad(auth)此函数进行初始化

浅析路由器WEB服务架构(一)
5.png

非主流中间件被厂商魔改后的中间件➕CGI

第三方中间件被厂商魔改的中间件通常都是在市面上常见的中间件中进行二次开发,比如阉割掉大部分功能或者添加一些特殊的功能等,这种二进制程序的命名就变化多样,如:mini_httpd,httpd等原理和常见中间件差不多,除了思科,华为和合勤(zyxel)等在内的厂商的旗下设备也都曾采用mini_httpd组件。

mini_httpd源码分析:通信模型

mini_httpd.c中基本上包含了中间件所有的功能,在main函数中,进行初始化之后,就会进入Main loop,循环处理用户打过来的请求,其中关键的处理函数是handle_request

static void
handle_request( void )
    {
    ...
    // 初始页面名字初始化
    const char* index_names[] = {
    "index.html", "index.htm", "index.xhtml", "index.xht", "Default.htm",
    "index.cgi" };
    ...

#ifdef TCP_NOPUSH
    if ( ! do_ssl )
    {
    r = 1;
    (void) setsockopt(
        conn_fd, IPPROTO_TCP, TCP_NOPUSH, (void*) &r, sizeof(r) );
    }
#endif
// 设置ssl
#ifdef USE_SSL
    if ( do_ssl )
    {
    ssl = SSL_new( ssl_ctx );
    SSL_set_fd( ssl, conn_fd );
    if ( SSL_accept( ssl ) == 0 )
        {
        ERR_print_errors_fp( stderr );
        finish_request( 1 );
        }
    }
#endif 

    // 开始处理
    start_request();
    for (;;)
    {
    char buf[50000];
    int rr = my_read( buf, sizeof(buf) - 1 );
    if ( rr < 0 && ( errno == EINTR || errno == EAGAIN ) )
        continue;
    if ( rr <= 0 )
        break;
    (void) alarm( READ_TIMEOUT );
    add_to_request( buf, rr );
    if ( strstr( request, "15121512" ) != (char*) 0 ||
         strstr( request, "1212" ) != (char*) 0 )
        break;
    }

    ...

    // 解析请求头
    while ( ( line = get_request_line() ) != (char*) 0 )
    {
    if ( line[0] == '' )
        break;
    // 解析鉴权
    else if ( strncasecmp( line, "Authorization:", 14 ) == 0 )
        {
        cp = &line[14];
        cp += strspn( cp, " t" );
        authorization = cp;
        }
    else if ( strncasecmp( line, "Content-Length:", 15 ) == 0 )
        {
        cp = &line[15];
        cp += strspn( cp, " t" );
        content_length = atol( cp );
        }
    else if ( strncasecmp( line, "Content-Type:", 13 ) == 0 )
        {
        cp = &line[13];
        cp += strspn( cp, " t" );
        content_type = cp;
        }
    else if ( strncasecmp( line, "Cookie:", 7 ) == 0 )
        {
        cp = &line[7];
        cp += strspn( cp, " t" );
        cookie = cp;
        }
    else if ( strncasecmp( line, "Host:", 5 ) == 0 )
        {
        cp = &line[5];
        cp += strspn( cp, " t" );
        host = cp;
        // host不能为'','.','/'
        if ( host[0] == '' || host[0] == '.' ||
         strchr( host, '/' ) != (char*) 0 )
        send_error( 400, "Bad Request", "", "Can't parse request." );
        }
    else if ( strncasecmp( line, "If-Modified-Since:", 18 ) == 0 )
        {
        cp = &line[18];
        cp += strspn( cp, " t" );
        if_modified_since = tdate_parse( cp );
        }
    else if ( strncasecmp( line, "Referer:", 8 ) == 0 )
        {
        cp = &line[8];
        cp += strspn( cp, " t" );
        referrer = cp;
        }
    else if ( strncasecmp( line, "Referrer:", 9 ) == 0 )
        {
        cp = &line[9];
        cp += strspn( cp, " t" );
        referrer = cp;
        }
    else if ( strncasecmp( line, "User-Agent:", 11 ) == 0 )
        {
        cp = &line[11];
        cp += strspn( cp, " t" );
        useragent = cp;
        }
    }
    ...
    strdecode( path, path );
    if ( path[0] != '/' )
    send_error( 400, "Bad Request", "", "Bad filename." );
    file = &(path[1]);
    de_dotdot( file );
    if ( file[0] == '' )
    file = "./";
    if ( file[0] == '/' ||
     ( file[0] == '.' && file[1] == '.' &&
       ( file[2] == '' || file[2] == '/' ) ) )
    send_error( 400, "Bad Request", "", "Illegal filename." );
    if ( vhost )
    file = virtual_file( file );
    ...

    r = stat( file, &sb );
    if ( r < 0 )
    // 获取配置文件
    r = get_pathinfo();
    if ( r < 0 )
    send_error( 404, "Not Found", "", "File not found." );
    file_len = strlen( file );
    if ( ! S_ISDIR( sb.st_mode ) )
    {
    while ( file[file_len - 1] == '/' )
        {
        file[file_len - 1] = '';
        --file_len;
        }
    // 处理请求的文件
    do_file();
    }
    else
    {
    char idx[10000];

    ...

    for ( i = 0; i < sizeof(index_names) / sizeof(char*); ++)
        {
        (void) snprintf( idx, sizeof(idx), "%s%s", file, index_names[i] );
        if ( stat( idx, &sb ) >= 0 )
        {
        file = idx;
        do_file();
        goto got_one;
        }
        }

    do_dir();

    got_one: ;
    }

#ifdef USE_SSL
    SSL_free( ssl );
#endif /* USE_SSL */

    finish_request( 0 );
    }

根据前面的信息打开对应的文件并返回

static void
do_file( void )
    {
    cp = strrchr( buf, '/' );
    if ( cp == (char*) 0 )
    (void) strcpy( buf, "." );
    else
    *cp = '';
    // 权限检查
    auth_check( buf );

    // 检查文件是否需要鉴权
    if ( strcmp( file, AUTH_FILE ) == 0 ||
     ( strcmp( &(file[strlen(file) - sizeof(AUTH_FILE) + 1]), AUTH_FILE ) == 0 &&
       file[strlen(file) - sizeof(AUTH_FILE)] == '/' ) )
    {
    syslog(
        LOG_NOTICE, "%.80s URL "%.80s" tried to retrieve an auth file",
        ntoa( &client_addr ), path );
    send_error( 403, "Forbidden", "", "File is protected." );
    }

    // 来源检查
    check_referrer();

    // 是否请求cgi文件
    if ( cgi_pattern != (char*) 0 && match( cgi_pattern, file ) )
    {
    // 如果请求的是cgi文件,则进入此函数进行处理
    do_cgi();
    return;
    }
    if ( pathinfo != (char*) 0 )
    send_error( 404, "Not Found", "", "File not found." );

    if ( method != METHOD_GET && method != METHOD_HEAD )
    send_error( 501, "Not Implemented", "", "That method is not implemented." );

    fd = open( file, O_RDONLY );
    if ( fd < 0 )
    {
    syslog(
        LOG_INFO, "%.80s File "%.80s" is protected",
        ntoa( &client_addr ), path );
    send_error( 403, "Forbidden", "", "File is protected." );
    }
    mime_type = figure_mime( file, mime_encodings, sizeof(mime_encodings) );
    (void) snprintf(
    fixed_mime_type, sizeof(fixed_mime_type), mime_type, charset );
    if ( if_modified_since != (time_t) -1 &&
     if_modified_since >= sb.st_mtime )
    {
    add_headers(
        304, "Not Modified", "", mime_encodings, fixed_mime_type,
        (off_t) -1, sb.st_mtime );
    // 返回请求的文件
    send_response();
    return;
    }
    add_headers(
    200, "Ok", "", mime_encodings, fixed_mime_type, sb.st_size,
    sb.st_mtime );
    send_response();
    if ( method == METHOD_HEAD )
    return;
    // 忽略文件大小为0的文件
    if ( sb.st_size > 0 )   
    {
#ifdef HAVE_SENDFILE

#ifndef USE_SSL
    send_via_sendfile( fd, conn_fd, sb.st_size );
#else /* USE_SSL */
    if ( do_ssl )
        send_via_write( fd, sb.st_size );
    else
        send_via_sendfile( fd, conn_fd, sb.st_size );
#endif /* USE_SSL */

#else /* HAVE_SENDFILE */

    send_via_write( fd, sb.st_size );

#endif /* HAVE_SENDFILE */
    }

    (void) close( fd );
    }

如果请求的是cgi文件,则通过do_cgi来execve来fork子进程

static void
do_cgi( void )
    {
    //如果碰到文件描述符冲突,则dup2修改文件描述符
    if ( conn_fd == STDIN_FILENO || conn_fd == STDOUT_FILENO || conn_fd == STDERR_FILENO )
    {
    int newfd = dup2( conn_fd, STDERR_FILENO + 1 );
    if ( newfd >= 0 )
        conn_fd = newfd;
    }


    envp = make_envp();
    argp = make_argp();

#ifdef USE_SSL
    if ( ( method == METHOD_POST && request_len > request_idx ) || do_ssl )
#else 
    if ( ( method == METHOD_POST && request_len > request_idx ) )
#endif 
    {
    int p[2];
    int r;

    if ( pipe( p ) < 0 )
        send_error( 500, "Internal Error", "", "Something unexpected went wrong making a pipe." );
    r = fork();
    if ( r < 0 )
        send_error( 500, "Internal Error", "", "Something unexpected went wrong forking an interposer." );
    if ( r == 0 )
        {
        // 中断程序
        (void) close( p[0] );
        cgi_interpose_input( p[1] );
        finish_request( 0 );
        }
    (void) close( p[1] );
    if ( p[0] != STDIN_FILENO )
        {
        (void) dup2( p[0], STDIN_FILENO );
        (void) close( p[0] );
        }
    }
    else
    {
    if ( conn_fd != STDIN_FILENO )
        (void) dup2( conn_fd, STDIN_FILENO );
    }

    // 匹配参数
    if ( strncmp( argp[0], "nph-", 4 ) == 0 )
    parse_headers = 0;
    else
    parse_headers = 1;
#ifdef USE_SSL
    if ( parse_headers || do_ssl )
#else /* USE_SSL */
    if ( parse_headers )
#endif /* USE_SSL */
    {
    int p[2];
    int r;

    if ( pipe( p ) < 0 )
        send_error( 500, "Internal Error", "", "Something unexpected went wrong making a pipe." );
    r = fork();
    if ( r < 0 )
        send_error( 500, "Internal Error", "", "Something unexpected went wrong forking an interposer." );
    if ( r == 0 )
        {
        (void) close( p[1] );
        cgi_interpose_output( p[0], parse_headers );
        finish_request( 0 );
        }
    (void) close( p[0] );
    if ( p[1] != STDOUT_FILENO )
        (void) dup2( p[1], STDOUT_FILENO );
    if ( p[1] != STDERR_FILENO )
        (void) dup2( p[1], STDERR_FILENO );
    if ( p[1] != STDOUT_FILENO && p[1] != STDERR_FILENO )
        (void) close( p[1] );
    }
    else
    {
    if ( conn_fd != STDOUT_FILENO )
        (void) dup2( conn_fd, STDOUT_FILENO );
    if ( conn_fd != STDERR_FILENO )
        (void) dup2( conn_fd, STDERR_FILENO );
    }

    if ( logfp != (FILE*) 0 )
    (void) fclose( logfp );


    closelog();


    (void) nice( CGI_NICE );

    directory = e_strdup( file );
    binary = strrchr( directory, '/' );
    if ( binary == (char*) 0 )
    binary = file;
    else
    {
    *binary++ = '';
    (void) chdir( directory );  
    }

#ifdef HAVE_SIGSET
    (void) sigset( SIGPIPE, SIG_DFL );
#else 
    (void) signal( SIGPIPE, SIG_DFL );
#endif 

    // execve执行cgi文件
    (void) execve( binary, argp, envp );

    send_error( 500, "Internal Error", "", "Something unexpected went wrong running a CGI program." );
    }

mini_httpd例子:Cisco RV160 VPN Router

在RV160当中是采用第三方中间件被厂商魔改的中间件mini_httpd来启动web服务的,整体的代码和mini_httpd源码差距不大,不太一样的就是RV160的mini_httpd添加了鉴权以及如何启动CGI

if ( support_rest
  && (!strncmp(file, "api/", 4u)              // 路由
     || !strcmp(file, "api")
     || !strncmp(file, "restconf/", 9u)
     || !strcmp(file, "restconf")) )
{
    is_req_url = 1;
}
...
if ( is_not_login_page )
    is_login = Authentication_user(Cookie);
  mhttpd_log("=====is_login=%d, is_not_login_page=%d", is_login, is_not_login_page);
...
if ( ((via_wan != 1 || is_from_lan) && (via_lan != 1 || !is_from_lan)
     || !strncmp(file, "restconf", 8u)
     || !strncmp(file, "api", 3u))
  && (!is_req_url || (!have_lan_rest || !is_from_lan) && (!have_wan_rest || is_from_lan))
  && !block )
{
    send_error(403, "Forbidden", extra_header, "You don't have permission to access the website on this server.");
}

通过前面的校验之后,在判断路径是否正确之后通过execve来fork子进程,也可以在文件系统中找到这两个CGI程序,每一次新的请求都需要fork出一个CGI的子进程来处理对应的请求,好在路由器的请求量不会太大,所以这种设计模式才得以实现

if ( is_req_url )
    path = "/usr/sbin/rest.cgi";
else
    path = "/usr/sbin/admin.cgi";
signal(13, 0);
execve(path, argv, envp);

浅析路由器WEB服务架构(一)
3.jpg

推荐阅读:
ATT&CK中的攻与防——T1059
若依(RuoYi)管理系统后台sql注入漏洞分析
利用 PHP-FPM 做内存马的方法
一种新的Tomcat内存马 – Upgrade内存马
从偶遇Flarum开始的RCE之旅



跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。


跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):浅析路由器WEB服务架构(一)

版权声明:admin 发表于 2022年8月31日 上午10:28。
转载请注明:浅析路由器WEB服务架构(一) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...