课。

2614 字
13 分钟
课。

第一站 服务器程序的注入#

注入是什么?#

注入是指通过恶意输入或操作,将不被预期的代码或命令插入到程序中,从而造成安全漏洞或执行未授权的操作。

原题【CTF】Web#

T573673 【CTF】Web - 洛谷

题目背景#

本题模拟一个包含账户系统的服务器。

对 Linux 和 MacOS 用户:不必担心,本题实际上并不需要运行程序。运行程序只是为了更直观地模拟。

题目描述#

一句话题意#

注册为管理员即可通过。

完整题意#

我们提供了一个基于 Windows 的服务器的代码。你的目标是:注册一个管理员账户。

请在附件中下载代码文件,编译运行(请链接 ws2_32 库)。或者也可以直接下载编译好的程序运行。

该程序将会在 localhost:4080 运行一个服务器。这个服务器提供的是题目背景里的注册服务。

你可以在浏览器中访问 localhost:4080/register/user 来注册用户 user

之后,服务器会根据用户名赋予权限——符合条件的用户将获得管理员身份。

注册完成之后,服务器会给这个账户返回一个整数,表示用户 ID。

为了方便查看,访问 localhost:4080/info/uid 查询 ID 为 uid 的用户的信息。

输入格式#

本题为提交答案题,无输入。

输出格式#

请输出能够获取管理员权限的注册 URL。

例如,若你发现能通过在浏览器内访问 http://localhost:4080/xxx/yyy/zzz 来得到管理员权限,则在本题提交 http://localhost:4080/xxx/yyy/zzz 即可。

说明/提示#

再次简述您要做的事情:

  • 在附件中下载源码编译(或直接下载成品 exe 文件),执行。
  • 程序将会启动一个用于模拟的服务器。您需要“黑”掉这个模拟服务器。
  • 接着您可以在浏览器中访问这个模拟服务器,尝试与这个模拟服务器互动。
  • 当您找到了某个能够“黑”掉模拟服务器的 URL(即网址)——也就是通过访问这个 URL,你可以注册成为管理员用户——提交这个 URL 到本题。

服务器接受 GBK 格式的编码,而不是 UTF-8。

您不必理会部分源代码,它们并不是考察的范围:

  • 开头(从 12 行到 231 行)的命名空间 MD5。它实现的是标准 MD5 哈希算法。
  • 末尾(从 414 行到 465 行)的两个函数 handle_requeststart_server。它们用于开启端口并处理请求。

运行测试#

双击即可打开服务器。

image
image

操作1:注册用户。localhost:4080/register/this_is_a_test_user

image
image

操作2:查看用户信息。localhost:4080/info/10000

image
image

代码审查#

我们先来看整体代码结构:

image
image

我们按上面的流程来看。首先是注册用户的请求:

// Register user
string registerUser(const string& name) {
int uid = database.size() + 10000;
InfoTable userInfo;
// STOP CRACKING MD5 PLEASE THIS IS NOT WHAT WE WANT
userInfo["admin"] = MD5Hash(name) == "f1d1c50c0e066b430a6d8ac375b75c57" ? "true" : "false";
userInfo["time"] = std::to_string(time(0));
userInfo["name"] = name;
// Store user information
database[uid] = toJson(userInfo);
return "Registered user. UID is " + std::to_string(uid);
}

这里传入一个 name 用户名,并且将用户名、注册时间、是否是管理员写入数据库中。这里说了不要试图通过逆向 MD5 的方式来获取管理员权限,那我们就不管了。

这里我们注意到一个 toJson 函数,要小心了:Json 是可以被注入的。

JSON 反序列化采用覆盖模式:指的是将 JSON 数据中的字段值直接覆盖目标对象中已有的同名字段值,而不是合并或保留原有数据。

JSON 标准(RFC 8259)并没有明确规定反序列化时采用覆盖模式或合并模式,它主要定义了 JSON 数据的格式和编码规则。JSON 的标准目标是确保数据的结构和格式一致性,而对于如何将 JSON 数据映射到程序中的对象(即反序列化)的具体行为,则由各编程语言的实现和库来决定。

因此,反序列化的行为,如是否采用覆盖模式或其他方式,通常取决于具体的 JSON 解析库或框架的设计,而不是 JSON 标准本身。

然后我们来看查看用户信息的部分:

// Execute user command
string info(const string& uid) {
int currentUser = atoi(uid.c_str());
if (database.find(currentUser) == database.end()) throw (string)"User not found";
InfoTable userInfo = toTable(database[currentUser]);
string result = "";
result += "Name: " + userInfo["name"] + "\n\n";
result += "Registered at: " + userInfo["time"] + "\n\n";
result += "You are ";
result += userInfo["admin"] == "true" ? "admin" : "user";
return result;
}

这里传入一个用户序号,其中我们可以看到,程序向数据库获取了当前用户的信息。是如何判断是否是管理员的呢?原来,获取到的 userInfo 中有一个 admin 的键,如果这个字符串是 true ,就被认为是管理员。

该程序对 Json 的处理:

// Convert info table to JSON format string
string toJson(const InfoTable& m) {
string result = "{";
bool first = true;
for (const auto& entry : m) {
if (first) first = false;
else result += ',';
result += '"' + escape(entry.first) + '"';
result += ':';
result += '"' + escape(entry.second) + '"';
}
return result + "}";
}

