一年前,我还只是SUCTF的参赛者,谁又能想到一年后的今天,我已经成为了SUCTF的共建者,很荣幸能作为SU的一员为这次SUCTF2026出题。

因为是第一次给XCTF分站赛出题,所以其实在出题的时候真的很慌,可以说是绞尽脑汁了。早在今年年初甚至是去年底,我们就已经确定今年2-3月份要办SUCTF了,所以那时候开始我也就尝试构思该出些什么题(虽然说并没有投入实操吧)。因为那段时间我发现作为出题人,其实是可以在不影响题目效果的情况下,夹带一些私货的,所以我也就在想能怎么夹带些私货好)当时想的就是看看能不能加入一些我刀剑的元素,而正好我们的题目都是以SU打头的,跟SAO的开头一拍即合,所以最终的目标就是去构建了一个缩写是SAO的题目名,这也是我的题目SU_Artifact_Online的由来,另外我在别的地方也夹带了刀剑的内容,比如pow的ui能看到link start,flag的内容是捏他了刀剑的台词什么的
至于题目本身,其实当初构思了很多题目包括但不限于流量,磁盘,社工,甚至是ARG等等,最后都因为各种原因没去实现(其实是我太菜了没有这种技术力)。最后还是回归到了一个我早就构思过的题目废案中(原本应该出现在羊城杯2025的,但是当时没实现)——本质上是一个魔方翻转+命令构造,命令构造的部分其实是参考了damctf2025的一道misc题 breach,跟我这题命令构造的部分几乎是一模一样的一比一还原,我在它的基础上,融入了魔方翻转的部分,构造了一个六面的5x5魔方,并对其进行随机的打乱,因此需要将其翻转还原到一个可以构造所需命令的状态,才能进行命令的构造并执行。除此之外,还加上了一层单表替换的密文考点,将字母及部分符号映射成了卢恩文字,实际上只需要进行一些词频分析统计就能还原大部分符号映射表(这部分映射足够解出题目),而且在测题过程中只需要不到10min的时间ai就能成功映射出附件的明文。
最后还是给各位师傅磕一个,实力有限只能做出这种垃圾题了,也没什么含金量,纯当玩了,要是给大家带来了不好的体验请见谅😭
(夹带个私货,我的首页换了张超帅的背景图,记得看看嘿嘿~)
SU_Signin
1if you want it then you have to take it.
2
3https://ctftime.org/team/29641签到题,描述是鬼泣的名台词,直接给出了SU的ctftime链接,在里面有一个flag,对其做一个简单的字符去重即可
SUCTF{W3lc0me_2_SUC7F2026!!!!}

SU_Artifact_Online
1By chance, you discovered a mysterious machine along with a strange file inscribed with cryptic runes. Can you decipher the runes and uncover the truth hidden within the machine?
2
3It is recommended to connect to the target machine using stty raw -echo; ncat --ssl xxxxxxxxx xxxx; stty sane for the best experience. 首先通过题目信息说提到的,nc进靶机后可以发现需要进行一个简单的pow验证,这里简单让ai写写脚本都能跑了,将爆破得到的S值提交即可进入正式的题目

通过验证后,显示的ui如下,可以看到它展示了两个面,分别是正前面和右侧面,并且5x5的格子里各有一个符文,不确定的情况下,我们需要对这个玩意进行一些操作。可以留意到下方有两个模式选择,分别是翻转和激活

