第三届阿里云CTF官方Writeup
阿里云CTF CTF 7499浏览 · 2025-02-24 14:04

Pwn

beebee

In this challenge, a vulnerable function has been added to the kernel:

The arg3_type of bpf_aliyunctf_xor_proto has been wrongly set with MEM_RDONLY, so we can abuse it and gain ability to modify read-only maps.

And in check_mem_access():

Now we found a way to create a register, whose actual value differs from the verifier tracks. With KASLR disabled, we adapt bpf_skb_load_bytes() to corrupt the stack and get the flag.

Full Exploit

runes

Overview

In this challenge, we gain the ability to execute syscalls and retrieve their results.

However, to overcome the "dark dragon" and survive, we must disrupt its magic.

Once we do, victory comes easily, and we gain the ability to execute any syscalls without argument limitations.

Solution

One possible solution is as follows:

Call prctl(PR_SET_MDWE, PR_MDWE_REFUSE_EXEC_GAIN, 0, 0, 0) to disrupt the mmap call. FYI:PR_SET_MDWE - Linux manual page

Call shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600) to get a shmid

Call shmat(shmid, NULL, 0) to create a writeable mapping

Call read(0, shmem_addr, 9) to read /bin/bash into memory

Call execve(shmem_addr, 0, 0) to getshell

It's straightforward, and you don't need to write a script to solve it—nc is all you need.

Example

One possible example input(shmem_addr is the leaked value when excute shmat syscall):

broken_compiler

题目给出了一个C语言子集的编译器,将用户的程序编译为MIPS汇编,使用SPIM运行。要读取flag,考虑先利用编译器的漏洞,在MIPS环境下实现任意内存地址写入,再劫持返回地址到shellcode区域,使用shellcode进行open-read-write,输出flag。

shellcode的编写

SPIM提供了少量syscall,支持基本的标准输入输出功能,以及文件的打开、读取、写入。SPIM的CPU/syscall.cpp中包括所有syscall的实现。解题需要用到openreadwrite。SPIM syscall的调用号在$v0寄存器,返回值也在$v0寄存器,参数依次存储在$a0$a1$a2openreadwrite的调用号为13、14、15。由此编写shellcode:

使用spim -dump exp.mips编译MIPS汇编,生成text.asm文件,即为shellcode的机器码。

实现任意地址写入和返回地址劫持

题目存在两个漏洞,一处位于对struct类型返回值的处理,一处位于struct类型的struct字段处理,使用任意一个漏洞均可实现任意地址写入,这里重点描述第一种。

题目漏洞的根源在于编译器使用的调用约定没有正确处理struct类型的返回值。调用约定使用单个寄存器$2传递返回值。当返回值类型为struct时,直接将对应变量的地址存储到寄存器中。若返回值变量为局部变量,则存在stack use-after-scope的问题。

现在考虑如何通过stack use-after-scope实现任意地址写入和返回地址劫持。编译器的struct实现是:先在栈上分配一个指向struct的指针变量,再在栈上分配空间,并将$sp赋值给指针变量。如果使用stack use-after-scope改写指针变量,就可以构造任意地址写入原语。

随后只需要实现返回地址劫持,就可以任意执行MIPS代码。程序使用的调用约定是,参数压栈,被调用者保存frame pointer,调用者保护所有其他寄存器。调用函数时,返回地址不直接压栈,而是存放到$ra中。当函数为非叶子函数时(即调用了其他函数),会在栈上保存$ra,并在返回时恢复$ra。因此,只需要在非叶子函数中改写栈上保存的$ra即可实现返回地址劫持。

还需要注意一点,编译器会将最近使用的变量存储到寄存器中,当通过stack use-after-scope劫持栈上的指针变量后,还需要强制刷新寄存器内容,才能向新的地址写入内容。编译器使用的是朴素寄存器分配算法,当进行函数调用时,会回写所有带有dirty标记的寄存器,并无效化所有寄存器的内容,因此只需要在合适的地方调用一个空函数barrier(),就可以强制编译器重新从栈上加载指针。具体barrier()插入时机见注释。

使用任意地址写入在text区布局shellcode并跳转到shellcode即可得到flag。

解题程序(可使用附件中sendexp.py发送到服务器):

第二处漏洞是没有禁止incomplete struct类型的struct字段,声明incomplete struct类型字段时,大小分配错误,导致struct访问时的越界读写。篇幅原因这里不给出exp,以下为PoC:

笔者注:编译器为struct分配栈内存的实现也有问题,循环内定义的struct会在循环体每次执行时重新分配内存,且没有尝试访问guard page,存在stack clash漏洞。可以通过stack clash攻击覆盖内存中的代码。但实施stack clash攻击需要执行的指令数较多,可能会触发沙箱3s的cpu时间限制,不是预期解法。

broken_simulator

本题目是一个real-world综合题。使用SPIM运行用户给出的MIPS汇编,要求用户首先利用SPIM的漏洞,逃逸SPIM模拟器,实现任意shellcode执行。再进行沙箱逃逸,读取根目录的/flag

0x01 泄漏SPIM基址

SPIM提供了少量syscall,支持基本的标准输入输出功能,以及文件的打开、读取、写入。SPIM的CPU/syscall.cpp中包括所有syscall的实现。其中,openreadwrite的调用号为13、14、15。SPIM syscall的调用号在$v0寄存器,返回值也在$v0寄存器,参数依次存储在$a0$a1$a2 沙箱环境挂载了/proc,考虑通过读取/proc/self/maps泄漏程序基址。SPIM是64位程序,模拟的是32位MIPS cpu,因此需要使用两个寄存器储存泄漏的基址。

笔者注:这里的shellcode使用32位加法模拟64位加法,没有考虑32位边界的进位,因此在利用时不会100%成功,但实际成功率很高。

0x02 利用SPIM类型混淆,实现任意地址读

