2025年Solar应急响应公益月赛-10月

生命在于学习 · 2025-10-31 · 6 人浏览
2025年Solar应急响应公益月赛-10月

ruoyi 解题步骤

ruoyiapi 遭到劫持,在一定条件下会触发跳转到外部链接。

附件:
反编译后在这个文件:
com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.class

这个拦截器里有两个核心方法:preHandle()preRedirect()
跳转外链的逻辑就藏在这里。

Flag:
flag{preRedirect}


某化工厂监控网络出现异常波动

你拿到了一份汇总的抓包文件。

工厂应急 Q1

问题: 谁把泵关了?
提交格式: flag{0xtransaction_id_0xfunction_code_0xcoil_address}

解题脚本:

# Extract Modbus/TCP Write Single Coil packet and get MBAP Transaction ID, Function Code, Coil Address
import struct, socket

path = "/mnt/data/challenge.pcap"
data = open(path, "rb").read()

def parse_pcap(data):
    magic = struct.unpack("<I", data[:4])[0]
    if magic == 0xa1b2c3d4:
        endian = "<"; nano = False
    elif magic == 0xd4c3b2a1:
        endian = ">"; nano = False
    elif magic == 0xa1b23c4d:
        endian = "<"; nano = True
    elif magic == 0x4d3cb2a1:
        endian = ">"; nano = True
    else:
        raise SystemExit("Unknown pcap format")
    offset = 24
    packets = []
    while offset + 16 <= len(data):
        ts_sec, ts_usec, incl_len, orig_len = struct.unpack(endian + "IIII", data[offset:offset + 16])
        offset += 16
        pkt = data[offset:offset + incl_len]
        offset += incl_len
        if nano:
            ts_usec = ts_usec / 1000.0
        packets.append((ts_sec, ts_usec, pkt))
    return packets

pkts = parse_pcap(data)

def parse_ipv4_tcp(pkt):
    if len(pkt) < 54: return None
    eth_type = struct.unpack("!H", pkt[12:14])[0]
    if eth_type != 0x0800: return None
    ver_ihl = pkt[14]
    ihl = (ver_ihl & 0x0f) * 4
    if (ver_ihl >> 4) != 4: return None
    total_len = struct.unpack("!H", pkt[16:18])[0]
    proto = pkt[23]
    src = socket.inet_ntoa(pkt[26:30])
    dst = socket.inet_ntoa(pkt[30:34])
    if proto != 6: return None
    off_ip = 14 + ihl
    if len(pkt) < off_ip + 20: return None
    tcph = pkt[off_ip:off_ip + 20]
    srcp, dstp, seq, ack, off_flags, win, csum, urgp = struct.unpack("!HHIIHHHH", tcph)
    doff = ((off_flags >> 12) & 0xF) * 4
    off_tcp = off_ip + doff
    payload = pkt[off_tcp:14 + total_len]
    return {"src": src, "dst": dst, "sport": srcp, "dport": dstp, "payload": payload, "tcph": tcph}

records = []
for ts_sec, ts_usec, pkt in pkts:
    info = parse_ipv4_tcp(pkt)
    if not info: continue
    pay = info["payload"]
    if not pay or len(pay) < 12: continue
    # MBAP header: 7 bytes, then PDU
    # func code at byte 7 (0-based within payload)
    func = pay[7]
    if func == 5:  # Write Single Coil
        # coil address and value
        addr = (pay[8] << 8) | pay[9]
        val = (pay[10] << 8) | pay[11]
        if val == 0 and addr in (13, 0x000d):
            # parse transaction id (first 2 bytes big-endian)
            trans_id = (pay[0] << 8) | pay[1]
            protocol_id = (pay[2] << 8) | pay[3]
            length = (pay[4] << 8) | pay[5]
            unit_id = pay[6]
            records.append({
                "ts": ts_sec + ts_usec / 1e6,
                "src": info["src"], "dst": info["dst"],
                "trans_id": trans_id, "protocol_id": protocol_id, "length": length, "unit_id": unit_id,
                "func": func, "addr": addr, "val": val
            })

records

结果:

[{'ts': 1741789589.614693,
  'src': '192.168.10.20',
  'dst': '192.168.10.30',
  'trans_id': 6699,
  'protocol_id': 0,
  'length': 6,
  'unit_id': 1,
  'func': 5,
  'addr': 13,
  'val': 0}]

Flag:
flag{0x1a2b_0x05_0x000d}


工厂应急 Q2

问题: 被写入的 NodeId

在数据中出现:
WriteRequest:NodeId=ns=2;s=Pump/SpeedSetpoint;Value=1200;

说明被写入的节点是:
ns=2;s=Pump/SpeedSetpoint

Flag:
flag{ns=2;s=Pump/SpeedSetpoint}


工厂应急 Q3

问题: 工程站域名解析结果
找出工程站域名 engws.plant.local 的 A 记录解析结果(目标 IP)。

Flag:
flag{198.51.100.5}


工厂应急 Q4

问题: 确定 HMI(源:10.0.0.x)到工程站(目的:10.0.0.x)上首个成功发起的时间点。
提交格式: flag{YYYY-MM-DDTHH:MM:SSZ}(UTC,精确到秒)

之前拿到的是首个从 HMI (10.0.0.5) → 工程站 (10.0.0.8) 的 TCP 连接建立相关数据。

Flag:
flag{2025-03-12T14:22:09Z}


工厂应急 Q5

问题: 在横向后不久,HMI 对工程站发起了 HTTP 请求。提交请求的 Host 与 URI,用下划线连接。

Flag:
flag{engws.plant.local_/rpc}

比赛
Theme Jasmine by Kent Liao 京ICP备2023023335号-2京公网安备11010802044340号