选择翻转,可以进入新的界面,可以看到上面提示了通过以下命令来进行一些翻转操作,并可以通过加上`符号来执行逆操作
1R1-R5 / C1-C5 / F1-F5其中R对应的是正前面的行,1-5则分别对应第一行到第五行,正向操作是进行顺时针翻转,也就是right面的行会朝着front面移动,逆操作则相反
C对应的是正前面的列,1-5则分别对应第一列到第五列,正向操作是进行顺时针翻转,也就是front面的列会向下翻转移动,逆操作则相反
F对应的是则是右侧面的列,1-5则分别对应第一列到第五列,正向操作是进行顺时针翻转,也就是right面的列会向下翻转移动,逆操作则相反

而选择激活,则会将魔方面锁定在正前面,以正前面作为激活面,进行横纵字符选择,从横向移动开始确认字符,确认后移动方向会转换成纵向,随后循环往复这个过程,在这个过程中需要选择合适的字符来构造对应的linux命令

在确定魔方的一些操作后,就是要确定符文代表的字符了,题目中提供了一个附件,里面是一长串经过符文映射后的密文,其实通过简单的字符统计就可以发现里面只有大概31种,并且有明显的分行,大抵能推断出是由英文字符+一些符号构造出来的文段经由映射变成的密文
1ᛗᚹᚱᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛨᛟᚠᛖᛨᚠᛨᛇᚠᛈᛨᛗᛟᚱᛈᛗᛣᚬᚬᚲᚺᛜᚱᛨᛣᚱᚠᛒᛖᛨᛉᛃᚨᛥᛨᛈᛉᛨᛗᚠᛃᛃᚱᛒᛨᛗᚹᚠᛈᛨᚺᛨᚠᛇᛥᛨᚦᚹᚺᛃᚨᚺᛖᚹᛨᚲᚱᚠᛗᛚᛒᚱᛖᛨᚠᛈᚨᛨᚠᛨᛗᛉᛚᚦᚹᛣᛨᛗᚱᛇᛋᚱᛒᛧᛨᚺᛨᚨᚺᚨᛈᚯᛗᛨᛃᚺᛁᚱᛨᚹᚺᛖᛨᛃᛉᛉᛁᛖᚬᚬᚬᚺᛨᛈᚱᛜᚱᛒᛨᚹᚠᚨᚬᚬᚬᚢᛚᛗᛨᚹᚱᛨᛟᚠᛖᛨᚠᛨᛃᚠᚨᛨᚺᛨᛟᚠᛖᛨᚹᚱᛒᚱᛨᛗᛉᛨᛒᚱᚦᛒᛚᚺᛗᛥᛨᚹᚱᛨᛟᚠᛖᛨᛇᛣᛨᚢᛉᛣᛧᛨᚺᛨᚷᚠᛜᚱᛨᚹᚺᛇᛨᛇᛣᛨᚢᚱᛖᛗᛨᚢᚠᛒᛁᚱᚱᛋᚯᛖᛨᛖᛇᚺᛃᚱᛧ
2ᛇᚠᛣᚢᚱᛨᚺᚯᛇᛨᛗᛉᛉᛨᚦᛒᚺᛗᚺᚦᚠᛃᛧᛨᚹᚱᛨᛟᚠᛖᛈᚯᛗᛨᛖᛟᚺᛖᚹᛦᛨᚹᚺᛖᛨᛈᚺᚦᛁᛈᚠᛇᚱᛨᚦᚠᛇᚱᛨᚲᛒᛉᛇᛨᛟᚹᚠᛗᛨᚹᚱᛨᚠᛃᛟᚠᛣᛖᛨᛖᚠᚺᚨᛨᛟᚹᚱᛈᛨᛖᛉᛇᚱᛨᛈᛉᛖᛣᛨᛗᛣᛋᚱᛨᚠᛖᛁᚱᚨᛨᚹᚺᛇᛨᚹᚺᛖᛨᛃᚺᛈᚱ:ᛨᚭᚺᚯᛇᛨᚠᛈᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛧᚭᛨᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛃᚱᛖᛖᛨᛗᚹᚠᛈᛨᛇᛚᛒᚨᚱᛒᛉᛚᛖᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᚠᚨᚨ:ᛨᚭᚠᛗᛨᚲᛉᛚᛒᛨᚦᚱᛈᛗᛖᛨᚠᛨᛟᛉᛒᚨᛧᛨᚺᛨᛟᛒᚺᛗᚱᛨᚦᛉᛈᚲᚱᛖᛖᚺᛉᛈᛨᛖᛗᛉᛒᚺᚱᛖᛧᚭ
3ᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛈᚠᛖᛗᛣᛥᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᛟᚠᚺᛗᛨᚲᛉᛒᛨᛖᛉᛇᚱᚢᛉᚨᛣᛨᛗᛉᛨᛇᚠᛁᚱᛨᛖᛉᛇᚱᛗᚹᚺᛈᚷᛨᛉᚲᛨᚺᛗᛧᛨᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᚱᛗᚹᚠᛃᛨᛖᛗᛣᛃᚱᛨᛉᚲᛨᚺᛈᚲᚺᚷᚹᛗᚺᛈᚷᛥᛨᛃᚺᛁᚱᛨᚠᛨᚲᚱᛇᚠᛃᚱᛨᚦᛉᛋᚬᚬᚬᛒᚱᚠᛖᛉᛈᛨᚺᛨᛟᚠᛈᛗᚱᚨᛨᚹᚺᛇᛧᛨᛈᛉᛗᛨᛗᚹᚱᛨᛉᛈᛃᛣᛨᛉᛈᚱᛧ
4ᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᛉᚠᚨᛨᛉᛈᛥᛨᚠᛈᚨᛨᚹᚺᛖᛨᚲᚠᚦᚱᛨᛖᚹᛉᛟᚱᚨᛨᛗᚹᚠᛗᛨᚹᚱᛨᚨᚱᛖᛋᚺᛖᚱᚨᛨᛋᚱᛉᛋᛃᚱᛨᛇᛉᛒᚱᛨᛗᚹᚠᛈᛨᛚᛖᛚᚠᛃᛧᛨᛖᚺᛃᚱᛈᛗᛃᛣᛨᚺᛨᛋᛉᛚᛒᚱᚨᛨᚠᛨᚨᛉᛚᚢᛃᚱᛨᛖᚹᛉᛗᛨᛉᚲᛨᛉᛃᚨᛨᛚᛈᚨᚱᛒᛟᚱᚠᛒᛨᚠᛈᚨᛨᛃᚱᚲᛗᛨᛗᚹᚱᛨᚢᛉᛗᛗᛃᚱᛧᛨᚹᚱᛨᚨᛒᚠᛈᛁᛨᚺᛗᛥᛨᛋᛉᛚᛒᚱᚨᛨᚠᛈᛉᛗᚹᚱᛒᛧ
5ᚺᛨᛟᚺᛋᚱᚨᛨᛗᚹᚱᛨᚢᚠᛒᛨᛗᛉᛋᛧᛨᚭᚹᛉᛟᚯᛖᛨᛗᚹᚱᛨᚯᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᚯᛨᛒᚠᚦᛁᚱᛗᛩᚭ
6ᚹᚺᛖᛨᚲᚺᛈᚷᚱᛒᛖᛨᛗᚺᚷᚹᛗᚱᛈᚱᚨᛨᛉᛈᛨᛗᚹᚱᛨᚷᛃᚠᛖᛖᛨᚠᛈᚨᛨᚹᚱᛨᛖᚱᚱᛇᚱᚨᛨᚠᚢᛉᛚᛗᛨᛗᛉᛨᛗᚹᛒᛉᛟᛨᚺᛗᛨᚠᛗᛨᛇᚱᛦᛨᚺᛨᚲᚱᛃᛗᛨᚲᛉᛒᛨᛗᚹᚱᛨᛖᚠᛋᛨᛚᛈᚨᚱᛒᛨᛗᚹᚱᛨᚢᚠᛒᛧᛨᚺᛈᛨᛗᚱᛇᛋᛉᛒᚠᛃᛨᛇᚠᛈᚺᛋᛚᛃᚠᛗᚺᛉᛈᛨᛣᛉᛚᛨᛗᛒᛣᛨᛗᛉᛨᚲᚺᚷᛚᛒᚱᛨᚱᛜᚱᛒᛣᛗᚹᚺᛈᚷᛥᛨᚢᛚᛗᛨᛗᚹᚱᛒᚱᛨᚠᛒᚱᛨᛖᛉᛨᛇᚠᛈᛣᛨᚲᚠᚦᛗᛉᛒᛖᛨᛗᚹᚠᛗᛨᛣᛉᛚᛨᛈᚱᛜᚱᛒᛨᛗᚠᛁᚱᛨᛈᚱᚱᚨᛃᚱᛖᛖᛨᛒᚺᛖᛁᛖᛧ对其进行词频统计,可以发现 ᛨ 出现最多,大概率就是空格

将 ᛨ 替换为空格后,用脚本替换一下字母和符号
1symbols = "abcdefghijklmnopqrstuvwxyz,?;\"\'-!" # 这里只需要写入26个字符加任意符号即可
2
3with open("text", "r", encoding="utf-8") as f:
4 text = f.read()
5
6unique_chars = []
7for ch in text:
8 if ch in [" ", ".",'\n']:
9 continue
10 if ch not in unique_chars:
11 unique_chars.append(ch)
12
13if len(unique_chars) > len(symbols):
14 raise ValueError("字符种类超过可映射数量")
15
16mapping = {ch: symbols[i] for i, ch in enumerate(unique_chars)}
17
18result = []
19for ch in text:
20 if ch == "\n":
21 result.append("\n")
22 elif ch == " ":
23 result.append(" ")
24 elif ch == ".":
25 result.append(".")
26 else:
27 result.append(mapping[ch])
28
29output = "".join(result)
30
31print(output)然后得到了
1abc defghhicj fkabch lgm g fge alceanoopiqc ncghm krjs ek agrrch abge i gfs tbirjimb pcgadhcm gej g akdtbn acfuchv i jijewa rixc bim rkkxmoooi ecqch bgjoooyda bc lgm g rgj i lgm bchc ak hcthdias bc lgm fn yknv i zgqc bif fn ycma yghxccuwm mfircv
2fgnyc iwf akk thiaitgrv bc lgmewa mlimb, bim eitxegfc tgfc phkf lbga bc grlgnm mgij lbce mkfc ekmn anuc gmxcj bif bim riec? ;iwf ge defghhicj fkabchv; ip bc pcra rcmm abge fdhjchkdm bc lkdrj gjj? ;ga pkdh tceam g lkhjv i lhiac tkepcmmike makhicmv;
3ip bc pcra egmans bc lkdrj lgia pkh mkfcykjn ak fgxc mkfcabiez kp iav bc bgj g rcabgr manrc kp iepizbaiezs rixc g pcfgrc tkuooohcgmke i lgeacj bifv eka abc kern kecv
4bc bgj g rkgj kes gej bim pgtc mbklcj abga bc jcmuimcj uckurc fkhc abge dmdgrv mircearn i ukdhcj g jkdyrc mbka kp krj dejchlcgh gej rcpa abc ykaarcv bc jhgex ias ukdhcj gekabchv
5i liucj abc ygh akuv ;bklwm abc wdefghhicj fkabchw hgtxca";
6bim piezchm aizbacecj ke abc zrgmm gej bc mccfcj gykda ak abhkl ia ga fc, i pcra pkh abc mgu dejch abc yghv ie acfukhgr fgeiudrgaike nkd ahn ak pizdhc cqchnabiezs yda abchc ghc mk fgen pgtakhm abga nkd ecqch agxc eccjrcmm himxmv然后拿到quipqiup里进行词频分析,则可以还原出大概得明文是什么,通过社工或者问ai就能锁定明文是来自Robert A. Heinlein的短篇小说《All You Zombies》

找到原文对应的部分,然后将他跟密文一一对应,即可还原映射表,简单拿脚本还原一下
1from collections import OrderedDict
2
3def build_mapping(plain, cipher):
4 plain = plain.replace("\n", "")
5 cipher = cipher.replace("\n", "")
6
7 if len(plain) != len(cipher):
8 raise ValueError(f"长度不一致: plain={len(plain)} cipher={len(cipher)}")
9
10 mapping = OrderedDict()
11
12 for p, c in zip(plain, cipher):
13 p = p.lower()
14
15 if p not in mapping:
16 mapping[p] = c
17 else:
18 if mapping[p] != c:
19 raise ValueError(f"映射冲突: '{p}' -> '{mapping[p]}' 与 '{c}'")
20
21 return mapping
22
23
24def print_mapping(mapping):
25 print("{")
26 for k, v in mapping.items():
27 if k == "'":
28 k = "\\'"
29 print(f" '{k}':'{v}',")
30 print("}")
31
32
33if __name__ == "__main__":
34 plaintext = """The Unmarried Mother was a man twenty--five years old, no taller than I am, childish features and a touchy temper. I didn't like his looks---I never had---but he was a lad I was here to recruit, he was my boy. I gave him my best barkeep's smile.
35Maybe I'm too critical. He wasn't swish; his nickname came from what he always said when some nosy type asked him his line: "I'm an unmarried mother." If he felt less than murderous he would add: "at four cents a word. I write confession stories."
36If he felt nasty, he would wait for somebody to make something of it. He had a lethal style of infighting, like a female cop---reason I wanted him. Not the only one.
37He had a load on, and his face showed that he despised people more than usual. Silently I poured a double shot of Old Underwear and left the bottle. He drank it, poured another.
38I wiped the bar top. "How's the 'Unmarried Mother' racket?"
39
40His fingers tightened on the glass and he seemed about to throw it at me; I felt for the sap under the bar. In temporal manipulation you try to figure everything, but there are so many factors that you never take needless risks."""
41 ciphertext = """ᛗᚹᚱᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛨᛟᚠᛖᛨᚠᛨᛇᚠᛈᛨᛗᛟᚱᛈᛗᛣᚬᚬᚲᚺᛜᚱᛨᛣᚱᚠᛒᛖᛨᛉᛃᚨᛥᛨᛈᛉᛨᛗᚠᛃᛃᚱᛒᛨᛗᚹᚠᛈᛨᚺᛨᚠᛇᛥᛨᚦᚹᚺᛃᚨᚺᛖᚹᛨᚲᚱᚠᛗᛚᛒᚱᛖᛨᚠᛈᚨᛨᚠᛨᛗᛉᛚᚦᚹᛣᛨᛗᚱᛇᛋᚱᛒᛧᛨᚺᛨᚨᚺᚨᛈᚯᛗᛨᛃᚺᛁᚱᛨᚹᚺᛖᛨᛃᛉᛉᛁᛖᚬᚬᚬᚺᛨᛈᚱᛜᚱᛒᛨᚹᚠᚨᚬᚬᚬᚢᛚᛗᛨᚹᚱᛨᛟᚠᛖᛨᚠᛨᛃᚠᚨᛨᚺᛨᛟᚠᛖᛨᚹᚱᛒᚱᛨᛗᛉᛨᛒᚱᚦᛒᛚᚺᛗᛥᛨᚹᚱᛨᛟᚠᛖᛨᛇᛣᛨᚢᛉᛣᛧᛨᚺᛨᚷᚠᛜᚱᛨᚹᚺᛇᛨᛇᛣᛨᚢᚱᛖᛗᛨᚢᚠᛒᛁᚱᚱᛋᚯᛖᛨᛖᛇᚺᛃᚱᛧ
42ᛇᚠᛣᚢᚱᛨᚺᚯᛇᛨᛗᛉᛉᛨᚦᛒᚺᛗᚺᚦᚠᛃᛧᛨᚹᚱᛨᛟᚠᛖᛈᚯᛗᛨᛖᛟᚺᛖᚹᛦᛨᚹᚺᛖᛨᛈᚺᚦᛁᛈᚠᛇᚱᛨᚦᚠᛇᚱᛨᚲᛒᛉᛇᛨᛟᚹᚠᛗᛨᚹᚱᛨᚠᛃᛟᚠᛣᛖᛨᛖᚠᚺᚨᛨᛟᚹᚱᛈᛨᛖᛉᛇᚱᛨᛈᛉᛖᛣᛨᛗᛣᛋᚱᛨᚠᛖᛁᚱᚨᛨᚹᚺᛇᛨᚹᚺᛖᛨᛃᚺᛈᚱ:ᛨᚭᚺᚯᛇᛨᚠᛈᛨᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᛧᚭᛨᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛃᚱᛖᛖᛨᛗᚹᚠᛈᛨᛇᛚᛒᚨᚱᛒᛉᛚᛖᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᚠᚨᚨ:ᛨᚭᚠᛗᛨᚲᛉᛚᛒᛨᚦᚱᛈᛗᛖᛨᚠᛨᛟᛉᛒᚨᛧᛨᚺᛨᛟᛒᚺᛗᚱᛨᚦᛉᛈᚲᚱᛖᛖᚺᛉᛈᛨᛖᛗᛉᛒᚺᚱᛖᛧᚭ
43ᚺᚲᛨᚹᚱᛨᚲᚱᛃᛗᛨᛈᚠᛖᛗᛣᛥᛨᚹᚱᛨᛟᛉᛚᛃᚨᛨᛟᚠᚺᛗᛨᚲᛉᛒᛨᛖᛉᛇᚱᚢᛉᚨᛣᛨᛗᛉᛨᛇᚠᛁᚱᛨᛖᛉᛇᚱᛗᚹᚺᛈᚷᛨᛉᚲᛨᚺᛗᛧᛨᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᚱᛗᚹᚠᛃᛨᛖᛗᛣᛃᚱᛨᛉᚲᛨᚺᛈᚲᚺᚷᚹᛗᚺᛈᚷᛥᛨᛃᚺᛁᚱᛨᚠᛨᚲᚱᛇᚠᛃᚱᛨᚦᛉᛋᚬᚬᚬᛒᚱᚠᛖᛉᛈᛨᚺᛨᛟᚠᛈᛗᚱᚨᛨᚹᚺᛇᛧᛨᛈᛉᛗᛨᛗᚹᚱᛨᛉᛈᛃᛣᛨᛉᛈᚱᛧ
44ᚹᚱᛨᚹᚠᚨᛨᚠᛨᛃᛉᚠᚨᛨᛉᛈᛥᛨᚠᛈᚨᛨᚹᚺᛖᛨᚲᚠᚦᚱᛨᛖᚹᛉᛟᚱᚨᛨᛗᚹᚠᛗᛨᚹᚱᛨᚨᚱᛖᛋᚺᛖᚱᚨᛨᛋᚱᛉᛋᛃᚱᛨᛇᛉᛒᚱᛨᛗᚹᚠᛈᛨᛚᛖᛚᚠᛃᛧᛨᛖᚺᛃᚱᛈᛗᛃᛣᛨᚺᛨᛋᛉᛚᛒᚱᚨᛨᚠᛨᚨᛉᛚᚢᛃᚱᛨᛖᚹᛉᛗᛨᛉᚲᛨᛉᛃᚨᛨᛚᛈᚨᚱᛒᛟᚱᚠᛒᛨᚠᛈᚨᛨᛃᚱᚲᛗᛨᛗᚹᚱᛨᚢᛉᛗᛗᛃᚱᛧᛨᚹᚱᛨᚨᛒᚠᛈᛁᛨᚺᛗᛥᛨᛋᛉᛚᛒᚱᚨᛨᚠᛈᛉᛗᚹᚱᛒᛧ
45ᚺᛨᛟᚺᛋᚱᚨᛨᛗᚹᚱᛨᚢᚠᛒᛨᛗᛉᛋᛧᛨᚭᚹᛉᛟᚯᛖᛨᛗᚹᚱᛨᚯᛚᛈᛇᚠᛒᛒᚺᚱᚨᛨᛇᛉᛗᚹᚱᛒᚯᛨᛒᚠᚦᛁᚱᛗᛩᚭ
46ᚹᚺᛖᛨᚲᚺᛈᚷᚱᛒᛖᛨᛗᚺᚷᚹᛗᚱᛈᚱᚨᛨᛉᛈᛨᛗᚹᚱᛨᚷᛃᚠᛖᛖᛨᚠᛈᚨᛨᚹᚱᛨᛖᚱᚱᛇᚱᚨᛨᚠᚢᛉᛚᛗᛨᛗᛉᛨᛗᚹᛒᛉᛟᛨᚺᛗᛨᚠᛗᛨᛇᚱᛦᛨᚺᛨᚲᚱᛃᛗᛨᚲᛉᛒᛨᛗᚹᚱᛨᛖᚠᛋᛨᛚᛈᚨᚱᛒᛨᛗᚹᚱᛨᚢᚠᛒᛧᛨᚺᛈᛨᛗᚱᛇᛋᛉᛒᚠᛃᛨᛇᚠᛈᚺᛋᛚᛃᚠᛗᚺᛉᛈᛨᛣᛉᛚᛨᛗᛒᛣᛨᛗᛉᛨᚲᚺᚷᛚᛒᚱᛨᚱᛜᚱᛒᛣᛗᚹᚺᛈᚷᛥᛨᚢᛚᛗᛨᛗᚹᚱᛒᚱᛨᚠᛒᚱᛨᛖᛉᛨᛇᚠᛈᛣᛨᚲᚠᚦᛗᛉᛒᛖᛨᛗᚹᚠᛗᛨᛣᛉᛚᛨᛈᚱᛜᚱᛒᛨᛗᚠᛁᚱᛨᛈᚱᚱᚨᛃᚱᛖᛖᛨᛒᚺᛖᛁᛖᛧ"""
47
48 mapping = build_mapping(plaintext, ciphertext)
49 print_mapping(mapping)符文映射构建成功后,就可以开始进行魔方的翻转构造和路径选择了
首先可以留意到靶机存在时间限制,必须在40s内获取到flag,否则就得重新连接靶机,所有状态都会打乱(每次的魔方都是随机的),所以如果频繁的跟服务器进行交互会浪费掉大量的时间,因此我们必须将大部分的计算量放在本地,运算完成后再一次性将结果发送给服务器让其进行翻转,最后在尝试命令构造即可。
核心的解题思路如下:
- 进入靶机后,选择翻转模式,通过翻转去构建出这个魔方的每一个面都有着哪些字符,都分别在什么位置
- 获取魔方的移动逻辑,在本地脚本中构建一个等效的魔方,还原所有操作
- 利用算法规划将合适的字符翻转到正面
- 最后进行命令构造
第一步很简单,对于魔方六面的探测,最简单的方法就是利用R和C操作,每行每列分别翻转4次(正反各两次),即可确认六面的所有内容
第二步也不难,通过执行RCF三个操作就可以确定他们的翻转方向和对应的位置,可以通过手动操作来确定魔方的翻转操作具体的效果,也可以利用脚本对每次翻转连续执行4次进行4种状态的观察,利用4-cycle的思路去确定每个位置的魔方格子对应的闭合环,即四种状态a0,a1,a2,a3,a0时X在格子A,a1时X在格子B,a2时X在格子C,a3时X在格子D,由于魔方每行四次翻转即可恢复原样,因此可以形成A-B-C-D-A的闭合,从而确定魔方的翻转操作
第三步通过算法,比如BFS求解魔方的操作序列,对于已经翻转到正面的格子,要保证其不被影响,始终停留在正面。当已固定的格子较多导致单步操作都无法保持不变时,还可以利用交换子(commutator)[m1,m2]=m1·m2·m1’·m2’ 作为复合操作,它只影响少量位置,从而在不破坏已有布局的前提下继续移动新的格子
第四步则需要我们考虑去构造什么样的命令,通过DFS算法找到命令的执行路径,从列不变开始,挑选第一行的任意字符,然后逐步切换纵横移动,当前面的思路实现后,我们可以进行一些简单的构造尝试,比如执行ls查看当前目录的内容,这里通过ls可以发现flag根本不在当前目录,而通过符文映射我们也会发现没有/这个符号,那就没办法直接去到根目录下查看,所以只能通过构造cd .. 并用;进行命令拼接,从而查看上一级目录的内容,拼接的命令为cd ..;ls,而到了上一级目录后我们就会发现在该目录下存在flag文件,所以只需要参考刚刚的拼接命令,再次拼接查看flag的命令即可,预期的解法有两种,分别是cd ..;nl flag和cd ..;cat flag,为了保证一定有可执行解,实际上我在源码里预设了cd ..;nl flag的路径,不过cd ..;cat flag也是可以构造的(存在一定偶然性,用nl的准确率会高点
)
最终exp如下,这里我直接用了全符文映射表,但是只靠附件给的量也是能构建出来的
1#!/usr/bin/env python3
2import hashlib
3import itertools
4import re
5import socket
6import ssl
7import string
8import sys
9import time
10from collections import deque
11
12rmap = {
13 'a':'ᚠ','b':'ᚢ','c':'ᚦ','d':'ᚨ','e':'ᚱ','f':'ᚲ','g':'ᚷ','h':'ᚹ',
14 'i':'ᚺ','j':'ᚾ','k':'ᛁ','l':'ᛃ','m':'ᛇ','n':'ᛈ','o':'ᛉ','p':'ᛋ',
15 'q':'ᛏ','r':'ᛒ','s':'ᛖ','t':'ᛗ','u':'ᛚ','v':'ᛜ','w':'ᛟ','x':'ᛞ',
16 'y':'ᛣ','z':'ᛤ',',':'ᛥ',';':'ᛦ','.':'ᛧ',' ':'ᛨ','?':'ᛩ',
17 '{':'ᚪ','}':'ᚫ','-':'ᚬ','"':'ᚭ','!':'ᚮ',"'":'ᚯ',
18}
19rrev = {v: k for k, v in rmap.items()}
20
21def decode(text):
22 return "".join(rrev.get(c, c) for c in text)
23
24def solve_pow(prefix):
25 charset = string.ascii_letters + string.digits
26 print(f'[*] Solving PoW: sha256("{prefix}" + S)[:6] == "000000"')
27 t0, cnt = time.time(), 0
28 for length in range(1, 20):
29 for combo in itertools.product(charset, repeat=length):
30 s = "".join(combo)
31 if hashlib.sha256((prefix + s).encode()).hexdigest()[:6] == "000000":
32 print(f'[+] PoW solved: S="{s}" ({cnt} attempts, {time.time()-t0:.2f}s)')
33 return s
34 cnt += 1
35 if cnt % 5_000_000 == 0:
36 e = time.time() - t0
37 print(f" [pow] {cnt/1e6:.1f}M attempts, {cnt/e/1e6:.2f}M/s, {e:.1f}s")
38
39cmd = "cd ..;nl flag"
40sz = 5
41fnames = ["F", "R", "B", "L", "U", "D"]
42all_moves = [f"{p}{i}{s}" for p in "RCF" for i in range(1, 6) for s in ("", "'")]
43allpos = [(f, r, c) for f in fnames for r in range(sz) for c in range(sz)]
44orbsz = {(0,0):6, (0,1):24, (0,2):24, (1,1):24, (1,2):48, (2,2):24}
45arrows = {"up": b"\x1b[A", "down": b"\x1b[B", "right": b"\x1b[C", "left": b"\x1b[D"}
46ansi_re = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
47grid_re = re.compile(
48 r'([\u16A0-\u16EB](?:\s+[\u16A0-\u16EB]){4})'
49 r'\s+\|\s+'
50 r'([\u16A0-\u16EB](?:\s+[\u16A0-\u16EB]){4})'
51)
52mapall_seq = (
53 [f"R{i}" for i in range(1, 6)] + [f"R{i}" for i in range(1, 6)] +
54 [f"R{i}'" for i in range(1, 6)] + [f"R{i}'" for i in range(1, 6)] +
55 [f"C{i}'" for i in range(1, 6)] + [f"C{i}" for i in range(1, 6)] +
56 [f"C{i}" for i in range(1, 6)] + [f"C{i}'" for i in range(1, 6)]
57)
58mapall_idx = {4: ("B", False), 9: ("L", False), 24: ("U", True), 34: ("D", True)}
59
60def orb(c, r):
61 a, b = abs(c - 2), abs(r - 2)
62 return (min(a, b), max(a, b))
63
64def inv(m):
65 return m[:-1] if m.endswith("'") else m + "'"
66
67class Conn:
68 def __init__(self, host, port, use_ssl=False):
69 raw = socket.create_connection((host, port), timeout=30)
70 if use_ssl:
71 ctx = ssl.create_default_context()
72 ctx.check_hostname = False
73 ctx.verify_mode = ssl.CERT_NONE
74 self.s = ctx.wrap_socket(raw, server_hostname=host)
75 else:
76 self.s = raw
77 self.buf = b""
78
79 def recvuntil(self, delim, timeout=15):
80 dl = time.time() + timeout
81 while delim not in self.buf:
82 rem = dl - time.time()
83 if rem <= 0: break
84 self.s.settimeout(max(rem, 0.1))
85 try:
86 d = self.s.recv(4096)
87 if not d: break
88 self.buf += d
89 except socket.timeout:
90 break
91 i = self.buf.find(delim)
92 if i >= 0:
93 r = self.buf[:i + len(delim)]
94 self.buf = self.buf[i + len(delim):]
95 return r
96 r = self.buf
97 self.buf = b""
98 return r
99
100 def send(self, data):
101 self.s.sendall(data + b"\n")
102
103 def close(self):
104 self.s.close()
105
106def parse_grid(raw):
107 clean = ansi_re.sub("", raw)
108 fr, rr = [], []
109 for line in clean.splitlines():
110 m = grid_re.search(line)
111 if m:
112 fr.append([decode(c) for c in m.group(1).split()])
113 rr.append([decode(c) for c in m.group(2).split()])
114 return (fr[-sz:], rr[-sz:]) if len(fr) >= sz else (None, None)
115
116def batch_send(r, mvs):
117 if mvs:
118 r.s.sendall(b"\n".join(m.encode() for m in mvs) + b"\n")
119
120def batch_recv(r, n):
121 return [r.recvuntil(b"move>", timeout=10).decode(errors="replace") for _ in range(n)]
122
123def read_faces(resps, front, right):
124 st = {"F": front, "R": right}
125 for idx, (name, use_f) in mapall_idx.items():
126 f, r = parse_grid(resps[idx])
127 g = f if use_f else r
128 if g is None: return None
129 st[name] = g
130 return st
131
132def cyc(t):
133 return (t[1], t[2], t[3], t[0])
134
135def deduce_perm(s0, s1, s2, s3):
136 perm, used = {}, set()
137 keys = {}
138 for p in allpos:
139 fn, r, c = p
140 keys[p] = (s0[fn][r][c], s1[fn][r][c], s2[fn][r][c], s3[fn][r][c])
141 for p in allpos:
142 k = keys[p]
143 if k[0] == k[1] == k[2] == k[3]:
144 perm[p] = p
145 used.add(p)
146 di = {}
147 for p in allpos:
148 if p in used: continue
149 di.setdefault(cyc(keys[p]), []).append(p)
150 amb = []
151 for p in allpos:
152 if p in perm: continue
153 k = keys[p]
154 if k[0] == k[1] == k[2] == k[3]: continue
155 cands = [q for q in di.get(k, []) if q not in used]
156 if len(cands) == 1:
157 perm[p] = cands[0]
158 used.add(cands[0])
159 elif cands:
160 amb.append((p, cands))
161 for p, cands in amb:
162 if p in perm: continue
163 fn, r, c = p
164 o = orb(c, r)
165 oc = [q for q in cands if q not in used and orb(q[2], q[1]) == o]
166 if oc:
167 perm[p] = oc[0]
168 used.add(oc[0])
169 for p in allpos:
170 if p not in perm:
171 perm[p] = p
172 return perm
173
174def probe_perms(conn, s0):
175 perms = {}
176 seq = list(mapall_seq)
177 for px in "RCF":
178 for i in range(1, 6):
179 base = f"{px}{i}"
180 ib = inv(base)
181 batch = [base] + seq + [base] + seq + [base] + seq + [base] + seq + [ib] * 4
182 batch_send(conn, batch)
183 resps = batch_recv(conn, 168)
184 states, ok = [], True
185 for j in range(3):
186 off = j * 41
187 f, r = parse_grid(resps[off])
188 if f is None: ok = False; break
189 st = read_faces(resps[off+1:off+41], f, r)
190 if st is None: ok = False; break
191 states.append(st)
192 if not ok or len(states) < 3: continue
193 pm = deduce_perm(s0, states[0], states[1], states[2])
194 perms[base] = pm
195 perms[base + "'"] = {v: k for k, v in pm.items()}
196 return perms
197
198def build_comms(perms):
199 comms = {}
200 for m1 in all_moves:
201 m1i = inv(m1)
202 p1, p1i = perms[m1], perms[m1i]
203 for m2 in all_moves:
204 if m2 == m1 or m2 == m1i: continue
205 m2i = inv(m2)
206 p2, p2i = perms[m2], perms[m2i]
207 res, nontrivial = {}, False
208 for p in allpos:
209 d = p2i[p1i[p2[p1[p]]]]
210 res[p] = d
211 if d != p: nontrivial = True
212 if nontrivial:
213 comms[(m1, m2)] = (res, [m1, m2, m1i, m2i])
214 return comms
215
216def safe_ops(frozen, perms, comms=None):
217 ops = [(perms[t], [t]) for t in all_moves if all(perms[t][p] == p for p in frozen)]
218 if comms:
219 ops += [(pm, mv) for (_, _), (pm, mv) in comms.items()
220 if all(pm[p] == p for p in frozen)]
221 return ops
222
223def bfs(start, goal, ops):
224 if start == goal: return []
225 vis = {start: None}
226 q = deque([start])
227 while q:
228 cur = q.popleft()
229 for pm, ms in ops:
230 nxt = pm[cur]
231 if nxt not in vis:
232 vis[nxt] = (cur, ms)
233 if nxt == goal:
234 path, n = [], nxt
235 while vis[n] is not None:
236 prev, m = vis[n]
237 path = list(m) + path
238 n = prev
239 return path
240 q.append(nxt)
241 return None
242
243def copy_state(st):
244 return {n: [row[:] for row in st[n]] for n in fnames}
245
246def apply_moves(st, tokens, perms):
247 for tok in tokens:
248 nf = {n: [[None] * sz for _ in range(sz)] for n in fnames}
249 for (sf, sr, sc), (df, dr, dc) in perms[tok].items():
250 nf[df][dr][dc] = st[sf][sr][sc]
251 for n in fnames:
252 st[n] = nf[n]
253
254def solve(state, path, perms, comms):
255 orderings = [
256 sorted(path, key=lambda x: orbsz[orb(x[1][0], x[1][1])]),
257 sorted(path, key=lambda x: -orbsz[orb(x[1][0], x[1][1])]),
258 list(path), list(reversed(path)),
259 sorted(path, key=lambda x: (x[1][0], orbsz[orb(x[1][0], x[1][1])])),
260 sorted(path, key=lambda x: (orbsz[orb(x[1][0], x[1][1])], x[1][0])),
261 sorted(path, key=lambda x: x[1][1]),
262 ]
263 seen = set()
264 for order in orderings:
265 k = tuple(order)
266 if k in seen: continue
267 seen.add(k)
268 sim = copy_state(state)
269 frozen, seq, ok = set(), [], True
270 for ch, (tc, tr) in order:
271 tp = ("F", tr, tc)
272 to = orb(tc, tr)
273 cands = [(f, r, c) for f in fnames for r in range(sz) for c in range(sz)
274 if sim[f][r][c] == ch and orb(c, r) == to]
275 if not cands: ok = False; break
276 cands.sort(key=lambda p: 0 if p[0] == 'F' else 1)
277 found = False
278 for uc in [False, True]:
279 ops = safe_ops(frozen, perms, comms if uc else None)
280 for cd in cands:
281 res = bfs(cd, tp, ops)
282 if res is not None:
283 apply_moves(sim, res, perms)
284 seq.extend(res)
285 frozen.add(tp)
286 found = True
287 break
288 if found: break
289 if not found: ok = False; break
290 if not ok: continue
291 if all(sim["F"][r][c] == ch for ch, (c, r) in path):
292 return seq
293 return None
294
295def find_paths(state):
296 needed = {}
297 for ch in cmd:
298 needed[ch] = needed.get(ch, 0) + 1
299 avail = {}
300 for ch in needed:
301 av = {}
302 for fn in fnames:
303 for r in range(sz):
304 for c in range(sz):
305 if state[fn][r][c] == ch:
306 o = orb(c, r)
307 if o != (0, 0):
308 av[o] = av.get(o, 0) + 1
309 avail[ch] = av
310 paths, used = [], set()
311 def dfs(idx, cx, cy, hz, cur):
312 if idx == len(cmd):
313 paths.append(cur[:])
314 return
315 ch = cmd[idx]
316 if ch not in avail: return
317 for c in range(sz):
318 for r in range(sz):
319 if hz and r != cy: continue
320 if not hz and c != cx: continue
321 if (c, r) in used: continue
322 o = orb(c, r)
323 if o == (0, 0): continue
324 if avail[ch].get(o, 0) == 0: continue
325 used.add((c, r))
326 cur.append((ch, (c, r)))
327 dfs(idx + 1, c, r, not hz, cur)
328 cur.pop()
329 used.discard((c, r))
330 dfs(0, 0, 0, True, [])
331 paths.sort(key=lambda p: (
332 -len(set(c for _, (c, _) in p)),
333 -len(set(r for _, (_, r) in p)),
334 sum(orbsz[orb(c, r)] for _, (c, r) in p)
335 ))
336 return paths
337
338def drain(conn, timeout=0.6):
339 chunks = [conn.buf]
340 conn.buf = b""
341 dl = time.time() + timeout
342 while True:
343 rem = dl - time.time()
344 if rem <= 0: break
345 conn.s.settimeout(max(rem, 0.05))
346 try:
347 d = conn.s.recv(4096)
348 if not d: break
349 chunks.append(d)
350 dl = time.time() + 0.3
351 except socket.timeout:
352 break
353 conn.buf = b""
354 return b"".join(chunks)
355
356def key_wait(conn, key):
357 conn.s.sendall(key)
358 conn.recvuntil(b"Cursor:", timeout=3)
359 conn.recvuntil(b"\n", timeout=1)
360 conn.recvuntil(b"\n", timeout=1)
361
362def navigate(conn, coords):
363 acts, cx, cy, hz = [], 0, 0, True
364 for tx, ty in coords:
365 if hz:
366 rd, ld = (tx - cx) % sz, (cx - tx) % sz
367 acts.append(("right", rd) if rd <= ld else ("left", ld))
368 cx = tx
369 else:
370 dd, ud = (ty - cy) % sz, (cy - ty) % sz
371 acts.append(("down", dd) if dd <= ud else ("up", ud))
372 cy = ty
373 hz = not hz
374 print(f" [nav] {len(coords)} chars, {sum(c for _, c in acts)} key presses")
375 conn.send(b"2")
376 got = conn.recvuntil(b"Cursor:", timeout=8)
377 if b"Cursor:" not in got:
378 return "(error)", "(error)"
379 conn.recvuntil(b"\n", timeout=1)
380 conn.recvuntil(b"\n", timeout=1)
381 for si, (d, n) in enumerate(acts):
382 for _ in range(n):
383 key_wait(conn, arrows[d])
384 conn.s.sendall(b"\r")
385 conn.recvuntil(b"Cursor:", timeout=3)
386 conn.recvuntil(b"\n", timeout=1)
387 sp = conn.recvuntil(b"\n", timeout=1)
388 print(f" [nav] step {si+1}/{len(acts)}: {ansi_re.sub('', sp.decode(errors='replace')).strip()}")
389 conn.s.sendall(b"x")
390 raw = drain(conn, timeout=4.0)
391 text = ansi_re.sub("", raw.decode(errors="replace"))
392 runes, plain, out = [], [], False
393 for line in text.splitlines():
394 s = line.strip()
395 if not s: continue
396 if "activating" in s: out = True; continue
397 if out:
398 if any(k in s for k in ["Enter", "continue", "Press", "hums"]): break
399 runes.append(s)
400 plain.append(decode(s))
401 conn.s.sendall(b"\r")
402 time.sleep(0.3)
403 return "\n".join(runes) or "(no output)", "\n".join(plain) or "(no output)"
404
405def main():
406 if len(sys.argv) < 3:
407 print(f"Usage: {sys.argv[0]} <host> <port> [--ssl]")
408 sys.exit(1)
409 host, port = sys.argv[1], int(sys.argv[2])
410 use_ssl = "--ssl" in sys.argv
411 t0 = time.time()
412 conn = Conn(host, port, use_ssl)
413
414 raw = conn.recvuntil(b"S: ", timeout=15).decode(errors="replace")
415 m = re.search(r'sha256\("([^"]+)"\s*\+\s*S\)', raw)
416 if not m:
417 print("[-] bad pow"); conn.close(); return
418 ans = solve_pow(m.group(1))
419 if not ans:
420 print("[-] pow failed"); conn.close(); return
421 conn.send(ans.encode())
422 data = conn.recvuntil(b"> ", timeout=15).decode(errors="replace")
423 if "OK" not in data:
424 print("[-] pow rejected"); conn.close(); return
425
426 front, right = parse_grid(data)
427 if front is None:
428 print("[-] parse failed"); conn.close(); return
429 conn.send(b"1")
430 conn.recvuntil(b"move>", timeout=10)
431 batch_send(conn, mapall_seq)
432 resps = batch_recv(conn, 40)
433 s0 = read_faces(resps, front, right)
434 if s0 is None:
435 print("[-] faces failed"); conn.close(); return
436
437 perms = probe_perms(conn, s0)
438 if len(perms) < 30:
439 print(f"[-] only {len(perms)} perms"); conn.close(); return
440
441 comms = build_comms(perms)
442 paths = find_paths(s0)
443 if not paths:
444 print("[-] no paths"); conn.close(); return
445
446 sol, used_path, tried = None, None, 0
447 for p in paths[:500]:
448 if time.time() - t0 > 30: break
449 tried += 1
450 ms = solve(s0, p, perms, comms)
451 if ms is not None:
452 sol, used_path = ms, p
453 break
454 if sol is None:
455 print(f"[-] no solution ({tried} tried)"); conn.close(); return
456 print(f"[+] Solution: {len(sol)} moves, tried {tried} paths ({time.time()-t0:.1f}s)")
457
458 batch_send(conn, sol)
459 batch_recv(conn, len(sol))
460 conn.send(b"q")
461 conn.recvuntil(b"> ", timeout=10)
462
463 coords = [(c, r) for _, (c, r) in used_path]
464 rout, pout = navigate(conn, coords)
465 elapsed = time.time() - t0
466
467 print(f"\n{'='*50}")
468 print(f"Moves: {len(sol)} | Time: {elapsed:.1f}s")
469 print(f"\nRune:\n{rout}")
470 print(f"\nPlain:\n{pout}")
471 print(f"{'='*50}")
472 try:
473 conn.send(b"q")
474 except Exception:
475 pass
476 conn.close()
477
478if __name__ == "__main__":
479 main()
Comments will be available soon.