PHP Code Audit 17—CLTPHP Code Audit


Foreword:

  • Audit system: CLTPHP 5.5.3
  • Audit environment: ph5.6.9+apache2.4.9+mysql5.7

1. System architecture analysis

1. System directory structure

/app           //系统程序所在目录
	common       //公共模块
	model_name   //home模块目录
		common.php //模块函数文件
		contraller //控制器目录
		model      //模型目录
		view       //视图目录
/extend        //拓展程序所在目录
/plugins       //系统插件所在目录
/public        //公开文件所在目录,包括html、js、图片等
/runtime       //运行目录
/think         //thinkphp框架目录
/vender        //thinkphp 依赖环境目录
/index.php     //系统入口文件

2. System routing

Foreground routing file: app/foute.php

return [
    '__pattern__' => [
        'name' => '\w+',
        'id' => '\d+',
        'catId' => '\d+',
    ],
    '[hello]'     => [
        ':id'   => ['home/hello', ['method' => 'get'], ['id' => '\d+']],
        ':name' => ['home/hello', ['method' => 'post']],
    ],
    'index' => 'home/index/index',                  //index路由到homemo模块的index控制器
    'news/:catId' => 'home/news/index',             //news/$id 路由到home模块的news控制器
    'newsInfo/:id/:catId' => 'home/news/info',      //newsInfo路由到home模块的news控制器的info()方法
    'about/:catId' => 'home/about/index',
    'system/:catId' => 'home/system/index',
    'services/:catId' => 'home/services/index',
    'servicesInfo/:id/:catId' => 'home/services/info',
    'team/:catId' => 'home/team/index',
    'contact/:catId' => 'home/contact/index',
];

3. System configuration

System configuration file: app/config.php

部分配置:
// 默认模块名
    'default_module'         => 'home',
    // 禁止访问模块
    'deny_module_list'       => ['common'],
    // 默认控制器名
    'default_controller'     => 'Index',
    // 默认操作名
    'default_action'         => 'index',
    // 默认验证器
    'default_validate'       => '',
    // 默认的空控制器名
    'empty_controller'       => 'EmptyController',
//应用命名空间
 		// 默认全局过滤方法 用逗号分隔多个
    'default_filter'         => '',
    // 默认语言
    'default_lang'           => 'zh-cn',
    // 应用类库后缀
    'class_suffix'           => false,
    // 控制器类后缀
    'controller_suffix'      => false,
// 视图输出字符串内容替换
    'view_replace_str'       => [
        '__PUBLIC__' => __PUBLIC__,//public目录的全局变量,在/public/home.php中定义
        '__STATIC__' =>__PUBLIC__.'/static',
        '__UPLOAD__' =>__PUBLIC__.'/uploads',
        '__ADMIN__'    => __PUBLIC__.'/static/admin',
        '__HOME__'     => __PUBLIC__.'/static/home',
    ],

4. System parameter acquisition and filtering

Raw global variables get: $_POST, $_GET,$_REQUEST

$request object acquisition: post(), get(), input().

The original variable gets:

GET示例:  index.php?id=123'<>"    
	die($_GET[id])      // result: 123'<>"
POST示例: index.php ,post_data: id=123<>'"
	die($_POST[id])      // result: 123<>'"
request示例:index.php ,post_data: id=123<>'"
	die($_REQUEST[id])      // result: 123<>'"

It can be seen that for the original methods of obtaining variables such as post and get, the global variable filtering method is not used for security defense.

The $request object gets variables:

POST示例: admin/ad/add POST_data:123<>'"
	var_dump(input('post.'))     //  'id' => string '123<>'"' (length=7)
GET示例: admin/ad/add?id=123<>'"
 	var_dump(input('get.'))     //  'id' => string '123<>'"' (length=7)

It can be seen that the parameters obtained through the input method of the helper function of the request object will also not be filtered globally. So there is a certain security risk.

Two, Xml external entity injection analysis

1. Vulnerability analysis

First, we use the simplexml_load_string() function to locate the middle: getMessage() method of app\wcaht\controller\Wchat.php:

