0x00 http协议

http/0.9

  1. http是基于tcp/ip协议的应用层协议,不涉及数据包传输,主要规定了客户端和服务器之间的通信格式。
  2. 0.9版本只有一个命令GET
1
GET /index.html
1
2
3
<html>
<body>Hello World</body>
</html>
  1. 协议规定,服务器只可以回应html格式的字符串,等服务器发送完毕,就关闭TCP连接。

http/1.0

  1. 加入了POST和HEAD命令,可以传输图像、视频、二进制文件。
  2. http的请求和回应的格式变了,除了之前的html部分,每次通信需要包括头信息,用来描述一些元数据(用来描述数据的数据)。
  3. 还新增状态码,多字符集支持,权限等等。
1
2
3
GET / HTTP/1.0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
1
2
3
4
5
6
7
8
9
10
HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>
<body>Hello World</body>
</html>

回应的格式是”头信息 + 一个空行(\r\n) + 数据”

Content-Type字段的作用是告诉客户端,将要传回的数据是什么格式的。
这些数据类型总称为MIME type

http1.0只能发送一个请求,发送数据完毕,连接就会关闭。如果还要请求其他的资源,就必须新建一个连接。为了解决这个问题,有些浏览器会在请求的时候,使用

1
Connection: keep-alive

这个字段要求服务器不关闭连接,方便请求复用,服务器会同样回应这个字段。只是这种方法不是标准字段,所以会有不同实现,所以不是解决问题的方法。

http/1.1

  1. http1.1最大的变化是引入了持久连接,即tcp连接默认不关闭,而且不需要声明Connection: keep-alive,当客户端和服务器发现对方有一段时间没有活动,就会主动关闭连接。更规范的做法是客户端在最后一个请求时,发送Connection: close以此要求服务器关闭。
  2. 1.1版本引入了管道机制,以前在同一个tcp连接中,发送了A请求,然后等待服务器作出回应,收到后再发出B请求。管道机制可以使浏览器同时发出AB请求,但是仍然按照顺序完成请求。
  3. 为了分清tcp连接的字段,使用Content-Length声明数据长度。在浏览器接收了声明的长度后,后面的字段就属于下一个回应了。在之前的版本中这个字段不是必须的,因为服务器关闭tcp连接,就表明数据包已经全了。
  4. Transfer-Encoding: chunked表示数据包由数量未定的数据块组成,在每个数据块之前会有一个16进制的数据,表示这个块的大小,最后一个是大小为0的块,表示数据发送完成。
  5. 1.1增加了PUT、PATCH、HEAD、OPTIONS、DELETE。
  6. 增加了host字段

http/2.0

  1. 2.0实现了在同一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不需要按照顺序。
  2. head信息可以压缩后再发送

0x01 web路由

  • 路由就是url到函数的映射,它用来跟后端服务器进行交互,通过不同的路径去请求不同的资源,请求不同的页面是路由的其中一项功能。
  • route是一条路由,它将一个URL路径和一个函数进行映射
1
2
/users        ->  getAllUsers()
/users/count -> getUsersCount()
  • router可以理解为一个容器,它管理了一组route,在接收到一个url之后,会去路由映射表中查找相应的函数,这个过程由router处理。

0x02 goahead

goadhead是一个嵌入式web服务器,在它的官方文档当中详细的阐述了route.txt定义的路由规则。

根据匹配的url来执行不同的handler:有action handler直接在goahead进程中执行c函数,cgi handler执行新的cgi程序,file handler处理文件请求,也可以自定义新的handler。开发者自己定义的goaction就是常见的审计点。

iot固件中常见的情况是使用2.18版本的goahead。

main.c@main

1
2
3
4
5
6
7
8
9
10
11
if (initWebs() < 0) {
return -1;
}
// 用于创建路由,将路由和handler绑定等初始化操作
while (!finished) {
if (socketReady(-1) || socketSelect(-1, 1000)) {
socketProcess(-1); // 在这里处理用户请求
}
websCgiCleanup();
emfSchedProcess();
}

main.c@initwebs

因为goahead的项目时间比较久了,所以在它中间找到漏洞比较困难,我们直接去看它的路由解析,转到不同的handler函数的审计。

