文章

课。

第一站 服务器程序的注入

注入是什么?

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

原题【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

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

image

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

image

代码审查

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

image

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 标准本身。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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\""}

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

1
2
3
4
5
Name: "admin":"true"

Registered at: 1744793493

You are user

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

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

解释分解:

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

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

    • 这指的是程序在处理字符串时会对双引号进行转义。在代码中,这一逻辑通过 StringChecker 命名空间中的 escape 函数实现。
    1
    2
    3
    4
    5
    6
    7
    8
    
    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 编码可能允许这种操作:
    1
    2
    3
    4
    
    if (json[idx] & 0x80) {
        result.push_back(json[idx]);
        idx++;
    }
    

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

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

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

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

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

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

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

代码段分析:

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

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

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

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

image

本文由作者按照 CC BY 4.0 进行授权

© Dignite. 保留部分权利。 由  提供CDN加速。

浙ICP备2023032699号 | 使用 Jekyll 主题 Chirpy