• 我们在哪一颗星上见过 ,以至如此相互思念 ;我们在哪一颗星上相互思念过,以至如此相互深爱
  • 我们在哪一颗星上分别 ,以至如此相互辉映 ;我们在哪一颗星上入睡 ,以至如此唤醒黎明
  • 认识世界 克服困难 洞悉所有 贴近生活 寻找珍爱 感受彼此

2021KCTF 春季赛

CTF赛事 云涯 4年前 (2021-05-13) 2263次浏览

第二题

这是一到迷宫题目,从S处出发,走过0路径,走过的0置为1,并最后到达出口。

查看代码,发现有8种移动方法

解题思路如下:

  1. 找到满足将所有0置为1的路径,即最优路径。通过递归算法,即可找到最优路径。
  2. 找到最优路径后,根据题目算法还原switchcode。
  3. 还原switchcode后,根据题目算法还原位置,并还原flag。

根据以上思路,编些代码如下:

import random
import copy

def mark(maze,pos):
    maze[pos[0]][pos[1]] = 1  # mark代表走过,走过的就变成墙

def remark(maze,pos):
    maze[pos[0]][pos[1]] = 0  # 就把这一步恢复

def passable(maze,pos):
    status = maze[pos[0]][pos[1]] == 0
    return status


# 这个递归函数的功能就是 走这一步,看看是否8个方向可以走,不可以走就返回false,回退到上一步,继续看其他方向
def find_path(dirs,maze,pos,end):
    mark(maze,pos) # pos位置已经走过
    if pos == end:
        correct_path.append(pos) # 到达了出口
        return True

    # 判断6个方向是否可以走动
    for i in range(8):
        nextpos = pos[0]+dirs[i][0],pos[1]+dirs[i][1] # 下一步,从左开始顺时针
        if nextpos[0] < 0 or nextpos[1] < 0: # 不能走出界限
            continue
        if nextpos[0] > 8 or nextpos[1] > 9:
            continue


        if passable(maze,nextpos):  # 该方向是否可以走动

            find_path_status = find_path(dirs,maze,nextpos,end) # 这个方向可以走动,就走动到这个方向
            if find_path_status:
                correct_path.append(pos) # 这就说明下一层已经是出口路线
                return True
            else:
                pass # 说明这个方向的下一步是死路,看看其他方向的下一步是否能走

    remark(maze, pos)
    return False # 8个方向不可以走动

def search_correct_path():
    global correct_path
    correct_sign = False  # 正确标志位
    dirsset = []
    dirs = [(0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1)]
    originalmaze = [['S',0,1,0,0,1,0,0,1,1], \
            [1,1,0,0,1,0,0,1,0,0], \
            [0,0,1,0,1,1,1,1,1,0], \
            [0,1,1,0,1,0,0,1,0,0], \
            [0,0,1,0,0,1,0,0,1,1], \
            [1,1,0,1,1,1,0,1,0,1], \
            [0,0,1,1,1,1,0,1,0,1], \
            [0,1,1,0,0,1,0,1,0,1], \
            [0,0,0,1,0,0,1,1,0,0]]
    maze = originalmaze

    originalstart = (0, 0)
    originalend = (8, 9)
    status = True


    while status:
        for row in maze:
            if 0 not in row:
                correct_sign = True
            else:
                correct_sign = False
                break
        if correct_sign:
            calculationop(list(reversed(correct_path)))
            return

        else:
            correct_path = []
            maze = copy.deepcopy(originalmaze)
            while dirs in dirsset:
                random.shuffle(dirs)
            dirsset.append(copy.deepcopy(dirs))

            status = find_path(dirs,maze, originalstart, originalend)


def calculationop(right_path):
    op = []

    for i in range(46):
        raw = right_path[i][0]
        con = right_path[i][1]

        nexraw = right_path[i+1][0]
        nexcon = right_path[i+1][1]

        RAW = nexraw - raw
        CON = nexcon - con

        if CON==1 and RAW==0:
            op.append(1)
        elif raw%2==1 and CON==0 and RAW==1:
            op.append(2)
        elif raw%2==0 and CON==1 and RAW==1:
            op.append(2)
        elif raw%2==1 and CON==-1 and RAW==1:
            op.append(3)
        elif raw%2==0 and CON==0 and RAW==1:
            op.append(3)
        elif CON == -1 and RAW==0:
            op.append(4)
        elif raw%2==1 and CON ==-1 and RAW ==-1:
            op.append(5)
        elif raw%2==0 and CON==0 and RAW ==-1:
            op.append(5)
        elif raw%2==1 and CON==0 and RAW ==-1:
            op.append(0)
        elif raw%2==0 and CON==1 and RAW== -1:
            op.append(0)

    get_flag(op)
    return