SPIM的read syscall实现存在类型混淆漏洞。mem_reference函数将MIPS cpu的地址转换为模拟器地址空间的地址。read syscall可以向text和data段写入文件中的内容。而text_seg的定义是instruction **text_seg;。因此通过read syscall可以构造虚假的instruction指针。

instruction结构体保存了指令对应的32位编码(MIPS是定长指令集),可以利用其encoding字段实现内存读。若需要读取addr处的内容,可以在addr-8处伪造一个instruction,并读取指令内存的内容即可。

内存读取逻辑如下:

在使用read syscall向text_seg写入伪造指针之前,需要找到一个可以写入内容的文件作为辅助。这里选取/proc/self/fd/0,这是MIPS的输入文件(一个memfd)。编写fake_inst函数,实现指令的伪造:

笔者注:此处也可以从fd 1中读取内容,fd 1是与用户交互的socket。预期解采用不需要用户交互的方案。

0x03 利用SPIM类型混淆,实现任意地址free,从而实现任意地址写

向指令内存写入内容时,如果原有内存已经有instruction了,会free掉原有指令,再创建新的指令。使用read syscall伪造一个指令指针,再向对应的指令内存写入内容,就可以实现任意地址free。

此时需要利用任意地址读和任意地址free实现任意地址写。k_data_seg的虚拟地址为0x90000000,指向的堆块内容可控,MIPS的.bss中存储mem_word *k_data_seg;。考虑使用unsafe unlink攻击,将k_data_seg改为&k_data_seg - 24,再修改k_data_topk_data_seg,实现任意地址写。 k_data_seg对应的堆块上构造三个chunk:0x480(free),0x480(used),0x20,在第一个chunk上伪造虚假的双向链表节点,再free第二个chunk,就可以实现unsafe unlink攻击。在任意free之前,要先利用任意地址读,读取k_data_seg的内容,得到第二个chunk的堆地址。

0x04 实现任意shellcode执行

SPIM的got表可写,使用got劫持,将open@got劫持为mprotect的地址,再用open syscall,将ELF_BASE开始的一页权限改为rwx。这样就可以布局shellcode。布局完shellcode后使用got劫持,将open@got劫持为ELF_BASE,再调用open syscall就调转到shellcode。

笔者注:shellcode执行流程复杂的原因是SPIM没有提供lseek syscall。如果提供了lseek,解题过程可以大幅度简化。具体解法请读者思考。

0x05 实现沙箱逃逸

沙箱隔离了pid、mount namespace,并在chroot后降权。每次执行都选取了不同的根目录(box0~box3),考虑使用unix socket向另一个沙箱发送根目录的fd,另一个沙箱接收fd并读取对应fd的父目录内容,就可以绕过chroot的限制。能绕过chroot限制的根本原因是沙箱没有重新挂载根目录,接收fd的父目录永远不会和当前的chroot相等,所以能一直穿越到真实根目录。 分别启动两个沙箱,一个发送fd,一个接收fd并读取flag。

0x06 解题EXP

Alimem

本题实现了一个简单的页内存管理模块,功能支持增删查改,并实现了mmap回调。

页管理结构体如下:

申请逻辑如下:

而在refcnt减小到0时,会通过rcu调用free_page_rcu,释放申请的页

从而可以发现,页的生命周期与结构体维护的引用计数强绑定。

refcnt++除alloc初始化外,只在mmap时触发

而操作refcnt--的操作分别调用于vma销毁时的alimem_vma_close与ioctl中ALIMEM_FREE分支。

可以发现,在单进程环境下,引用计数是平衡的。

但注意到,mmap的操作是先拿到page的reference,再进行引用计数++,并且free时并未使用rcu锁,存在race window

从而可能出现以下竞争情况:

从而造成映射至用户态的page已被释放,puaf。

漏洞利用

puaf后利用较为简单,喷射pipe_buffer篡改其flags,转化为任意文件写原语,提权即可

trust_storage

参考文章

https://blog.csdn.net/yangguoyu8023/article/details/121281700

简单来说,ATF的BL31是运行在EL3级别的runtime service,作为非安全世界(REE)与安全世界(TEE)转换的monitor,其也是存在于安全世界的。

逆向分析

题目给出的只有一个flash.bin,而我们的目标是BL31,所以要进行一定的解包与逆向分析

FIP.bin分离

TF中常用FIP.bin将BL2,BL31,BL32,BL33打包在一起

fip.bin的标志如下

在010中进行查找然后使用dd可以对其进行分割

dd if=flash.bin of=fip.bin skip=4 bs=0x10000

然后使用atf中的fiptool进行fip.bin的分割

./fiptool unpack ./fip.bin

解出来的soc-fw.bin就是BL31.bin

BL31逆向

偏移恢复

对照ATF源代码找到 sub_E0A1CA0

bl31_setup()->bl31_plat_arch_setup()

可以确定基地址为0xE0A0000

handler查找

对照ATF源代码找到 sub_E0A5874

bl31_main()->runtime_svc_init()

使用结构体恢复handler数组

得到STORAGE_SVC与LOG_SVC两个自定义接口

image.png
图片加载失败


handler分析

直接分析比较困难,因为没有建立bss段,需要补个bss

STORAGE_SVC

存在两个命令字,分别是往bss中存与从bss中取。

image.png
图片加载失败




检查了size,idx,并限制了ptr不能是安全世界的内存。

如果出错的话会退出,并调用sub_E0A5F1C保存出错信息

LOG_SVC

分析sub_E0A5F1C,发现其是一个自己实现的log函数,ptr需要指向结构体下方的区域。

否则就会对结构体进行重置

对LOG_SVC进行逆向,可知其有两个处理函数,分别是

2 -> sub_E0A5838

3 -> sub_E0A263C

逆向后可知

sub_E0A5838可以对pLog进行设置,仅判断其不能为安全内存。

