WEBPWN-初探
WEBPWN-初探
前言
在好几次接触到webpwn的一直没去复现,打完熊猫杯就准备以这个为基础开始复现
0x00 2024年熊猫杯
这道题给了一个docker环境
检查一下保护
发现只有简单的保护
前置知识
CGI
CGI,全称是Common Gateway Interface(通用网关接口),是一种标准,用于定义Web服务器与外部应用程序(通常称为CGI脚本)之间的交互方式。CGI脚本可以用多种编程语言编写,如C、C++、Perl、Python、PHP等。组成CGI通信系统的是两部分:一部分是html页面,就是在用户端浏览器上显示的页面。另一部分则是运行在服务器上的Cgi程序。它们之间的通讯方式如下图:
服务器和客户端之间的通信,是客户端的浏览器和服务器端的http服务器之间的HTTP通信,我们只需要知道浏览器请求执行服务器上哪个CGI程序就可以了,其他不必深究细节,因为这些过程不需要程序员去操作。
服务器和CGI程序之间的通讯才是我们关注的。一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。
1. 服务器将URL指向一个应用程序
2. 服务器为应用程序执行做准备
3. 应用程序执行,读取标准输入和有关环境变量
4. 应用程序进行标准输出
对于Windows系统而言,还可以通过profile文件进行数据传输(如ini文件),但在这里不做研究。环境变量在CGI中有着重要的地位!每个CGI程序只能处理一个用户请求,所以在激活一个CGI程序进程时也创建了属于该进程的环境变量。
对于CGI程序来说,它继承了系统的环境变量。CGI环境变量在CGI程序启动时初始化,在结束时销毁。
CGI的环境变量
当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制。当这个CGI程序被HTTP服务器调用时,它的环境变量就会多了以下关于HTTP服务器、客户端、CGI传输过程等项目。
POST和GET
REQUEST_METHOD:它的值一般包括两种:POST和GET,但我们写CGI程序时,最后还要考虑其他的情况。
1.POST方法
如果采用POST方法,那么客户端来的用户数据将存放在CGI进程的标准输入中,同时将用户数据的长度赋予环境变量中的CONTENT_LENGTH。客户端用POST方式发送数据有一个相应的MIME类型(通用Internet邮件扩充服务:Multi-purpose Internet Mail Extensions)。目前,MIME类型一般是:application/x-wwww-form-urlencoded,该类型表示数据来自HTML表单。该类型记录在环境变量CONTENT_TYPE中,CGI程序应该检查该变量的值。
2.GET方法
在该方法下,CGI程序无法直接从服务器的标准输入中获取数据,因为服务器把它从标准输入接收到得数据编码到环境变量QUERY_STRING(或PATH_INFO)。
GET与POST的区别:采用GET方法提交HTML表单数据的时候,客户机将把这些数据附加到由ACTION标记命名的URL的末尾,用一个包括把经过URL编码后的信息与CGI程序的名字分开:
1 | http://www.mycorp.com/hello.html?name=hgq$id=1 |
QUERY_STRING的值为name=hgq&id=1
有些程序员不愿意采用GET方法,因为在他们看来,把动态信息附加在URL的末尾有违URL的出发点:URL作为一种标准用语,一般是用作网络资源的唯一定位标示。
环境变量是一个保存用户信息的内存区。当客户端的用户通过浏览器发出CGI请求时,服务器就寻找本地的相应CGI程序并执行它。在执行CGI程序的同时,服务器把该用户的信息保存到环境变量里。接下来,CGI程序的执行流程是这样的:查询与该CGI程序进程相应的环境变量:第一步是request_method,如果是POST,就从环境变量的len,然后到该进程相应的标准输入取出len长的数据。如果是GET,则用户数据就在环境变量的QUERY_STRING里。
3.POST与GET的区别
以 GET 方式接收的数据是有长度限制,而用 POST 方式接收的数据是没有长度限制的。并且,以 GET 方式发送数据,可以通过URL 的形式来发送,但 POST方式发送的数据必须要通过 Form 才到发送。
FastCGI 是对传统 CGI(Common Gateway Interface)的扩展和改进,旨在克服 CGI 的性能和扩展性问题。以下是 FastCGI 的功能、工作原理以及与 CGI 的区别:
FastCGI 的功能
- 持久化进程:
- FastCGI 保持应用程序进程在内存中持久运行,而不是为每个请求创建一个新的进程。这大大减少了进程创建和销毁的开销。
- 高效的请求处理:
- FastCGI 可以同时处理多个请求,支持并发连接,从而提高了服务器的吞吐量和响应速度。
- 语言无关:
- FastCGI 不依赖于特定的编程语言,可以与多种编程语言(如 PHP、Python、Perl、Ruby 等)配合使用。
- 分布式架构:
- FastCGI 支持在多个服务器之间分布应用程序,可以实现负载均衡和高可用性。
FastCGI 配置示例
以 Nginx 配置 PHP-FPM(PHP FastCGI Process Manager)为例,展示如何配置 FastCGI:
安装 PHP-FPM
首先,需要确保安装了 PHP-FPM。可以通过以下命令安装:
1 | sudo apt-get install php-fpm |
配置 Nginx
在 Nginx 配置文件中配置 FastCGI:
1 | server { |
include snippets/fastcgi-php.conf;
:包含 FastCGI 的基本配置。fastcgi_pass unix:/run/php/php7.4-fpm.sock;
:指定 FastCGI 进程的监听地址,这里使用 Unix 套接字。
调试环境
由于这个是在nignx的环境下去运行的,并且给了dockerfile,于是我们只需要去在docker中安装gdb,然后再patch一下http文件(要调试的文件),然后在main函数前下个断点,等成功attach后再跳转到main函数入口点,这里感谢一下EX给的建议
dockerfile
在dockerdfile中下载gdb环境
1 | # for debug |
patch elf
在调试的时候,需要在文件的开头进行patch,形成一个死循环,否则没时间patch上去。
1 | .text:00000000004017DE endbr64 |
这是一段main函数的代码,我们可以选择patch其中的跳转汇编,例如这里的
1 | jnz short loc_401884 |
这段代码是用于在判断是否满足情况下的跳转,也就是满足情况需要跳转,我们可以把他patch成jmp $
,也就是跳转到自己,形成一个死循环。
用ida的edit->patch program->Assembly来patch。
这里只是介绍一直死循环的方式,大家也可以自己去找合适的patch,jmp $
是两个字节的,大家对应修改即可。
调试
调试的时候直接用下面的脚本去调试
1 | set $rip=dest_addr |
直接设置$ip到对应的正常执行流程,然后在要调试的地方下断点,执行过去就行了
分析
配置文件check(FastCGI)
在配置文件中有这样的检查,只有内部才能访问/cgi-bin/note_handler,并且如果/cgi-bin/开头的话,访问/cgi-bin/note_handler就会报错
1 | location /cgi-bin/note_handler { |
然后在访问/get_flag路由的时候,重定向到了/tmp/flag,只需要用这个路由即可读取/tmp/flag的数据
1 | location /get_flag { |
main函数的处理字符串
获取环境变量
1 | v15 = __readfsqword(0x28u); |
这里通过getenv
函数获取CGI环境变量REQUEST_METHOD
、QUERY_STRING
和SCRIPT_NAME
。
检查环境变量
1 | if (!REQUEST_METHOD && !SCRIPT_NAME) |
检查REQUEST_METHOD
和SCRIPT_NAME
是否存在,并且它们的长度是否小于128(0x80)字节。
处理非法文件名
1 | if (strstr(SCRIPT_NAME, "..")) |
如果SCRIPT_NAME
中包含“..”,表示可能存在目录遍历攻击,返回403 Forbidden错误。
处理查询字符串过长
1 | else if (QUERY_STRING && strlen(QUERY_STRING) > 0x80) |
如果QUERY_STRING
存在且长度大于128字节,返回403 Forbidden错误。
主处理函数
X-Forwarded-For check
在传递的参数里面有个限制,X-Forwarded-For必须为127.0.0.1。于是我们在传递参数的时候加上“X-Forwarded-For: 127.0.0.1\r\n”即可
URL check
并且对URL也进行了检查,URL的开头必须为/cgi-bin/note_handler
URL解码绕过
然后我们发现在验证长度后对script_ name进行了URL解码,也就是会解码两次,于是我们就能绕过前面说的配置文件的检查了,我们只需要在前面验证的时候,验证解码前的数据,也就是我们需要在第一次在fastCGI验证的时候传递/cgi-bin/note_handle%72(r),然后解码后就是正常的/cgi-bin/note_handler了,也就绕过了
处理QUERY_STRING
在处理查询语句的时候发现action和content的分割符是”&”,action里面的分割符号是”,”,于是知道了查询语句的组成"action=act1,act2,act3&content=conten"
这里在分析content的时候发现snprintf的用法出现了错误
标准用法应该是
1 | snprintf(dest,maxlen,format,....) |
但是这里的我们的输入到了format去了,于是我们能控制format数据了,也就是格式化字符串漏洞
利用
利用思路
现在已经能够成功找到格式化字符串漏洞了,我们现在只需要找到对应的偏移,然后改写puts函数的got表为system即可
因为在printf路由中,会打印一段数据,这段数据来源是dest11。
然后发现dest11的来源是add函数的数据,也就是a1到了dest,然后dest地址给了的dest11
然后add函数的参数也就是我们输入的数据部分,于是我们需要的将flag复制给/tmp/flag在这里输入给字符串即可
格式化字符串任意写
然后经过测试,我发现往v14输入的数据最多为0x23,然后我puts的got表的地址为0x404028
,也是只需要三位,于是我们前面0x20可以写要执行的命令,最后三位写got表的地址,最后效果如下
于是测试出来的偏移就为14,需要写入的数据为0x4011E0
然后格式化字符串利用脚本就为
1 | %30$c%30$c%30$c%30$c%30$c%136c%14$hhn |
exp
最后总的脚本为
1 | def exploit(): |