0x00 前言

Revel 是一个基于Golang的灵活的Web框架,大量应用了Golang的反射特性,使得其可以类似于Django那样快速地建立一个网站。前段时间在翻阅Revel框架的文档时,发现其某个特性可能存在DoS问题,故对该特性的相关源码进行了审计,发现了一处非常容易利用的DoS问题,利用单个请求即可打挂Revel的服务器。

好在该DoS的利用是有条件的,当且仅当网站使用了Revel框架获取slice类型的参数时才会触发。

0x01 分析

Revel框架为开发者提供了许多有用的特性,其中一个特性允许开发者直接获取数组类型的数据。如当用户访问http://example.com/?keys[]=1&keys[]=2时,开发者可直接将keys参数视为slice类型(Golang中对数组的封装)。

该特性在Revel中是通过反射+Binder实现的,其中专门用于处理slice类型的函数如下。

func bindSlice(params *Params, name string, typ reflect.Type) reflect.Value {
	// Collect an array of slice elements with their indexes (and the max index).
	maxIndex := -1
	numNoIndex := 0
	sliceValues := []sliceValue{}

	// Factor out the common slice logic (between form values and files).
	processElement := func(key string, vals []string, files []*multipart.FileHeader) {
        // ...
        // 省略相关用于处理单个slice元素的内容
	}

	for key, vals := range params.Values {
		processElement(key, vals, nil)
	}
	for key, fileHeaders := range params.Files {
		processElement(key, nil, fileHeaders)
	}

	resultArray := reflect.MakeSlice(typ, maxIndex+1, maxIndex+1+numNoIndex)
	for _, sv := range sliceValues {
		if sv.index != -1 {
			resultArray.Index(sv.index).Set(sv.value)
		} else {
			resultArray = reflect.Append(resultArray, sv.value)
		}
	}

	return resultArray
}

该函数的的作用主要用于解析数组类型的参数,进行类型转换并确定slice的最大下标maxIndex,最终申请一个足够大的slice来容纳这些内容。

其中,maxIndex是由最大数组下标决定的,且没有大小限制。也就是说此处只要maxIndex足够大,那么Revel自动调用的reflect.MakeSlice()所申请的内存空间可能会超过实体内存,触发OOM Killer或内存换页。

由于processElement()函数中会自动解析传入参数的下标信息,maxIndex的值也变得容易控制,只需访问类似于下面的网址,即可制造一个足够大的maxIndex

https://example.com/?keys[1234567890]=1

访问上述网址后,服务器CPU和内存使用率将会迅速飙升,如图所示,最终导致Revel进程结束。

0x02 利用条件

一般情况下,bindSlice不会被自动调用,当且仅当controller的代码中获取了slice类型的参数时,该漏洞才能利用,如下面的两种情况。

func (c App) Test1(name []string) revel.Result {
	return c.Render(name)
}
func (c App) Test2() revel.Result {
	var name []string
	c.Params.Bind(&name, "name")
	return c.Render(name)
}

0x03 总结

当开发者利用Revel框架获取slice类型的数据时,攻击者只需发起一次请求,即可使相关Web服务宕机,危害不大不小。因此,如果网站中使用了Revel框架,又恰好用到了数组类型的参数,应当重新审视自己的业务,是否非得使用数组类型的参数。