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

admin 2023年2月24日11:13:47评论97 views字数 22657阅读75分31秒阅读模式

浅析路由器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出漏洞呢?现在一切都明了了....人家就是处理逻辑的地方,那不就是挨审计的地方吗?

1.png

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

WebServer开发模式

主流的中间件➕CGI

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

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

  1. MAIN(goahead, int argc, char **argv, char **envp)
  2. {
  3. ...
  4. route = "route.txt";
  5. auth = "auth.txt";
  6. for (argind = 1; argind < argc; argind++) {
  7. argp = argv[argind];
  8. if (*argp != '-') {
  9. break;
  10. } else if (smatch(argp, "--auth") || smatch(argp, "-a")) {
  11. auth = argv[++argind];
  12. #if BIT_UNIX_LIKE && !MACOSX
  13. } else if (smatch(argp, "--background") || smatch(argp, "-b")) {
  14. websSetBackground(1);
  15. #endif
  16. } else if (smatch(argp, "--debugger") || smatch(argp, "-d") || smatch(argp, "-D")) {
  17. websSetDebug(1);
  18. } else if (smatch(argp, "--home")) {
  19. if (argind >= argc) usage();
  20. home = argv[++argind];
  21. if (chdir(home) < 0) {
  22. error("Can't change directory to %s", home);
  23. exit(-1);
  24. }
  25. } else if (smatch(argp, "--log") || smatch(argp, "-l")) {
  26. if (argind >= argc) usage();
  27. logSetPath(argv[++argind]);
  28. } else if (smatch(argp, "--verbose") || smatch(argp, "-v")) {
  29. logSetPath("stdout:2");
  30. } else if (smatch(argp, "--route") || smatch(argp, "-r")) {
  31. route = argv[++argind];
  32. } else if (smatch(argp, "--version") || smatch(argp, "-V")) {
  33. printf("%s\n", BIT_VERSION);
  34. exit(0);
  35. } else if (*argp == '-' && isdigit((uchar) argp[1])) {
  36. lspec = sfmt("stdout:%s", &argp[1]);
  37. logSetPath(lspec);
  38. wfree(lspec);
  39. } else {
  40. usage();
  41. }
  42. }
  43. documents = BIT_GOAHEAD_DOCUMENTS;
  44. if (argc > argind) {
  45. documents = argv[argind++];
  46. }
  47. initPlatform();
  48. if (websOpen(documents, route) < 0) {
  49. error("Can't initialize server. Exiting.");
  50. return -1;
  51. }
  52. if (websLoad(auth) < 0) {
  53. error("Can't load %s", auth);
  54. return -1;
  55. }
  56. logHeader();
  57. if (argind < argc) {
  58. while (argind < argc) {
  59. endpoint = argv[argind++];
  60. if (websListen(endpoint) < 0) {
  61. return -1;
  62. }
  63. }
  64. } else {
  65. endpoints = sclone(BIT_GOAHEAD_LISTEN);
  66. for (endpoint = stok(endpoints, ", \t", &tok); endpoint; endpoint = stok(NULL, ", \t,", &tok)) {
  67. #if !BIT_PACK_SSL
  68. if (strstr(endpoint, "https")) continue;
  69. #endif
  70. if (websListen(endpoint) < 0) {
  71. return -1;
  72. }
  73. }
  74. wfree(endpoints);
  75. }
  76. #if BIT_ROM && KEEP
  77. websAddRoute("/", "file", 0);
  78. #endif
  79. #if BIT_UNIX_LIKE && !MACOSX
  80. if (websGetBackground()) {
  81. if (daemon(0, 0) < 0) {
  82. error("Can't run as daemon");
  83. return -1;
  84. }
  85. }
  86. #endif
  87. websServiceEvents(&finished);
  88. logmsg(1, "Instructed to exit");
  89. websClose();
  90. ...
  91. return 0;
  92. }

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

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

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

  1. PUBLIC int websListen(char *endpoint)
  2. {
  3. ...
  4. if (listenMax >= WEBS_MAX_LISTEN) {
  5. error("Too many listen endpoints");
  6. return -1;
  7. }
  8. socketParseAddress(endpoint, &ip, &port, &secure, 80);
  9. if ((sid = socketListen(ip, port, websAccept, 0)) < 0) {
  10. error("Unable to open socket on port %d.", port);
  11. return -1;
  12. }
  13. sp = socketPtr(sid);
  14. sp->secure = secure;
  15. if (sp->secure) {
  16. if (!defaultSslPort) {
  17. defaultSslPort = port;
  18. }
  19. } else if (!defaultHttpPort) {
  20. defaultHttpPort = port;
  21. }
  22. listens[listenMax++] = sid;
  23. if (ip) {
  24. ipaddr = smatch(ip, "::") ? "[::]" : ip;
  25. } else {
  26. ipaddr = "*";
  27. }
  28. trace(2, "Started %s://%s:%d", secure ? "https" : "http", ipaddr, port);
  29. if (!websHostUrl) {
  30. if (port == 80) {
  31. websHostUrl = sclone(ip ? ip : websIpAddr);
  32. } else {
  33. websHostUrl = sfmt("%s:%d", ip ? ip : websIpAddr, port);
  34. }
  35. }
  36. if (!websIpAddrUrl) {
  37. if (port == 80) {
  38. websIpAddrUrl = sclone(websIpAddr);
  39. } else {
  40. websIpAddrUrl = sfmt("%s:%d", websIpAddr, port);
  41. }
  42. }
  43. wfree(ip);
  44. return sid;
  45. }

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

  1. PUBLIC int websAccept(int sid, char *ipaddr, int port, int listenSid)
  2. {
  3. ...
  4. assert(sid >= 0);
  5. assert(ipaddr && *ipaddr);
  6. assert(listenSid >= 0);
  7. assert(port >= 0);
  8. if ((wid = websAlloc(sid)) < 0) {
  9. return -1;
  10. }
  11. wp = webs[wid];
  12. assert(wp);
  13. wp->listenSid = listenSid;
  14. strncpy(wp->ipaddr, ipaddr, min(sizeof(wp->ipaddr) - 1, strlen(ipaddr)));
  15. ...
  16. len = sizeof(ifAddr);
  17. if (getsockname(socketList[sid]->sock, (struct sockaddr*) &ifAddr, (Socklen*) &len) < 0) {
  18. error("Can't get sockname");
  19. return -1;
  20. }
  21. socketAddress((struct sockaddr*) &ifAddr, (int) len, wp->ifaddr, sizeof(wp->ifaddr), NULL);
  22. #if BIT_GOAHEAD_LEGACY
  23. // 检查是不是本地发起的请求
  24. if (strcmp(wp->ipaddr, "127.0.0.1") == 0 || strcmp(wp->ipaddr, websIpAddr) == 0 ||
  25. strcmp(wp->ipaddr, websHost) == 0) {
  26. wp->flags |= WEBS_LOCAL;
  27. }
  28. #endif
  29. lp = socketPtr(listenSid);
  30. trace(4, "New connection from %s:%d to %s:%d", ipaddr, port, wp->ifaddr, lp->port);
  31. #if BIT_PACK_SSL
  32. if (lp->secure) {
  33. wp->flags |= WEBS_SECURE;
  34. trace(4, "Upgrade connection to TLS");
  35. if (sslUpgrade(wp) < 0) {
  36. error("Can't upgrade to TLS");
  37. return -1;
  38. }
  39. }
  40. #endif
  41. assert(wp->timeout == -1);
  42. wp->timeout = websStartEvent(PARSE_TIMEOUT, checkTimeout, (void*) wp);
  43. // 创建IO读写的功能
  44. socketEvent(sid, SOCKET_READABLE, wp);
  45. return 0;
  46. }