sub_E0A263C可以将log向外copy

漏洞

LOG_SVC中的结构体存在于非安全世界,REE可以对其进行控制,虽然写入log时有判断,但其重置后仍可以被竞争篡改,可以实现一次任意地址写log。

漏洞利用

交互

1与EL3交互需要执行smc命令,该命令需要在EL1执行,自行编译驱动插入即可。

2STORAGE_SVC交互的命令字直接逆向可以得到

3 LOG_SVC交互的命令字根据rt_svc_desc结构体的定义,结合atf源码,使用OEN,TYPE,NUM组合可以得到

思路

1使用任意地址写log,将log内容写入storage_size 0xE0D70B0

2此时可以溢出storage,观察后发现log的handler结构体在storage下方,通过溢出可以实现任意函数调用,将其改为memcpy可以转换为任意读写原语

3目标需要实现任意shellcode执行,通过调试或unicorn模拟执行很容易找到aarch64页表的位置,提前copy shellcode后使用任意读写修改页表为rx即可执行

4由于无符号,不好找到直接的串口输出函数,可以开多个线程,一个线程不停打印内存,另一个线程触发漏洞向内存中写入flag

Crypto

OhMyDH

题目中的 action 是 CSIDH 群组行为在四元代数下的实现,曲线同源的安全性在于从曲线计算出其对应自同态环是困难的。

已知
非法 TeX 公式
,计算
非法 TeX 公式


由于在四元代数下已知两 order,计算 connect ideal 是容易的

一个简单的想法,对于
非法 TeX 公式
,理想
非法 TeX 公式
,满足对于
非法 TeX 公式


非法 TeX 公式
分别为
非法 TeX 公式
的右理想和左理想,计算得到
非法 TeX 公式
后不能直接使用,因为
非法 TeX 公式
下的元素是非交换的,但是
非法 TeX 公式
实际上可以看作
非法 TeX 公式
的扩张,因此可以在
非法 TeX 公式
下找到
非法 TeX 公式
的嵌入
非法 TeX 公式
,这个操作在实现的时候可以通过计算
非法 TeX 公式
对应的 Lattice 所张成的空间上在
非法 TeX 公式
处系数为 0 的向量

在得到嵌入
非法 TeX 公式
后由于
非法 TeX 公式
可交换的性质,类似 Diffie-Hellman 的操作即可还原出目标 order

Reference

[1] Rational isogenies from irrational endomorphisms

LinearCasino

McEliece 框架下
非法 TeX 公式
在特定参数下的可区分性

即区分
非法 TeX 公式
非法 TeX 公式
为随机
非法 TeX 公式
矩阵或
非法 TeX 公式


对于
非法 TeX 公式


非法 TeX 公式


论文中提到了一种区分方式,即
非法 TeX 公式
以大概率成立

还需要考虑的问题是这个性质在
非法 TeX 公式
下的情况

由于矩阵乘法可以看出对某个线性空间的同态映射,在
非法 TeX 公式
满秩的情况下可以视为行向量空间的同构映射,非满秩的情况下视为同态映射,仍然保留上述的相等关系,而同样右乘的列变换矩阵也不影响论文中证明的结果

实际上这里的区分还有一种更加简单的方式,注意到对于置换矩阵
非法 TeX 公式
,有
非法 TeX 公式


故对于
非法 TeX 公式
非法 TeX 公式


考虑
非法 TeX 公式
的形式,
非法 TeX 公式


注意到
非法 TeX 公式
非法 TeX 公式
的矩阵,秩最多为 50,故
非法 TeX 公式
,从而可以透过
非法 TeX 公式
观察这一点

Reference

[1] The problem with the SURF scheme

PRFCasino

利用 ARX 和 Feistel 结构构造的 PRF,题目需要区分 PRF 输出与随机输出。

需要观察到 T+T<<<20 结构的特殊性

非法 TeX 公式


非法 TeX 公式


非法 TeX 公式


非法 TeX 公式
,其中
非法 TeX 公式


非法 TeX 公式


下面考虑这个性质的扩散程度,注意到对于一轮
非法 TeX 公式
为 0,15,16 的概率分别为
非法 TeX 公式


考虑经过 15 次叠加后的
非法 TeX 公式
在模 17 上的分布状态,可以视为多项式卷积

由于 2 与 11 在分布上存在较大差异,利用这一点进行区分

Misc

softHash

本题是一个发散性的娱乐题目,主要是给选手们设计了一个本质上是优化问题的神经网络哈希碰撞,本意是为了让选手熟悉和了解GCG算法,但我们都知道优化问题的解法是不拘一格的,因此在比赛过程中会出现非常多的其他做法,希望大家都能从本题目中学到一些知识。

题目衍生自笔者在N1CTF 2021所出的collision。要求是选取了embedding 1024位中的128位按符号变换组成了01串(这128位是笔者挑选过比较容易优化出满足题解的答案的),然后拼接之后形成一个hex串,作为最后的哈希,选手需要构造填充,使得给出的字符串do you know how to get the flag?经过填充之后的hash只有少于等于6bit与give me the flag right now!的哈希结果不同,并且还要满足一些额外的要求,例如需要提交6个不同的样本,样本中不能出现某些字符串,并且有两个stage,一个包含special token,另一个则不包含。主要考察选手GCG所使用思想的相关实现,GCG是NLP领域对抗中的基础算法,但可能需要读过文章才能知道预期解怎么做的。具体可以研究GCG的原文的思想,只需要将loss设计成合理的即可。

