0x00 前言
闲来无事,寻思着找个洞玩玩,顺便练一下codeql
的使用,故出此文。
0x01 找洞之旅
我在gitee
上找到了一个使用旧版本beego
框架开发的OA项目。我在github
找到相同项目,随后便尝试使用https://lgtm.com/dashboard
项目在线扫描。结果并没有找到漏洞...
那么官方库看起来是有问题的,我们看一下官方对于beego
获取用户输入的实现。
string modulePath() { result = ["github.com/astaxie/beego", "github.com/beego/beego"] }
string packagePath() { result = package(modulePath(), "") }
string contextPackagePath() { result = package(modulePath(), "context") }
// 上面三个函数可以获得 "github.com/astaxie/beego/context"
private class BeegoInputSource extends UntrustedFlowSource::Range {
string methodName;
FunctionOutput output;
BeegoInputSource() {
exists(DataFlow::MethodCallNode c | this = output.getExitNode(c) |
c.getTarget().hasQualifiedName(contextPackagePath(), "BeegoInput", methodName)
// 找到 "github.com/astaxie/beego/context" 中的BeegoInput struct的对应方法
) and
(
methodName = "Bind" and
output.isParameter(0)
or
methodName in [
"Cookie", "Data", "GetData", "Header", "Param", "Params", "Query", "Refer", "Referer",
"URI", "URL", "UserAgent"
] and
output.isResult(0)
)
}
predicate isSafeUrlSource() { methodName in ["URI", "URL"] }
}
可以看到,在获取MethodCallNode
时,官方仅对beego/context
包中的获取用户输入的方法进行了筛选。但实际上beego
框架的开发方法并没有直接调用那么简单。
我们来看一下官方文档的用例。
type NestPreparer interface {
NestPrepare()
}
type baseController struct {
web.Controller // 在旧版本中为 beego.Controller
i18n.Locale
user models.User
isLogin bool
}
...
type BaseAdminRouter struct {
baseController
}
...
func (this *BaseAdminRouter) Get(){
this.TplName = "Get.tpl"
}
func (this *BaseAdminRouter) Post(){
this.TplName = "Post.tpl"
}
在官方文档中,用户使用beego
自带的controller
组成baseController
,随后使用这个baseController
继续嵌套进其它的Controller
中。
我找了多个beego
二次开发的系统,都是按照这样开发的。
-
https://github.com/mindoc-org/mindoc
-
https://github.com/TruthHun/DocHub
明显官方获取beego
用户输入的方式是有问题的。根据大多数开发者的使用框架的方法,获取用户输入的地方应该定义在beego.Controller
中,而不是BeegoInput
中。那么我们只能重写一下了。
0x02 重写
结合上面的信息,我们有以下结论:
1. 所有的Controller
都是一个结构体。
2. 所有的Controller
都继承了BaseController
。
3. BaseController
继承了beego.Controller
。
4. 获取用户输入的方法定义在beego.Controller
。
我们根据以上特性,便可以写出codeql
代码。
string beegoModulePath() { result = ["github.com/astaxie/beego", "github.com/beego/beego"] }
// 获取开发者定义的BaseController,原理是获取结构体全部的属性,筛选出`beego.Controller`类型的。
Ident getBaseControllerIdent() {
exists(
StructTypeExpr baseController
|
baseController.getAField().getType().hasQualifiedName(
beegoModulePath(), "Controller"
)
|
result = baseController.getParent().getAChild()
)
}
string beegoBaseControllerPath() {
result = getBaseControllerIdent().getType().getPackage().getPath()
}
string beegoBaseControllerName() {
result = getBaseControllerIdent().getType().getName()
}
class BeegoUserDataNode extends UserDataNode {
BeegoUserDataNode() {
exists(DataFlow::MethodCallNode call, string methodName |
// 这里我保留了原获取用户输入的方法。
(
call.getTarget().hasQualifiedName(
package(beegoModulePath(), "context"), "BeegoInput", methodName
)
or call.getTarget().hasQualifiedName(
beegoModulePath(), "Controller", methodName
)
or call.getTarget().hasQualifiedName(
beegoBaseControllerPath(), beegoBaseControllerName(), methodName
)
)
and
methodName in [
"Cookie", "Data", "GetData", "Header", "Param", "Params", "Query", "Refer", "Referer", "GetString",
"URI", "URL", "UserAgent"
] |
this = call.getResult(0)
)
}
}
result = baseController.getParent().getAChild()
这句我不知道是不是codeql
的设计失误,我没有找到对应的方法将StructTypeExpr
类型转换为对应定义的struct
名。而这两者在语法树上是同级的。所以只能使用这样的笨办法来进行获取。
随后测试正常。
0x03 获取终点
因为挖掘漏洞要用到污点追踪,所以污点追踪终点也需要进行定义。
这里我打算挖掘比较简单的sql注入漏洞。查看项目后发现beego
拥有自己的orm
,所以要对其进行特殊处理。
在本例中,所有的查询均使用到了orm/QueryBuilder
接口中的方法。beego
源码中如下定义:
type QueryBuilder interface {
Select(fields ...string) QueryBuilder
ForUpdate() QueryBuilder
From(tables ...string) QueryBuilder
InnerJoin(table string) QueryBuilder
LeftJoin(table string) QueryBuilder
RightJoin(table string) QueryBuilder
On(cond string) QueryBuilder
Where(cond string) QueryBuilder
And(cond string) QueryBuilder
Or(cond string) QueryBuilder
In(vals ...string) QueryBuilder
OrderBy(fields ...string) QueryBuilder
Asc() QueryBuilder
Desc() QueryBuilder
Limit(limit int) QueryBuilder
Offset(offset int) QueryBuilder
GroupBy(fields ...string) QueryBuilder
Having(cond string) QueryBuilder
Update(tables ...string) QueryBuilder
Set(kv ...string) QueryBuilder
Delete(tables ...string) QueryBuilder
InsertInto(table string, fields ...string) QueryBuilder
Values(vals ...string) QueryBuilder
Subquery(sub string, alias string) string
String() string
}
我们提取出使用了string
的方法作为methobName
,结合接口名可写出以下代码。
class SqlInjectNode extends DataFlow::Node {
SqlInjectNode() {
exists(
Method queryMethod, string methodName
|
(
methodName in [
"Select", "From", "InnerJoin", "LeftJoin", "RightJoin", "On","Where", "And", "Or", "In", "OrderBy", "Limit", "Offset", "GroupBy","Having"
]
and queryMethod.hasQualifiedName(
package(beegoModulePath(), "orm"), "QueryBuilder", methodName
)
)
|
this = DataFlow::exprNode(queryMethod.getACall().getCall().getArgument(0))
)
}
}
0x04 最后的污点追踪
将定义的代码import
进来,随后进行污点追踪。
import go
import UserData
import SqlInject
from SqlInjectNode sqlNode, BeegoUserDataNode userDataNode, SqlInjectConfiguration sqlFlow
where sqlFlow.hasFlow(userDataNode, sqlNode)
select userDataNode, sqlNode
成功查询到12条路径。
我们来尝试跟踪其中一条。
在某路由中,获取用户GET传参传入的type
后,污染了condArr map
并传入ListCheckwork
函数中。
ListCheckwork
函数中,qb.And("type=" + condArr["type"])
一句对查询语句进行了动态拼接,并且没有过滤,导致sql注入。
成功延时10秒。
0x05 后记
codeql
真好玩((
一次性交一坨洞好爽
我个人感觉官方使用BeegoInput
应该是有原因的。但技术迭代,beego
已经v2了,php都出到8了,以前为什么要这么写也不知道了。希望有了解的师傅可以帮我答疑解惑。
感谢您阅读到这里。