分配句柄和initWebs结构体

  1. PUBLIC int websAlloc(int sid)
  2. {
  3. if ((wid = wallocObject(&webs, &websMax, sizeof(Webs))) < 0) {
  4. return -1;
  5. }
  6. wp = webs[wid];
  7. assert(wp);
  8. initWebs(wp, 0, 0);
  9. wp->wid = wid;
  10. wp->sid = sid;
  11. wp->timestamp = time(0);
  12. return wid;
  13. }

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

  1. static void initWebs(Webs *wp, int flags, int reuse)
  2. {
  3. // 如果wp结构体被复用,恢复wp结构体中原来的value
  4. if (reuse) {
  5. rxbuf = wp->rxbuf; // Raw receive buffer
  6. wid = wp->wid;
  7. sid = wp->sid;
  8. timeout = wp->timeout;
  9. ssl = wp->ssl;
  10. } else {
  11. wid = sid = -1;
  12. timeout = -1;
  13. ssl = 0;
  14. }
  15. memset(wp, 0, sizeof(Webs));
  16. wp->flags = flags; // Current flags
  17. wp->state = WEBS_BEGIN; // Current state
  18. wp->wid = wid; // Index into webs
  19. wp->sid = sid; // Socket id
  20. wp->timeout = timeout; // Timeout handle
  21. wp->docfd = -1; // File descriptor for document being served
  22. wp->txLen = -1; // Tx content length header value
  23. wp->rxLen = -1; // Rx content length
  24. wp->code = HTTP_CODE_OK; // Response status code
  25. wp->ssl = ssl; // SSL context
  26. #if !BIT_ROM
  27. wp->putfd = -1; // File handle to write PUT data
  28. #endif
  29. #if BIT_GOAHEAD_CGI
  30. wp->cgifd = -1; // File handle for CGI program input
  31. #endif
  32. #if BIT_GOAHEAD_UPLOAD
  33. wp->upfd = -1; // Upload file handle
  34. #endif
  35. if (!reuse) {
  36. wp->timeout = -1;
  37. }
  38. wp->vars = hashCreate(WEBS_HASH_INIT);
  39. ...
  40. }

