LuCI系路由系统逆向分析-下篇

admin 2022年5月7日12:22:17评论567 views字数 6137阅读20分27秒阅读模式


一、模块定义 



本小节中提到的从entry 定义得出接口地址,仅适用于 openwrt 原生、类原生系统,即具备前文无 shell 判断的第二种特征,如下图:


LuCI系路由系统逆向分析-下篇


将全部功能集中在一个页面(node)需要另行分析。

了解一个模块的代码定义,从 controller 代码分析得出:接口地址、处理文件路径。

注意!entry 定义是模块不是页面,自然不具备访问权限控制。页面是 node ,权限由 node track.sysauth 控制,具体见后文页面定义&访问权限

以 openwrt luCI 的页面举例:系统-系统

LuCI系路由系统逆向分析-下篇

 --控制器路径
 module("luci.controller.admin.apmng_status",package.seeall)
 function index()
     local fs = require "nixio.fs"
     --entry为模块注册
     entry({"admin","system"}, alias("admin", "system","system"), _("System"), 30).index = true
     entry({"admin","system", "system"}, cbi("admin_system/system"),_("System"), 1)
     entry({"admin","system", "clock_status"}, post_on({ set = true },"action_clock_status"))
     entry({"admin","system", "admin"}, cbi("admin_system/admin"),_("Administration"), 
2)

 end


    在 controller 的 index() 函数中,使用 entry() 函数为每个模块(model)函数完成注册,定义如下:
  • entry() 在 /usr/lib/lua/luci/dispatcher.lua 中定义
  • entry(path, target, title=nil, order=nil)
  • Path菜单路径,URL 路径。如:{"admin", "system", "system"} -->/cgi-bin/luci/admin/system/system
  • Target:调用目标,处理方法。分别有以下几种:
  • alias:指向 entry 的别名
  • call:调用当前 lua 文件下执行函数。如:call("get_systime") --> function get_systime()
  • cbi:调用模块(model)lua 程序。如:cbi("admin_system/admin")--> /usr/lib/lua/luci/model/cbi/admin/system.lua
  • template:调用已有的 htm 模版
  • Title:页面标题。如使用 _("Administration") 会从语言库替换为本地语言
  • Order:同级菜单下,此菜单项的位置,从小到大,表现为从左到右,从上到下

对应逆向分析了解到这里差不多了,重点关注 controller 模块定义的 path 路径、target 路径用于交叉调用 lua 文件。后面怎么调用 Map 生成页面知识不太用得到。




二、 页面定义 | 访问权限


2.1  node-tree 节点树


在 controller 目录下,每个 .lua 文件中,都有一个 index() 函数,其中主要调用 entry() 函数,形如:

 entry(path,target,title,order)

path 形如:{admin,network,wireless} ,entry() 函数根据这些创建一个 node :

 function entry(path, target, title, order, userlist)
     local e = node(unpack(path)) --创建node
     e.target = target
     e.title = title
     e.order = order
     e.module = getfenv(2)._NAME
     e.userlist = userlist
     return e
 end

并把它放在全局 node-tree 的相应位置,后面的参数都是该 node 的属性,还可以有其他的参数。其中最重要的就是 target 。

createtree() 函数就是要找到 controller 目录下所有的 .lua 文件,并找到其中的 index() 函数执行,从而生成一个 node-tree 。为了效率,第一次执行后,把生成的 node-tree 放在 /tmp/treecache 文件中(配置文件可以定义缓冲文件),以后只要没有更新,直接读该文件即可。生成的 node-tree 如下:

LuCI系路由系统逆向分析-下篇

这里要注意的是,每次 dispatch() 会根据 path_info 逐层索引,且每一层都把找到的节点信息放在一个变量 track 中,这样做使得上层 node 的信息会影响下层 node ,而下层 node 的信息又会覆盖上层 node 。比如 {/admin/system} ,最后的 auto=false,target=aa,而由于 admin 有 sysauth 值,它会遗传给它的子节点,也即所有 admin 下的节点都需要认证。


2.2  target 简介


对每个节点最重要的属性是 target ,这也是 dispatch() 流程最后要执行的方法。target 主要有:alisefirstchildcallcbiformtemplate 。这几个总体上可以分成两类,前两种主要用于链接其它 node ,后一个则是主要的操作、以及页面生成。下面分别描述。


  • 链接方法

在介绍初始登录流程时,已经讲到了这种方法。比如初始登录时,url 中的 path_info 仅为 / ,这应该会索引到 rootnode 节点。而该节点本身是没有内容显示的,所以它用 alias('admin') 方法,自动链接到 admin 节点。再比如,admin 节点本身也没有内容显示,它用 firstchild() 方法,自动链接到它的第一个子节点 /admin/status


  • 操作方法

