官方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-kerberos 和 https://blog.enxiaohao.cn/posts/Pentration/DomainPentration
NTLMHash解密 最开始的第一部分是使用的NTLM进行的身份验证 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), 跟进看一下这个函数,看到把目标计算机上的内容base64编码后放到task description中去
把759的task description解一下,解码之后确实能观察到是zip,提取 尝试发现这个压缩包的密码和之前的一样,taylorswift<3
然而这个hint.zip里没有有用的东西,也就是说前面使用ntlm验证的内容是无效的,只有后面使用Kerberos验证的内容才是有效的
Kerberos通信解密 筛选一下Kerberos认证的记录,发现一共有两次记录。 第一次Kerberos通信,所有的内容都集中在tcp.stream=5(frame844-2652)里面:
这里尝试之后发现制作keytab的密码也是taylorswift<3,制作好之后导入首选项即可
首先,frame 869左右完成了Kerberos认证,申请的服务票据是host/dc01.wire.com 完成认证之后在frame 844-879完成了Kerberos的认证挂到到rpc上
frame 881-2572:SchRpcRegisterTask request 注册了一个任务
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
try { $description = $definition .RegistrationInfo.Description $decryptedDescription = Decrypt-Data $encryptionKey $description $decodeData = ConvertFrom-Base64 $decryptedDescription $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操作的逻辑是:
外层 Base64 解码
前 16 字节取 IV
用 PYake61OOYCKw0zg+oT/Qg== 做 AES-CBC 解密
解出来还是一段 Base64
再 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
接下来写脚本自动拼接请求包并使用subkey解密
import subprocess, refrom pathlib import Pathfrom minikerberos.protocol.encryption import Key, Enctype, _AES256CTSfrom 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=startwhile 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 Pathimport base64from Crypto.Cipher import AESfrom 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
┌──(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自己的票
接着在前面的TGT-REQ也可以看出,enc-tkt-in-skey: True,意思是返回的service ticket不用服务账号长期密钥加密,而是使用TGT Session Key加密
所以也就是说,TGS-REP返回的这个TGS票据正常来说应该是使用某个服务的长期密钥来加密的,但是这里使用了Administer的TGT Session Key加密,而我们已经拿到了这个key,所以可以解密这部分的enc-part
import subprocessfrom pathlib import Pathfrom impacket.krb5.asn1 import AD_IF_RELEVANT, EncTicketPart, TGS_REPfrom impacket.krb5.ccache import CCachefrom impacket.krb5.crypto import Key, _enctype_tablefrom impacket.krb5.pac import ( NTLM_SUPPLEMENTAL_CREDENTIAL, PAC_CREDENTIAL_DATA, PAC_CREDENTIAL_INFO, PAC_INFO_BUFFER, PACTYPE, )from minikerberos.protocol.external.rpcrt import TypeSerialization1from 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 ): 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 subprocessfrom pathlib import Pathfrom impacket.krb5.asn1 import AD_IF_RELEVANT, EncTicketPart, TGS_REPfrom impacket.krb5.ccache import CCachefrom impacket.krb5.crypto import Key, _enctype_tablefrom impacket.krb5.pac import ( NTLM_SUPPLEMENTAL_CREDENTIAL, PAC_CREDENTIAL_DATA, PAC_CREDENTIAL_INFO, PAC_INFO_BUFFER, PACTYPE, )from minikerberos.protocol.external.rpcrt import TypeSerialization1from 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 ): 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给的那个网站)
让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
frame2671之后的第二次Kerberos我们还没用到,导入之后成功解密
后面的流量还是一样的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, base64from 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 数据) 攻击的原理就是枚举nthash去和mac碰撞。 从 pcap 里提取TimeRoast哈希,其中服务端响应是775和789,从中提取出hash然后roasting攻击
import subprocess, structfor 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 结束。
文章作者: Xiaohao
文章链接: https://blog.enxiaohao.cn/posts/Misc/2026SUCTFSU_LightNovelWriteWp/
版权声明:除另有声明外,本博客文章均采用 CC BY-NC-SA 4.0 许可协议。转载请注明原作者与文章出处。