创建readEventwriteEvent的IO操作

  1. static void socketEvent(int sid, int mask, void *wptr)
  2. {
  3. ...
  4. if (! websValid(wp)) {
  5. return;
  6. }
  7. if (mask & SOCKET_READABLE) {
  8. readEvent(wp);
  9. }
  10. if (mask & SOCKET_WRITABLE) {
  11. writeEvent(wp);
  12. }
  13. if (wp->flags & WEBS_CLOSED) {
  14. websFree(wp);
  15. }
  16. }

读取事件的处理

  1. static void readEvent(Webs *wp)
  2. {
  3. ...
  4. websNoteRequestActivity(wp);
  5. rxbuf = &wp->rxbuf;
  6. if (bufRoom(rxbuf) < (BIT_GOAHEAD_LIMIT_BUFFER + 1)) {
  7. if (!bufGrow(rxbuf, BIT_GOAHEAD_LIMIT_BUFFER + 1)) {
  8. websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Can't grow rxbuf");
  9. websPump(wp);
  10. return;
  11. }
  12. }
  13. // 读取消息队列中的数据
  14. if ((nbytes = websRead(wp, (char*) rxbuf->endp, BIT_GOAHEAD_LIMIT_BUFFER)) > 0) {
  15. wp->lastRead = nbytes;
  16. bufAdjustEnd(rxbuf, nbytes);
  17. bufAddNull(rxbuf);
  18. }
  19. if (nbytes > 0 || wp->state > WEBS_BEGIN) {
  20. // 处理数据
  21. websPump(wp);
  22. }
  23. if (wp->flags & WEBS_CLOSED) {
  24. return;
  25. } else if (nbytes < 0 && socketEof(wp->sid)) {
  26. if (wp->state < WEBS_READY) {
  27. if (wp->state > WEBS_BEGIN) {
  28. websError(wp, HTTP_CODE_COMMS_ERROR, "Read error: connection lost");
  29. websPump(wp);
  30. }
  31. complete(wp, 0);
  32. }
  33. } else if (wp->state < WEBS_READY) {
  34. sp = socketPtr(wp->sid);
  35. socketCreateHandler(wp->sid, sp->handlerMask | SOCKET_READABLE, socketEvent, wp);
  36. }
  37. }

写事件的处理

  1. static void writeEvent(Webs *wp)
  2. {
  3. WebsBuf *op;
  4. op = &wp->output;
  5. if (bufLen(op) > 0) {
  6. websFlush(wp);
  7. }
  8. if (bufLen(op) == 0 && wp->writeData) {
  9. (wp->writeData)(wp);
  10. }
  11. if (wp->state != WEBS_RUNNING) {
  12. websPump(wp);
  13. }
  14. }

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

  • WEBS_BEGIN
  • WEBS_CONTENT
  • WEBS_READY
  • WEBS_RUNNING
  • WEBS_COMPLETE

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

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

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

  1. PUBLIC void websPump(Webs *wp)
  2. {
  3. for (canProceed = 1; canProceed; ) {
  4. switch (wp->state) {
  5. case WEBS_BEGIN:
  6. canProceed = parseIncoming(wp);
  7. break;
  8. case WEBS_CONTENT:
  9. canProceed = processContent(wp);
  10. break;
  11. case WEBS_READY:
  12. if (!websRunRequest(wp)) {
  13. // 通过route交给对应的处理程序
  14. websRouteRequest(wp);
  15. wp->state = WEBS_READY;
  16. canProceed = 1;
  17. continue;
  18. }
  19. canProceed = (wp->state != WEBS_RUNNING);
  20. break;
  21. case WEBS_RUNNING:
  22. return;
  23. case WEBS_COMPLETE:
  24. complete(wp, 1);
  25. canProceed = bufLen(&wp->rxbuf) != 0;
  26. break;
  27. }
  28. }
  29. }
WEBS_BEGIN

