Xiaohao's Blog

官方wp: https://blog.0ran9e.cn/posts/ctf/suctf2026/

附件和解题过程文件: https://img.enxiaohao.cn/CTFAttachments/SU_LightNovel.zip

五一学习了域流量的知识并复现了一下2026SUCTF SU_LightNovel这道题。说实话除非是ai一把梭选手,自己一步一步做下来,对于域的基本知识和原理的要求还是挺高的。

2025的SU_AD是结合SMB来考的,今年更进一步,结合了windows的rpc计划任务流量,还融入了ADCS、U2U和TimeRoasting攻击的部分偏域渗透的知识,难度又有提升。

题目出的挺好,我复现的过程中也学习到了很多。官方wp后半部分ai味比较重,我看的时候一知半解,所以我自己写了一篇,如有不足请大家多多指教。

有关Kerberos通信协议的具体原理和过程不过多解释,有需要可以看 https://www.roguelynn.com/words/explain-like-im-5-kerberoshttps://blog.enxiaohao.cn/posts/Pentration/DomainPentration

NTLMHash解密

最开始的第一部分是使用的NTLM进行的身份验证
Pasted image 20260504002524
ntlmrawunhide.py+hashcat直接解

kanna.seto::wire.com:e9b597a6e03a5122:c4ec074163bee82d9f829d1aa22de185:0101000000000000402a64de67addc01393769656779706e000000000200080057004900520045000100080044004300300031000400100077006900720065002e0063006f006d0003001a0044004300300031002e0077006900720065002e0063006f006d000500100077006900720065002e0063006f006d0007000800402a64de67addc010900120063006900660073002f0044004300300031000000000000000000:taylorswift<3

后面的流量是Windows主机通过 Task Scheduler RPC远程操作计划任务的流量,有关的操作类型对应:

SchRpcRegisterTask (opnum 1):创建或覆盖注册任务
这是最常见的大包,请求里通常带完整任务XML
SchRpcRetrieveTask (opnum 2):取回任务XML
SchRpcDelete (opnum 13):删除任务
SchRpcRename (opnum 14):重命名任务
SchRpcEnableTask (opnum 19):启用/禁用任务

这个流量包的主要流程:客户端远程连接目标主机的任务计划服务,注册任务、运行任务、轮询任务状态、取回任务 XML、最后删除任务

先配置好NTLMSSP的首选项之后,wireshark能在frame 42和759解出任务的xml,提取出来:

tshark -o "ntlmssp.nt_password:taylorswift<3" -r suctf-ad.pcapng -Y "frame.number==42" -T fields -e frame.number -e dcerpc.decrypted_stub_data | xxd -r -p

解密decodetext = base64.b64decode(payload1).decode("utf-16le")
分析一下这个脚本,最后做的是DownloadByPs($taskname),
Pasted image 20260503145515
跟进看一下这个函数,看到把目标计算机上的内容base64编码后放到task description中去
Pasted image 20260503145615

把759的task description解一下,解码之后确实能观察到是zip,提取
Pasted image 20260504004717
Pasted image 20260503150131
尝试发现这个压缩包的密码和之前的一样,taylorswift<3

然而这个hint.zip里没有有用的东西,也就是说前面使用ntlm验证的内容是无效的,只有后面使用Kerberos验证的内容才是有效的

Kerberos通信解密

筛选一下Kerberos认证的记录,发现一共有两次记录。
第一次Kerberos通信,所有的内容都集中在tcp.stream=5(frame844-2652)里面:

这里尝试之后发现制作keytab的密码也是taylorswift<3,制作好之后导入首选项即可

  1. 首先,frame 869左右完成了Kerberos认证,申请的服务票据是host/dc01.wire.com
    Pasted image 20260503165614
    完成认证之后在frame 844-879完成了Kerberos的认证挂到到rpc上
  2. frame 881-2572:SchRpcRegisterTask request 注册了一个任务
  3. frame 2636-2644 :SchRpcRetrieveTask 取回任务结果xml

一样,提取出计划任务的xml

tshark -o kerberos.decrypt:TRUE -o kerberos.file:login.keytab -r suctf-ad.pcapng -Y "frame.number==2644" -T fields -e dcerpc.decrypted_stub_data | xxd -r -p

