前言
在路由器安全研究中,web服务是主要攻击面之一。
常见的路由器web框架类型有以下几种:
1.一个web主程序包揽所有任务,仅有简单的前端文件,无cgi程序。虽然听起来很离谱,但是确实有厂商的路由器把大部分功能直接丢到web主程序里面执行,由web主程序来直接执行命令与本地交互。
2.前端+web主程序+后端,其中后端由cgi程序实现。
3.前端+web主程序+后端,其中后端由解析器+脚本实现,常见的有luci+lua。
比较常用的现成框架有lighttpd、apache、nginx等几种,本文以一个使用了lighttpd的路由器为例来进行探讨。该lighttpd使用的是第二种:前端+web主程序+后端,其中后端由cgi程序实现。
lighttpd框架简述
一、基础架构:
客户端(浏览器) ←→ web服务主程序 ←→ 后端程序(即cgi,可以各种语言实现) ←→ 数据库
↑
↓
静态资源(前端)
二、前端部分:静态资源的处理流程
前端通常由 HTML、CSS、JavaScript、图片等静态文件组成,web主程序可直接处理这些文件,无需后端参与。
客户端请求静态资源的过程:
- 步骤 1:用户访问
http://服务器IP/index.html - 步骤 2:浏览器发送对应请求给web主程序
- 步骤 3:web主程序接收请求,解析 URL 得知需要访问
/var/www/html/index.html。 - 步骤 4:web主程序生成 HTTP 响应(包含
Content-Type: text/html等头信息),将index.html内容返回给浏览器。 - 步骤 5:浏览器解析 HTML,发现引用了
css/style.css和js/app.js,再次向web主程序发送请求,重复上述流程获取这些资源并渲染页面。
三、后端部分:动态请求的处理流程
像修改本地配置、用户认证等需要涉及数据库的内容,就需要与后端交互了。
客户端请求动态资源的过程
- 步骤 1:用户通过前端页面输入账号密码
- 步骤 2:浏览器生成对应请求,发送给Lighttpd。
- 步骤 3:web主程序根据根据请求,把请求里的参数解析出来,调用对应的cgi程序进行处理
- 步骤 5:后端的cgi程序执行验证逻辑,生成响应内容,返回给web主程序。
- 步骤 6:web主程序将后端响应包装成 HTTP 响应,返回给浏览器,前端 JS 接收并处理结果。
lighttpd的工作流程具体分析
启动项和配置分析
学习lighttpd可以直接看源码,本文则通过逆向一个路由器来分析,重点放在lighttpd的web主程序与cgi的交互上。
lighttpd的主程序名字就是lighttpd,首先通过启动项来看该路由器是如何启动lighttpd程序的,启动项是/etc/init.d目录下的lighttpd,这个lighttpd是个bash文件,核心语句是:
service_start /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf
通过lighttpd.conf配置来启动lighttpd主程序,如图所示:

接着跟进lighttpd.conf,该配置文件指定了监听端口、默认路径等基本操作,重点在于模块的调用,它只调用了”mod_redirect”这个模块,其他的全都注释掉,如图所示:

