一个简单的随想而已,一堆废话
0. 序章——梦想&执念
自从本人大二开始学习Solidity,我就一直想要自己做一个动态的靶机,而且也确实这么做了,在AuroraCTF 2023(我们学校的新生赛)我就已经做了几个和Sepolia Testnet交互的动态靶机,不过那个时候我才发现学校的CTFd靶场是不出网的…
虽然在此之后也是继续在学Solidity,同时还给某个比赛供了几道题,但是想做一个“不出网的动态靶机环境”一直悬在我心上,不过因为社团的事务还有各种比赛也是被硬控了许久
期间也是有参考国外的repo和文章,比如最出名最泛用的paradigm-ctf-infrastructure,SolidCTF还有Zellic的文章Hosting an Ethereum CTF Challenge, the Easy Way | Zellic — Research,但是发现它们基本都是出网的,但是我也没有钱去租一个服务器专门去为学校搞Blockchain CTF,因此一直搁置…
1. 梦的开始——RemedyCTF 2025
今年2月有幸和大哥们打了一次RemedyCTF,当时从中学到很多东西,也在大哥们的提供的思路下完成了一部分exp的编写,也算是出了绵薄之力吧www
比赛结束后官方放出了他们所有题目的源文件:Hexens/remedy-ctf-2025,而这又让我重新燃起了希望:说不定这一次就能完成愿望呢?
事实证明,现实和梦想的差距是巨大的:

看不出来有什么问题?CTFd作为一个非常古早的CTF靶场平台,它的动态靶机功能都是由插件实现的,而这个插件是不支持docker-compose的;除此之外,这个插件只能上传一个docker image,只能frp 1个端口;最大的问题是:这个infra是部署在公网上的,它出网,而我们学校的CTFd它不出网…
事已至此,是时候开始自己搓一个靶机了…
2. 基础环境配置——多阶段搭建
虽然RemedyCTF的infra用不上,但是参考价值还是满满的,因此我在搭建的时候主要都是参考了Remedy和Paradigm两个比赛的infra,同时测试用的题目实例是直接用的Remedy的Diamond Heist
首先确认了基础的配置应该是怎么样的:
- 前端
- Flask进行HTML页面渲染
- JavaScript向API路由发送请求,使用Jquery
- BootstrapCSS简单美化一下页面
(实际完全没有美化)
- 后端
- Flask路由,包括API路由和用户使用的RPC路由
- Web3py和私链进行交互
- 私链
- Foundry Anvil作为私链
- Forge进行Solidity的编译和脚本运行
其实我本人还想多加一个IDE的路由的,就是希望像Remix那样能直接在靶机上编写简单的合约并且部署和交互,因此还想着整个Monaco Editor,所以实际上还有一个Nodejs,不过我不确定自己之后是否会真的去实现这一点,所以…到时候再说吧www
OK,接下来就是使用Dockerfile进行多阶段的搭建了,不过由于CTFd的限制,我需要将所有的环境集成在一个环境下,而这个环境要保证Foundry可以运行(我是真的出了这个问题,参考forge fails with ‘GLIBC_2.33’ not found (required by forge) in github action CI · Issue #3827 · foundry-rs/foundry),最后是从RemedyCTF参考后确认使用python:3.11.6-slim
是完全OK的,因此我们最后的环境容器肯定是使用的它
剩下的有关Foundry的下载和Foundry项目的预搭建自然是从RemedyCTF那里照搬的,而前面提到的Flask+Anvil的基础配置则需要使用一个多进程管理器去进行管理,我这里使用的是supervisord,所以最后的Dockerfile长这样:
1 | # 第一阶段:构建Monaco资源 |
我不保证这个Dockerfile一定是最简洁的(因为有一些组件我不确定用没用到,比如socat可能是没用到的的),但是这样能跑,这就已经win了(笑)
需要注意的是,如果使用我这冗杂的靶机,且对私链的硬分叉有要求,需要你自行手动更改第62行里面对anvil hardfork的参数设置,不然默认是latest
还有一点,由于我们学校的靶机不出网,因此要提前在Releases · ethereum/solidity下载好对应的solc二进制文件,我这里是选择放在
/usr/bin/solc
下,不过后面还需要指定solc路径,所以无所谓
3. 靶机的核心——Flask&Web3py
接下来就是后端交互的核心逻辑了,首先我们先写好和Anvil交互相关的代码
3.1 (非常冗杂的)靶机初始化
由于我们前面在使用supervisord管理anvil进程的时候并没有指定助记词,因此Anvil会自动使用默认的助记词test test test test test test test test test test test junk
,这样就会导致生成的钱包是固定的,而这有很大可能会导致问题,因此我们希望是能生成一个随机的钱包,每次使用靶机用户使用的钱包都是不一样的
这怎么办呢?我们能发现Anvil为了方便开发者调试,它自带了一些非常强大的API方法,比如anvil_setBalance
,它可以调整Anvil链上指定地址的余额,而我自己的解决方法正是使用这个:
1 | from web3 import Web3 |
如果有看过前面的两个CTF infra的话就能发现我根据助记词获取地址的代码明显是在抄这两个infra的(笑)
看起来很乱,所以我将获取账户的几个函数都放在另外一个Python文件了,实际上也就是照搬的remedy-ctf-2025/paradigm-ctf-infrastructure/paradigmctf.py/ctf_server/types/_init_.py at main · Hexens/remedy-ctf-2025
至于题目合约的初始化部署deploy
也是照搬的remedy-ctf-2025/paradigm-ctf-infrastructure/paradigmctf.py/ctf_launchers/utils.py at main · Hexens/remedy-ctf-2025,唯一的差别就是运行forge的时候多加了一个--use /usr/bin/solc
的参数,表示使用指定的solc二进制文件进行编译,这里就不放代码了,有点长
如果不指定solc路径,则forge会自动尝试联网下载
foundry.toml
里指定版本的solc,而这对于我们不出网的靶机是致命的(你下不下来啊www)
初始化相比更新实例只是多了对账号余额的操作,因此我们可以顺带写出创建新实例的代码:
1 | def new_instance(): |
3.2 封印!——RPC的封禁策略
刚刚我们提到Anvil提供了许多强大的API方法,虽然大部分人应该都是正常解题,但是总会有人想打非预期,所以只能下点狠料了(虽然一般情况是直接返回错误说方法不被允许,但是我就是喜欢封禁ο(=•ω<=)ρ⌒☆)
策略很简单:Anvil提供的API方法为了和普通的Ethereum API方法区分,都是以anvil_***
格式进行命名的,所以只需要一点简单的字符串操作…
1 |
|
3.3 中间商不赚差价——端口数据转发
对于正常的数据,使用requests
库进行常规的端口转发即可:
1 | def forward_request(url_suffix=""): |
至此基本的Python后端部分已经结束了
4. 面子工程——HTML&JavaScript
美化这一方面我也不是专门搞前端的,随便找个Bootstrap美化一下显示框和按钮差不多得了(笑)核心主要还是和按钮绑定的JavaScript代码,fetch
肯定是要用的,然后有关按钮的启用和禁用可以用jQuery进行实现,总之内嵌的JS代码如下:
1 | <script> |
至于完整的HTML这里就不再展示了
5. 最终成果

至此回收了1年前的执念和各种伏笔,中途出现各种曲折(比如多阶段搭建就卡了一周多),但是最后也是收获颇丰,可以安心处理其它事了o(*≧▽≦)ツ┏━┓
其实这个靶机问题还是很多的,比如启动环境的时候因为要执行20次anvil_setBalance
的操作,导致整个环境的初始化需要近30秒(学校内部靶场实测得到的时间),而且前端太简陋了,不太好看,还有Dockerfile里面一堆可能是冗余的库和组件,都是很大的问题,不过现阶段的话能跑就是胜利,等之后给战队留一点题目的时候再慢慢调整就行了,至于Monaco和它的IDE功能…看我什么时候不懒了再说吧www
总之我整个的Docker源码刚刚push到Github上了:Cauliweak9/My-Single-Container-Docker-Templates-for-CTF,包括上一篇的Modbus靶机,反正能水一个(超级小的)项目谁不乐意呢?(笑)
- 本文作者: 9C±Void
- 本文链接: https://cauliweak9.github.io/2025/05/28/blockchain-docker-instance/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!