Pasted image 20260503163147

try {
$description = $definition.RegistrationInfo.Description
$decryptedDescription = Decrypt-Data $encryptionKey $description
# base64 decode get raw data and save it to file
$decodeData = ConvertFrom-Base64 $decryptedDescription
# if target path not exists, create it
$dir = Split-Path $target_path
if (!(Test-Path -Path $dir)) {
New-Item -ItemType Directory -Path $dir
}
$decodeData | Set-Content -Path "C:\cert.zip" -Encoding Byte
$result = "[+] Success."
}

这里实现的是从task description中提取并上传一个aes加密了的文件,具体对description操作的逻辑是:

  1. 外层 Base64 解码
  2. 前 16 字节取 IV
  3. 用 PYake61OOYCKw0zg+oT/Qg== 做 AES-CBC 解密
  4. 解出来还是一段 Base64
  5. 再 Base64 解码得到原始 zip

那么cert.zip肯定就是在前面的任务注册的包中,实际上之前我们成功提取的3个xml内容由于整个包的内容比较少,wireshark能够自动帮我们重组好并解密,但是这个地方的这个任务注册的包明显是太大了,wireshark不能自动帮我们重组解密,所以这样需要我们自己提取subkey并拼接解密。

这个RegisterTask 的DCE/RPC流量开启了Packet privacy,所以 RPC stub 被GSSAPI/Kerberos加密保护,需要从keytab衍生的一个subkey来解密rpc流量。幸运的是wireshark能够通过keytab自动学习得到这个subkey

frame876里面找到subkey
Pasted image 20260503182538
Pasted image 20260503170902

接下来写脚本自动拼接请求包并使用subkey解密

import subprocess, re
from pathlib import Path
from minikerberos.protocol.encryption import Key, Enctype, _AES256CTS
from minikerberos.gssapi.gssapi import GSSWrapToken, GSSAPI_AES, KG_USAGE

pcap='suctf-ad.pcapng'
subkey_hex='6c729591c51fd38f4c462d74566eeb4a40a4511a9c85bc81232e737a98d8d1f2'

key=Key(Enctype.AES256, bytes.fromhex(subkey_hex))
gss=GSSAPI_AES(key, _AES256CTS, None)

out = subprocess.check_output([
'tshark','-r',pcap,
'-Y','tcp.stream==5 && ip.src==192.168.183.132 && tcp.len>0 && frame.number>=881 && frame.number<=2572',
'-T','fields','-e','frame.number','-e','tcp.seq','-e','tcp.payload'
], text=True, encoding='utf-8')

segments=[]
for line in out.strip().splitlines():
parts=line.split('\t')
if len(parts) < 3 or not parts[2]:
continue
seq=int(parts[1])
data=bytes.fromhex(parts[2].replace(':',''))
segments.append((seq, data))
segments.sort()

base_seq=segments[0][0]
stream=bytearray()
for seq, data in segments:
off=seq-base_seq
end=off+len(data)
if end <= len(stream):
continue
if off < len(stream):
data = data[len(stream)-off:]
off = len(stream)
stream.extend(data)

s=bytes(stream)
start=s.find(bytes.fromhex('05000001100000009c10440002000000'))

parts=[]
pos=start
while pos + 24 <= len(s):
frag_len=int.from_bytes(s[pos+8:pos+10],'little')
auth_len=int.from_bytes(s[pos+10:pos+12],'little')
call_id=int.from_bytes(s[pos+12:pos+16],'little')
flags=s[pos+3]
if call_id != 2:
break
frag=s[pos:pos+frag_len]
stub_len=frag_len - 24 - 8 - auth_len
enc_stub=frag[24:24+stub_len]
auth=frag[24+stub_len+8:24+stub_len+8+auth_len]

t=GSSWrapToken.from_bytes(auth)
rotated = auth[16:] + enc_stub
cipher_text = gss.unrotate(rotated, t.RRC + t.EC)
plain = _AES256CTS().decrypt(key, KG_USAGE.INITIATOR_SEAL.value, cipher_text)
plain = plain[:-(t.EC + 16)]
parts.append(plain)