跟进 mod_redirect,发现搜不到,只有mod_redirect.so,这个应该就是模块,搞成了动态链接库,至于server.modules(“mod_redirect”)是如何转化成加载mod_redirect.so的,应该是在lighttpd程序中才能找到了。
继续往下看,lighttpd.conf中出现了include “/etc/lighttpd/conf.d/*.conf”:

找到这个目录:

查看这几个conf文件,发现全都是如下内容:

也就是说虽然前面server.modules()把其他模块都注释掉了,但后面又把他们导进来使用。
主程序分析
初始化
现在可以开始lighttpd主程序的分析了,ida打开,main函数一开始通过 sub_40C14C 完成基础环境准备,包括解析命令行参数、处理环境变量、设置信号处理机制等,确保服务器能在正确的环境中启动。若初始化失败,程序会直接退出。随后,sub_407994 创建服务器核心上下文结构(v5),这个结构类似 “总管”,存储着服务器的配置、连接状态、插件信息等关键数据,为后续运行提供基础。
接着是核心运行阶段。sub_40988C 函数承担核心初始化工作,包括解析配置文件(如端口、根目录、插件列表)、加载插件(如 mod_cgi 处理 CGI 请求)、初始化网络监听等。若初始化成功,服务器进入事件循环(sub_40BF5C)—— 这是服务器的 “心脏”,通过 fdevent_poll 持续监听网络事件(如客户端连接、数据读写),并调用对应模块处理请求,直到收到停止信号或超时。
最后是资源清理与循环控制阶段,此处略过。

sub_40988C函数中实现了.so模块的加载和初始化,流程为:
1.调用 plugins_load来加载:
→ 解析配置中的 server.modules
→ 动态加载插件 .so 文件到内存
2.调用 plugins_call_init来初始化:
→ 执行.so 插件初始化函数
→ 注册.so 插件的核心处理回调
简单来说就是,sub_40988C函数在加载.so 文件到内存后,会调用plugins_call_init来调用该.so 文件的初始化函数(初始化函数在.so中),把该函数的各个回调函数注册到lighttpd主程序初始化时候创建的结构体(plugin 结构体)中,后面通过结构体指针就可以很便捷地访问各个模块的回调函数了。
mod_cgi.so模块是专门负责cgi程序调用的模块,以它为例子,其初始化函数为mod_cgi_plugin_init,会把它的各个回调函数注册到a1这个结构体中,a1由plugins_call_init函数传参,是plugin 结构体,如图所示:

所谓回调函数,指的就是在某个特定条件满足之后会触发的函数,mod_cgi.so的如下:
a1[13] = sub_19AC;:配置解析与请求匹配函数,负责解析cgi.assign等配置,判断当前请求是否需要用 CGI 处理(如匹配.cgi后缀)。a1[14] = sub_3EB4;:CGI 核心处理函数,负责创建管道、fork 子进程、执行 CGI 程序、建立与 CGI 进程的通信。a1[5] = sub_1EA4;:请求处理开始阶段的回调(如初始化 CGI 处理上下文)。a1[2] = sub_20B4;:配置加载回调(解析并验证 CGI 相关配置项)。a1[4] = sub_1660;:模块内部资源初始化(如缓冲区、状态变量)。a1[3] = sub_132C;:模块状态检查回调(如监控 CGI 进程池状态)。
事件循环处理
初始化成功后,main函数调用sub_40BF5C来进入事件循环处理,流程如下:
- 初始化事件监控
循环开始前,函数会初始化事件监控机制(通过fdevent库),将服务器的监听套接字、客户端连接的文件描述符、CGI 进程的通信管道等事件源注册到监控列表中,并关联对应的处理回调(如 “客户端连接事件” 绑定连接处理函数,“CGI 管道可读事件” 绑定mod_cgi的输出处理函数)。(注:这一步在核心初始化的时候就已经完成,而不是在sub_40BF5C函数中完成) - 等待事件触发(阻塞阶段)
调用fdevent_poll函数(事件循环的核心阻塞点),进入等待状态。fdevent_poll会阻塞等待,直到有事件发生(如客户端连接到达、数据可读 / 可写)。 - 处理触发的事件
当事件触发后,函数会遍历所有触发的事件,根据事件类型(读 / 写 / 异常)和事件源(网络套接字 / 管道),调用预先注册的回调函数:- 新客户端连接:调用连接建立函数(如
connection_accept,这个函数在lighttpd程序里),创建新连接并注册后续读写事件; - 客户端数据可读:调用请求读取函数(如
connection_read,这个函数在lighttpd程序里),解析 HTTP 请求; - CGI 管道可读:调用
mod_cgi的cgi_handle_output函数,读取 CGI 程序的输出并转发给客户端; - 客户端可写:调用响应发送函数(如
connection_write),将处理结果发送给客户端。
- 新客户端连接:调用连接建立函数(如
- 清理退出
也就是说,事件循环处理函数sub_40BF5C首先通过监控机制来监听各种事件源,当对应的事件被触发时,就会调用对应的回调函数来进行处理。
同样以mod_cgi.so为例,在它的回调函数中,负责CGI核心处理的函数是sub_3EB4,核心流程如下:
CGI进程启动:
- 创建双向管道(
pipe系统调用) - 通过
fork()创建子进程:- 子进程端:
- 重定向标准输入/输出(
dup2) - 设置环境变量(包括
LD_PRELOAD等) - 切换工作目录(
chdir) - 关闭所有非必要文件描述符(3-255循环关闭)
- 最终执行CGI程序(
execve)
- 重定向标准输入/输出(
- 父进程端:
- 管理管道文件描述符
- 注册事件监听器(
fdevent_register) - 设置非阻塞IO(
fdevent_fcntl_set_nb)
- 子进程端:
执行CGI程序的代码如图所示:

可供调用的cgi函数在cgi-bin文件夹中:

总结
综上所述,就得到了lighttpd与后端cgi程序的交互流程:
首先,lighttpd程序通过lighttpd.conf配置文件来进行初始化,加载指定的各种模块(.so文件),其中mod_cgi.so文件是负责cgi程序调用的核心模块,在加载了mod_cgi.so模块后,注册其回调函数到plugin结构体中,并与对应事件进行关联,之后进入事件循环监听处理阶段,当需要调用某个cgi的请求从前端发过来之后,会触发事件,被lighttpd程序监听到之后,它会调用实现关联的注册函数进行处理,即mod_cgi.so模块中的对应回调函数,假设触发的是sub_3EB4函数,则会根据lighttpd传参过来的cgi程序名字和路径,调用execve来执行这个cgi程序,实现lighttpd程序与cgi程序的交互。