1
2
3
4
5
6
7
8
9
10
11
/*
* First create the URL handlers. Note: handlers are called in sorted order
* with the longest path handler examined first. Here we define the security
* handler, forms handler and the default web page handler.
*/
websUrlHandlerDefine(T(""), NULL, 0, websSecurityHandler,
WEBS_HANDLER_FIRST);
websUrlHandlerDefine(T("/goform"), NULL, 0, websFormHandler, 0);
websUrlHandlerDefine(T("/cgi-bin"), NULL, 0, websCgiHandler, 0);
websUrlHandlerDefine(T(""), NULL, 0, websDefaultHandler,
WEBS_HANDLER_LAST);

websUrlHandlerDefine会在读取完http请求头后,根据url前缀来执行相应的handler函数。

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
34
35
36
37
38
39
int websUrlHandlerDefine(char_t *urlPrefix, char_t *webDir, int arg,
int (*handler)(webs_t wp, char_t *urlPrefix, char_t *webdir, int arg,
char_t *url, char_t *path, char_t *query), int flags)
{
websUrlHandlerType *sp;
int len;

a_assert(urlPrefix);
a_assert(handler);

/*
* Grow the URL handler array to create a new slot
*/
len = (websUrlHandlerMax + 1) * sizeof(websUrlHandlerType);
if ((websUrlHandler = brealloc(B_L, websUrlHandler, len)) == NULL) {
return -1;
}
sp = &websUrlHandler[websUrlHandlerMax++];
// websUrlHandler为全局路由对象数组
memset(sp, 0, sizeof(websUrlHandlerType));

sp->urlPrefix = bstrdup(B_L, urlPrefix);
sp->len = gstrlen(sp->urlPrefix);
if (webDir) {
sp->webDir = bstrdup(B_L, webDir);
} else {
sp->webDir = bstrdup(B_L, T(""));
}
sp->handler = handler;
sp->arg = arg;
sp->flags = flags;

/*
* Sort in decreasing URL length order observing the flags for first and last
*/
qsort(websUrlHandler, websUrlHandlerMax, sizeof(websUrlHandlerType),
websUrlHandlerSort);
return 0;
}

代码都比较简单,就是进行路由初始化,就不多介绍了,我们使用vscode的shift+f12可以找到在哪里还引用了该路由数组。

image-20220726140222719

gstrncmp函数会将请求的path和路由对象的urlPrefix字符串进行匹配,对于空字符串一定会匹配成功。

所以它自己定义的第一条和第四条是一定会被匹配到的。

image-20220726142829458

然后handler返回1直接结束路由分发,返回0继续匹配其他路由。

我们继续寻找websUrlHandlerRequest的引用,会找到webs.c@websReadEvent,这个函数会for循环处理用户请求数据。websUrlHandlerRequest会设置好环境变量,然后websReadEvent就会处理。

asp.c@websAspDefine

main.c@initWebs函数内部会调用asp.c@websAspDefine定义的一些asp函数,这些函数是给asp文件调用的。

image-20220726150216842

asp通过<%....;%>的格式去调用上面定义的asp函数

漏洞点

  1. 定位websSecurityHandler函数,可以分析登录验证的逻辑
  2. 定位websDefaultHandler函数,可以分析asp文件请求
  3. 自定义的handler

0x03 cgi

1
http://hacker.com/cgi-bin/test.cgi

在web服务器调用test.cgi之前,会把http请求中的信息以环境变量的形式写入os。cgi可以通过本身函数获取环境变量,从而获得数据输入。除了环境变量,cgi程序还可以通过标准输入获得。比如post请求一个cgi的url,那么post的数据,cgi可以通过标准输入(stdin)得到。

cgi构造数据(比如html页面)时,只要按照标准输出的方式就可以,因为web服务器已经做好了重定向,将标准输出重定向给web服务器与浏览器的socket。

cgi程序本质上是os上一个可执行程序,作为http服务器的时候,客户端可以通过get或者post请求调用这段可执行程序。因为html是静态页面,所以没办法实现一些复杂的功能,但是cgi可以。

0x04 分析思路

首先在分析GoAhead的固件之前,要先知道该框架的版本,这样除了能够知道历史漏洞之外,还能对着该版本源码进行分析,快速定位到关键点。可以直接在ida里搜字符串即可得知。

goahead支持类似asp的服务器端脚本语言,该版本和微软版本语法基本相同,使用asp可以高效开发动态页面。