惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
LINUX DO - 最新话题
罗磊的独立博客
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
Security Archives - TechRepublic
Security Archives - TechRepublic
H
Hacker News: Front Page
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The Hacker News
The Hacker News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
大猫的无限游戏
大猫的无限游戏
PCI Perspectives
PCI Perspectives
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Proofpoint News Feed
NISL@THU
NISL@THU
小众软件
小众软件
S
Security Affairs
腾讯CDC
人人都是产品经理
人人都是产品经理
C
Check Point Blog
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
Forbes - Security
Forbes - Security
Project Zero
Project Zero
G
Google Developers Blog
博客园 - 三生石上(FineUI控件)
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园_首页
Jina AI
Jina AI
The Cloudflare Blog
C
Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
L
Lohrmann on Cybersecurity
Y
Y Combinator Blog
W
WeLiveSecurity
V
Visual Studio Blog
Scott Helme
Scott Helme
N
News | PayPal Newsroom
I
Intezer
C
CXSECURITY Database RSS Feed - CXSecurity.com
Attack and Defense Labs
Attack and Defense Labs
Spread Privacy
Spread Privacy
Webroot Blog
Webroot Blog
T
Threatpost
雷峰网
雷峰网
MongoDB | Blog
MongoDB | Blog
N
News and Events Feed by Topic

秋实-Allenyou 的小窝

【笔记】人工智能与模式识别 复习笔记 - 秋实-Allenyou 的小窝 【教程】使用 Keyd 将 Copilot 键映射为可用的组合键并修复触摸板防误触失效问题 机械革命无界 15X 的 Arch Linux 开荒指南 我在用什么?(2026 版) - 秋实-Allenyou 的小窝 关于 Next.js SSG 生成 RSS Feed 文件的另一思路 从零开始的 NixOS 体验 - 秋实-Allenyou 的小窝 2025:迷茫中转向 - 秋实-Allenyou 的小窝 我在用什么?(2025 版) - 秋实-Allenyou 的小窝 【杂谈】关于 AIGC 的一些思考 - 秋实-Allenyou 的小窝 【记录】一种在主机通过 Docker 容器名解析容器 IP 的方法 【记录】如何在 Next.js 中优雅的调用 Matomo 统计 从信号与系统角度看 OI/XCPC 中的傅里叶变换运用 - 秋实-Allenyou 的小窝 【杂谈】答《博客作者呀,我想采访你这 9 个问题!》 - 秋实-Allenyou 的小窝 使用 Golang 重写 gh-proxy - 秋实-Allenyou 的小窝 2024:“我”的大学生活 - 秋实-Allenyou 的小窝 【游记】CCPC 2024 重庆站尾杀记 - 秋实-Allenyou 的小窝 【游记】ICPC 2024 亚洲区域赛南京站打铁记 - 秋实-Allenyou 的小窝 【杂谈】这次 Furrygooo 结束后我的一些体验 - 秋实-Allenyou 的小窝 【教程】使用阿里云 OSS + CDN 加速 Gravatar 【记录】 一次罗技鼠标驱动冲突问题的排查经历 - 秋实-Allenyou 的小窝 【记录】我是如何用 Next.js 重构我的博客的 - 秋实-Allenyou 的小窝 【教程】如何用 Git 快速搭建简单的 CI - 秋实-Allenyou 的小窝 我的DN42 Peering信息 - 秋实-Allenyou 的小窝 【进度】博客重构计划 - 秋实-Allenyou 的小窝 这是一个正经的2023总结 - 秋实-Allenyou 的小窝 【题解】Luogu P9769 [HUSTFC 2023] 简单的加法乘法计算题 2023新年谜题总结 - 秋实-Allenyou 的小窝 【教程】浅谈Linux服务器的一些基础安全技巧 - 秋实-Allenyou 的小窝 Goodbye, 2022~ - 秋实-Allenyou 的小窝 【教程】使用 Docker 开服,优雅地隔离不同版本JDK - 秋实-Allenyou 的小窝 【旧文补档】2020年终总结 & 2021新年计划 - 秋实-Allenyou 的小窝 【旧文补档】写个脚本监控你的VPS SolusVM面板适用 - 秋实-Allenyou 的小窝 【旧文补档】建立自己的私人网盘!在CentOS VPS上安装NextCloud - 秋实-Allenyou 的小窝 【旧文补档】2019,再见!2020,你好! - 秋实-Allenyou 的小窝 【旧文补档】新一代USB来袭,一起捋一捋USB的“前世今生” - 秋实-Allenyou 的小窝 【旧文补档】矩阵学习笔记 - 秋实-Allenyou 的小窝
FurryCTF 2024 寒假赛 调试窗口 题解
2025-01-15 · via 秋实-Allenyou 的小窝

这是 FurryCTF 2024 寒假赛 “调试窗口” 一题的出题人题解,这也是我首次参与 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}

小彩蛋

本题没有彩蛋,因为笨蛋出题人忘记塞彩蛋了!