LilCTF 2025 Ekko_note 详细思路及复现
LilCTF 2025 Ekko_note 详细思路及复现
说来惭愧,除了这道就没出啥题了
题目信息
Ekko_note
时间刺客Ekko成功当上了某上市公司的老板。于是他让员工给他写一个只有他能用的RCE接口…… 但是,这个员工写的代码好像有点问题?
题目分析
写的这些分析才是关键,后边的解题步骤没有啥营养
用户侧
我们首先从用服务端侧分析页面逻辑
该页面为一个存在登录注册以及找回密码功能的命令执行接口。直接注册为普通用户,存在一个命令执行页面。但是该页面只有当现实时间为2066年时才可使用。同时通过页面得知admin用户可以在后台调整时间api的地址。
基于上述分析,我们可以得出初步解题步骤
使用admin用户登录,修改时间api地址到一个伪造的时间api接口上,其时间设为2066年,使命令执行页面可以执行命令,最终获得flag。
服务端侧
首先看这一部分
1 | SERVER_START_TIME = time.time() |
定义了一个变量SERVER_START_TIME
,他的值为time.time()
,该函数用来获取当前时间的时间戳,返回一个浮点数,单位是秒,就像1692252287.123456
这样
之后引用了random
模块,这是Python中的伪随机的一个模块,关于伪随机就是同一个种子生成完全相同的随机数序列。
再之后使用random.seed()
来初始化了伪随机数生成器的种子。
这一段代码就造成了一个漏洞点
伪随机数生成器的种子是SERVER_START_TIME这个变量的值
也就是说,我们只要获得了SERVER_START_TIME
这个变量的值,即可伪造出相同的伪随机数序列,使其生成的随机数变得可预测
再看这一个
1 | admin_super_strong_password = token_urlsafe() |
1 | if not User.query.filter_by(username='admin').first(): |
第一条代码是在前边定义了一个变量admin_super_strong_password
,他的值是使用token_urlsafe()
方法随机生成的一个字符串。token_urlsafe()
这个方法是真随机,所以说我们没办法预测,并没有什么漏洞点
接着看一下初始化admin用户的这一段代码。
可以看到一些信息,admin用户的邮箱为admin@example.com
,他的密码是之前说的token_urlsafe()生成的真随机字符串,所以说我们无从下手,只是从这段代码中找到了一个关键信息,就是admin用户的注册邮箱为admin@example.com
,这对我们之后的利用有很大的帮助
再看一条代码
1 | time_api = db.Column(db.String(200), default='https://api.uuni.cn//api/time') |
这一行代码是定义 User 表的一个字段,可以得知time_api
的值默认为https://api.uuni.cn//api/time
这个接口,我们访问这个接口,发现是一个返回当前时间的一个接口,格式如下:
1 | {"date":"2025-08-17 03:21:50","weekday":"星期日","timestamp":1755372110,"remark":"一些推广信息"} |
在程序中,这个字段用于
1 | user = User.query.get(session['user_id']) |
也就是 从用户指定的 URL 获取时间数据,然后判断
1 | current_time.year >= 2066 |
在这块代码中没有任何限制和校验,暴漏出一个问题,并没有校验返回的时间是否真实,如果用户伪造了一个假的时间api接口,将返回的时间伪造成2066年之后,即可绕过current_time.year >= 2066
这一判断
再看一个很关键的代码
1 |
|
这一段定义了一个路由/server_info
,然后通过@login_required
限制了只允许登录用户访问
接下来是关键,该页面返回了server_start_time
和current_time
这两个字段,其中server_start_time
的值是SERVER_START_TIME
变量的值,我们通过分析之前的代码可以得知,这个值也被传入到random.seed()
中作为了初始化伪随机数生成器的种子。
也就是说,我们得知了server_start_time
的值,就是得到了这整个代码的伪随机数种子,使其代码中用到伪随机数生成器的值可被预测,这是这道题中利用的关键。
再最后看一段代码,这是最关键的
1 |
|
写到这快四点了,有点困了
接着看,首先定义了一个路由/forgot_password
,是一个找回密码的界面,用户提交一个邮箱,然后系统查找对应的用户。但是邮件发送功能并没有开放,我们无法获得token的值。
接下来是的分析关键
生成一个Token用作密码重置
这里边使用了uuid8
来进行生成,传入了一个参数a
,他的值是padding(user.username)
生成的整数参数padding()
这个方法在给出的代码有定义,会把用户名截断、然后填充成整数。
关于这个uuid8,一开始以为是出题人自定义的生成方法,因为在当前python的uuid库中只有uuid1
,uuid3``,uuid4
,uuid5
这四种生成uuid的方法,找不到uuid8这种方法
在困扰很久之后题目上了一个提示:
1 | 艾克喜欢新东西…… 好像他的员工也是这样的。uuid.uuid8()不是所有python版本都有哦~ |
经过搜索后找到了**uuid.uuid8()**的相关文档:

这是python的官方文档,地址是https://docs.python.org/zh-cn/3.14/library/uuid.html#uuid.uuid8
我们可以看到uuid.uuid8()
这个方法是在python3.14中新添加,但是现在python最新的正式版版本为3.13,3.14还是预览版,所我们正常安装的python中并没有uuid8,切换到3.14即可使用uuid8来生成token。
我们首先看一下文档里对uuid.uuid8()的相关定义
uuid.uuid8()
存在三个参数,分别为a
,b
,c
,其中传入的参数为空时,这三个参数将会被替换成伪随机数
文档中提示,默认情况下,a、b 和 c 不是由加密安全伪随机数生成器(CSPRNG)生成的。
也就是说,这里的伪随机数生成器,使用的就是代码在开头通过random.seed()
定义种子的那个伪随机数生成器为同一个生成器。
我们接着看代码中是怎么生成的token,代码中使用了uuid.uuid8()
这个方法,他只传入了a这个值,b,c都是空会被伪随机数生成器生产的随机整数替代。
关于a这个值的产生方法,在代码中已经给出,在用户名已知的情况下,a的值是固定的,并且b和c的值是可以被预测的
最终最终,实现了对uuid.uuid8()
方法a,b,c三个值的完全可控,即可伪造与服务器相同的token
攻击流程
好,分析了这么多,对于这道题的攻击方法已经很明确了:
首先注册一个普通用户,登录后访问/server_info
路由拿到SERVER_START_TIME
的值
然后使用admin@example.com
这个邮箱进行重置密码,在后端生成一个为admin用户重置密码的token
然后我们本地伪造这个token,因为a的值已知,生成b,c两个参数所用的伪随机数生成器的种子已知,在不改变随机序列的情况下,我们完全可以在本地伪装出一个完全相同的Token
使用这个Token重置admin用户的密码,即可成功登录admin用户后台
在后台可以修改时间api接口的地址
我们在公网部署一个页面,内容可以是固定的,只要页面返回的时间,大于2066年即可
然后我们回到命令执行页面,此时current_time.year >= 2066
满足,我们可以执行命令
os.system(command)
并不会回显,我们通过时间盲注的方式来用脚本获得flag
解题步骤
我们首先访问靶机,进入到了这个题目的主页

先点击右上角的注册按钮进入到注册页面

注册一个普通用户,邮箱可以随意填写,这个用户的用途就是登录之后方法/server_info
接口,没有别的用途

使用注册的用户进行登录

登录成功后跳转到仪表盘,有一个执行命令功能,但是不能用,只要在时间大于2066年的时候才可以用

我们访问/server_info
路由,可以看到成功获取到了我需要的SERVER_START_TIME
的值

我们先记录下来,等会伪造Token的是需要用到
1 | 1755376934.7671177 |
先把普通用户退出登录。

点击登录页面下方的忘记密码链接,跳转到忘记密码页面

在邮箱这里输入代码中生成admin用户所使用的邮箱,即admin@example.com

可以看到说token已经发送,实际上没有这个功能,我们也看不到token实际是啥

我们直接伪造一个token,SERVER_START_TIME = 1755376934.7671177
是我们在/server_info
路由中获得到的
padding()
这个方法我们直接复制代码中给的就可以,然后一模一样生成一个Token输出出来
我们生成的这个token,就跟服务端在后台生成的token是一样的
1 | import random, uuid |
运行这个代码后得到伪造的token

在重置密码界面输入伪造的token,然后重新为admin用户设置一个密码

重置成功后回到登录页面,使用admin用户和自己设置的密码进行登录

发现成功进入到了后台,并且比普通用户多了一个管理员设置页面

进入这个页面,发现这里可以自定义时间API的接口

我们写一个简单的页面,输出跟真实时间接口API一样的josn格式,只不过是把时间改到了2066年
1 | from flask import Flask, jsonify |
我们需要在一个可以被公网访问的机器上运行代码,使其可以题目靶机进行访问,这个访问过程是在服务器进行访问的,所以说这个假的接口需要部署在公网上

我们访问这个假的时间接口,发现可以成功访问了。返回的时间也是在2066年之后

我们复制这个假的接口地址粘贴到时间API设置的的文本框,并且进行保存,此时服务端获取的时间即是2066年,成功绕过了current_time.year >= 2066
的判断

来的命令执行的页面,发现已经可以执行命令了
但是输入的任何内容都没有回显

我们使用sleep来进行测试,发现命令生效了,说明命令可以正常执行,只是没有回显

这时候我们可以写一个脚本,根据时间延迟的这个特性,对字符进行逐个尝试,来穷举出完整的flag
1 | import requests |
运行脚本,需要等很长时间才能完整跑出flag