解析http内容

  1. static bool parseIncoming(Webs *wp)
  2. {
  3. rxbuf = &wp->rxbuf;
  4. while (*rxbuf->servp == '\r' || *rxbuf->servp == '\n') {
  5. bufGetc(rxbuf);
  6. }
  7. if ((end = strstr((char*) wp->rxbuf.servp, "\r\n\r\n")) == 0) {
  8. if (bufLen(&wp->rxbuf) >= BIT_GOAHEAD_LIMIT_HEADER) {
  9. websError(wp, HTTP_CODE_REQUEST_TOO_LARGE | WEBS_CLOSE, "Header too large");
  10. return 1;
  11. }
  12. return 0;
  13. }
  14. trace(3 | WEBS_RAW_MSG, "\n<<< Request\n");
  15. c = *end;
  16. *end = '\0';
  17. trace(3 | WEBS_RAW_MSG, "%s\n", wp->rxbuf.servp);
  18. *end = c;
  19. // 解析http首行信息
  20. parseFirstLine(wp);
  21. if (wp->state == WEBS_COMPLETE) {
  22. return 1;
  23. }
  24. // 解析http头部信息
  25. parseHeaders(wp);
  26. if (wp->state == WEBS_COMPLETE) {
  27. return 1;
  28. }
  29. wp->state = (wp->rxChunkState || wp->rxLen > 0) ? WEBS_CONTENT : WEBS_READY;
  30. websRouteRequest(wp);
  31. if (wp->state == WEBS_COMPLETE) {
  32. return 1;
  33. }
  34. #if !BIT_ROM
  35. #if BIT_GOAHEAD_CGI
  36. if (strstr(wp->path, BIT_GOAHEAD_CGI_BIN) != 0) {
  37. if (smatch(wp->method, "POST")) {
  38. wp->cgiStdin = websGetCgiCommName();
  39. if ((wp->cgifd = open(wp->cgiStdin, O_CREAT | O_WRONLY | O_BINARY, 0666)) < 0) {
  40. websError(wp, HTTP_CODE_NOT_FOUND | WEBS_CLOSE, "Can't open CGI file");
  41. return 1;
  42. }
  43. }
  44. }
  45. #endif
  46. if (smatch(wp->method, "PUT")) {
  47. WebsStat sbuf;
  48. wp->code = (stat(wp->filename, &sbuf) == 0 && sbuf.st_mode & S_IFDIR) ? HTTP_CODE_NO_CONTENT : HTTP_CODE_CREATED;
  49. wp->putname = websTempFile(BIT_GOAHEAD_PUT_DIR, "put");
  50. if ((wp->putfd = open(wp->putname, O_BINARY | O_WRONLY | O_CREAT, 0644)) < 0) {
  51. error("Can't create PUT filename %s", wp->putname);
  52. websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Can't create the put URI");
  53. wfree(wp->putname);
  54. return 1;
  55. }
  56. }
  57. #endif
  58. return 1;
  59. }
WEBS_CONTENT
  1. static bool processContent(Webs *wp)
  2. {
  3. if (!filterChunkData(wp)) {
  4. return 0;
  5. }
  6. #if BIT_GOAHEAD_CGI && !BIT_ROM
  7. if (wp->cgifd >= 0 && websProcessCgiData(wp) < 0) {
  8. return 0;
  9. }
  10. #endif
  11. #if BIT_GOAHEAD_UPLOAD
  12. if ((wp->flags & WEBS_UPLOAD) && websProcessUploadData(wp) < 0) {
  13. return 0;
  14. }
  15. #endif
  16. #if !BIT_ROM
  17. if (wp->putfd >= 0 && websProcessPutData(wp) < 0) {
  18. return 0;
  19. }
  20. #endif
  21. if (wp->eof) {
  22. wp->state = WEBS_READY;
  23. socketDeleteHandler(wp->sid);
  24. return 1;
  25. }
  26. return 0;
  27. }
WEBS_READY

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

  1. PUBLIC bool websRunRequest(Webs *wp)
  2. {
  3. ...
  4. if ((route = wp->route) == 0) {
  5. websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no route for request");
  6. return 1;
  7. }
  8. if (!route->handler) {
  9. websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no handler for route");
  10. return 1;
  11. }
  12. if (!wp->filename || route->dir) {
  13. wfree(wp->filename);
  14. wp->filename = sfmt("%s%s", route->dir ? route->dir : websGetDocuments(), wp->path);
  15. }
  16. if (!(wp->flags & WEBS_VARS_ADDED)) {
  17. if (wp->query && *wp->query) {
  18. websSetQueryVars(wp);
  19. }
  20. if (wp->flags & WEBS_FORM) {
  21. websSetFormVars(wp);
  22. }
  23. wp->flags |= WEBS_VARS_ADDED;
  24. }
  25. wp->state = WEBS_RUNNING;
  26. trace(5, "Route %s calls handler %s", route->prefix, route->handler->name);
  27. #if ME_GOAHEAD_LEGACY
  28. if (route->handler->flags & WEBS_LEGACY_HANDLER) {
  29. return (*(WebsLegacyHandlerProc) route->handler->service)(wp, route->prefix, route->dir, route->flags) == 0;
  30. } else
  31. #endif
  32. if (!route->handler->service) {
  33. websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Configuration error - no handler service callback");
  34. return 1;
  35. }
  36. return (*route->handler->service)(wp);
  37. }

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

  1. PUBLIC void websSetQueryVars(Webs *wp)
  2. {
  3. // 分割环境变量
  4. if (wp->query && *wp->query) {
  5. wp->decodedQuery = sclone(wp->query);
  6. addFormVars(wp, wp->decodedQuery);
  7. }
  8. }
  9. static void addFormVars(Webs *wp, char *vars)
  10. {
  11. keyword = stok(vars, "&", &tok);
  12. while (keyword != NULL) {
  13. if ((value = strchr(keyword, '=')) != NULL) {
  14. *value++ = '\0';
  15. websDecodeUrl(keyword, keyword, strlen(keyword));
  16. websDecodeUrl(value, value, strlen(value));
  17. } else {
  18. value = "";
  19. }
  20. if (*keyword) {
  21. // 如果已经设置了关键字,则将新值附加到已存储的内容中。
  22. if ((prior = websGetVar(wp, keyword, NULL)) != 0) {
  23. websSetVarFmt(wp, keyword, "%s %s", prior, value);
  24. } else {
  25. websSetVar(wp, keyword, value);
  26. }
  27. }
  28. keyword = stok(NULL, "&", &tok);
  29. }
  30. }

