Thinkphp-popchain

之前发简书的thinkphp-vuln系列直接被转自己可见把我搞怕了......这里试着发下tp5反序列化的pop链分析。不过不放exp.应该不会被吞了吧。

...

tp5.0

这一部分主要是跟下tp5.0版本的反序列化pop链。不过这里不会分享exp.(网上跟先知应该都能很方便找到)。如果需要的话自己SCTF2020wp里有绕过短标签的exp。以及以前跟php框架有几个其他的exp可以自行寻找。当然我记得wh1t3P1g大佬自己把tp的popchain集成到phpggc中了。也可以自动生成。

然后就是windows下写文件的方法。目前能够在php7以前的版本写shell exp是有的。但php7的windows写shell我还没成功过。理论上windows不能成功的原因只是因为文件名不允许<,?的。但是如果用过滤器绕过的话应该是没问题的......
php5.4.45+windows 成功写入phpinfo() (关闭短标签)

上面口胡了。php7可写可写。就是用的上面说的方法

这里我就直接跟下linux的payload吧。
首先是入口点。肯定是找__destruct函数。不难发现一共只有几个可用。我们找到 library\think\process\pipes下windows.php。发现其调用了$this->removeFiles.而removeFiles又调用了file_exists可以触发__toString方法。

private function removeFiles()
{
    foreach ($this->files as $filename) {
        if (file_exists($filename)) {
            @unlink($filename);
        }
    }
    $this->files = [];
}

ps.关于file_exists可以触发__toString自己以前还没有注意过。具体不妨去l3m0n师傅这篇文章下评论看看。
(应该是因为file_exists接受字符串参数,而只要对象被当做字符串即会触发__toString)
全局继续找toString.也只有几个选择。这里找到 think\Model.php
.它调用了toJson。而toJson继续调用了toArray.

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
    return json_encode($this->toArray(), $options);
}

同时注意这里Model类是抽象类。所以实际编写exp时我们必须用它的子类。比如此处的Pivot。
我们直接来到toArray。这里首先主要看有没有可以触发__call的情况。5.0.24版本下应该又三处都是可以满足的

$relation = $this->getAttr($key);
$value = $this->getRelationData($modelRelation);
$item[$key] = $value ? $value->getAttr($attr) : null;

与其说是找触发__call的。不如说是找可用方法。多数情况下这些方法基本利用不了。但是如果满足this->xxx($var)或者进一步可控类->xxx(可控变量)。我们就能找任意类的__call进行进一步挖掘。这也是pop链中call方法经常用到的原因之一。

为了了解我们如何控制这一步用来调用__call。我们先放一下全局找__call的过程。来选择一个触发方式。
对于$item[$key] = $value ? $value->getAttr($attr) : null;
这里看看$value$attr依次是怎么被赋值的。

value

$relation = Loader::parseName($name, 1, false);
$modelRelation = $this->$relation();
$value         = $this->getRelationData($modelRelation);

主要是对$name调用parseName。而$name来自可控数组$this->append

也就是说。$modelrelation是Model这个类任意方法的返回值。(.$relation())。所以找一个直接返回可控数据的方法即可。比如getError

public function getError()
{
    return $this->error;
}

回到上面。现在我们继续跟进getRelationData

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;
}

首先它接收的参数是Relation类的。所以我们上面返回的结果$this->error肯定也是一个Relation的对象了。(Relation也是抽象类。所以实例化时要用它的子类)
然后此处我们自然要走第一个if分支来控制返回值。分别看下isSelfRelation()getModel()发现都只是简单返回this->relationthis->query->model()。全部可控。
那只剩下让get_class($modelRelation->getModel()) == get_class($this->parent)成立了。
意思就是$modelRelation->getModel()$this->parent为同类,也就是要求$value->getAttr($attr)中的$value和上面可控的model为同类
那么现在$value->getAttr($attr)value跟完了。我们来看看$attr

$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
    foreach ($bindAttr as $key => $attr)
    ......

上面提到过,modelRelation因为取自可控方法所以是任意值。我们直接全局找getBindAttr方法。只有一个接口类:Relation的子类OnetoOne

public function getBindAttr()
{
    return $this->bindAttr;
}

数据可控。不过OnetoOne是抽象类。所以继续找子类。这里就只有两个子类。我们选择HasOne.

现在。我们做到了任意调用__call。剩下的就是找可用的__call

那么全局找可用的__call。此处可以找到
think\console\Output 类。

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);
    }
}

那么首先上面触发点里$this->parent肯定是要传Output类的实例了。
下面看这里的$this->block()

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);
}

调用的是$this->handle->write。既然$this->handle可控,那么此处找一个同名的write方法。我们全局搜索找到Memcached类

public function write($sessID, $sessData)
{
    return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
}