pos += frag_len
if flags & 0x02:
break

full=b''.join(parts)
start_xml = full.find(b'<\x00T\x00a\x00s\x00k\x00')
xml_txt = full[start_xml:].decode('utf-16le', errors='ignore')
end_xml = xml_txt.find('</Task>')
xml_txt = xml_txt[:end_xml+7]

desc = re.search(r'<Description>(.*?)</Description>', xml_txt, re.S).group(1)
Path('stream5_desc_fixed.txt').write_text(desc, encoding='ascii')
print('wrote stream5_desc_fixed.txt', len(desc))
from pathlib import Path
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

desc = Path("stream5_desc_fixed.txt").read_text(encoding="ascii").strip()
key = base64.b64decode("PYake61OOYCKw0zg+oT/Qg==")

blob = base64.b64decode(desc)
iv, ct = blob[:16], blob[16:]
pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
raw = base64.b64decode(pt)

Path("recovered_cert.zip").write_bytes(raw)
print("wrote recovered_cert.zip", len(raw), raw[:4])

hint.txt和cert.jpg是未加密的,其中cert.jpg使用steghide隐写了一个poem.txt
Pasted image 20260503190032

┌──(root㉿XiaohaoVictusGamingLaptop)-[/mnt/d/Xiaohao/Desktop/CTF/2026SUCTF/SU_LightNovel/cert]
└─# steghide extract -sf cert.jpg
Enter passphrase:
wrote extracted data to "poem.txt".
hint.txt:
潮声只听开口处
The sea listens where the lines begin

poem.txt:
濑水晚霞映海天,户外潮声入远烟。
环佩清姿临碧浪,奈何人间少此颜。
倾心落日添柔影,城畔微风动鬓边。
绝代芳华如画里,色映云霞胜月妍。

可以联想到是藏头诗,所以密码是濑户环奈倾城绝色

直接7z解好像有点问题,用unzip可以解

unzip -P '濑户环奈倾城绝色' cert.zip -d cert

解密之后我们就拿到一个Administrator@WIRE.COM的tgt票据缓存和一张域内的证书,说明攻击者是已经拿到了域管的Kerberos票据的

证书信息:

┌──(root㉿XiaohaoVictusGamingLaptop)-[/mnt/d/Xiaohao/Desktop/CTF/2026SUCTF/SU_LightNovel/cert]
└─# openssl pkcs12 -in administrator.pfx -info

Enter Import Password:
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
PKCS7 Data
Certificate bag
Bag Attributes
friendlyName:
localKeyID: AB AD F4 6E 1D 76 C3 07 FA 3B 16 55 0F FA 67 0B E9 08 21 AB
subject=CN=Kanna.seto
issuer=DC=com, DC=wire, CN=wire-DC01-CA

ccache信息:

┌──(root㉿XiaohaoVictusGamingLaptop)-[/mnt/d/Xiaohao/Desktop/CTF/2026SUCTF/SU_LightNovel/cert]
└─# klist -c wiredc.ccache
Ticket cache: FILE:wiredc.ccache
Default principal: Administrator@WIRE.COM

Valid starting Expires Service principal
03/06/2026 20:52:11 03/07/2026 06:52:11 krbtgt/WIRE.COM@WIRE.COM

破解Administrator NTHash和明文密码

使用impacket提取出ccache中的tgt session key

from impacket.krb5.ccache import CCache

cc = CCache.loadFile('wiredc.ccache')

for cred in cc.credentials:
client = cred['client'].prettyPrint().decode()
server = cred['server'].prettyPrint().decode()
keytype = cred['key']['keytype']
keyvalue = cred['key']['keyvalue']

print('client:', client)
print('server:', server)
print('keytype:', keytype)
print('session_key:', keyvalue.hex())
print()

"""
client: Administrator@WIRE.COM
server: krbtgt/WIRE.COM@WIRE.COM
keytype: 18
session_key: e7d900a23fd982ccf1f4142a360291735e4af423e0e7255a53e6102afd27f352
"""

