FurryCTF 2025 寒假赛 调试窗口 题解
2025/1/15
这是 FurryCTF 2025 寒假赛 “调试窗口” 一题的出题人题解,这也是我首次参与 CTF 类赛事的出题,非常感谢组织方白风猫猫给我这次机会。题目如有错漏之处,还敬请海涵。以下是以出题人视角写下的题解。
前言
本题目的 IDEA 来自于我跟导师做大创项目时候尝试复现的几个 CVE 漏洞(TOTOLINK NR1800X)。CVE 编号如下:
- CVE-2022-41525(绕过验证)
- CVE-2022-41518(任意命令执行)
为了降低难度,防止初见杀,本题的 Flag 被分为两段。第一段只需要利用绕过验证漏洞即可取得,第二段需要综合利用两个漏洞。故第一段实际上是第二段的 Hint。
解题思路
打开题目页面,有一个网址。打开网址即为本题的 Web 部分。页面中的“别看啦,你没有 admin 权限的!”提示了可能需要绕过一些权限验证/提权的操作。
顺便请忽视这丑到批爆的前端页面,实在是懒得优化了(逃)
Flag 1
点击“点我获取 Flag1”按钮尝试获取 Flag 1,下面的红字提示变成了“Forbidden.”。这里打开开发者工具查看,发现按钮的 onclick
属性调用了 submit_flag1()
函数,而该函数的定义在下面的 <script></script>
块中。
function submit_flag1() {
let output = document.querySelector(".flag1");
fetch("/cgi-bin/getflag.cgi?is_admin=0")
.then((r) => {
if (r.ok) {
return r.text();
} else {
return Promise.reject("no reason");
}
})
.then((r) => {
output.innerText = r;
});
}
阅读该段代码可知,该函数实际上是通过 fetch
函数访问了 /cgi-bin/getflag.cgi
这个 CGI 文件,并且附带了 is_admin=0
的 URL Query 参数,将得到的返回值显示出来。联系前面的“没有 admin 权限”,在此处可尝试将 is_admin
参数改为 1
后访问,即可得到 Flag 1 furryCTF{Be_The_K1
Flag 2
很显然,Flag 1 并不是完整的 Flag,我们需要寻找下半段 Flag 拼接构成完整 Flag。继续检查,发现下面有一个设置主机名的入口,查看提交按钮发现调用 submit_hostname
函数如下:
function submit_hostname() {
let output = document.querySelector(".flag2");
const hostname = encodeURIComponent(
document.getElementById("hostname").value
);
fetch(`/cgi-bin/sethostname.cgi?is_admin=0&hostname=${hostname}`)
.then((r) => {
if (r.ok) {
return r.text();
} else {
return Promise.reject("no reason");
}
})
.then((r) => {
output.innerText = r;
});
}
很显然,该函数先对输入的 hostname
参数进行 URL 编码,然后调用了 /cgi-bin/sethostname.cgi
这个 CGI 文件。照例,我们使用 is_admin=1
参数绕过验证,然后发现输出了 Hostname updated successfully
。
这时候,前面的附件派上了用场。附件中有一个 sethostname.cgi,正是我们此处调用的 CGI 文件。将其丢进 IDA 进行反编译,阅读生成的伪 C++ 代码(如下):
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rdx
__int64 v4; // rax
char v5; // al
__int64 v6; // rdx
__int64 v7; // rax
__int64 v8; // rdx
__int64 v9; // rax
__int64 v10; // rax
__int64 v11; // rax
const char *v12; // rax
__int64 v13; // rdx
__int64 v14; // rax
int i; // [rsp+8h] [rbp-108h]
int v17; // [rsp+Ch] [rbp-104h]
__int64 v18; // [rsp+10h] [rbp-100h] BYREF
__int64 v19; // [rsp+18h] [rbp-F8h] BYREF
__int64 v20; // [rsp+20h] [rbp-F0h] BYREF
__int64 v21; // [rsp+28h] [rbp-E8h] BYREF
char *s; // [rsp+30h] [rbp-E0h]
char *v23; // [rsp+38h] [rbp-D8h]
__int64 v24; // [rsp+40h] [rbp-D0h]
char *v25; // [rsp+48h] [rbp-C8h]
__int64 v26; // [rsp+50h] [rbp-C0h]
__int64 v27; // [rsp+58h] [rbp-B8h]
FILE *stream; // [rsp+60h] [rbp-B0h]
_BYTE *v29; // [rsp+68h] [rbp-A8h]
char v30[32]; // [rsp+70h] [rbp-A0h] BYREF
__int64 v31[4]; // [rsp+90h] [rbp-80h] BYREF
char v32[32]; // [rsp+B0h] [rbp-60h] BYREF
char v33[40]; // [rsp+D0h] [rbp-40h] BYREF
unsigned __int64 v34; // [rsp+F8h] [rbp-18h]
v34 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Content-type:text/plain\r\n\r\n", envp);
s = getenv("QUERY_STRING");
if ( s )
{
std::vector<std::string>::vector(v30);
v17 = strlen(s);
std::allocator<char>::allocator(v31);
std::string::basic_string<std::allocator<char>>(v32, &unk_5013, v31);
std::allocator<char>::~allocator(v31);
for ( i = 0; i < v17; ++i )
{
if ( s[i] == 38 )
{
UrlDecode(v33, v32);
std::vector<std::string>::push_back(v30, v33);
std::string::~string(v33);
std::string::operator=(v32, &unk_5013);
}
else
{
v5 = tolower(s[i]);
std::string::operator+=(v32, (unsigned int)v5);
}
}
if ( (unsigned __int8)std::operator!=<char>(v32, &unk_5013) )
{
UrlDecode(v33, v32);
std::vector<std::string>::push_back(v30, v33);
std::string::~string(v33);
}
v23 = v30;
v21 = std::vector<std::string>::begin(v30);
v31[0] = std::vector<std::string>::end(v30);
while ( 1 )
{
if ( !(unsigned __int8)__gnu_cxx::operator!=<std::string *,std::vector<std::string>>(&v21, v31) )
{
v7 = std::operator<<<std::char_traits<char>>(&std::cout, "Forbidden.", v6);
std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
goto LABEL_34;
}
v24 = __gnu_cxx::__normal_iterator<std::string *,std::vector<std::string>>::operator*(&v21);
if ( (unsigned __int8)std::operator==<char>(v24, "is_admin=1") )
break;
__gnu_cxx::__normal_iterator<std::string *,std::vector<std::string>>::operator++(&v21);
}
std::vector<std::string>::vector(v31);
v25 = v30;
v18 = std::vector<std::string>::begin(v30);
v19 = std::vector<std::string>::end(v25);
while ( (unsigned __int8)__gnu_cxx::operator!=<std::string *,std::vector<std::string>>(&v18, &v19) )
{
v26 = __gnu_cxx::__normal_iterator<std::string *,std::vector<std::string>>::operator*(&v18);
std::vector<std::string>::clear(v31);
std::string::operator=(v32, &unk_5013);
v27 = v26;
v20 = std::string::begin(v26);
v21 = std::string::end(v27);
while ( (unsigned __int8)__gnu_cxx::operator!=<char const*,std::string>(&v20, &v21) )
{
v29 = (_BYTE *)__gnu_cxx::__normal_iterator<char const*,std::string>::operator*(&v20);
if ( *v29 == 61 )
{
std::vector<std::string>::push_back(v31, v32);
std::string::operator=(v32, &unk_5013);
}
else
{
std::string::operator+=(v32, (unsigned int)(char)*v29);
}
__gnu_cxx::__normal_iterator<char const*,std::string>::operator++(&v20);
}
if ( (unsigned __int8)std::operator!=<char>(v32, &unk_5013) )
std::vector<std::string>::push_back(v31, v32);
if ( std::vector<std::string>::size(v31) != 2 )
{
v9 = std::operator<<<std::char_traits<char>>(&std::cout, "Bad Request.", v8);
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
break;
}
v10 = std::vector<std::string>::operator[](v31, 0LL);
if ( (unsigned __int8)std::operator==<char>(v10, "hostname") )
{
stream = 0LL;
v11 = std::vector<std::string>::operator[](v31, 1LL);
std::operator+<char>(v33, "/tools/sethostname ", v11);
v12 = (const char *)std::string::c_str(v33);
stream = popen(v12, "r");
if ( stream )
{
fread(&buf, 1uLL, 0x800uLL, stream);
pclose(stream);
}
v14 = std::operator<<<std::char_traits<char>>(&std::cout, &buf, v13);
std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
std::string::~string(v33);
break;
}
__gnu_cxx::__normal_iterator<std::string *,std::vector<std::string>>::operator++(&v18);
}
std::vector<std::string>::~vector(v31);
LABEL_34:
std::string::~string(v32);
std::vector<std::string>::~vector(v30);
}
else
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Forbidden.", v3);
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
return 0;
}
其中,我们在处理 hostname
参数的部分(如下)可看到:
if ( (unsigned __int8)std::operator==<char>(v10, "hostname") )
{
stream = 0LL;
v11 = std::vector<std::string>::operator[](v31, 1LL);
std::operator+<char>(v33, "/tools/sethostname ", v11);
v12 = (const char *)std::string::c_str(v33);
stream = popen(v12, "r");
if ( stream )
{
fread(&buf, 1uLL, 0x800uLL, stream);
pclose(stream);
}
v14 = std::operator<<<std::char_traits<char>>(&std::cout, &buf, v13);
std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
std::string::~string(v33);
break;
}
这里直接将取得的 hostname
参数拼接到了 "/tools/sethostname "
字符串后面,存放在 v33
中,然后调用 c_str()
方法将其转换为 const char *
类型,作为参数调用 popen()
方法,然后从返回的流中读取内容并输出。
一个比较显而易见的漏洞点是,此处对于拼接的参数没有做任何过滤和验证,直接拼接导致可以很容易构造出类似这样的注入方法:hostname=;【任意命令】
(实际使用时需要转义)。
故此处通过任意命令执行漏洞,可以一步步使用 ls
/pwd
等命令查看文件结构,然后找到 flag2.txt
通过传递令 hostname
参数为 1;cat ../../flag/flag2.txt
即可查看到 The latter part: ng_Of_The_M0un7}
。
综上,Flag 为 furryCTF{Be_The_K1ng_Of_The_M0un7}
。
小彩蛋
本题没有彩蛋,因为笨蛋出题人忘记塞彩蛋了!
加载评论中……