课。
第一站 服务器程序的注入
注入是什么?
注入是指通过恶意输入或操作,将不被预期的代码或命令插入到程序中,从而造成安全漏洞或执行未授权的操作。
原题【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_request
和start_server
。它们用于开启端口并处理请求。
运行测试
双击即可打开服务器。
操作1:注册用户。localhost:4080/register/this_is_a_test_user
操作2:查看用户信息。localhost:4080/info/10000
代码审查
我们先来看整体代码结构:
我们按上面的流程来看。首先是注册用户的请求:
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 注入。以下是逐步分解:
-
“程序会对输入的字符串里的双引号进行转义。”
- 这指的是程序在处理字符串时会对双引号进行转义。在代码中,这一逻辑通过
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 对象中的字符串结构或引发其他问题。 - 这指的是程序在处理字符串时会对双引号进行转义。在代码中,这一逻辑通过
-
“但是又能发现,程序遇到行转义,如果在引号前面加入一个字节比如 0x80 等特殊字节会跳过后面的字符不进 0xdf”
- 这指的是利用特殊字符操作转义序列,可能绕过某些逻辑。
toTable
函数中提到的GBK 编码可能允许这种操作:
1 2 3 4
if (json[idx] & 0x80) { result.push_back(json[idx]); idx++; }
这行代码检查字符是否是多字节字符编码(GBK,常用于中文字符编码)。这可能允许恶意输入通过利用编码绕过某些检查。
- 这指的是利用特殊字符操作转义序列,可能绕过某些逻辑。
-
“就会让程序略过后面的双引号不转义。”
- 这再次与之前提到的转义逻辑相关。特殊字节(如
0x80
)可能导致程序以不同方式解释某些字符,从而有效跳过或错误地解释诸如双引号之类的字符。
- 这再次与之前提到的转义逻辑相关。特殊字节(如
-
“另外,JSON 反序列化的时候遇到右花括号 } 就会直接停止运行。”
- 这一行描述了程序在 JSON 反序列化时遇到右花括号
}
后停止运行的情况。在toTable
函数中,我们可以看到使用了readChar('}')
函数,当遇到 JSON 对象的右花括号时,它会停止解析:
1
readChar('}');
这一点非常重要,因为攻击者可以通过注入
}
来提前结束 JSON 对象,从而导致潜在的注入或数据格式错误。 - 这一行描述了程序在 JSON 反序列化时遇到右花括号
-
“所以注入完之后立刻添加个 } 字符 就可以了。”
- 这将所有内容联系起来:如果攻击者能够注入数据并强制程序提前终止 JSON 解析(通过
}
),他们就可以控制系统的行为。如果攻击者成功地将内容注入到字符串中,可能通过额外的}
来关闭 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'}
。