frame 795是TGS-REP,正常应该返回一个服务的票据,这里返回了Administrator的票据,说明使用的U2U,相当于是请求了一张发给Administrator自己的票
Pasted image 20260503225118

接着在前面的TGT-REQ也可以看出,enc-tkt-in-skey: True,意思是返回的service ticket不用服务账号长期密钥加密,而是使用TGT Session Key加密
Pasted image 20260503230213
Pasted image 20260503230447

所以也就是说,TGS-REP返回的这个TGS票据正常来说应该是使用某个服务的长期密钥来加密的,但是这里使用了Administer的TGT Session Key加密,而我们已经拿到了这个key,所以可以解密这部分的enc-part

import subprocess
from pathlib import Path

from impacket.krb5.asn1 import AD_IF_RELEVANT, EncTicketPart, TGS_REP
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import Key, _enctype_table
from impacket.krb5.pac import (
NTLM_SUPPLEMENTAL_CREDENTIAL,
PAC_CREDENTIAL_DATA,
PAC_CREDENTIAL_INFO,
PAC_INFO_BUFFER,
PACTYPE,
)
from minikerberos.protocol.external.rpcrt import TypeSerialization1
from pyasn1.codec.der import decoder


ROOT = Path(__file__).resolve().parent
PCAP = ROOT.parent / "suctf-ad.pcapng"
CCACHE = ROOT / "wiredc.ccache"
AS_REPLY_KEY = ROOT / "key"
FRAME = "795"

def load_tgt_session_key():
cc = CCache.loadFile(str(CCACHE))
for cred in cc.credentials:
client = cred["client"].prettyPrint().decode()
server = cred["server"].prettyPrint().decode()
keytype = cred["key"]["keytype"]
keyvalue = cred["key"]["keyvalue"]

print("client:", client)
print("server:", server)
print("keytype:", keytype)
print("session_key:", keyvalue.hex())
print()

if server.lower().startswith("krbtgt/"):
return Key(keytype, keyvalue)
raise RuntimeError("No krbtgt credential found in ccache")

def read_frame_tcp_payload(frame_number):
payload = subprocess.check_output(
[
"tshark",
"-r",
str(PCAP),
"-Y",
f"frame.number=={frame_number}",
"-T",
"fields",
"-e",
"tcp.payload",
],
text=True,
)
return bytes.fromhex(payload.strip().replace(":", ""))

def extract_pac_from_tgs_rep(tgt_session_key, frame_number):
# Kerberos over TCP has a 4-byte record marker before the DER message.
raw = read_frame_tcp_payload(frame_number)[4:]
tgs_rep, _ = decoder.decode(raw, asn1Spec=TGS_REP())

enc_ticket = tgs_rep["ticket"]["enc-part"]
etype = int(enc_ticket["etype"])
cipher = bytes(enc_ticket["cipher"])
ticket_plain = _enctype_table[etype].decrypt(tgt_session_key, 2, cipher)

enc_ticket_part, _ = decoder.decode(ticket_plain, asn1Spec=EncTicketPart())
ad_if_relevant, _ = decoder.decode(
bytes(enc_ticket_part["authorization-data"][0]["ad-data"]),
asn1Spec=AD_IF_RELEVANT(),
)
return bytes(ad_if_relevant[0]["ad-data"])

if __name__ == "__main__":
tgt_key = load_tgt_session_key()
print("TGT session key used for frame 795 ticket:", tgt_key.contents.hex())
print()

pac_blob = extract_pac_from_tgs_rep(tgt_key, FRAME)

print("type:", type(pac_blob))
print("len:", len(pac_blob))
print("first64 hex:", pac_blob[:64].hex())

pac_type = PACTYPE(pac_blob)
print("PAC buffers:", pac_type["cBuffers"])

for i in range(pac_type["cBuffers"]):
info = PAC_INFO_BUFFER(pac_blob[8 + i * 16 : 8 + (i + 1) * 16])
print(i, "type=", info["ulType"], "size=", info["cbBufferSize"], "offset=", info["Offset"])

解密后的enc-ticket包含:

flags
key
crealm
cname
authorization-data