主要思想简单来说就是初始化adversarial prefix,设计loss,根据梯度来选择topk个使得loss往下降方向走的candidates,然后用这些topk的优秀token进行prefix中的替换,为了去掉误差,再forward一下来算一下准确的loss,取最小的loss及其对应的token,并相应地替换prefix中的token,一步步地进行迭代,具体的实现可以看exp,exp需要多跑几次,因为具有一定的随机性,可能陷入局部最优,笔者并没有做出更多的优化。此时也有一些小的技巧,比如GCG中prefix的选取也是很有讲究的,对优化难度具有很大的影响,因此为了贴近target,笔者这里直接把prefix初始化为几个target。此时的loss设计比较简单,使用了修改过的Dice loss和l2上的loss,并且分配一定的weight来进行平衡,当然loss的构造不唯一,笔者的构造肯定不是最好的方案。

本题的两个场景对应的是tokenizer encode的时候是否加了special token的场景,两种场景在GCG替换的时候idx对齐上会有微小的差异。(如果对不齐的话会导致GCG在优化的时候prefix或suffix越来越长,这也是笔者在早期自己实现GCG的时候经常遇到的bug之一)

本题的难度已经下降了,因为可以预期地存在一些非预期解,例如使用暴力的不依赖梯度的greedy search等传统基于搜索的优化算法,也有可能能达到diff=6的情况,而exp中的可能可以达到diff<=5的情况,笔者在测试的时候最多的时候达到了diff=4。而且最后需要提交6个样本,6个样本可以很容易地从1个样本中衍生出来,例如直接修改无关紧要的标点符号之类的方法就可以达到,因为128bit的hash还是挺少的,相比于1024的embedding是很少的,笔者为了降低题目难度并没有做进一步的要求例如要求组间cossim<x。