public function getMessage()
    {
    
    
        $from_xml = file_get_contents('php://input'); //通过php://input伪协议读取POSt内容
        if (empty($from_xml)) {
    
      return;  }
        $signature = input('msg_signature', '');
        $signature = input('timestamp', '');
        $nonce = input('nonce', '');
        $url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING'];
        $ticket_xml = $from_xml; //将POST读取的内容赋值到$ticket_xml
        $postObj = simplexml_load_string($ticket_xml, 'SimpleXMLElement', LIBXML_NOCDATA);  //使用simplexml_load_string 解析XML内容,存入$POSTobj中
        $this->instance_id = 0;
        if (!empty($postObj->MsgType)) {
    
    
            switch ($postObj->MsgType) {
    
    
                case "text":
                    //用户发的消息   存入表中
                    //$this->addUserMessage((string)$postObj->FromUserName, (string) $postObj->Content, (string) $postObj->MsgType);
                    $resultStr = $this->MsgTypeText($postObj);//调用msgtype解析XML 对象
                    break;
                case "event":
                    $resultStr = $this->MsgTypeEvent($postObj);
                    break;
                default:
                    $resultStr = "";
                    break;
            }
        }
        if (!empty($resultStr)) {
    
    
            echo $resultStr;
        } else {
    
    
            echo '';
        }
    }

It can be seen that here the content is obtained through the php pseudo-protocol, and the simplexml_load_string() function is used to convert it into an XML object, and the MsgTypeText() and MsgTypeEven functions are called to process the XML object depending on the msgtype. First follow the MsgTypeTex() function (app/wchat/controller/wchat.php):

 private function MsgTypeText($postObj)
    {
    
    
        $funcFlag = 0; // 星标
        $wchat_replay = $this->wchat->getWhatReplay($this->instance_id, (string)$postObj->Content);  //将XML对象的内容获取到,并保存到数组中。
        // 判断用户输入text
        if (!empty($wchat_replay)) {
    
     // 关键词匹配回复
            $contentStr = $wchat_replay; // 构造media数据并返回
        } elseif ($postObj->Content == "uu") {
    
    
            $contentStr = "shopId:" . $this->instance_id;
        } elseif ($postObj->Content == "TESTCOMPONENT_MSG_TYPE_TEXT") {
    
    
            $contentStr = "TESTCOMPONENT_MSG_TYPE_TEXT_callback"; // 微店插件功能 关键词,预留口
        } elseif (strpos($postObj->Content, "QUERY_AUTH_CODE") !== false) {
    
    
            $get_str = str_replace("QUERY_AUTH_CODE:", "", $postObj->Content);
            $contentStr = $get_str . "_from_api"; // 微店插件功能 关键词,预留口
        } else {
    
    
            $content = $this->wchat->getDefaultReplay($this->instance_id);
            if (!empty($content)) {
    
    
                $contentStr = $content;
            } else {
    
    
                $contentStr = '欢迎!';
            }
        }
        if (is_array($contentStr)) {
    
     //调用event_key_news()处理XML对象和$contentStr
            $resultStr = $this->wchat->event_key_news($postObj, $contentStr);
        } elseif (!empty($contentStr)) {
    
    
            $resultStr = $this->wchat->event_key_text($postObj, $contentStr);
        } else {
    
    
            $resultStr = '';
        }
        return $resultStr;
    }

You can see here, process the reply message, and then judge whether the contentstr is an array, if it is, call the event_key_news() function, if not, call the event_key_text() function for processing. First follow up the event_key_news() function (extend/dlt/WchatOauth.php):

