记一次使用codeql进行的代码审计

AFKL 2021-12-03 09:54:00

0x00 前言

闲来无事,寻思着找个洞玩玩,顺便练一下codeql的使用,故出此文。

0x01 找洞之旅

我在gitee上找到了一个使用旧版本beego框架开发的OA项目。我在github找到相同项目,随后便尝试使用https://lgtm.com/dashboard项目在线扫描。结果并没有找到漏洞...

image-20211130215400428

那么官方库看起来是有问题的,我们看一下官方对于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二次开发的系统,都是按照这样开发的。

  1. https://github.com/mindoc-org/mindoc
    image-20211130223325733
    image-20211130223354647

  2. https://github.com/TruthHun/DocHub
    image-20211130224825082

明显官方获取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名。而这两者在语法树上是同级的。所以只能使用这样的笨办法来进行获取。
image-20211130230422165

随后测试正常。
image-20211130230612799

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条路径。
image-20211130231622487

我们来尝试跟踪其中一条。
image-20211130231803076

在某路由中,获取用户GET传参传入的type后,污染了condArr map并传入ListCheckwork函数中。
image-20211130232009049

ListCheckwork函数中,qb.And("type=" + condArr["type"])一句对查询语句进行了动态拼接,并且没有过滤,导致sql注入。
image-20211130232220605

成功延时10秒。

0x05 后记

codeql真好玩((
一次性交一坨洞好爽

我个人感觉官方使用BeegoInput应该是有原因的。但技术迭代,beego已经v2了,php都出到8了,以前为什么要这么写也不知道了。希望有了解的师傅可以帮我答疑解惑。
感谢您阅读到这里。

评论

Y

ybdt 2021-12-05 00:36:47

tql

AFKL

这个人很懒,没有留下任何介绍

随机分类

密码学 文章:13 篇
iOS安全 文章:36 篇
APT 文章:6 篇
数据分析与机器学习 文章:12 篇
业务安全 文章:29 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录