同理。还是可以全局找set方法。第一个就是我们曾经在tp5RCE中见到的File类

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;
    }
}

之前曾经说过rce时缓存文件名不可控。但是在反序列化中就不存在这个问题。

$filename = $this->getCacheKey($name, true);

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;
}

文件名来自$filename = $this->options['path'] . $name . '.php';.可控。
我们再看文件内容如何控制。

$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

这里就是pop链最大的难点了。$data其实并不可控。具体可以回溯到我刚刚上面放的一连串调用write方法的源码处。注意到

public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
    $this->write($messages, true, $type);
}

write第二个参数是写死的true。它会一路传到File类作为$data写入。那么我们写文件等于控制不了写入内容。

但是没有关系。set在这个file_put_contents下还调用了一个函数setTagItem

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);
    }
}

在这里我们又一次调用了set。并且两个参数全部可控。所以最后循环调用我们就知道,写入文件的文件名为md5('tag_' . md5($this->tag)).'.php'。内容为$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

当然。这里显然存在一个绕过死亡exit的问题。使用rot13即可。然后rot13的payload绕不过默认的短标签。所以会需要加过滤器组合拳。除了常见的base64,WMCTF中使用到其他的iconv或者其他组合也是可行的。
原理不再赘述
那么。控制payload只要控制File类$this->options['path'] = php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>即可。

执行exp打的话。会发现存在两个文件。这是上面我们调用了两次set的缘故。而文件名由我们的$tag决定。具体计算方法也在上面提及了。当然最好的方法永远是本地自己打一遍。这样才能确信文件名这种远程不可见的东西。

另外我相信大家肯定发现这个pop链有个变招。那就是linux,windows通用的写目录。回到上面getCacheKey

$dir = dirname($filename);

if ($auto && !is_dir($dir)) {
mkdir($dir, 0755, true);
}

只要把$this->options['path']设置为目录的话。直接可以写755权限目录。

SCTF2020 考察tp5.024那道题当时使用了python脚本高强度删文件。导致我以为当前目录不可写。但是换成写目录的payload后发现可以创建目录。并且可以存在相同于靶机重启时间的3分钟。所以使用这种payload黑盒探测不失为一种办法。

tp5.1

今天来跟下5.1的pop链。相比5.0而言思路大致相同。只有几个类的区别。并且其exp已经集成到phpggc上了。

还是从起点开始看。跟昨天5.0的链子是一样的。从Windows类开始。然后=>file_exists => __toString()。然后接着全局搜索。此处利用Conversion的__toString() => __toJson() => toArray()
而不是5.0中的Model类

看到thinkphp\library\think\model\concern\Conversion.php
中的toArray() .我们同样寻找可以触发__call()的代码

此处主要是$relation->visible($name)会触发__call.选择这一处的代码是因为,relation来自$this->getRelation($key).$name来自$this->append.
看到getRelation