获取到环境变量

  1. PUBLIC void websRouteRequest(Webs *wp)
  2. {
  3. ...
  4. safeMethod = smatch(wp->method, "POST") || smatch(wp->method, "GET") || smatch(wp->method, "HEAD");
  5. plen = slen(wp->path);
  6. /*
  7. Resume routine from last matched route. This permits the legacy service() callbacks to return false
  8. and continue routing.
  9. */
  10. if (wp->route && !(wp->flags & WEBS_REROUTE)) {
  11. for (i = 0; i < routeCount; i++) {
  12. if (wp->route == routes[i]) {
  13. i++;
  14. break;
  15. }
  16. }
  17. if (i >= routeCount) {
  18. i = 0;
  19. }
  20. } else {
  21. i = 0;
  22. }
  23. wp->route = 0;
  24. for (; i < routeCount; i++) {
  25. route = routes[i];
  26. assert(route->prefix && route->prefixLen > 0);
  27. if (plen < route->prefixLen) continue;
  28. len = min(route->prefixLen, plen);
  29. trace(5, "Examine route %s", route->prefix);
  30. /*
  31. Match route
  32. */
  33. if (strncmp(wp->path, route->prefix, len) != 0) {
  34. continue;
  35. }
  36. if (route->protocol && !smatch(route->protocol, wp->protocol)) {
  37. trace(5, "Route %s does not match protocol %s", route->prefix, wp->protocol);
  38. continue;
  39. }
  40. if (route->methods >= 0) {
  41. if (!hashLookup(route->methods, wp->method)) {
  42. trace(5, "Route %s does not match method %s", route->prefix, wp->method);
  43. continue;
  44. }
  45. } else if (!safeMethod) {
  46. continue;
  47. }
  48. if (route->extensions >= 0 && (wp->ext == 0 || !hashLookup(route->extensions, &wp->ext[1]))) {
  49. trace(5, "Route %s doesn match extension %s", route->prefix, wp->ext ? wp->ext : "");
  50. continue;
  51. }
  52. wp->route = route;
  53. #if ME_GOAHEAD_AUTH
  54. if (route->authType && !websAuthenticate(wp)) {
  55. return;
  56. }
  57. if (route->abilities >= 0 && !websCan(wp, route->abilities)) {
  58. return;
  59. }
  60. #endif
  61. if ((handler = route->handler) == 0) {
  62. continue;
  63. }
  64. if (!handler->match || (*handler->match)(wp)) {
  65. /* Handler matches */
  66. return;
  67. }
  68. wp->route = 0;
  69. if (wp->flags & WEBS_REROUTE) {
  70. wp->flags &= ~WEBS_REROUTE;
  71. if (++wp->routeCount >= WEBS_MAX_ROUTE) {
  72. break;
  73. }
  74. i = 0;
  75. }
  76. }
  77. if (wp->routeCount >= WEBS_MAX_ROUTE) {
  78. error("Route loop for %s", wp->url);
  79. }
  80. websError(wp, HTTP_CODE_NOT_FOUND, "Cannot find suitable route for request.");
  81. assert(wp->route == 0);
  82. }