其中authorization-data包含PAC,PAC在域中主要用于权限控制(用户访问服务端时,服务端会拿着PAC去向DC验证,DC拿到 PAC后进行解密,返回对应用户的权限内容),包含了主要包含用户的SID,用户的组等信息

PAC中包含:

PAC_CREDENTIAL_INFO
Version
EncryptionType
SerializedData

其中PAC_CREDENTIAL_INFO.SerializedData会包含NTLM Hash等信息,使用AS reply key加密

这个AS reply key是PKINIT认证方式特有的。正常来说AS-REQ的enc-part是使用客户端密钥加密的,但是这里的AS-REP阶段使用了证书进行预认证(PKINIT),此时客户端和KDC是通过证书协商出一把AS reply key,KDC 用这把key加密AS-REP的enc-part。

这里的这个key就是刚刚cert.zip解出来的key文件:01ea8c39173e5e4afbb5a6580b118e4cc21b16d399b8e2322b9090e68acd080a

解密出来PAC_CREDENTIAL_INFO就能拿到NtPassword

NTLM_SUPPLEMENTAL_CREDENTIAL
Version
Flags
LmPassword
NtPassword

完整脚本:

import subprocess
from pathlib import Path

from impacket.krb5.asn1 import AD_IF_RELEVANT, EncTicketPart, TGS_REP
from impacket.krb5.ccache import CCache
from impacket.krb5.crypto import Key, _enctype_table
from impacket.krb5.pac import (
NTLM_SUPPLEMENTAL_CREDENTIAL,
PAC_CREDENTIAL_DATA,
PAC_CREDENTIAL_INFO,
PAC_INFO_BUFFER,
PACTYPE,
)
from minikerberos.protocol.external.rpcrt import TypeSerialization1
from pyasn1.codec.der import decoder


ROOT = Path(__file__).resolve().parent
PCAP = ROOT.parent / "suctf-ad.pcapng"
CCACHE = ROOT / "wiredc.ccache"
AS_REPLY_KEY = ROOT / "key"
FRAME = "795"


def load_tgt_session_key():
cc = CCache.loadFile(str(CCACHE))
for cred in cc.credentials:
client = cred["client"].prettyPrint().decode()
server = cred["server"].prettyPrint().decode()
keytype = cred["key"]["keytype"]
keyvalue = cred["key"]["keyvalue"]

print("client:", client)
print("server:", server)
print("keytype:", keytype)
print("session_key:", keyvalue.hex())
print()

if server.lower().startswith("krbtgt/"):
return Key(keytype, keyvalue)
raise RuntimeError("No krbtgt credential found in ccache")


def read_frame_tcp_payload(frame_number):
payload = subprocess.check_output(
[
"tshark",
"-r",
str(PCAP),
"-Y",
f"frame.number=={frame_number}",
"-T",
"fields",
"-e",
"tcp.payload",
],
text=True,
)
return bytes.fromhex(payload.strip().replace(":", ""))


def extract_pac_from_tgs_rep(tgt_session_key, frame_number):
# Kerberos over TCP has a 4-byte record marker before the DER message.
raw = read_frame_tcp_payload(frame_number)[4:]
tgs_rep, _ = decoder.decode(raw, asn1Spec=TGS_REP())

enc_ticket = tgs_rep["ticket"]["enc-part"]
etype = int(enc_ticket["etype"])
cipher = bytes(enc_ticket["cipher"])
ticket_plain = _enctype_table[etype].decrypt(tgt_session_key, 2, cipher)

enc_ticket_part, _ = decoder.decode(ticket_plain, asn1Spec=EncTicketPart())
ad_if_relevant, _ = decoder.decode(
bytes(enc_ticket_part["authorization-data"][0]["ad-data"]),
asn1Spec=AD_IF_RELEVANT(),
)
return bytes(ad_if_relevant[0]["ad-data"])


def extract_nt_hash_from_pac(pac, as_reply_key):
pac_type = PACTYPE(pac)
as_key = Key(18, bytes.fromhex(as_reply_key.read_text(encoding="ascii").strip()))

for i in range(pac_type["cBuffers"]):
info = PAC_INFO_BUFFER(pac[8 + i * 16 : 8 + (i + 1) * 16])
if info["ulType"] != 2:
continue