public function getRelation($name = null)
{
    if (is_null($name)) {
        return $this->relation;
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    return;
}

我们要进入visble的分支。必须要$relation为空。所以$this->relation直接置空即可。

此时$relation$relation = $this->getAttr($key);决定。它会依次调用\thinkphp\library\think\model\concern\Attribute.php 的getAttr()与getData()

 public function getAttr($name, &$item = null)
{
    try {
        $notFound = false;
        $value    = $this->getData($name);
    } catch (InvalidArgumentException $e) {
        ......

public function getData($name = null)
{
    if (is_null($name)) {
        return $this->data;
    } elseif (array_key_exists($name, $this->data)) {
        return $this->data[$name];
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

这样就能确认我们的$relation来自Attribute类的$this->data[$name].

现在需要注意的是。我们必须得找到一个既能调用Conversion还能调用Attribute属性的类。即继承了Attribute类和Conversion类的子类。这个其实就是我们之前5.0链子中用过的Model.php。
加上Model是抽象类。所以编写exp中使用它的子类Pivot实例化。这点不必多说。

接下来看$relation->visible($name)中的$name.它是遍历$this->append得到的。可控。只需注意将其赋值为数组即可。(因为要进入if (is_array($name))的分支)

既然已经拥有触发__call的条件了。我们现在找一个可用的__call。5.1版本中的gadget就是来自Request类的__call

public function __call($method, $args)
{
    if (array_key_exists($method, $this->hook)) {
        array_unshift($args, $this);
        return call_user_func_array($this->hook[$method], $args);
    }

    throw new Exception('method not exists:' . static::class . '->' . $method);
}

显然$this->hook可以让我们控制为["visable"->"arbitrary method"]这个数组任意调用方法。但是注意array_unshift($args, $this)会强行把$this放到$args数组的第一位。其后果是怎样的呢?我们看下call_user_func_array

call_user_func_array([$obj,"arbitrary method"],[$this,$arg])
=>
$obj->$func($this,$argv)

这种方法执行几乎没有。所以这就限制我们要找一个不受这种调用方式影响的函数。

在以前tp5的漏洞分析中,曾经用到过think\Request类中的input 方法。里面有call_user_func($filter,$data)可以用于命令执行。
但是前面说过, $args 数组变量的第一个元素,是一个固定死的类对象,所以这里我们不能直接调用 input 方法,而应该寻找调用 input 的方法。

整个Request类中一共有7处调用input方法的其他方法。我们选择param方法为例

public function param($name = '', $default = null, $filter = '')
{
    if (!$this->mergeParam) {
        $method = $this->method(true);

        // 自动获取请求变量
        switch ($method) {
            case 'POST':
                $vars = $this->post(false);
                break;
            case 'PUT':
            case 'DELETE':
            case 'PATCH':
                $vars = $this->put(false);
                break;
            default:
                $vars = [];
        }

        // 当前请求参数和URL地址中的参数合并
        $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

        $this->mergeParam = true;
    }

    if (true === $name) {
        // 获取包含文件上传信息的数组
        $file = $this->file();
        $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

        return $this->input($data, '', $default, $filter);
    }

    return $this->input($this->param, $name, $default, $filter);
}

调用了input方法但是只有一个$param是可控的。所以还要继续找调用param的方法。

public function isAjax($ajax = false)
{
    $value  = $this->server('HTTP_X_REQUESTED_WITH');
    $result = 'xmlhttprequest' == strtolower($value) ? true : false;

    if (true === $ajax) {
        return $result;
    }

    $result   = $this->param($this->config['var_ajax']) ? true : $result;
    $this->mergeParam = false;
    return $result;
}

isAjax方法返回值由$this->config['var_ajax']控制。那么等于控制了param的参数$name.等于控制了input 的参数$name.

最后再来到input方法这看调用。

public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
        // 获取原始数据
        return $data;
    }

    $name = (string) $name;
    if ('' != $name) {
        // 解析name
        if (strpos($name, '/')) {
            list($name, $type) = explode('/', $name);
        }

        $data = $this->getData($data, $name);

        if (is_null($data)) {
            return $default;
        }

        if (is_object($data)) {
            return $data;
        }
    }

    // 解析过滤器
    $filter = $this->getFilter($filter, $default);

    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        if (version_compare(PHP_VERSION, '7.1.0', '<')) {
            // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
            $this->arrayReset($data);
        }
    } else {
        $this->filterValue($data, $name, $filter);
    }

    if (isset($type) && $data !== $default) {
        // 强制类型转换
        $this->typeCast($data, $type);
    }

    return $data;
}

getData顺着看一下可控。且$data=$data[$name],$filter来自$this->filter.最后到了array_walk_resursive相当于直接对数组每一个值调用了回调函数$this->filterValue($filter)

if (is_array($data)) {
    array_walk_recursive($data, [$this, 'filterValue'], $filter);

$this->filterValue是通过call_user_func执行的自然不必说了。
既然如此。控制$this->filter为system,$data数组第一个值为命令whoami之类的就可以执行命令了。

至此pop链就完整了。其实中间有个步骤就是解决call_user_func_array那找到不受定死参数影响的命令执行这块,主要思路就是利用thinkphp过滤器,覆盖filter的方法去执行代码。
而在找到input作为主要下手点时,call_user_func_array(array(任意类,任意方法),$args)$args 数组的第一个变量,即我们前面说的一个固定死的类对象会作为 $data 传给 input 方法,那么在强转成字符串的时候,框架就会报错退出。所以我们找不到就继续找上层调用input的函数。直到找到可控参数的函数isAjax.就能解决参数不可控的问题。

tp5.2.x-unserialize

5.2版本的链子貌似跟之前没啥区别。但是我composer一直安装不上。加上5.2版本作为dev版本本身出现的不多,所以这里用thinkphp-vuln里的例子简单提一下。

前面入手点大同小异。唯一有区别的地方在触发__call的代码$relation->visible($name)这。看似tp5.2已经把这句代码删了。但是实际上是被转移到了appendAttrToArray这个方法中。因此基本没有区别。我就不跟了。

放上几张图


真正的执行点在下面的$closure($value,$this->data)这里的动态调用。参数均可控。所以赋值命令执行的参数即可。

因为后面部分具体细节跟6.0一样。所以我会把内容在6.0里提一下。

tp6.0.x-unserialize

今天重新看了下之前在 php框架反序列化练习 文章里的内容。才想起来5.2跟6.0的链子应该是跟过了。不过当时没有动态调试,理解也没那么深刻。所以还是再看一下。

还是老样子更改Index.php

<?php
namespace app\controller;

class Index
{
    public function index()
    {
        $u = unserialize($_GET['c']);
        return 'ThinkPHP V6.x';
    }
}

直接用wh1t3p1g 师傅集成好的。顺带也推荐下师傅在安全客上针对thinkphp链子的分析文章。


首先6.0版本的主要问题是前面5.*版本的利用起点Windows类都没了。也就是少了一个__destruct()。那么我们需要找到一个替代的__destruct作为起点。并且最好它能够在中间某个环节起到与其他链子相同作用比如触发__call,__toString之类的。这样的逻辑也是第5空间laravel那题的解题思路吧。因为跟过链子的人都知道只需要两个类就能rce.既然其中一个destruct被处理了。找一个替代的自然是最简单的办法。

vendor/topthink/think-orm/src/Model.php

public function __destruct()
{
    if ($this->lazySave) {
        $this->save();
    }
}

构造lazySave为真值。进入save函数

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.不过既然如此我们不能进入上面那个if分支。
isEmpty与trigger

public function isEmpty(): bool
{
    return empty($this->data);
}


protected function trigger(string $event): bool
{
    if (!$this->withEvent) {
        return true;
    }
    ......

显然。需要
1.$this->data为非空数组。
2.$this->withEvent为false
3.$this->exists为true进入updateData函数

跟进到updateData后。我们不妨先看下哪一个函数可以利用,再回头考虑参数的构造。这里顺着看到checkAllowFields后

$table = $this->table ? $this->table . $this->suffix : $query->getTable();

存在可控变量的拼接。那么我们就可以触发__toString了。在经历了前面几个版本的反序列化构造后,我们当然清楚tp5.1~5.2版本的链子分别是__destruct()=> __toString() => __call() => call_user_func_array / $closure($value,$this->data)的一系列调用。那么此处我们自然可以继续达成__toString来延续链子。

回头再检查updateData这的参数需要。首先第一个trigger我们已经满足条件了。然后if (empty($data))这个分支不能进入。那就要看向getChangedData

public function getChangedData(): array
{
    $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
        if ((empty($a) || empty($b)) && $a !== $b) {
            return 1;
        }

        return is_object($a) || $a != $b ? 1 : 0;
    });

    // 只读字段不允许更新
    foreach ($this->readonly as $key => $field) {
        if (isset($data[$field])) {
            unset($data[$field]);
        }
    }

    return $data;
}

$this->force为true.然后data就可控了。

接下来是回到利用函数checkAllowFields.拼接处之前的代码

if (empty($this->field)) {
    if (!empty($this->schema)) {
        $this->field = array_keys(array_merge($this->schema, $this->jsonType));
    } else {
        $query = $this->db();

需要
1.$this->field为空进入分支
2.$this->schema为空进入else

看一眼db()

public function db($scope = []): Query
{
    /** @var Query $query */
    $query = self::$db->connect($this->connection)
        ->name($this->name . $this->suffix)
        ->pk($this->pk);

原来db()函数这里也有一个变量拼接......不过殊途同归。我们用哪一个都差不多。例如exp中链子是把$this->suffix作为触发的对象的。

然后后面就是一路畅通了。这里跟5.1(注意不是5.0,5.0 的 toString用的是model类的)一样用的是Conversion类里的__toString() => toJson => toArray => getAttr => getValue()

我们主要在getAttr,getValue里构造

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);
}

public function getData(string $name = null)//$name='wh1t3p1g'
{
    if (is_null($name)) {
        return $this->data;
    }
    $fieldName = $this->getRealFieldName($name);
    if (array_key_exists($fieldName, $this->data)) {//$this->data = array("wh1t3p1g"=>"whoami");
        return $this->data[$fieldName];//返回'whoami',回到getAttr
    } elseif (array_key_exists($fieldName, $this->relation)) {
        return $this->relation[$fieldName];
    }

protected function getValue(string $name, $value, bool $relation = false)
{                 //$name='wh1t3p1g' $value=‘ls’ $relation=false
    // 检测属性获取器
    $fieldName = $this->getRealFieldName($name);  //该函数默认返回$name='wh1t3p1g'=$fieldName 
    $method    = 'get' . App::parseName($name, 1) . 'Attr';  //拼接字符:getlinAttr

    if (isset($this->withAttr[$fieldName])) { //['wh1t3p1g'=>'system']
        if ($relation) { //$relation=false
            $value = $this->getRelationValue($name);
        }

        $closure = $this->withAttr[$fieldName]; //$closure='system'
        $value   = $closure($value, $this->data);//system('whoami',$this->data
    }
    .......
    return $value;
}

至此完成整条利用链。注意它的调用方法是system("whoami", ["wh1t3p1g"=>"whoami"]).这是一种合法调用。

小结下。tp所有系列反序列化链就这么多了。大体上思路都是一样的。只有5.0版本是较为复杂的写文件。其他版本都可以直接rce.魔术方法也基本都是destruct,toString,call调用。其中5.2,6.0是没有call的必要的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271