public function event_key_news($postObj, $arr_item, $funcFlag = 0)
    {
    
    
        // 首条标题28字,其他标题39字
        if (! is_array($arr_item)) {
    
     return;}
        $itemTpl = "<item>
                        <Title><![CDATA[%s]]></Title>
                        <Description><![CDATA[%s]]></Description>
                        <PicUrl><![CDATA[%s]]></PicUrl>
                        <Url><![CDATA[%s]]></Url>
                    </item>
                ";
        $item_str = "";
  			//获取回复内容,输出到$item_str
        foreach ($arr_item as $item) {
    
    
            $item_str .= sprintf($itemTpl, $item['Title'], $item['Description'], $item['PicUrl'], $item['Url']);
        }
        $newsTpl = "<xml>
        <ToUserName><![CDATA[%s]]></ToUserName>
        <FromUserName><![CDATA[%s]]></FromUserName>
        <CreateTime>%s</CreateTime>
        <MsgType><![CDATA[news]]></MsgType>
        <Content><![CDATA[]]></Content>
        <ArticleCount>%s</ArticleCount>
        <Articles>$item_str</Articles>
        <FuncFlag>%s</FuncFlag>
        </xml>";
  			//通过sprintf函数返回6个节点,其中ToUserName、FromUserName来自于我们传入的XML文档,所以可控。所以也就能通过FromUserName或者ToUserName够构造paylaod来读取文件或者执行命令。
        $resultStr = sprintf($newsTpl, $postObj->FromUserName, $postObj->ToUserName, time(), count($arr_item), $funcFlag);
        return $resultStr;
    }

2. Payload analysis

The following paylaod is used here:

<?xml version="1.0" encoding="utf-8"?>  
//构造XML实体,读取系统文件
<!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "file:///C:/windows/win.ini" >]>                                 
<root>
  <MsgType>text</MsgType>            //构造msgType为text,使其调用MsgTypeText()函数
  <ToUserName>&xxe; </ToUserName>    //通过ToUserName代用XML实体&XXE,也可以使用FromUserName
</root>  

If you want to reproduce, you only need to visit our /wchat/Wchat/getMessage, and then use the POST method to submit our payload.

3. Arbitrary file deletion and download

1. Black box testing

Arbitrary file download:

First, we enter the background, find the database management module, first back up a database file, and then enter the restore database option:

insert image description here

You can download and delete files here. We download the files first, observe the data packets after capturing the packets, and find that the files are downloaded by passing in the file name:

insert image description here

We changed the file name to a system file, and used the directory traversal character to traverse to the root directory and then launched the data package. We found that the ini file of the system was successfully downloaded, indicating that there is an arbitrary file download vulnerability.

insert image description here

Arbitrary file deletion:

Here we still capture the packet first, and find that the method of transferring the file name is still used to delete:

insert image description here

We still modify the file name to any file in the system, such as win.ini:

insert image description here

As you can see, according to the prompt, we have successfully deleted it. Then we use any file download to verify that it has indeed been deleted:

insert image description here

It can be seen that the prompt file does not exist, indicating that the file has indeed been deleted.

2. Source code analysis

Arbitrary file download:

According to the URL of the black-box test, it can be known that the vulnerability of any file download is in the downfile() function under the database controller in the /admin module.

public function downFile() {
    
    
        $file = $this->request->param('file'); //获取file名,通过测试我们知道,这里并不会对目录穿越符进行过滤。
        $type = $this->request->param('type');
        if (empty($file) || empty($type) || !in_array($type, array("zip", "sql"))) {
    
    
            $this->error("下载地址不存在");
        }
  			//拼接文件的路径+问价名
        $path = array("zip" => $this->datadir."zipdata/", "sql" => $this->datadir);
        $filePath = $path[$type] . $file;
        if (!file_exists($filePath)) {
    
     //判断文件是否存在
            $this->error("该文件不存在,可能是被删除");
        }
        $filename = basename($filePath);  //获取文件的基本路径
        header("Content-type: application/octet-stream");
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header("Content-Length: " . filesize($filePath)); /
        readfile($filePath);  //读取文件进行输出
    }

It can be seen that the file name we passed in is not filtered in any way, but the file name is directly spliced ​​into the file path, resulting in an arbitrary file download vulnerability.

Arbitrary file deletion:

Vulnerability point: /admin module, delSqlFiles() function under the database controller