WEBS_COMPLETE

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

  1. static void complete(Webs *wp, int reuse)
  2. {
  3. ...
  4. if (reuse && wp->flags & WEBS_KEEP_ALIVE && wp->rxRemaining == 0) {
  5. reuseConn(wp);
  6. socketCreateHandler(wp->sid, SOCKET_READABLE, socketEvent, wp);
  7. trace(5, "Keep connection alive");
  8. return;
  9. }
  10. trace(5, "Close connection");
  11. assert(wp->timeout >= 0);
  12. websCancelTimeout(wp);
  13. assert(wp->sid >= 0);
  14. #if BIT_PACK_SSL
  15. sslFree(wp);
  16. #endif
  17. socketDeleteHandler(wp->sid);
  18. socketCloseConnection(wp->sid);
  19. wp->sid = -1;
  20. bufFlush(&wp->rxbuf);
  21. wp->state = WEBS_BEGIN;
  22. wp->flags |= WEBS_CLOSED;
  23. }

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

  1. PUBLIC void websServiceEvents(int *finished)
  2. {
  3. WebsTime delay, nextEvent;
  4. if (finished) {
  5. *finished = 0;
  6. }
  7. delay = 0;
  8. while (!finished || !*finished) {
  9. if (socketSelect(-1, delay)) {
  10. socketProcess();
  11. }
  12. #if BIT_GOAHEAD_CGI && !BIT_ROM
  13. delay = websCgiPoll();
  14. #else
  15. delay = MAXINT;
  16. #endif
  17. nextEvent = websRunEvents();
  18. delay = min(delay, nextEvent);
  19. }
  20. }

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

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

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

6.png

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