blob = pac[info["Offset"] : info["Offset"] + info["cbBufferSize"]]
cred_info = PAC_CREDENTIAL_INFO(blob)
cipher = _enctype_table[cred_info["EncryptionType"]]
decrypted = cipher.decrypt(as_key, 16, cred_info["SerializedData"])

type1 = TypeSerialization1(decrypted)
cred_data = PAC_CREDENTIAL_DATA(decrypted[len(type1) + 4 :])

hashes = []
for credential in cred_data["Credentials"]:
raw_credential = b"".join(credential["Credentials"])
ntlm = NTLM_SUPPLEMENTAL_CREDENTIAL(raw_credential)
hashes.append((ntlm["LmPassword"].hex(), ntlm["NtPassword"].hex()))
return hashes

raise RuntimeError("PAC_CREDENTIAL_INFO was not found")


if __name__ == "__main__":
tgt_key = load_tgt_session_key()
print("TGT session key used for frame 795 ticket:", tgt_key.contents.hex())
print("AS reply key used for PAC_CREDENTIAL_INFO:", AS_REPLY_KEY.read_text().strip())
print()

pac_blob = extract_pac_from_tgs_rep(tgt_key, FRAME)
for lm_hash, nt_hash in extract_nt_hash_from_pac(pac_blob, AS_REPLY_KEY):
print("LM:", lm_hash)
print("NT:", nt_hash)
client: Administrator@WIRE.COM
server: krbtgt/WIRE.COM@WIRE.COM
keytype: 18
session_key: e7d900a23fd982ccf1f4142a360291735e4af423e0e7255a53e6102afd27f352

TGT session key used for frame 795 ticket: e7d900a23fd982ccf1f4142a360291735e4af423e0e7255a53e6102afd27f352
AS reply key used for PAC_CREDENTIAL_INFO: 01ea8c39173e5e4afbb5a6580b118e4cc21b16d399b8e2322b9090e68acd080a

LM: 00000000000000000000000000000000
NT: bedcf78571904538b1919672e4521c4e

这个ntlmhash对应的密码是Talor@1989
(cmd5付费记录,也可以用官方wp给的那个网站)
Pasted image 20260503233609

让ai帮我总结了一下:

frame 793/795 是一次 Kerberos U2U 请求:攻击者拿 Administrator 的 TGT,向 KDC 请求一张“发给 Administrator 自己”的票。因为启用了 enc-tkt-in-skey,KDC 返回的 frame 795 ticket 可以用 Administrator 的TGT session key解开。解开 ticket 后,PAC 里带着 PAC_CREDENTIAL_INFO,这个字段又用 PKINIT 的 AS reply key 二次加密。用 cert/key 按 key usage 16 解开后,就能从 NTLM_SUPPLEMENTAL_CREDENTIAL 里取出 Administrator 的 NT hash

拿到明文密码,那么我们就可以制作域管的Keytab
Pasted image 20260503234425

frame2671之后的第二次Kerberos我们还没用到,导入之后成功解密
Pasted image 20260503234508

后面的流量还是一样的rpc流量,我们继续提取xml

tshark -o kerberos.decrypt:TRUE -o kerberos.file:administrator.keytab -r suctf-ad.pcapng -Y "frame.number==2772" -T fields -e dcerpc.decrypted_stub_data | xxd -r -p

tshark -o kerberos.decrypt:TRUE -o kerberos.file:administrator.keytab -r suctf-ad.pcapng -Y "frame.number==2772" -T fields -e dcerpc.decrypted_stub_data | xxd -r -p

跟刚刚一样wireshark直接拼接提取还是失败了,我们还是用脚本

import subprocess, re, base64
from pathlib import Path

out = subprocess.check_output([
'tshark',
'-o', 'kerberos.decrypt:TRUE',
'-o', 'kerberos.file:administrator.keytab',
'-r', 'suctf-ad.pcapng',
'-Y', 'tcp.stream==10 && dcerpc.cn_call_id==21 && dcerpc.pkt_type==2',
'-T', 'fields',
'-e', 'frame.number',
'-e', 'dcerpc.decrypted_stub_data'
], text=True, encoding='utf-8', errors='ignore')