public function delSqlFiles() {
    
    				
				$batchFlag = input('param.batchFlag', 0, 'intval');
        //批量删除
        if ($batchFlag) {
    
    
            $files = input('key', array());
        }else {
    
    
            $files[] = input('sqlfilename' , '');  //获取文件名,可以有多个
        }
        if (empty($files)) {
    
    
            $result['msg'] = '请选择要删除的sql文件!';
            $result['code'] = 0;
            return $result;
        }
        foreach ($files as $file) {
    
      //直接将文件名拼接到路径中,然后使用unlink函数循环删除file数组中的文件名。
            $a = unlink($this->datadir.'/' . $file);
        }
        if($a){
    
    
            $result['msg'] = '删除成功!';
            $result['url'] = url('restore');
            $result['code'] = 1;
            return $result;
        }else{
    
    
            $result['msg'] = '删除失败!';
            $result['code'] = 0;
            return $result;
        }
    }

It can be seen that the cause of the vulnerability is also that the file name is spliced ​​into the file path without any filtering, resulting in the vulnerability of arbitrary file deletion.

4. Any file upload

1. Black box testing

We enter the member center, upload a picture and capture the package:

insert image description here

Modify the file name and file content as shown below:

insert image description here

It prompts that the upload is successful, let's visit the file:public/upload/20220819/ae022e7c463afa05ea2dde9a4825270d.php

insert image description here

It can be seen that the upload was successful and phpinfo() was executed. Indicates that a vulnerability exists.

2. Code Analysis

We enter the vulnerability point: the upload function of the app/user/controller/upFiles.php file:

public function upload(){
    
    
        // 获取上传文件表单字段名
        $fileKey = array_keys(request()->file());
        // 获取表单上传的第一个文件
        $file = request()->file($fileKey['0']);
        // 移动到框架应用根目录/public/uploads/ 目录下
        $info = $file->move(ROOT_PATH . 'public' . DS . 'uploads');
        if($info){
    
    
            $result['code'] = 1;
            $result['info'] = '图片上传成功!';
            $path=str_replace('\\','/',$info->getSaveName());
            $result['url'] = '/uploads/'. $path;
            return $result;
        }else{
    
    
            // 上传失败获取错误信息
            $result['code'] =0;
            $result['info'] = '图片上传失败!';
            $result['url'] = '';
            return $result;
        }
    }

It can be seen that the file name and file function are directly obtained here, and then the move function (/think/libray/think/file.php) is used to move the file. Let's follow up and see:

public function move($path, $savename = true, $replace = true)
    {
    
    
        // 文件上传失败,捕获错误代码
        if (!empty($this->info['error'])) {
    
    
            $this->error($this->info['error']);
            return false;
        }
        // 检测合法性
        if (!$this->isValid()) {
    
    
            $this->error = '非法上传文件';
            return false;
        }
        // 验证上传
        if (!$this->check()) {
    
    
            return false;
        }
        $path = rtrim($path, DS) . DS;
        // 文件保存命名规则
        $saveName = $this->buildSaveName($savename);
        $filename = $path . $saveName;
        // 检测目录
        if (false === $this->checkPath(dirname($filename))) {
    
    
            return false;
        }
        /* 不覆盖同名文件 */
        if (!$replace && is_file($filename)) {
    
    
            $this->error = '存在同名文件' . $filename;
            return false;
        }
        /* 移动文件 */
        if ($this->isTest) {
    
    
            rename($this->filename, $filename);
        } elseif (!move_uploaded_file($this->filename, $filename)) {
    
    
            $this->error = '文件上传保存错误!';
            return false;
        }
        // 返回 File对象实例
        $file = new self($filename);
        $file->setSaveName($saveName);
        $file->setUploadInfo($this->info);
        return $file;
    }

It can be seen that the isValid() function is called here to check the validity of the file without file, and we continue to follow up:

public function isValid()
    {
    
    
        if ($this->isTest) {
    
    
            return is_file($this->filename);  //检测是否是文件
        }
        return is_uploaded_file($this->filename);  //是否是通过http POST上传的,是则返回true
    }

It can be seen that whether it is a file is detected here, and the file suffix of the content of the file is not detected.

Then rename the file name in the move function, and check whether the file name already exists, etc., and finally save the file to the public directory.

Therefore, it can be seen that there is no security check on the file upload operation, which leads to the long-term transmission of arbitrary files.

5. References

Guess you like

Origin blog.csdn.net/qq_45590334/article/details/126503369