├── .gitignore ├── README.md ├── class.php.txt ├── cmd └── popmaster │ └── popmaster.go ├── go.mod ├── go.sum └── internal ├── parser └── parser.go ├── tracer ├── trace.go └── travel.go └── utils ├── datacontainer.go └── helper.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pop-master-go 2 | 3 | ## 前言 4 | 5 | 最近看到强网杯中有道需要自动化代码审计能力的题——pop-master,正好与自己技能相关,尝试解一波。 6 | 7 | ## 分析 8 | 9 | ```php 10 | Dvickz($b); 18 | ``` 19 | 指定函数名调用,去class.php里搜了下,只有一个类中存在这个函数,说明这个类就是入口了。 20 | 21 | ![image.png](https://i.loli.net/2021/07/13/1i97BAnbOIMTzED.png) 22 | 23 | 很明显,就是从这16w行代码中,找到一条以xd4a55为起点,拥有eval函数的类为终点的pop链。 24 | 25 | ### 寻找调用关系 26 | 27 | 以上图为例,call site的名称是确定的,需要寻找拥有同名函数的类作为`$this->RSphyE8`的值。 28 | 29 | 可以用嵌套hash表,将类名与类中的方法名作为索引,方法的body作为值。这样只用查询hash表中是否存在对应名称的方法就能跳转到目标方法。 30 | 31 | ### 污点传播 32 | 33 | 在本题中,存在将常量直接赋值给变量的情况,这样变量传递到eval中了,因为变量不可控,造成不了危害。所以要进行污点传播,对变量不可控的路径进行剪枝。 34 | 35 | 在本题中,没有涉及到全局变量,函数中的变量要么来自参数,要么来自类,要么在函数中产生,均不会直接传递到函数外。 36 | 37 | 所以,每进到一个函数中,就新建一个变量污点状态,以变量名为索引,查询变量的污染情况。因为如果能进到这个函数,说明这个函数的参数被污染了,所以初始化参数的污染情况为true。 38 | 39 | #### 污染情况改变 40 | 41 | 在本题中,变量污染情况改变只来自于赋值,所以对赋值语句进行分析。 42 | 43 | 赋值语句三种情况: 44 | 1. `$left = $right` 45 | 2. `$left = $right.'xxxxx'` 46 | 3. `$left = 'xxxxx'` 47 | 48 | 对于3情况,可以直接将`$left`的污染情况置为false; 对于1、2,则可以抽象成当右操作数中存在受污染的变量,则左操作数也受污染。 49 | 50 | #### 函数间传播 51 | 52 | 例如: 53 | ```php 54 | public function wsECXy($dmOxI){ 55 | for($i = 0; $i < 37; $i ++){ 56 | $arNzsS= $dmOxI; 57 | } 58 | if(method_exists($this->qhBmwZR, 'mrXAvd')) $this->qhBmwZR->mrXAvd($dmOxI); 59 | if(method_exists($this->qhBmwZR, 'AovQfA')) $this->qhBmwZR->AovQfA($dmOxI); 60 | 61 | } 62 | ``` 63 | 64 | 遇到call site时,判断参数是否受污染,如果受污染,就将目标函数加入到worklist中,反之则无视发生。 65 | 66 | 如果遇到eval,如果参数受污染,则打印当前调用栈,因为只需找到一条pop链即可,打印完就可以退出。 67 | 68 | #### 其他语句 69 | 70 | 本题中,只涉及到assign、call、eval需要具体分析,所以其他语句可直接利用递归下降的方法来寻找目标语句分析。 71 | 72 | 73 | ### 运行 74 | 75 | ```bash 76 | go build -o popmaster cmd/popmaster/popmaster.go 77 | 78 | ./popmaster -file=class.php.txt -class=xd4a55 -method=Dvickz 79 | ``` 80 | 81 | 得到: 82 | 83 | ![image.png](https://i.loli.net/2021/07/13/orpwztVsnHykuG5.png) 84 | 85 | 与答案一致: 86 | 87 | ![image.png](https://i.loli.net/2021/07/13/fZ9RN6oaYtQ2yLJ.png) 88 | 89 | ## 后话 90 | 91 | 因为这题的限制挺多的,所以适合拿来练手。真实的白盒场景其实挺复杂的,希望有越来越多的人能够做白盒,一起交流一起进步。 92 | 93 | ## 相关文章 94 | 95 | [强网杯[pop_master]与[陀那多]赛题的出题记录](https://www.anquanke.com/post/id/244153) 96 | 97 | [第五届强网杯线上赛冠军队 WriteUp - Web 篇](https://mp.weixin.qq.com/s/Y9HdvGtGkr3JCP__pZwWqw) 98 | 99 | [pop_master的花式解题思路](https://www.freebuf.com/articles/web/279680.html) -------------------------------------------------------------------------------- /cmd/popmaster/popmaster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/LuckyC4t/pop-master-go/internal/parser" 6 | "github.com/LuckyC4t/pop-master-go/internal/tracer" 7 | "github.com/LuckyC4t/pop-master-go/internal/utils" 8 | "github.com/z7zmey/php-parser/node/stmt" 9 | "log" 10 | ) 11 | 12 | func main() { 13 | classFile := flag.String("file", "", "class file path") 14 | entryClass := flag.String("class", "", "entry class name") 15 | entryMethod := flag.String("method", "", "entry method name") 16 | 17 | flag.Parse() 18 | 19 | if *classFile == "" || *entryClass == "" || *entryMethod == "" { 20 | flag.Usage() 21 | return 22 | } 23 | 24 | rootNode, errs := parser.ParsePhpFile(*classFile) 25 | if len(errs) > 0 { 26 | for _, e := range errs { 27 | log.Println(e) 28 | } 29 | return 30 | } 31 | 32 | // init class list 33 | utils.ClsList = make(map[string]map[string]*stmt.ClassMethod) 34 | utils.GetAllClass(rootNode) 35 | 36 | entry := utils.ClsList[*entryClass][*entryMethod] 37 | utils.WorkList = append(utils.WorkList, utils.ClsMethod{ 38 | Cls: *entryClass, 39 | Method: entry, 40 | }) 41 | 42 | // 进行遍历 43 | tracer.Travel() 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LuckyC4t/pop-master-go 2 | 3 | go 1.16 4 | 5 | require github.com/z7zmey/php-parser v0.7.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 2 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= 6 | github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE= 7 | github.com/z7zmey/php-parser v0.7.2 h1:hnSNxn6tqK3n8JrevuBRVSI856v4yUJWgTonUVps5zA= 8 | github.com/z7zmey/php-parser v0.7.2/go.mod h1:r03mwVJvNhQKrTqKFzK0MIepU1uO62Z0p9ES3A7KTu4= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 11 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 12 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 14 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 15 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 16 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 20 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 21 | golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 22 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 26 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 27 | -------------------------------------------------------------------------------- /internal/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "github.com/z7zmey/php-parser/node" 6 | "github.com/z7zmey/php-parser/php7" 7 | "os" 8 | ) 9 | 10 | func ParsePhpFile(src string) (node.Node, []error) { 11 | content, err := os.ReadFile(src) 12 | if err != nil { 13 | return nil, []error{err} 14 | } 15 | 16 | parser := php7.NewParser(content, "7.4") 17 | parser.Parse() 18 | 19 | parserErrs := parser.GetErrors() 20 | if len(parserErrs) != 0 { 21 | errs := make([]error, len(parserErrs)) 22 | for i, e := range parserErrs { 23 | errs[i] = errors.New(e.String()) 24 | } 25 | return nil, errs 26 | } 27 | 28 | rootNode := parser.GetRootNode() 29 | return rootNode, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/tracer/trace.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/LuckyC4t/pop-master-go/internal/utils" 6 | "github.com/z7zmey/php-parser/node" 7 | "github.com/z7zmey/php-parser/node/expr" 8 | "github.com/z7zmey/php-parser/node/expr/assign" 9 | "github.com/z7zmey/php-parser/node/stmt" 10 | "os" 11 | ) 12 | 13 | func trace(n node.Node, pollution map[string]bool) map[string]bool { 14 | switch n.(type) { 15 | case *assign.Assign: 16 | pollution = traceAssign(n, pollution) 17 | case *expr.Eval: 18 | pollution = traceEval(n, pollution) 19 | case *expr.MethodCall: 20 | pollution = traceMethodCall(n, pollution) 21 | case *stmt.Expression: 22 | pollution = traceExprssion(n, pollution) 23 | case *stmt.For: 24 | pollution = traceFor(n, pollution) 25 | case *stmt.If: 26 | pollution = traceIf(n, pollution) 27 | case *stmt.StmtList: 28 | pollution = traceStmtList(n, pollution) 29 | } 30 | 31 | return pollution 32 | } 33 | 34 | func traceEval(n node.Node, pollution map[string]bool) map[string]bool { 35 | eval := n.(*expr.Eval) 36 | arg := utils.GetVarName(eval.Expr) 37 | if pollution[arg] { 38 | for _, call := range utils.CallStack { 39 | fmt.Println("->", call) 40 | } 41 | // 只用找到一条有效路径就够了 42 | os.Exit(0) 43 | } 44 | return pollution 45 | } 46 | 47 | func traceAssign(n node.Node, pollution map[string]bool) map[string]bool { 48 | a := n.(*assign.Assign) 49 | leftName := utils.GetVarName(a.Variable) 50 | // right 51 | for _, name := range utils.ResolveVarName(a.Expression) { 52 | if pollution[name] { 53 | pollution[leftName] = true 54 | return pollution 55 | } 56 | } 57 | 58 | pollution[leftName] = false 59 | return pollution 60 | } 61 | 62 | func traceIf(n node.Node, pollution map[string]bool) map[string]bool { 63 | ifStmt := n.(*stmt.If) 64 | // condition 65 | pollution = trace(ifStmt.Cond, pollution) 66 | 67 | // stmt 68 | pollution = trace(ifStmt.Stmt, pollution) 69 | 70 | return pollution 71 | } 72 | 73 | func traceStmtList(n node.Node, pollution map[string]bool) map[string]bool { 74 | sl := n.(*stmt.StmtList) 75 | for _, st := range sl.Stmts { 76 | pollution = trace(st, pollution) 77 | } 78 | return pollution 79 | } 80 | 81 | func traceExprssion(n node.Node, pollution map[string]bool) map[string]bool { 82 | e := n.(*stmt.Expression) 83 | pollution = trace(e.Expr, pollution) 84 | return pollution 85 | } 86 | 87 | func traceFor(n node.Node, pollution map[string]bool) map[string]bool { 88 | forStmt := n.(*stmt.For) 89 | // init 90 | for _, e := range forStmt.Init { 91 | pollution = trace(e, pollution) 92 | } 93 | // cond 94 | for _, e := range forStmt.Cond { 95 | pollution = trace(e, pollution) 96 | } 97 | // loop 98 | for _, e := range forStmt.Loop { 99 | pollution = trace(e, pollution) 100 | } 101 | 102 | // stmts 103 | pollution = trace(forStmt.Stmt, pollution) 104 | 105 | return pollution 106 | } 107 | 108 | func traceMethodCall(n node.Node, pollution map[string]bool) map[string]bool { 109 | mc := n.(*expr.MethodCall) 110 | // arg 111 | arg := utils.GetVarName(mc.ArgumentList.Arguments[0].(*node.Argument).Expr) 112 | if pollution[arg] { 113 | // find target 114 | targetName := utils.GetVarName(mc.Method) 115 | for cls, methods := range utils.ClsList { 116 | for name, method := range methods { 117 | if name == targetName { 118 | // 找到目标,加入worklist 119 | clsM := utils.ClsMethod{ 120 | Cls: cls, 121 | Method: method, 122 | } 123 | utils.WorkList = append(utils.WorkList, clsM) 124 | } 125 | } 126 | } 127 | } 128 | 129 | return pollution 130 | } 131 | -------------------------------------------------------------------------------- /internal/tracer/travel.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "fmt" 5 | "github.com/LuckyC4t/pop-master-go/internal/utils" 6 | "github.com/z7zmey/php-parser/node" 7 | "github.com/z7zmey/php-parser/node/stmt" 8 | ) 9 | 10 | func Travel() { 11 | for len(utils.WorkList) > 0 { 12 | l := len(utils.WorkList) 13 | 14 | // pop 15 | current := utils.WorkList[0] 16 | utils.WorkList = utils.WorkList[1:] 17 | // push stack 18 | utils.CallStack = append(utils.CallStack, fmt.Sprint(current)) 19 | 20 | // 函数内污点传播 21 | param := utils.GetVarName(current.Method.Params[0].(*node.Parameter).Variable) 22 | pollution := map[string]bool{ 23 | param: true, // 如果能到达当前函数,说明参数可控 24 | } 25 | 26 | // trace stmts 27 | for _, st := range current.Method.Stmt.(*stmt.StmtList).Stmts { 28 | pollution = trace(st, pollution) 29 | if len(utils.WorkList) >= l { 30 | // 说明有新的函数 31 | Travel() 32 | } 33 | } 34 | 35 | // 当前函数分析完毕,离开 36 | utils.CallStack = utils.CallStack[:len(utils.CallStack)-1] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/utils/datacontainer.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/z7zmey/php-parser/node/stmt" 4 | 5 | type ClsMethod struct { 6 | Cls string 7 | Method *stmt.ClassMethod 8 | } 9 | 10 | var ClsList map[string]map[string]*stmt.ClassMethod 11 | 12 | var WorkList []ClsMethod 13 | 14 | var CallStack []string 15 | 16 | func (cm ClsMethod) String() string { 17 | return cm.Cls + "." + GetVarName(cm.Method.MethodName) 18 | } 19 | -------------------------------------------------------------------------------- /internal/utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/z7zmey/php-parser/node" 5 | "github.com/z7zmey/php-parser/node/expr" 6 | "github.com/z7zmey/php-parser/node/expr/binary" 7 | "github.com/z7zmey/php-parser/node/stmt" 8 | ) 9 | 10 | func GetAllClass(rootNode node.Node) { 11 | root := rootNode.(*node.Root) 12 | 13 | for _, s := range root.Stmts { 14 | if class, ok := s.(*stmt.Class); ok { 15 | clsName := class.ClassName.(*node.Identifier).Value 16 | if _, has := ClsList[clsName]; !has { 17 | ClsList[clsName] = make(map[string]*stmt.ClassMethod) 18 | } 19 | 20 | for _, m := range class.Stmts { 21 | if clsMethod, ok := m.(*stmt.ClassMethod); ok { 22 | methodName := clsMethod.MethodName.(*node.Identifier).Value 23 | ClsList[clsName][methodName] = clsMethod 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | func GetVarName(n node.Node) string { 31 | switch n.(type) { 32 | // this->xxx 33 | case *expr.PropertyFetch: 34 | varName := GetVarName(n.(*expr.PropertyFetch).Variable) 35 | property := GetVarName(n.(*expr.PropertyFetch).Property) 36 | return varName + "." + property 37 | // $xxx 38 | case *expr.Variable: 39 | return GetVarName(n.(*expr.Variable).VarName) 40 | // xxxx() 41 | case *node.Identifier: 42 | return n.(*node.Identifier).Value 43 | 44 | } 45 | return "" 46 | } 47 | 48 | // 题目中等号右边存在变量只有两种情况 49 | func ResolveVarName(n node.Node) []string { 50 | switch n.(type) { 51 | case *expr.Variable: 52 | return []string{GetVarName(n)} 53 | case *binary.Concat: 54 | concat := n.(*binary.Concat) 55 | left := ResolveVarName(concat.Left) 56 | right := ResolveVarName(concat.Right) 57 | return append(left, right...) 58 | } 59 | 60 | return []string{} 61 | } 62 | --------------------------------------------------------------------------------