这种方法一般用于一个路径的叶节点 leaf ,它们会去执行相应的操作,如修改 interface 参数等,并且动态生成页面 html 文件,传递给 client 。这里实际上是利用了所谓的 MVC 架构,这在后面再描述,这里主要描述 luci 怎么把生成的 html 发送给 client 端。


callcbiformtemplate 这几种方法,执行的原理各不相同,但最终都会生成完整的 http-response 报文(包括 html 文件),并调用 luci.template.render() luci.http.redirect() 等函数,它们会调用几个特殊的函数,把报文内容返回给 luci.running() 流程。


LuCI系路由系统逆向分析-下篇


如上图所示,再联系 luci.running() 流程,就很容易看出,生成的完整 http-response 报文会通过 io.write() 写在 stdout 上,而 uhttpd 架构已决定了,这些数据将传递给父进程,并通过 tcp 连接返回给 client 端。


2.3  sysauth 用户认证


由于节点是由上而下逐层索引的,所以只要一个节点有 sysauth 值,那么它所有的子节点都需要认证。不难想象,/admin 节点有 sysauth 值,它以下的所有子节点都是需要认证才能查看、操作的;/mini 节点没有 sysauth 值,那么它以下的所有子节点都不需要认证。

LuCI系路由系统逆向分析-下篇

luci 中关于登陆密码,用到的几个函数为:

LuCI系路由系统逆向分析-下篇

可以看出它的密码是用的 linux 的密码,而 openwrt 的精简内核没有实现多用户机制,只有一个 root 用户,且开机时自动以 root 用户登录。要实现多用户,必须在 web 层面上,实现另外一套 (userpasswd) 系统。

另外认证后,server 端会发给 client 一个 session 值,且它要一直以 cookie 的形式存在于 request 报文中,供 sesrver 端来识别用户。这是 web 服务器的一般做法,这里就不多讲了。(下图第三步即为 sysauth 权限校验)


LuCI系路由系统逆向分析-下篇




三、从WEB服务器解析到脚本文件过程



3.1  工作框架


LuCI系路由系统逆向分析-下篇


client 端和 server 端采用 cgi 方式交互,uhttpd服务器的cgi方式中,fork 出一个子进程,子进程利用 execl 替换为 luci 进程空间,并通过 setenv 环境变量的方式,传递一些固定格式的数据(如 PATH_INFO )给 luci 。另外一些非固定格式的数据( post-data )则由父进程通过一个 w_pipe 写给 luci stdin ,而 luci 的返回数据则写在 stdout 上,由父进程通过一个 r_pipe 读取。


3.2  Web数据交互


LuCI系路由系统逆向分析-下篇


1.首次运行时,是以普通的 file 方式获得 docroot/index.html ,该文件中以 meta 的方式自动跳转到 cgi url ,这是 web 服务器的一般做法。

2. 然后第一次执行 luci path_info='/',会 alise '/admin' '/' 会索引到 tree.rootnode ,并执行其 target 方法,即 alise('/admin'),即重新去索引 adminnode ,这在后面会详细描述),该节点需要认证,所以返回一个登录界面。

3.  3 次交互,过程同上一次的,只是这时已 post 来了登录信息,所以 server 端会生成一个 session 值,然后执行 '/admin' target (它的 target firstchild ,即索引第一个子节点),最终返回 /admin/status.html ,同时会把 session 值以 cookie 的形式发给 client 。这就是从原始状态到得到显示页面的过程,之后主要就是点击页面上的连接,产生新的 request

4. 每个链接的 url 中都会带有一个 stok 值(它是 server 生成的,并放在 html 中的 url 里),并且每个新 request 都要带有 session 值,它和 stok 值一起供 server 端联合认证。

初始阶段 http 报文,可以看到从第 2 次交互开始,所有 request 都是 cgi 方式(除一些 cssjs resource 文件外),且执行的 cgi 程序都是 luci ,只是带的参数不同,且即使所带参数相同(如都是 '/' ),由于需要认证,执行的过程也是不同的。

注:cssjs文件保存在/www/luci-static文件下


正是由于多种情况的存在,使得 luci 中需要多个判断分支,代码多少看起来有点乱,但 openwrt 还是把这些分支都糅合在了一个流程线中。下面首先给出整体流程,首先介绍一下 lua 语言中一个执行方式 coroutine ,它可以创造出另一个执行体,但却没有并行性,如下图所示,每一时刻只有一个执行体在执行,通过 resumeyield 来传递数据,且数据可以是任意类型,任意多个的。