可以看到一个逻辑:如果碰到和之前相同的键名,它不会做出判断,而是直接覆盖掉。

这就为我们的攻击提供了契机:registerUser 中有在前面的 admin 值,而我们的 name 是在后面的位置。这意味着,我们可以给 name 字段动动手脚,让它也“生”出一个键值对来,然后覆盖掉之前的 admin 字段。所以,如果能够想办法在 JSON 字符串的末尾添加一个 "admin":"true" ,那就能覆盖掉 前面的 false 注册成为管理员。

继续#

直接拿 “admin”:“true” 当用户名是不行的,需要闭合前面的引号。

例如,我们现在正常注册的字符串是这样的:

{"admin": false, "time": 114514, "name": "example_name"}

如果我们直接拿 “admin”:“true” 当用户名:

{"admin": false, "time": 114514, "name": "\"admin\":\"true\""}

这里的引号会被直接转义掉,打不成预期的效果。

Name: "admin":"true"
Registered at: 1744793493
You are user

程序会对输入的字符串里的双引号进行转义。但是又能发现,程序遇到行转义,如果在引号前面加入一个字节比如 0x80 等特殊字节会跳过后面的字符不进 0xdf ,就会让程序略过后面的双引号不转义。另外, JSON 反序列化的时候遇到右花括号 } 就会直接停止运行。所以注入完之后立刻添加个 } 字符 就可以了。

让我们分解代码,并将其与提供的解释进行关联。我们可以识别解释中提到的操作在代码中发生的位置,并详细解释每个逻辑部分。

解释分解:#

该解释实质上是在描述程序中的安全漏洞,通过利用字符串中的转义序列来允许JSON 注入。以下是逐步分解:

  1. “程序会对输入的字符串里的双引号进行转义。”

    • 这指的是程序在处理字符串时会对双引号进行转义。在代码中,这一逻辑通过 StringChecker 命名空间中的 escape 函数实现。
    string escape(const string& input) {
    string result = "";
    for (auto c : input) {
    if (c == '\"' || c == '\\') result.push_back('\\');
    result.push_back(c);
    }
    return result;
    }

    这确保了输入中的双引号(")或反斜杠(\)会被正确转义,防止它们破坏 JSON 对象中的字符串结构或引发其他问题。

  2. “但是又能发现,程序遇到行转义,如果在引号前面加入一个字节比如 0x80 等特殊字节会跳过后面的字符不进 0xdf”

    • 这指的是利用特殊字符操作转义序列,可能绕过某些逻辑。toTable 函数中提到的GBK 编码可能允许这种操作:
    if (json[idx] & 0x80) {
    result.push_back(json[idx]);
    idx++;
    }

    这行代码检查字符是否是多字节字符编码(GBK,常用于中文字符编码)。这可能允许恶意输入通过利用编码绕过某些检查。

  3. “就会让程序略过后面的双引号不转义。”

    • 这再次与之前提到的转义逻辑相关。特殊字节(如 0x80)可能导致程序以不同方式解释某些字符,从而有效跳过或错误地解释诸如双引号之类的字符。
  4. “另外,JSON 反序列化的时候遇到右花括号 } 就会直接停止运行。”

    • 这一行描述了程序在 JSON 反序列化时遇到右花括号 } 后停止运行的情况。在 toTable 函数中,我们可以看到使用了 readChar('}') 函数,当遇到 JSON 对象的右花括号时,它会停止解析:
    readChar('}');

    这一点非常重要,因为攻击者可以通过注入 } 来提前结束 JSON 对象,从而导致潜在的注入或数据格式错误。

  5. “所以注入完之后立刻添加个 } 字符 就可以了。”

    • 这将所有内容联系起来:如果攻击者能够注入数据并强制程序提前终止 JSON 解析(通过 }),他们就可以控制系统的行为。如果攻击者成功地将内容注入到字符串中,可能通过额外的 } 来关闭 JSON 对象。

代码段分析:#

这些漏洞可能被利用的关键部分是在JSON 解析逻辑中:

  • toTable 函数中,执行了读取和解析 JSON 对象的逻辑,处理字符串和键值对。如果攻击者能够将 } 字符注入到字符串中,它可能会提前关闭 JSON 对象,从而绕过验证或引发意外行为:

    // readChar 用于跳过空格并检查字符如 '}'
    readChar('}');
  • toJson 函数将一个 InfoTable(键值对的映射)转换成一个 JSON 格式的字符串。如果攻击者能够插入特殊字节或以某种方式操控输入,跳过某些字符的转义,他们就可能操控最终的 JSON 输出。

这就是 GBK 编码注入抗转义,然后就会成功让双引号不被转义,然后闭合引号实现逃逸。接着还 会发现,处理 JSON 的时候还是允许使用单引号的,所以可以用单引号的版本: ‘admin’:‘true’ 。 最终答案是: http://localhost:4080/register/%df",'admin':'true'}

image
image

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
课。
https://www.0x3f.foo/posts/cybersecurity/
作者
Dignite
发布于
2025-04-16
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Dignite
When nothing goes right, go left.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
146
分类
5
标签
271
总字数
314,753
运行时长
0
最后活动
0 天前

目录