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,其实并不是,且没有对为什么要发送三个包作出详细说明。接下来我们来仔细分析一下 ...