5.png

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

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

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

  1. static void
  2. handle_request( void )
  3. {
  4. ...
  5. // 初始页面名字初始化
  6. const char* index_names[] = {
  7. "index.html", "index.htm", "index.xhtml", "index.xht", "Default.htm",
  8. "index.cgi" };
  9. ...
  10. #ifdef TCP_NOPUSH
  11. if ( ! do_ssl )
  12. {
  13. r = 1;
  14. (void) setsockopt(
  15. conn_fd, IPPROTO_TCP, TCP_NOPUSH, (void*) &r, sizeof(r) );
  16. }
  17. #endif
  18. // 设置ssl
  19. #ifdef USE_SSL
  20. if ( do_ssl )
  21. {
  22. ssl = SSL_new( ssl_ctx );
  23. SSL_set_fd( ssl, conn_fd );
  24. if ( SSL_accept( ssl ) == 0 )
  25. {
  26. ERR_print_errors_fp( stderr );
  27. finish_request( 1 );
  28. }
  29. }
  30. #endif
  31. // 开始处理
  32. start_request();
  33. for (;;)
  34. {
  35. char buf[50000];
  36. int rr = my_read( buf, sizeof(buf) - 1 );
  37. if ( rr < 0 && ( errno == EINTR || errno == EAGAIN ) )
  38. continue;
  39. if ( rr <= 0 )
  40. break;
  41. (void) alarm( READ_TIMEOUT );
  42. add_to_request( buf, rr );
  43. if ( strstr( request, "\015\012\015\012" ) != (char*) 0 ||
  44. strstr( request, "\012\012" ) != (char*) 0 )
  45. break;
  46. }
  47. ...
  48. // 解析请求头
  49. while ( ( line = get_request_line() ) != (char*) 0 )
  50. {
  51. if ( line[0] == '\0' )
  52. break;
  53. // 解析鉴权
  54. else if ( strncasecmp( line, "Authorization:", 14 ) == 0 )
  55. {
  56. cp = &line[14];
  57. cp += strspn( cp, " \t" );
  58. authorization = cp;
  59. }
  60. else if ( strncasecmp( line, "Content-Length:", 15 ) == 0 )
  61. {
  62. cp = &line[15];
  63. cp += strspn( cp, " \t" );
  64. content_length = atol( cp );
  65. }
  66. else if ( strncasecmp( line, "Content-Type:", 13 ) == 0 )
  67. {
  68. cp = &line[13];
  69. cp += strspn( cp, " \t" );
  70. content_type = cp;
  71. }
  72. else if ( strncasecmp( line, "Cookie:", 7 ) == 0 )
  73. {
  74. cp = &line[7];
  75. cp += strspn( cp, " \t" );
  76. cookie = cp;
  77. }
  78. else if ( strncasecmp( line, "Host:", 5 ) == 0 )
  79. {
  80. cp = &line[5];
  81. cp += strspn( cp, " \t" );
  82. host = cp;
  83. // host不能为'\0','.','/'
  84. if ( host[0] == '\0' || host[0] == '.' ||
  85. strchr( host, '/' ) != (char*) 0 )
  86. send_error( 400, "Bad Request", "", "Can't parse request." );
  87. }
  88. else if ( strncasecmp( line, "If-Modified-Since:", 18 ) == 0 )
  89. {
  90. cp = &line[18];
  91. cp += strspn( cp, " \t" );
  92. if_modified_since = tdate_parse( cp );
  93. }
  94. else if ( strncasecmp( line, "Referer:", 8 ) == 0 )
  95. {
  96. cp = &line[8];
  97. cp += strspn( cp, " \t" );
  98. referrer = cp;
  99. }
  100. else if ( strncasecmp( line, "Referrer:", 9 ) == 0 )
  101. {
  102. cp = &line[9];
  103. cp += strspn( cp, " \t" );
  104. referrer = cp;
  105. }
  106. else if ( strncasecmp( line, "User-Agent:", 11 ) == 0 )
  107. {
  108. cp = &line[11];
  109. cp += strspn( cp, " \t" );
  110. useragent = cp;
  111. }
  112. }
  113. ...
  114. strdecode( path, path );
  115. if ( path[0] != '/' )
  116. send_error( 400, "Bad Request", "", "Bad filename." );
  117. file = &(path[1]);
  118. de_dotdot( file );
  119. if ( file[0] == '\0' )
  120. file = "./";
  121. if ( file[0] == '/' ||
  122. ( file[0] == '.' && file[1] == '.' &&
  123. ( file[2] == '\0' || file[2] == '/' ) ) )
  124. send_error( 400, "Bad Request", "", "Illegal filename." );
  125. if ( vhost )
  126. file = virtual_file( file );
  127. ...
  128. r = stat( file, &sb );
  129. if ( r < 0 )
  130. // 获取配置文件
  131. r = get_pathinfo();
  132. if ( r < 0 )
  133. send_error( 404, "Not Found", "", "File not found." );
  134. file_len = strlen( file );
  135. if ( ! S_ISDIR( sb.st_mode ) )
  136. {
  137. while ( file[file_len - 1] == '/' )
  138. {
  139. file[file_len - 1] = '\0';
  140. --file_len;
  141. }
  142. // 处理请求的文件
  143. do_file();
  144. }
  145. else
  146. {
  147. char idx[10000];
  148. ...
  149. for ( i = 0; i < sizeof(index_names) / sizeof(char*); ++i )
  150. {
  151. (void) snprintf( idx, sizeof(idx), "%s%s", file, index_names[i] );
  152. if ( stat( idx, &sb ) >= 0 )
  153. {
  154. file = idx;
  155. do_file();
  156. goto got_one;
  157. }
  158. }
  159. do_dir();
  160. got_one: ;
  161. }
  162. #ifdef USE_SSL
  163. SSL_free( ssl );
  164. #endif /* USE_SSL */
  165. finish_request( 0 );
  166. }

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

  1. static void
  2. do_file( void )
  3. {
  4. cp = strrchr( buf, '/' );
  5. if ( cp == (char*) 0 )
  6. (void) strcpy( buf, "." );
  7. else
  8. *cp = '\0';
  9. // 权限检查
  10. auth_check( buf );
  11. // 检查文件是否需要鉴权
  12. if ( strcmp( file, AUTH_FILE ) == 0 ||
  13. ( strcmp( &(file[strlen(file) - sizeof(AUTH_FILE) + 1]), AUTH_FILE ) == 0 &&
  14. file[strlen(file) - sizeof(AUTH_FILE)] == '/' ) )
  15. {
  16. syslog(
  17. LOG_NOTICE, "%.80s URL \"%.80s\" tried to retrieve an auth file",
  18. ntoa( &client_addr ), path );
  19. send_error( 403, "Forbidden", "", "File is protected." );
  20. }
  21. // 来源检查
  22. check_referrer();
  23. // 是否请求cgi文件
  24. if ( cgi_pattern != (char*) 0 && match( cgi_pattern, file ) )
  25. {
  26. // 如果请求的是cgi文件,则进入此函数进行处理
  27. do_cgi();
  28. return;
  29. }
  30. if ( pathinfo != (char*) 0 )
  31. send_error( 404, "Not Found", "", "File not found." );
  32. if ( method != METHOD_GET && method != METHOD_HEAD )
  33. send_error( 501, "Not Implemented", "", "That method is not implemented." );
  34. fd = open( file, O_RDONLY );
  35. if ( fd < 0 )
  36. {
  37. syslog(
  38. LOG_INFO, "%.80s File \"%.80s\" is protected",
  39. ntoa( &client_addr ), path );
  40. send_error( 403, "Forbidden", "", "File is protected." );
  41. }
  42. mime_type = figure_mime( file, mime_encodings, sizeof(mime_encodings) );
  43. (void) snprintf(
  44. fixed_mime_type, sizeof(fixed_mime_type), mime_type, charset );
  45. if ( if_modified_since != (time_t) -1 &&
  46. if_modified_since >= sb.st_mtime )
  47. {
  48. add_headers(
  49. 304, "Not Modified", "", mime_encodings, fixed_mime_type,
  50. (off_t) -1, sb.st_mtime );
  51. // 返回请求的文件
  52. send_response();
  53. return;
  54. }
  55. add_headers(
  56. 200, "Ok", "", mime_encodings, fixed_mime_type, sb.st_size,
  57. sb.st_mtime );
  58. send_response();
  59. if ( method == METHOD_HEAD )
  60. return;
  61. // 忽略文件大小为0的文件
  62. if ( sb.st_size > 0 )
  63. {
  64. #ifdef HAVE_SENDFILE
  65. #ifndef USE_SSL
  66. send_via_sendfile( fd, conn_fd, sb.st_size );
  67. #else /* USE_SSL */
  68. if ( do_ssl )
  69. send_via_write( fd, sb.st_size );
  70. else
  71. send_via_sendfile( fd, conn_fd, sb.st_size );
  72. #endif /* USE_SSL */
  73. #else /* HAVE_SENDFILE */
  74. send_via_write( fd, sb.st_size );
  75. #endif /* HAVE_SENDFILE */
  76. }
  77. (void) close( fd );
  78. }

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

  1. static void
  2. do_cgi( void )
  3. {
  4. //如果碰到文件描述符冲突,则dup2修改文件描述符
  5. if ( conn_fd == STDIN_FILENO || conn_fd == STDOUT_FILENO || conn_fd == STDERR_FILENO )
  6. {
  7. int newfd = dup2( conn_fd, STDERR_FILENO + 1 );
  8. if ( newfd >= 0 )
  9. conn_fd = newfd;
  10. }
  11. envp = make_envp();
  12. argp = make_argp();
  13. #ifdef USE_SSL
  14. if ( ( method == METHOD_POST && request_len > request_idx ) || do_ssl )
  15. #else
  16. if ( ( method == METHOD_POST && request_len > request_idx ) )
  17. #endif
  18. {
  19. int p[2];
  20. int r;
  21. if ( pipe( p ) < 0 )
  22. send_error( 500, "Internal Error", "", "Something unexpected went wrong making a pipe." );
  23. r = fork();
  24. if ( r < 0 )
  25. send_error( 500, "Internal Error", "", "Something unexpected went wrong forking an interposer." );
  26. if ( r == 0 )
  27. {
  28. // 中断程序
  29. (void) close( p[0] );
  30. cgi_interpose_input( p[1] );
  31. finish_request( 0 );
  32. }
  33. (void) close( p[1] );
  34. if ( p[0] != STDIN_FILENO )
  35. {
  36. (void) dup2( p[0], STDIN_FILENO );
  37. (void) close( p[0] );
  38. }
  39. }
  40. else
  41. {
  42. if ( conn_fd != STDIN_FILENO )
  43. (void) dup2( conn_fd, STDIN_FILENO );
  44. }
  45. // 匹配参数
  46. if ( strncmp( argp[0], "nph-", 4 ) == 0 )
  47. parse_headers = 0;
  48. else
  49. parse_headers = 1;
  50. #ifdef USE_SSL
  51. if ( parse_headers || do_ssl )
  52. #else /* USE_SSL */
  53. if ( parse_headers )
  54. #endif /* USE_SSL */
  55. {
  56. int p[2];
  57. int r;
  58. if ( pipe( p ) < 0 )
  59. send_error( 500, "Internal Error", "", "Something unexpected went wrong making a pipe." );
  60. r = fork();
  61. if ( r < 0 )
  62. send_error( 500, "Internal Error", "", "Something unexpected went wrong forking an interposer." );
  63. if ( r == 0 )
  64. {
  65. (void) close( p[1] );
  66. cgi_interpose_output( p[0], parse_headers );
  67. finish_request( 0 );
  68. }
  69. (void) close( p[0] );
  70. if ( p[1] != STDOUT_FILENO )
  71. (void) dup2( p[1], STDOUT_FILENO );
  72. if ( p[1] != STDERR_FILENO )
  73. (void) dup2( p[1], STDERR_FILENO );
  74. if ( p[1] != STDOUT_FILENO && p[1] != STDERR_FILENO )
  75. (void) close( p[1] );
  76. }
  77. else
  78. {
  79. if ( conn_fd != STDOUT_FILENO )
  80. (void) dup2( conn_fd, STDOUT_FILENO );
  81. if ( conn_fd != STDERR_FILENO )
  82. (void) dup2( conn_fd, STDERR_FILENO );
  83. }
  84. if ( logfp != (FILE*) 0 )
  85. (void) fclose( logfp );
  86. closelog();
  87. (void) nice( CGI_NICE );
  88. directory = e_strdup( file );
  89. binary = strrchr( directory, '/' );
  90. if ( binary == (char*) 0 )
  91. binary = file;
  92. else
  93. {
  94. *binary++ = '\0';
  95. (void) chdir( directory );
  96. }
  97. #ifdef HAVE_SIGSET
  98. (void) sigset( SIGPIPE, SIG_DFL );
  99. #else
  100. (void) signal( SIGPIPE, SIG_DFL );
  101. #endif
  102. // execve执行cgi文件
  103. (void) execve( binary, argp, envp );
  104. send_error( 500, "Internal Error", "", "Something unexpected went wrong running a CGI program." );
  105. }

在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);

3.jpg

FROM:tttang . com

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年2月24日11:13:47
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析路由器WEB服务架构(一)https://cn-sec.com/archives/1266590.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息