LuCI系路由系统逆向分析-下篇


Luci 正是利用了这种方式,它首先执行的是 running() 函数,其中 create 出另一个执行体 httpdispatch ,每次 httpdispatch 执行 yield 返回一些数据时, running() 函数就读取这些数据,做相应处理,然后再次执行 resume(httpdispath) ,如此直到 httpdispatch 执行完毕,如上图所示:


LuCI系路由系统逆向分析-下篇


如上图所示,其实 luci 真正的主体部分正是 dispatch ,该函数中有多个判断分支,全部糅合在一起。

注:所有的页面跳转都是dispatch来分配的


3.2.1       服务器架构


main() 进行一些初始化(首先 parse config-file ,然后 parse argv ),然后进入一个回圈,不断地监听,每当有一个客户请求到达时,则对它进行处理。


对于 web 服务器,所要做的处理主要就是分析 url ,判断出是 file-requestcgi-request lua-request ,这主要是根据 url 的最前面的字串(称为字首 prefix )得出的,然后就用相应的形式进行处理,如下图所示:


LuCI系路由系统逆向分析-下篇


3.2.2   cig-response流程


前面已提到,openwrt 系统中使用的 uhttpd 服务,主要是用 cgi 方式来回应客户请求的,下面就对这种方式详细阐述。


3.2.2.1url解析


由上图红色字所示,uh_cgi_request 需要两个二外的引数 pathinfo interpreter ,其中 pin 是一个 struct ,包含了路径中各种有用信息,ipr 指明所用的 cgi 程式,因为一个服务器中可以有多个 cgi 程式。


LuCI系路由系统逆向分析-下篇


如图所示,docroot 是服务器的资源目录,是为了 os 准确定位资源位置,由 uhttpd config 设定,如 openwrt 中为 /www 。后面的是 client 传来的 url ,开头的为 cgi-prefix ,也是有 uhttpd config 设定的,它指明 server 端采用 cgi 处理方式,如 openwrt 中的为 /www/cgi-bin,紧接着的是 cgi 的程式名,它指明了使用哪个 cgi 程式,再后面就是实际的 path 信息了,在 cgi 方式中,它会被当成引数供 cgi 程式使用。


3.2.2.2cgi处理框架


要执行 cgi 程式,首先意味着需 fork 出一个子程序,并通过 execl 函式替换程序空间为 cgi 程式,其次,资料传递、子程序替换了程序空间后,怎么获得原信息,有怎么把回馈资料传输给父程序( uhttpd ),父程序又怎么接收这些资料。


LuCI系路由系统逆向分析-下篇


首先建立了两个 pipe ,这实际上是利用 AF_UNIX 协议域,建立两个相连的 socket_unix ,那么它们对映的档案描述符(即这里的fd[0]fd[1])就构成了一个 pipe ,且这种关系即使 fork 后也仍然存在,因为 fork 仅是增加档案的引用次数,而 os 维护的 file 结构和 socket 结构都没变,这就是父子程序间传递资料的方式。然后 fork 出一个子程序。


子程序中首先把两个管道的一端 close ,注意这仅是使得档案引用次数变为 1 。由于子程序待会要 excel 替换,替换后 rfdwfd 就不存在了,因此先把它们 dup2 给知名的 stdinstdout ,这样即使 execl 替换后,ipt->extu 程式可以以此来和父程序传递资料。另外, execl 替换后,cgi 程式仍需要之前的一些引数信息,如 PATH_INFO 等,这种情况下,最简单的办法就是 setenv ,把需要的引数设为环境变数。


为什么要两个 pipe ,因为子程序向父程序传递回馈资料需要一个 out-pipe ,而若有 post 资料,子程序还需要一个 in-pipe ,从父程序读取post资料。父程序中首先也是 close ,同上所述。若有 post 资料,先从 httprequest-header 中得到 content-length ,为后面传递给子程序做准备。然后进入一个回圈(为什么要回圈,什么时候退出,后面讲),通过 select 轮询 io ,超时、中断的情况就不看了,轮询的 io 一个是 reader ,即从子程序读取回馈资料,而若有 post 资料的话,还要另一个 iowriter ,向子程序写 post 资料。主要的处理就是上图中红色字所示,具体如下:


LuCI系路由系统逆向分析-下篇


LuCI系路由系统逆向分析-下篇

原文始发于微信公众号(山石网科安全技术研究院):LuCI系路由系统逆向分析-下篇

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月7日12:22:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   LuCI系路由系统逆向分析-下篇http://cn-sec.com/archives/983862.html

发表评论

匿名网友 填写信息