最后,其实GCG对梯度的利用率有非常巨大的提升空间,传统的GCG本质上与传统的搜索算法的速度并无特别大的差异,这也是为什么没有把难度上成优化为一个传统意义上的collision的原因,因为笔者也做不到100%的hash collision:( 只是GCG可以通过调整loss适应更多种的任务,也就比每次都针对某任务写一个search算法要便捷许多了。

详情请参见exp中的代码实现。

exp_with_special.py

exp_without_special.py

exp_final.py

Gacha Game

Intro

本题实现了一个简单的抽卡与地牢探险游戏。玩家可以使用 SOL 代币抽取角色,通过合并相同角色来提升其属性,最终用升级后的角色挑战拥有 5 个 boss 的地牢以获得 flag。题目中预置了两个漏洞:

漏洞一:允许玩家在不消耗资源的情况下对角色进行升级;

漏洞二:导致地牢生成失败,从而在满足角色等级要求后直接获得 flag。

这道题的趣味性在于这两个漏洞均难以从合约层面直接发现。第一个漏洞需要选手理解 Anchor 框架宏展开后的代码细节,而第二个漏洞则要求选手熟悉 Solana runtime 中 system program 的实现细节。

Vulnerability 1: Duplicate mutable accounts

由于玩家初始仅有 8 SOL,每次抽卡需要消耗 1 SOL,而获得 flag 的条件要求选择的三个进入地牢的角色总等级至少达到 10 级,因此显然必须找到一种既不额外消耗 SOL 又能提升角色等级的方法。

通过简单阅读抽卡的 gacha instruction 的实现,可以看出每次抽卡均会固定通过 transfer 支付 1 SOL,因此无法实现免费抽卡。于是我们只能寻找不消耗资源即可提升角色等级的途径。

通过审计该合并角色以提升等级的 merge instruction,我们注意到对合并角色合法性的检查主要依赖传入的 character1character2 两个参数,并没有确保这两个参数所对应的账户与实际传入的 accounts 一致。这意味着,攻击者可以将 character1character2 都传入同一个账户。由于合约采用 Anchor 框架编写,Anchor 会自动处理账户的反序列化和序列化。当传入同一账户时,虽然程序中分别反序列化得到的 character1_accountcharacter2_account 都能正确更新,但在执行结束时 Anchor 会对同一账户先后写入两次数据。这样一来,恶意用户在获得一个 2 级角色后,就可以利用该漏洞实现无限升级而不消耗任何资源。

Vulnerability 2: Improper account initialization

获得 flag 的条件是所有 5 个 boss 账户均为空,只有在通过 boss_fight instruction 击败 boss 后,相关 boss 账户才会被关闭,从而满足检查条件;但题目设定的 boss 拥有极高的攻击力与防御力,即使玩家使用三个满级角色进入地牢也无法击败最后一个 boss。因此,我们只能考虑在不击败 boss 的情况下获得 flag。

由于玩家可以在 admin 创建地牢之前发起操作,我们可以利用 generate_dungeon instruction 中的漏洞对其进行拒绝服务攻击,从而阻止 boss 账户的正常创建,直接达到获得 flag 的条件。

审计 generate_dungeon instruction 的实现时可知,boss 账户是通过 PDA 生成,并且使用 system program 的 create_account 进行创建。值得注意的是,system program 的 create_account 在创建账户之前会检查目标地址的余额是否为 0,如果不为 0,则会认为该地址已被占用,从而拒绝创建账户(详见:https://github.com/solana-labs/solana/blob/7700cb3128c1f19820de67b81aa45d18f73d2ac0/programs/system/src/system_processor.rs#L157-L168

因此,我们可以提前计算出 boss 的地址,并向其转入一定数量的 lamports,导致 admin 无法正常创建 boss 账户,从而直接获得 flag。

完整 Exploits

攻击合约

攻击框架

mba

本题实现了一个基于MBA-Blast算法的简易的MBA简化器。具体的逻辑被作为一个Tactic实现在了Z3Prover的内部,并通过Z3 Python API调用。选手需要构造能够使简化过程出现错误的符合要求的表达式,提交并获得FLAG。

详细解析

通过阅读mba_tactic.cpp可以发现,MBA中每一项的系数是通过coeff_type存放的(即long long类型)。然而在construct_simplified_mba函数中计算basis_comb时使用的类型却是int。由于在server.py中,使用的所有BV均属于64-bit BV sort,会导致简化过程存在整数溢出问题。 由于给定的Lark Parser的规则不允许多于8位的整数,要触发整数溢出问题必须通过多个term计算触发。同时注意到题目要求给定的MBA必须拥有15个及以下的term数,所以必须选用basis vector中包含2或-2的bool function。能够满足要求的一个表达式为:

哈基游

http://127.0.0.1:8888/?c=int$c&algo=crc32&file=/flag&h=1

可以在错误信息中泄露hash_file的结果。

通过阅读php官方文档可以得知,`hash_file`函数支持多种哈希算法,而其中包含以下几种算法:

众所周知CRC并不是一个安全的哈希算法。在给定足够多的不同CRC校验码的情况下,可以恢复出校验前的内容。

观察到flag中未知字节一共15个,并且有3组不同的CRC,需要进行`2^(3*8)`次的爆破,从中恢复flag。

poc:

https://gist.github.com/marche147/abc48861e1d75cb15553c80bd5a915a8

Web

Rust Action

题目模仿 GitHub Action 的功能编写了一个简化版的 Rust Action

主要路由如下

通过编写适当的 workflow 可以构建 Rust 项目并下载 binary (artifact)

根据 model.rs 内的各种结构体, 不难得出 workflow.yaml 的格式如下

Job 目录结构示例

程序配置文件 config.toml

题目的整体思路是利用 Rust 的过程宏在编译期间执行代码

route::upload_job 函数内, 直接使用了 format 宏格式化 Cargo.toml 的内容

Cargo.toml.tpl

format 宏并不会对字符串进行转义, 因此这里存在配置文件注入的问题, 我们可以在 workflow.yaml 内构造特定 payload 向 Cargo.toml 内添加其它参数

如上的 workflow 利用了 description 字段向 Cargo.toml 添加了与过程宏相关的配置, 允许我们在项目中定义和使用过程宏

但接下来存在一个问题: 配置文件中的 workflow.security.files 字段仅允许 Job 在运行时获取 main.rs 这一个文件

而过程宏的定义和使用必须分开成两个文件, 例如在 lib.rs 内定义, 在 main.rs 内使用, 不能仅在 main.rs 一个文件内既定义又使用, 这会导致编译不通过

并且 /jobs/upload 路由在解压 Job zip 之后会调用 validate_job 函数验证 Job 目录结构是否符合如下条件

1仅包含 workflow.yaml 文件和 files 目录

2files 目录下仅允许存在 main.rs 文件, 且不允许存在子目录或软链接

这导致无法在 zip 包内添加 lib.rs, Cargo.toml 或者其它任何文件

解决办法是上传两个不同的 Job, 然后利用 Cargo.toml 的 lib.path 字段跨目录引用另一个 Job 内的 main.rs 作为 library

因为 lib.path 并不会对路径进行验证, 允许我们通过 ../../../path/to/main.rs 的方式进行目录穿越

(另外 lib.path 对文件后缀也没有验证, 因此也可以使用形如 ../../../path/to/image.jpg 格式的路径)

我们可构造两个 Job: A 和 B

Job A 的 workflow.yaml 和 main.rs

在上传 Job A 之后拿到 Job ID, 替换到 Job B 的 lib.path

Job B 的 workflow.yaml 和 main.rs

之后运行 Job B 即可实现 RCE

不过因为 config.toml 内关闭了 artifacts 功能, 这意味着我们只能执行 Job, 但是不能下载构建好的 artifact, 也就无法拿到执行命令的回显

同时题目环境不出网, 不能直接反弹 shell, 因此需要找到其它方法带出 flag 的内容

注意到 /jobs/{id}/run 路由会在 Job 运行完毕后判断 status, 如果不为 success 则会返回 exit code

同时结合题目所用的 Docker 镜像, 发现 cargo 命令和其所在目录的权限都为 777

图片加载失败


因此可以考虑覆盖 cargo 命令, 然后依次将 flag 的每一个字符转换为 ASCII 码作为 exit code 返回

shell 脚本如下

执行如下命令替换 cargo

在 Job 运行完成之后, 后续再次运行任何 Job 时就会调用我们自己的 cargo 命令, 然后会依次将 flag 的每一位转换成 ASCII 码作为 exit code 输出, 这样多运行几次 Job 就能拿到 flag 了

最终 exploit 如下

Jtools

发现题目只有一个路由存在fury反序列化

图片加载失败
对比官方的fury黑名单是多了一些内容的

图片加载失败
通过审计发现com.feilong.core.util.comparator.PropertyComparator的compare方法可以触发getter调用,然后利用动态代理触发MapProxy的invoke,到达BeanConverter的jdk二次反序列化点绕过黑名单

图片加载失败
图片加载失败
这里的jdk反序列化直接利用

poc

题目不出网,将flag写入/tmp/desc.txt回显

Espresso Coffee

题目要求选手基于 GraalVM Espresso JDK Continuation API 的功能特性挖掘出一条 Only JDK Gadget

整个程序的代码非常简单, 仅使用 JDK 自带的 HttpServer 启动了一个 Web 服务器以接收反序列化请求

Jar 包内不包含其它任何第三方依赖 (pom.xml 中的 continuations 依赖实际上在运行时由 Espresso JDK 自动提供)

唯一不同的点在于这道题使用了 GraalVM Espresso JDK, 下载地址如下

GraalVM Espresso 简单来说就是 GraalVM 对 JVM 规范的一种实现, 类似于众所周知的 HotSpot 虚拟机

https://www.graalvm.org/latest/reference-manual/espresso/

https://github.com/oracle/graal/blob/master/espresso/README.md

Espresso JDK 提供了 Continuation API, 用于保存和恢复程序运行时的调用栈

https://github.com/oracle/graal/blob/master/espresso/docs/continuations.md

https://github.com/oracle/graal/blob/master/espresso/docs/serialization.md

https://github.com/oracle/graal/blob/master/espresso/docs/generators.md

Continuation API 有两种使用方法: ContinuationEntryPoint 和 Generator, 分别对应 Low Level 和 High Level 两个不同层面

以 ContinuationEntryPoint 为例

输出

题目的出题思路来源于官方文档的一句话

Deserializing a continuation supplied by an attacker will allow complete takeover of the JVM. Only resume continuations you persisted yourself!

我们先重点关注 Continuation API 的源码实现部分

org.graalvm.continuations.Continuation 本身是一个接口, 它的实现位于 org.graalvm.continuations.ContinuationImpl

下面是 ContinuationImpl 类的 Javadoc, 描述了在 suspend 和 resume 的时候 JVM 调用栈的变化过程

suspend 时会调用到 org.graalvm.continuations.ContinuationImpl#trySuspend

图片加载失败


最终调用到 suspend0 native 方法, 主要是一些 check

https://github.com/oracle/graal/blob/47d997e14c7581136fe01e68dda0f606a7941fde/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/substitutions/Target_org_graalvm_continuations_ContinuationImpl.java#L49

之后需要将 Continuation 进行序列化, 因此会调用 org.graalvm.continuations.ContinuationImpl#writeObjectExternal

图片加载失败


其中的 ensureMaterialized 会调用到 materialize0 native 方法, 将当前调用栈保存至 stackFrameHead 链表

https://github.com/oracle/graal/blob/47d997e14c7581136fe01e68dda0f606a7941fde/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/substitutions/Target_org_graalvm_continuations_ContinuationImpl.java#L165

反序列化时调用 org.graalvm.continuations.ContinuationImpl#readObjectExternalImpl, 主要就是恢复相关字段

图片加载失败


后续 resume 时会调用 org.graalvm.continuations.ContinuationImpl#resume

图片加载失败


其中的 ensureDematerialized 会调用 dematerialize0 native 方法, 依照 stackFrameHead 链表的内容恢复 JVM 调用栈

https://github.com/oracle/graal/blob/47d997e14c7581136fe01e68dda0f606a7941fde/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/substitutions/Target_org_graalvm_continuations_ContinuationImpl.java#L182

重点关注 stackFrameHead 字段, 其本质就是一个单向链表, 由 org.graalvm.continuations.ContinuationImpl.FrameRecord 结构实现

图片加载失败


图片加载失败


FrameRecord 表示一个栈帧 (Stack Frame), 记录了如下信息

next: 指向下一个 FrameRecord (单向链表结构)

primitives & pointers: 基本类型和引用类型的 Stack 和局部变量表 (Local Variables Table, 每个存储单元被称为 slot)

method: 当前调用的方法

bci: bytecode index, 当前执行的 opcode 在字节码中的偏移 (功能上类似于 EIP/RIP 寄存器)

我们可以在 resume 前调用 toDebugString 方法来观察 stackFrameHead 的内容

输出

Job.start 方法的 bci 为 9, 正好对应字节码中的 invokevirtual, 即调用 suspend 方法的位置

(bci 偏移可以使用 IDEA 的 jclasslib 插件或 javap -c -s -p -l className 命令查看)

图片加载失败


也就是说 bci 指向的是当前已经执行完毕的 opcode, 即后续 resume 时会从 bci 后面的 opcode 开始执行

另外需要注意 bci 指向的 opcode 只能为 invokestatic/invokespecial/invokeinterface/invokevirtual 其中之一, 不能为 invokedynamic

https://github.com/oracle/graal/blob/dafeca6d4db8f45b20496e193f8e926db27423f7/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/vm/continuation/HostFrameRecord.java#L134

结合以上知识, 不难想到可能存在这么一种利用手法:

通过修改 stackFrameHead 链表的内容可以改变 resume 时恢复的 JVM 调用栈, 进而可以劫持控制流, 实现 "ROP"

重点在于修改 FrameRecord 结构的 next, method, pointers, primitives 和 bci 字段

仅修改 bci: 在当前 method 内跳转到特定的 opcode

修改 bci + method: 跳转到特定 method 的特定 opcode (gadget)

修改 next: 构造多个不同 method 之间的调用关系, 劫持控制流, 实现 "ROP"

修改 pointers 和 primitives: 可以修改局部变量表, 进而控制调用方法时传入的参数, 或是 return 的返回值

与 ROP 中的 gadget (汇编指令片段) 类似, 这里的 gadget 指的是 JVM 指令片段 (method + bci)

我们需要在 JDK 库中找到合适的能够实现 RCE 的 gadget (其实也就是找 method 和 bci), 其必须满足如下条件:

1 方法内部必须有多个方法调用, 即 invokestatic/invokespecial/invokeinterface/invokevirtual 指令 (不能为 invokedynamic), 因为 bci 仅能指向 invoke 系列指令, 同时会从该 invoke 指令的下一条指令开始继续执行

2方法必须为静态方法, 否则必须保证对象本身可以被序列化 (Serializable), 或者待执行的 JVM 指令片段 (bci 后面的一系列指令) 中没有对 this 指针的访问

理论上不止一个 gadget, 而且不止一种类型的 gadget (RCE/反序列化/读写文件/SSRF)

这里给出一个 RCE gadget: sun.print.UnixPrintJob.PrinterSpooler#run

方法内部存在对 Runtime.exec 的调用

图片加载失败


在 102 位置存在 Runtime.exec 调用, 因此可以将 bci 设置成 98 (invokevirtual 指令)

图片加载失败


到这里按理来说就可以成功 RCE 了, 但是实际上调试一会可以发现尽管我们能够控制局部变量表, 但仍然无法控制传入 Runtime.exec 的参数

这个不可控的参数来自于 printExecCmd 方法的返回值

sun.print.UnixPrintJob#printExecCmd

图片加载失败


那么可以换个思路曲线救国, 先设法控制 printExecCmd 方法的返回值, 然后在调用完毕后继续执行上面提到的 run 方法 (即构造两个相邻的栈帧), 这样就可以间接控制传入 Runtime.exec 的参数

根据上图的字节码可以发现在 367 的位置存在 invokevirtual 指令, 满足 bci 字段的条件

即设置 bci = 367, 再适当修改 primitives 和 pointers 以控制局部变量表, 使得方法内部执行 areturn 指令的时候返回我们特定的 execCmd, 后续将这个 execCmd 作为参数传入 Runtime.exec 实现 RCE

最终 payload 如下

OnlyJdkGadget

Job

ReflectUtil

SerializeUtil

stackFrameHead

需要注意题目中仅有 challenge.Web 这一个类, 因此在构造的过程中得先使用自己写的 Job 类 (继承 ContinuationEntryPoint), 然后在序列化前从 stackFrameHead 链表内删除带有 Job 的栈帧, 并将 Continuation 对象内带有 Job 信息的 entryPoint 字段设置为 null (即上面用 [1][2] 标记的行)

最后发送 payload 反弹 shell 拿到 flag

FakeJumpServer

这题主要是考察选手对堡垒机这类realworld场景的漏洞挖掘,思路对上了,做起来就非常简单。

题目入口是一个nginx,但是这里面啥都没有。根据题目名字Jump Server,可以联想到题目可能跟堡垒机相关,可以扫描22端口以及3389端口,因为大多数堡垒都是可以通过ssh/rdp端口来访问和管理服务器,很多厂商ssh/rdp都是自己写代码实现的,所以难免会出现漏洞。

扫描题目的端口,发现开放了22端口。

连接题目的22端口,看到ssh banner,猜测这个ssh server大概率是自己实现的。

既然是要输入账号密码,第一反应肯定是要测试sql注入,可以先通过sleep测试数据库类型,这里就不举例了,题目使用的是pgsql

这里密码长度限制是64,并没有严格的长度限制和字符过滤让选手去绕,直接堆叠注入命令执行即可

exp:

Offens1ve

题目机器提供一个公网IP,选手需要自行在本地配置 hosts:

题目开放两个应用:

首先访问https://oa.offensive.local:8443/,将自动跳转到ADFS联合身份验证页面:

图片加载失败


现在的攻击思路就是,需要绕过ADFS Portal,访问到oa系统,才能得到flag。这里就需要我们伪造AD FS 安全令牌(AD FS security tokens)。

伪造 AD FS security tokens 的前提是从 ADFS 的本地 Wid 数据库中提取出令牌签名证书,并从Active Directory 中拿到 DKM 解密密钥。

访问 https://monitor.offensive.local:8080/ 是一个网络监控系统,并显示了当前内网的拓扑图(假的,,,)

图片加载失败


这里设计的比较友好,点击“ADFS01”或者“ADFS02”节点,可以直接导出 ADFS 配置数据:

图片加载失败


图片加载失败


AdfsConfigurationV4_.IdentityServerPolicy_.ServiceSettings_.sql 中的EncryptedPFX blob可以找到加密的令牌签名证书:

将其保存到TKSKey.txt中。

然后点击“DC”节点,这里可以查询LDAP语句:

图片加载失败


通过(&(thumbnailphoto=*)(objectClass=contact)(!(cn=CryptoPolicy)))查询语句可以从LDAP中查询出 DKM Key:

图片加载失败


将逗号替换成空格:

然后保存到DKMKey.txt中。

现在我们得到了两个文件:

DKMKey.txt:将包含 DKM 密钥。

TKSKey.txt:将包含令牌签名密钥。

接下来,需要通过以下命令,将信息转换为工具可以使用的格式:

现在,我们拥有了伪造 ADFS 登录令牌所需的所有详细信息。此示例使用 ADFSpoof 工具为用户 “Finley_Blaze1” 创建 Golden SAML 令牌。

首先,将 ADFSpoof/templates/o365.xml 模版文件的内容修改成如下,并将其中的 XML 进行 Minify 操作:

执行如下命令,生成伪造的 SAML 令牌:

图片加载失败


现在只需使用伪造的 SAML 令牌以 Administrator 用户的身份登录 OA 发起联合身份验证。这可以通过使用 Burp Suite 的 Repeater 模块重放 Web 请求来实现:

图片加载失败


登录成功后,可以使用 “Show response in browser” 功能在浏览器中查看此请求的响应。一旦完成,我们将成功进入到 OA 系统:

图片加载失败


在“公司机密”处点击“查看更多”,即可得到flag:

图片加载失败


ezoj

打开题目的web页面后是一个OJ,OJ里面有五个题目,页面最下面提示了/source查看源码。

/source中可以发现,该OJ在执行python代码时,会使用audithook限制代码的行为。限制方法为白名单,只允许["import","time.sleep","builtins.input","builtins.input/result"]的事件执行。

先尝试获取python版本,发现OJ会将程序的退出码回显给用户,可以利用这个回显信息。

获取了sys.version_info的三个值后,可以得到python版本3.12.9

根据白名单的内容,允许导入模块,但是导入其他模块需要用到compile和exec,因此只能导入内部模块。

在内部模块中发现了_posixsubprocess,该模块能够fork_exec执行任意命令同时内部没有触发审计。

由于题目不出网而且也无法直接回显,因此需要把执行程序的标准输出读出来。在源码中可以发现c2pwrite参数会重定向到子进程的标准输出

因此使用下面的脚本,执行命令并将结果写入到退出码中。

由于os.read可能会将程序卡住,因此在os.read之前先sleep一下。最后在根目录找到flag文件,直接读取获得flag。

打卡OK

偷懒用了开源镜像导致root弱口令非预期十分抱歉

~泄漏,发现adminer_481.php,登陆后修改用户密码登陆 MD5 ("12345asdasdasdasdad") = 5d710c8773a7415726cd25b3ffebfa3e 5d710c8773a7415726cd25b3ffebfa3e:12345 //asdasdasdasdad

审计代码,利用\绕过date函数反序列化逃逸

然后pearcmd即可

Re

babygame

虽然后面仔细研究了一下没有符号应该也能做,但确实比较折磨,为了不浪费大家时间还是给了符号。

要找到这题的验证点,首先需要明白一下bevy的基础逻辑,通过向app注册诺干systems,并指定其运行的实际,在游戏开始时就会在对应的时刻调用对应的函数,比如:

fn main() { App::new().add_systems(Update, hello_world_system).run(); }

fn hello_world_system() { println!("hello world"); }

就会在Update的时候调用hello_world,而编译出来可以发现,这里会把hello_world_system包装成一个虚表,实际调用的是一个叫run_unsafe的方法。

回到这道题,通过寻找一些特征,能找到这道题的原型 https://github.com/PraxTube/tsumi.git

通过阅读源码可以大致理清整个处理流:

在src/aspect/combiner.rs中会处理用户选择的两个元素的合并,其中check_all_aspects_full是触发下一阶段的函数,原始逻辑为当垫子上的元素充满时会触发src/world/map/bed.rs中的逻辑

在函数spawn_bed中能看到当all_sockets_full为true时,会出现一张床。

用户选择床之后会产生事件PlayerWentToBed,最后会调用到spawn_ima_final_dialogue,这里会读取assets/dialogue/others.yarn,最后再显示完对话后触发<<game_over>>

game_over的触发是在src/ui/dialogue/runner.rs中的spawn_runner函数注册的。

当然,如果这个题没有找到源码,一个个通过调试分析system当然也能做。

整套流程大致如上,可能有一些遗漏的细节。所以这里我们也可以顺着源码的这套逻辑找修改的地方:

AspectCombinerPlugin的虚表在141034EF0处,可以找到其build方法,从这里可以找到select_combined_aspect的虚表在off_1410558C8 处,其run_unsafe方法在0140048612处,为什么我说可以不用符号也能找呢,可以发现在注册的时候其实是传递了alictf::aspect::combiner::select_combined_aspect这个字符串的,可以通过这个字符串找到注册的地方,再通过特定偏移找到run_unsafe。寻找其他地方的方法不再赘述。

可以找到大致修改过的流程如下:

is_socket_combination_possible(已经被内联)改成了输入20个数字之后直接不让选择

每次输入完触发的spawn_dialogue_runner中会收集输入的数字,并会和固定数字进行运算。

check_all_aspects_full会在输入完毕后进行一次xxtea

highlight_and_select_bed会在选择bed时进行一次xxtea

determine_ending时会进行一次xxtea和异或,并和最终的结果进行比较,来决定输出的是正确还是错误的结果。这里还有一个判断,一个是输入的时间不能太短或者太长,第二个是你需要真的输入一定的次数才行,要不然会直接判负。详细的算法可以参考:

flag-LS

题目实现了一个.flag文件的Language Server Extension,用户输入时Language Server会对当前编辑器内的内容分别使用base58 encode、前后翻转、凯撒密码进行处理,凯撒密码使用的偏移是从13开始,每次从`textDocument/completion`调用都会offset + 1。之后将处理后的结果及结果的md5第一字节进行拼接,然后通过`textDocument/completion`返回给编辑器。用户必须选择一个补全项,否则下次输入会随机选择一个算法将上次输入后编辑器的内容加密,然后通过`workspace/applyEdit`强制修改编辑器内容。Language Server会记录用户的所有输入,并通过 `textDocument/publishDiagnostics` 返回当前所有输入及 flag 的检查结果等信息。Language Server会记录用户的所有输入,并通过 `textDocument/publishDiagnostics` 返回当前所有输入及 flag 的检查结果等信息。

Language Server是一个exe文件,使用go 1.23编译,编译时对package paths等信息进行混淆,但保留了部分方法名,可以根据方法名快速找到对应的handler。Language Server只允许打开input.flag这一个文件,否则会直接exit,并且启动时会检查父进程是否为vscode("code.exe"),检查不通过也会exit,可以通过patch绕过这些检查。

my.flag是一个加密后的flag,解密即可得到flag。解密时由于凯撒密码最后使用的偏移量未知,所以需要爆破得到。解密时尝试三种算法分别解密,然后根据结果及结果的md5第一个字符来验证当前解密是否正确,最终逐字节解密合并得到flag。

解密脚本:

easy-cuda-rev

最近,受到 DeepSeek 直接使用 PTX 汇编编写优化部分 cuda 代码的启发,设计了一道简单的 cuda 逆向题目,让选手学习 PTX 汇编,遥遥领先!

选手需要了解一些 cuda 的基本编程模式(并行计算编程),例如 cuda 核函数、gird、block、threads 、block 同步。学习并行编程与传统编程模型的差异。

cuda 逆向需要的一些二进制工具,主要以 cuda 开发包提供的 binutils 为主。

逆向反汇编 easy_cuda 程序

根据官方指令手册以及自己编译的 CUDA 程序,对比在短时间内快速学习 PTX 汇编。同时,选手也可以借助 LLMs 辅助理解 PTX 汇编。

题目设计了一个简单的分组算法,分组长度为 256 字节,算法分为了 6 个加密过程。

为了降低题目的难度,题目输出了每个加密过程的中间结果,选手可以通过观察输入和输出分析算法。最后一个加密过程无法仅通过观察输入和输出进行总结,需要选手认真逆向 PTX 汇编。同时,选手也可以通过观察输入输出与 PTX 汇编的对比来进行学习。

题目中涉及的算法,涉及到较多次循环计算,建议用 cuda 实现编程实现解题脚本。

如下是最终实现的解题程序


1 条评论
某人
表情
可输入 255