def get_flag(op):
    flag = []
    strspace = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'

    def com(count, opfirst, opsecond):
        for i in range(36):
            first = 5 - (i + count) % 6
            second = (count + i // 6) % 6
            if first == opfirst and second == opsecond:
                flag.append(strspace[i])
                return

    for count in range(23):
        com(count, op[0], op[1])
        for i in range(2):
            op.pop(0)

    print(''.join(flag))
    return

if __name__ == '__main__':
    correct_path = []
    search_correct_path()

第三题 

这是一个若依管理系统登录界面,绕过登录界面,即可看到flag。
前后端分离系统,修改服务端返回数据包,满足前端验证条件即可进行路由跳转。

程序首先将用户名密码等信息POST到服务端,服务端进行验证,前端接收服务端返回数据并验证。

整体思路就是修改后端返回来的数据包。通过burpsuit截获返回数据包,进行修改,再传递给其前端,欺骗前端,进入首页。
第一层验证经分析发现其要求code=200以及token
因此构造数据包如下:

HTTP/1.1 200 OK
X-Powered-By: Express
vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
access-control-allow-origin: http://121.36.145.157
access-control-allow-credentials: true
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
content-type: application/json;charset=UTF-8
date: Thu, 13 May 2021 11:22:05 GMT
connection: close
Content-Length: 49
{"msg":"用户/密码正确","code":200,"token":"123465"}

第一层验证通过。
第二层验证经分析发现其要求roles,permissions,msg,code=200,user等信息。
因此构造数据包如下:

HTTP/1.1 200 OK
X-Powered-By: Express
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
content-type: application/json;charset=utf-8
content-length: 84
date: Thu, 13 May 2021 11:14:06 GMT
connection: close
{"roles":"[admin]","permissions":"all","msg":"成功获取用户信息","code":200,"user":{"avatar":"123456","useName":"admin"}}

第二层验证通过。
第三层验证经分析发现其要求code=200,data等信息。
因此构造数据包如下:

HTTP/1.1 200 OK
X-Powered-By: Express
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
content-type: application/json;charset=utf-8
content-length: 84
date: Thu, 13 May 2021 11:39:19 GMT
connection: close
{"msg":"成功获取动态路由","code":200,"data":[{"path""/","name""index","component""index"}]}

第三层验证通过。
成功进入到首页,获取flag

第三题官方思路解

该系统利用了springboot+mysql+redis搭建,通过阅读部署文档发现目标正好开启了redis的6379端口,并且可以未授权访问。

若依的用户信息是缓存在redis中的,并且通过JWT进行认证。

链接redis,发现了缓存了用户信息

找到若依演示网址http://vue.ruoyi.vip/login?redirect=%2Findex,登录一下,了解一下过程。发现在登陆成功后会返回一个token,这个toke就是jwt消息。把redis里面的token和名字都改成本地抓到的token

然后在登录页登录,修改返回的response包,就可以成功跳转到首页

HTTP/1.1 200
Server: nginx
Date: Sun, 16 May 2021 07:57:01 GMT
Content-Type: application/json;charset=UTF-8
Connection: close
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Content-Length: 228

{“msg”:”操作成功”,”code”:200,”token”:”eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJhNTAzOWQ4LTUwYjAtNDMxOC1hMGEzLTBlNjhiYzM2MmYwNiJ9.KBBbhPRF1_0jybTuLywexorgvGlM0wZvDEwpKuMLG6JzsC3TQBcY-eKFzpscJ74sNtGRJB4NZcIXWua0tKC7vQ”}

第四题

打开main函数,基本思路如下

看条件2(401240函数):

整体思路就是,输入的字符,在buffer_temp里寻找其位置position,并做运算存储到buffer里面。如果遇到数字(0-9),与已经判断过的字符个数相加,如果是9则修改一些值。

再看条件3(401000):

将条件2中获得的数字,补充到以下0的位置。

补充后检查行数字是否存在相同的,再检查列是否相同,这就是数独。通过查看缓存区,发现是9*9数独。

而且条件3中需要填写的字符长度是54,但是调用401000函数传入的参数是长度-9,通过条件三可以确定54个数独字符,但是还缺9个字符无法确定。

条件2中如果传入数字,则会进行处理。如果是9*9数独,则每一行添加一个数字,就可以补充到缺失的9个字符。

if ( i + input_one_byte == ‘9’ )
{
i = 0;
v5 += 9;
goto LABEL_13;
}

继续看几个函数,发现4014e0和4015b0发现是对输入进行md5运算,然后401ed0对AES密钥初始化,然后在下文通过AES对代码进行解密,然后则执行这段解密后的代码。md5可以确定这九个数字是唯一的。

再研究一下条件2的代码,数独每一行有9个字符,以第一行为例,添加7个字符(此时i=7),那么输入是2则满足这个条件。输入其他1-9(排除2)的数字都是一样的效果。因此可以断定,这缺失的9个字符,就是代码每一行已填充的字符个数。

if ( i + input_one_byte == ‘9’ )
{
i = 0;
v5 += 9;
goto LABEL_13;
}

写出解密代码:

1. 先解出数独要添加的数字:

import re
import copy

# 默认模板-->在这里写准备求的数独
sudoku_template1 = [[0, 4, 0, 7, 0, 0, 0, 0, 0],
                    [9, 2, 0, 0, 0, 0, 6, 0, 7],
                    [8, 3, 0, 0, 0, 5, 4, 0, 0],
                    [0, 1, 0, 0, 0, 3, 0, 0, 0],
                    [0, 0, 0, 2, 0, 1, 0, 0, 0],
                    [0, 0, 0, 5, 0, 0, 0, 4, 0],
                    [0, 0, 4, 9, 0, 0, 0, 7, 1],
                    [3, 0, 5, 0, 0, 0, 0, 9, 4],
                    [0, 0, 0, 0, 0, 8, 0, 6, 0]]

# 芬兰数学家英卡拉(Arto Inkala)设计的号称“最难数独” - 1000次平均耗时320ms/次
sudoku_template2 = [[8, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 3, 6, 0, 0, 0, 0, 0],
                    [0, 7, 0, 0, 9, 0, 2, 0, 0],
                    [0, 5, 0, 0, 0, 7, 0, 0, 0],
                    [0, 0, 0, 0, 4, 5, 7, 0, 0],
                    [0, 0, 0, 1, 0, 0, 0, 3, 0],
                    [0, 0, 1, 0, 0, 0, 0, 6, 8],
                    [0, 0, 8, 5, 0, 0, 0, 1, 0],
                    [0, 9, 0, 0, 0, 0, 4, 0, 0]]


def crack_it(sudoku=sudoku_template1):
    '''主函数,输入数独进行运算,如未输入则调用默认数独,格式为9x9的二维列表'''
    init_sudoku = str_to_num(copy.deepcopy(sudoku))  # Python的坑!列表或字典等对象作为函数参数时,函数可能修改其元素的指针,导致外部列表也会改变
    if is_valid_sudoku(sudoku):  # 判断输入的Sudoku是否合理(是否冲突)
        candidate_list = filter_candidate_list(init_sudoku, init_candidate_list(init_sudoku),
                                               start=0)  # 针对Sudoku中的每一个空格(空格都默认填入数字0),都算出其可能的备选数,存入data_list中;每当空格被确认唯一值时,剩余data_list都需要再被刷新
        cracked_sudoku = fill_blank(init_sudoku, candidate_list, start=0)  # 破解数独
        print_sudoku(cracked_sudoku)  # 在控制台显示已破解的数独,默认开启
        return cracked_sudoku
    else:
        return '请检查一下输入是否有误- -0'


def str_to_num(data):
    '''初步校验+统一格式,空字符转0,无效字符转0'''
    for i in range(9):
        for j in range(9):
            if re.match('[1-9]', str(data[i][j])):  # 1-9字符转int 1-9
                data[i][j] = int(data[i][j])
            elif re.match('', str(data[i][j])):  # 空位转int 0
                data[i][j] = 0
            else:  # 无效字符转int 0,或者也可以return False,拒绝服务
                data[i][j] = 0
    return data


def is_valid_sudoku(data):
    '''判断整个数独是否有效'''
    for y in range(9):
        for x in range(9):
            if data[y][x] > 9:
                return False

            if data[y][x] != 0 and data[y].count(data[y][x]) > 1:
                return False

            for col in range(9):
                if data[y][x] != 0 and col != y:
                    if data[col][x] == data[y][x]:
                        return False

            for i in range(3):
                for j in range(3):
                    if data[y][x] != 0 and (i + 3 * (y // 3), j + 3 * (x // 3)) != (y, x):
                        if data[i + 3 * (y // 3)][j + 3 * (x // 3)] == data[y][x]:
                            return False
    return True


def init_candidate_list(data):
    '''初始化建立一个数独的备选数列表,一个空格就对应其坐标以及填上1~9的备选数字,格式为81x9的二维列表'''
    data_list = []
    for y in range(9):
        for x in range(9):
            if data[y][x] == 0:
                data_list.append([(x, y), [1, 2, 3, 4, 5, 6, 7, 8, 9]])
    return data_list


def filter_candidate_list(data, data_list, start):
    '''对数独的备选数表进行过滤,删除无效的备选数'''
    for blank_index in range(start, len(data_list)):
        data_list[blank_index][1] = []
        for num in range(1, 10):
            if is_valid_num(data, data_list[blank_index][0][0], data_list[blank_index][0][1], num):
                data_list[blank_index][1].append(num)
    return data_list


def is_valid_num(data, x, y, num):
    '''输入数独、坐标、数字,判断该位置填入该数字是否合理'''
    if data[y].count(num) > 0:  # 行判断
        return False

    for col in range(9):  # 列判断
        if data[col][x] == num:
            return False

    for a in range(3):  # 九宫格判断
        for b in range(3):
            if data[a + 3 * (y // 3)][b + 3 * (x // 3)] == num:
                return False
    return True


def fill_blank(data, data_list, start):
    '''
    核心函数,递归尝试代入备选数,类似深度优先遍历算法。
    一旦某位置填入为True(由is_valid_num函数判断),则开始下一位置的填入;若某位置填入为False,则return回上一级。
    参数解释:
    data: 数独矩阵,二维列表
    data_list: 备选数表,二维列表
    start: 递归进行的位置,对应data_list的下标
    '''
    all_data = []
    if start < len(data_list):
        one = data_list[start]
        for num in one[1]:
            if is_valid_num(data, one[0][0], one[0][1], num):
                data[one[0][1]][one[0][0]] = num  # 赋值,如果能给每一格成功赋值,则意味破解成功;如果出现失败,则需要将错误赋值清零
                # data_list = filter_candidate_list(data, data_list, start)   # 每一步赋值都会改变备选数表,但刷新备选数表的操作非常耗时,若加上这句,速度会慢100倍
                tem_data = fill_blank(data, data_list, start + 1)  # start+1,使递归进入下一格点
                if tem_data:  # 注意!如果下一格点return,分两种情况:1.成功破解所有格点;2.发生错误,for loop结束也会return,此时返回值为None
                    return tem_data
        data[one[0][1]][one[0][0]] = 0  # 注意!可能向下递归了若干格才发现前面是错误的(即for loop结束,return None),此时需要将所有错误的赋值清零。
    else:
        return data


def print_sudoku(data):
    '''打印数独到控制台'''
    print('>>> 破解结果:')
    # for i in range(9):
    #     for j in range(9):
    #         print('{:^3}'.format(data[i][j]), end='')
    #     print('')
    # print('')
    s = ""
    for i in range(9):
        for j in range(9):
            if sudoku_template1[i][j] == 0:
                s = s + str(data[i][j])
    print(s)  # 输出原始数独中是0的部分


if __name__ == '__main__':
    crack_it()

 

2. 解出来是

5619238183457621978469254539786692871328563617281793452

然后依据程序,解出来待输入得字符。

import re

sudoku_sequence = '5619238183457621978469254539786692871328563617281793452'
buffer = '3E4E74445D7D70596662753A5650422477274F6072482A23514A6D474D5E54612B6E402D7A5A6F7B796468213063696A5278555829575B653C672F5F7325263F282D4B766B45433D6C2E3B41524C5346'
buffer1 = "$BPV:ubfYp}]DtN>aT^MGmJQ#*Hr`O'wjic0!hdy{oZz-@n+?&%s_/g<e[W)XUxRFSLRA;.l=CEkvK-("
buffer_list = re.sub(r"(?<=\w)(?=(?:\w\w)+$)", " ", buffer).split(' ')
input_list = []

def find_str(sudokustr):
    for index,value in enumerate(buffer1):
        op = (index%9)+1
        if op == sudokustr:
            input_list.append(value)
            break

for i in sudoku_sequence:
    find_str(int(i))

print(''.join(input_list))

3. 解出来字符是:u$YBPf$fPV:buB$YbfVuYB:V:PYbfuuYBfb$PBf:uPu$bBf$bYPV:B

然后在每一行数独最后添加个数,这样flag就是::u$YBPf2pa]Dt4#QM^H4ic’j0`w2y{d-Zzo2%/n_s@+2<UW)e4AR;F.4=-qEkvC2

 

 

 

 

 

 

 

 

 

 

 


云涯历险记 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:2021KCTF 春季赛
喜欢 (0)