parts = []
for line in out.splitlines():
if '\t' not in line:
continue
fr, hexdata = line.split('\t', 1)
hexonly = re.sub(r'[^0-9A-Fa-f]', '', hexdata)
if not hexonly:
continue
parts.append((int(fr), hexonly))

parts.sort()
full = b''.join(bytes.fromhex(h) for _, h in parts)

idx = full.find(b'<\x00?\x00x\x00m\x00l\x00')
if idx == -1:
idx = full.find(b'<\x00T\x00a\x00s\x00k\x00')
if idx == -1:
raise SystemExit('Task XML not found')

xml = full[idx:].decode('utf-16le', errors='ignore')
end = xml.find('</Task>')
if end == -1:
raise SystemExit('</Task> not found')
xml = xml[:end + 7]

Path('stream10_retrieve.xml').write_text(xml, encoding='utf-8')

m = re.search(r'<Description>(.*?)</Description>', xml, re.S)
if not m:
raise SystemExit('Description not found')

raw = base64.b64decode(m.group(1))
Path('flag.jpg').write_bytes(raw)

print('wrote stream10_retrieve.xml')
print('wrote flag.jpg', len(raw), raw[:4])

Taylor@1989作为passphrase解steghide:

┌──(root㉿XiaohaoVictusGamingLaptop)-[/mnt/d/Xiaohao/Desktop/CTF/2026SUCTF/SU_LightNovel/flag]
└─# steghide extract -sf flag.jpg
Enter passphrase:
wrote extracted data to "flag.txt".

flag.txt还是看不到明文flag,还有一层加密。

TimeRoasting攻击

https://www.thehacker.recipes/ad/movement/kerberos/timeroast

由于Kerberos对时间敏感,为了防止伪造时间响应,域内机器和DC同步时间时,DC返回时间的同时会生成一个认证MAC:认证 MAC = MD5_HMAC(域内机器客户端账户 NT hash, NTP 数据)
Pasted image 20260504001757
攻击的原理就是枚举nthash去和mac碰撞。
Pasted image 20260504001728
从 pcap 里提取TimeRoast哈希,其中服务端响应是775和789,从中提取出hash然后roasting攻击

import subprocess, struct

for fr in [775, 789]:
out = subprocess.check_output(
['tshark', '-r', 'suctf-ad.pcapng', '-Y', f'frame.number=={fr}', '-T', 'fields', '-e', 'udp.payload'],
text=True
).strip().replace(':', '')
raw = bytes.fromhex(out)

salt = raw[:48]
rid = struct.unpack('<I', raw[-20:-16])[0]
md5h = raw[-16:]

print(f'{rid}:$sntp-ms${md5h.hex()}${salt.hex()}')
$sntp-ms$cb1877ec7aeeffb785f5689e483f0a3b$1c0111e900000000000a4c034c4f434ced54e820c41a9b8ce1b8428bffbfcd0aed554c56e832914ced554c56e833a7cd
$sntp-ms$8e8bab42e2cac7e5ef5d252f1eb63a5b$1c0111e900000000000a4c274c4f434ced54e820c5fea811e1b8428bffbfcd0aed554c868a16e29aed554c868a176f88

爆破一下

hashcat.exe -m 31300 --username hashes.txt rockyou.txt
$sntp-ms$8e8bab42e2cac7e5ef5d252f1eb63a5b$1c0111e900000000000a4c274c4f434ced54e820c5fea811e1b8428bffbfcd0aed554c868a16e29aed554c868a176f88:*joker*123

获得的明文为*joker*123

最终使用明文的sha256分别作为key和iv解AES-CBC
Pasted image 20260504000516
结束。

文章作者: Xiaohao

文章链接: https://blog.enxiaohao.cn/posts/Misc/2026SUCTFSU_LightNovelWriteWp/

版权声明:除另有声明外,本博客文章均采用 CC BY-NC-SA 4.0 许可协议。转载请注明原作者与文章出处。

域渗透 流量分析

春秋云境 Privilege WriteUp «
上一篇 «
» 2026FIC初赛服务器浅析:容器桌面和RAID磁盘结构
» 下一篇