본문 바로가기
Hack/Web

[go-and-dont-return] Github Security Lab CodeQL CTF

by Becoming a Hacker 2023. 8. 21.
반응형

문제 요약

CTF Link : https://securitylab.github.com/ctf/go-and-dont-return/

 

MinIO는 Amazon S3 호환 객체 저장소로, 2023년 4월 MinIO Admin API에서 Authentication Bypass 취약점이 발견되었습니다.

 

해당 취약점은 Admin Access Key를 알고 있다면, Secret Key을 모르는 상태에서도 Admin API 작업을 수행할 수 있는 취약점으로 CVE-2020-11012를 할당받았습니다.

 

취약점이 조치된 Commit을 보면, 에러가 발생했을 때 Return이 누락된 것을 볼 수 있습니다.

func validateAdminSignature(ctx context.Context, r *http.Request, region string) (auth.Credentials, map[string]interface{}, bool, APIErrorCode) {
	var cred auth.Credentials
	var owner bool
	s3Err := ErrAccessDenied
	if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
		getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
		// We only support admin credentials to access admin APIs.
		cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
		if s3Err != ErrNone {
			return cred, nil, owner, s3Err
		}
		// we only support V4 (no presign) with auth body
		s3Err = isReqAuthenticated(ctx, r, region, serviceS3)
	}
	if s3Err != ErrNone {
		reqInfo := (&logger.ReqInfo{}).AppendTags("requestHeaders", dumpRequest(r))
		ctx := logger.SetReqInfo(ctx, reqInfo)
		logger.LogIf(ctx, errors.New(getAPIError(s3Err).Description), logger.Application)
		+ return cred, nil, owner, s3Err // 조치 코드
	}

 

이러한 사소한 실수가 코드 검토 과정 중에 검출되지 않았고, 이에 따라 해당 취약점이 발견되었습니다. CodeQL을 통해 이러한 실수들을 찾는 것이 목표입니다.


기본 환경 셋팅

1. CodeQL 설치 방법은 아래의 링크를 참고해주세요.

 

CodeQL 설치 및 사용 방법

CodeQL 설치 방법 ※ 아래의 설치 방법은 Linux 및 Windows의 설치 방법이며, Mac은 이 링크를 참고 1. CodeQL CLI 설치를 위해 CodeQL Bundle을 다운로드합니다. 해당 Bundle에는 아래의 프로그램이 포함됩니다.

hacksms.tistory.com

 

2. vscode-codeql-starter Repository를 다운로드 받습니다.

$ git clone https://github.com/github/vscode-codeql-starter.git

이때 반드시 하위 모듈을 git submodule update --init --remot 로 다운로드 받아줘야 합니다.

$ git submodule update --init --remote

 

3. 패치 전 MinIO에 대한 CodeQL Database를 다운로드 받습니다.

 

4. codeql-custom-queries-go\example.ql 파일을 통해 CodeQL 환경 셋팅이 완료되었는 지 테스트해봅니다.


CodeQL로 취약점 찾기

1. 버그 찾기

먼저 취약점을 찾기 위해서는 취약점의 근본 원인을 이해해야 합니다. CVE-2020-11012는 ErrNone 변수를 테스트하고 return 문을 반환하지 않는 Block에서 발생했습니다.

 

1.1. ErrNone에 대한 모든 Reference 찾기

Hint

- Query의 기본 포맷은 아래와 같음

- 참고 : https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-go/

- 정답은 231개의 결과를 반환해야 함

import go
from <variable_type> <variable_name> // this is the declaration
    where <filter>
    select <variable_name>

 

Write Up : 231개 결과 반환

import go

from Ident i
    where i.toString()="ErrNone"
    select i

 

1.2. ErrNone에 대한 Equality Test Case 찾기

Hint

- 참고 : https://codeql.github.com/docs/ql-language-reference/expressions/#casts

- 정답은 158개의 결과를 반환해야 함

 

Write Up : 158개 결과 반환

- ident와 엮어서 사용하고 싶었는데.. 어떻게 해야될 지를 모르겠네요

import go

from BinaryExpr binary
    where binary.getOperator().matches("_=") and
        binary.getLeftOperand().toString() = "ErrNone" or
        binary.getRightOperand().toString()= "ErrNone"
    select binary

 

1.3. "1.2" 테스트를 수행하는 if-blocks 찾기

Hint

- 참고 : https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-go/#statements

- 정답은 133개 결과를 반환해야 함

 

Write Up : 133개 결과 반환

import go

from BinaryExpr binary, IfStmt ifstmt
    where binary.getParent() = ifstmt and
        (binary.getLeftOperand().toString() = "ErrNone" or binary.getRightOperand().toString()= "ErrNone" ) 
    select binary, ifstmt

 

1.4. 모든 Return Statements 찾기

Hint

- 정답은 10,651개 결과를 반환해야 함

 

Wirte Up : 10,651개 결과 반환

import go

from ReturnStmt return
    select return

 

1.5. Return이 없는 if Blocks 찾기

Hint

- instanceof를 사용하여 variable의 type check를 수행할 수 있음

- 정답은 3,541개 결과를 반환해야 함

 

Write Up : 3,541개 결과 반환

import go

from ReturnStmt return, IfStmt ifstmt
    where not ifstmt.getThen().getAChildStmt() instanceof ReturnStmt
    select ifstmt

 

1.6. 취약점 찾기

Hint

- 1.3과 1.5을 결합하여 Return이 없으면서 If-block 내 ErrNone의 Equility가 존재하는 케이스 찾기

- 정답은 7개 결과를 반환해야 함

 

Write Up : 7개 결과 반환

import go

from BinaryExpr binary, IfStmt ifstmt
    where not ifstmt.getThen().getAChildStmt() instanceof ReturnStmt and
    binary.getParent() = ifstmt and (binary.getAnOperand().toString() = "ErrNone")
    select ifstmt

 

2. 고도화

존재하고 있던 취약점은 찾았지만, False positives도 존재하였습니다.

1. 치명적이지 않은 Error

2. writeErrorResponseJSON 응답 값 반환

3. Error에 직/간접적으로 응답하는 경우 

 

조금 더 코드를 고도화할 수 있는 방법 중 하나는 실제 취약점이 발생하는 구간인 isReqAuthenticated의 반환 코드가 존재하는 지 확인하는 방법입니다.

(해당 코드는 CodeQL의 Data Flow를 사용하면 된다고 함)

 

2.1. isReqAuthenticated를 호출한 이후 발생하는 조건문 찾기

Hint

- 모든 Equality Test 피연산자를 추적하는 DataFlow Configuration을 작성하여, 임의의 호출에서 isReqAuthenticated로 가는 데이터 흐름을 찾습니다.

참고 : https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-go/#global-data-flow-and-taint-tracking

- Query는 모든 EqualityTest를 선택(Type : DataFlow:EqualityTestNode)해야 하며, 여기서 피연산자는 위 구성의 Sink 입니다.

- any aggregate를 배우면 좋다

- 정답은 64개 결과를 반환해야 함

 

Write Up : 64개 결과 반환

- where 구문의 "_"는 자동으로 Entity를 채워준다고 함 / predicate hasFlow ( Node source , Node sink )

- 결과를 보면 CheckAuthType 또는 isReqAuthenticated의 반환 값을 비교하는 로직만을 찾은 것을 볼 수 있음

CheckAuthType 로직을 따라가다보면, isReqAuthenticated가 호출될 수 있어 같이 찾은 것으로 보임

import go

class ReqAuthConfig extends DataFlow::Configuration{
    ReqAuthConfig() { this = "TestConfig"}

    override predicate isSource(DataFlow::Node source) {
        source.asExpr().getAChildExpr().toString() ="isReqAuthenticated"
    }

    override predicate isSink(DataFlow::Node sink) {
        sink = any(DataFlow::EqualityTestNode n).getAnOperand()
    }
}

from ReqAuthConfig c, DataFlow::Node sink, DataFlow::EqualityTestNode compare
where c.hasFlow(_, sink) and compare.getAnOperand()=sink
select compare

 

2.2. 진짜 버그 찾기

Hint

- 1.6과 .21을 결합하여 아래의 조건에 충족하는 모든 IF문을 찾을 수 있습니다.

  • 2.1에서 반환된 Equality Test 결과 중 하나
  • ErrNone에 대한 Equality Test 결과 중 하나
  • 다음 분기에서 return이 존재하지 않는 결과 중 하나

- 정답은 1개 결과를 반환해야 함

 

Write Up : 1개 결과 반환

import go

class ReqAuthConfig extends DataFlow::Configuration{
    ReqAuthConfig() { this = "TestConfig"}

    override predicate isSource(DataFlow::Node source) {
        source.asExpr().getAChildExpr().toString() ="isReqAuthenticated"
    }

    override predicate isSink(DataFlow::Node sink) {
        sink = any(DataFlow::EqualityTestNode n).getAnOperand()
    }
}

from ReqAuthConfig c, DataFlow::Node sink, DataFlow::EqualityTestNode compare, BinaryExpr binary, IfStmt ifstmt
where not ifstmt.getThen().getAChildStmt() instanceof ReturnStmt and
binary.getParent() = ifstmt and (binary.getAnOperand().toString() = "ErrNone")
and c.hasFlow(_, sink) and compare.getAnOperand()=sink
and compare.getExpr() = ifstmt.getAChildExpr()
select compare

 

2.3. 최종 점검

- MinIO의 패치된 버전의 Database에서도 Query를 날려보자!

- 정상적인 Query라면, 조치된 버전에서는 결과가 반환되지 않아야 함

- 패치된 버전의 Database :  https://github.com/github/securitylab/releases/download/ctf-go-and-dont-return/minio-db-2020-11012-fixed.zip

 

Write Up : 결과 0개 반환

 

3. 쿼리 확장

- 지금까지 작성한 Query는 특정 버전의 MinIO에서는 작동하지만, 아직은 많이 부족합니다.

- 그래서 우리는 몇 가지 다른 이슈가 포함된 저장소를 만들었습니다. Query를 개선하여 많은 버그와 최소한의 오탐을 식별하도록 노력해주세요.

새 Database : https://github.com/github/securitylab/releases/download/ctf-go-and-dont-return/go-minio-ctf-extensions.zip

- 새 Database는 새로운 프로젝트이기 때문에 Query를 변경해야 합니다.

  • errorSource를 error Value를 확인할 Source로 지정합니다. (isReqAuthenticated 와 유사)
  • ErrNone은 오류가 없음을 나타내는 유일한 값과 동일한 패키지에 존재합니다.? (ErrNone in the same package as the unique value indicating no error.)

 

3.1. 조건 극성(polarity)

Hint

- 1.6에서 알아챘을 수 있지만, 우리의 코드는 ==와 !=의 Equality Test를 포함하며, 두 경우 모두 then Block을 확인합니다.

그러나 사실은 if 조건에 따라 then 또는 else BlocK을 확인하는 것이 올바른 로직입니다.

- 이 문제를 해결하기 위해 Query를 수정해주세요. conditionalPolarities.go 파일의 모든 Bad Example를 탐지할 수 있어야 합니다.

- predicate EqualityTestExpr.getPolarity를 확인해보세요!

 

Write Up : Bad 3개 반환

- 흠..predicate EqualityTestExpr.getPolarity를 써서 뭘 어쩌라는 건 지 모르겠네요. 일단 여기서 패스하겠습니다.

import go

class ReqAuthConfig extends DataFlow::Configuration{
    ReqAuthConfig() { this = "TestConfig"}

    override predicate isSource(DataFlow::Node source) {
        source.asExpr().getAChildExpr().toString() ="errorSource"
    }

    override predicate isSink(DataFlow::Node sink) {
        sink = any(DataFlow::EqualityTestNode n).getAnOperand()
    }
}

BlockStmt getErrorBranch(IfStmt ifstmt) {
    if ifstmt.getCond().(BinaryExpr).getOperator() = "==" then result = ifstmt.getElse() else result = ifstmt.getThen()
}

from ReqAuthConfig c, DataFlow::Node sink, DataFlow::EqualityTestNode compare, BinaryExpr binary, IfStmt ifstmt, EqualityTestExpr eqtest
where not getErrorBranch(ifstmt).getAStmt() instanceof ReturnStmt
and binary.getParent() = ifstmt and (binary.getAnOperand().toString() = "ErrNone")
and c.hasFlow(_, sink) and compare.getAnOperand()=sink
and compare.getExpr() = ifstmt.getAChildExpr()
and ifstmt.getFile().toString()="/Users/chris/codeql-ctf-go-return/conditionalPolarities.go"
select ifstmt

 

3.2. 더 많은 블록 검색

- Return이 없는 더 많은 블록을 탐지해봅시다. 예를 들어 else 구문에서의 Return 문이나 다른 케이스(switch/case)를 같이 처리해야 되는 경우가 있을 수 있습니다.

- 이번에도 Query를 수정해야 하고 moreWaysToReturn.go에 있는 Bad Example을 모두 탐지하세요.

Hint

- if Block 내부의 제어 흐름을 재귀적으로 검사할 수도 있지만, control-flow graph을 사용하는 것이 더 효율적입니다. 그리고  IR::ReturnInstruction Class 문서를 확인해보십시오. 해당 Class는 Return Statment에 일치하는 control-graph-node를 의미합니다. 또 해당 Class의 상위 Class인 ContractFlow::Node의 getAPredecessor() 와 getASuccssor() 메서드를 같이 확인하세요.이 메서드들은 Control-flow-graph edges를 탐색하는데 사용됩니다.

- Pass or Fail하는 if Test는 항상 어떤 분기가 선택 되었는 지 나타내는 ConditionGuardNode 뒤에 따라옵니다.

참고 : 다음과 같은 Query를 이용하여 Control flow graph이 어떻게 작동하는 지 확인할 수 있습니다.

from ControlFlow::Node pred, ControlFlow::Node succ 
    where succ = pred.getASuccessor() // you can also restrict `pred` to come from a particular source file
    select pred, succ

 

Write Up :  2개 결과 반환

- Key Point는 if든 else든 switch든 Return이 존재하는 지 검증하는 것이었습니다.

- 이번 문제도  IR::ReturnInstruction을 안썼고, 편법을 쓴 느낌이라 출제자가 원하는 방식으로 푼 건 아닌 것 같았습니다.

- 아무튼 이번 Query 의도는 err가 존재하면서(err!=ErrNone), Return이 존재하지 않는다면, 최종적으로 doSomething 함수가 실행될 것이기 때문에 ControlFlow를 재귀적으로 호출해 최종적으로 Return이 존재하지 않아 doSomething 함수가 실행된 Flow의 이전 Flow(Vulnerable Point)를 탐색하도록 했습니다.

  • 처음에는 재귀적으로 호출을 하면 안된다고 생각을 했는데, CFG를 제대로 쓰려면 재귀적으로 호출이 필요하다고 생각을 해서, 이렇게 Query를 짰습니다.
import go

ControlFlow::Node getNotReturn(ControlFlow::Node pred, ControlFlow::Node succ){
    if pred.isBranch() then succ = getNotReturn(pred, succ) else succ = pred.getASuccessor()
    and succ.getAQlClass() != "ReturnInstruction"
    and (succ.toString()="doSomething" and succ.getAPredecessor() = pred)
}

from ControlFlow::Node pred, ControlFlow::Node succ, Stmt stmt, ControlFlow::ConditionGuardNode cgnode
    where succ = getNotReturn(pred, succ)
    and (cgnode.getCondition().(BinaryExpr).getAnOperand().toString()="ErrNone" and cgnode.getCondition().(BinaryExpr).getOperator()="!=")
    and not pred instanceof ControlFlow::ConditionGuardNode
    and succ.getFile().toString()="/Users/chris/codeql-ctf-go-return/moreWaysToReturn.go"
    and pred.getFile().toString()="/Users/chris/codeql-ctf-go-return/moreWaysToReturn.go"
    and cgnode.getFile().toString()="/Users/chris/codeql-ctf-go-return/moreWaysToReturn.go"
    select pred, succ

 

3.3. Wrapped conditionals

- ErrNone에 대한 Equlity Test가 조건문에서 직접적으로 사용되지 않고, Utility 함수 내부에 존재하는 경우를 탐지하고자 합니다.

Query는 wrapperFunctions.go에서 발생하는 모든 Bad Example을 탐지할 수 있어야 합니다.

Hint

- Wrap 안에 여러 개의 Layer를 넣을 수 있습니다.

- CallExpr::getTaret(),  DataFlow::CallNode::getTarget(), Function::getFuncDecl()으로 callsite와 callee를 탐색하는 방법을 찾아보십시오.

 

Write Up : 4개 결과 반환

- 점점 산으로 가는 것 같은데... 일단 패스하겠습니다.

import go

BlockStmt getWrappedCond(Expr expr, IfStmt ifstmt){
    "ErrNone" = expr.(BinaryExpr).getAnOperand().toString()
    and if expr.(BinaryExpr).getOperator() = "!=" then result = ifstmt.getThen() else result = ifstmt.getElse()
}

from CallExpr cex, IfStmt ifstmt, DataFlow::EqualityTestNode compare
    where ifstmt.getFile().toString()="/Users/chris/codeql-ctf-go-return/wrapperFunctions.go" 
    and compare.getFile().toString()="/Users/chris/codeql-ctf-go-return/wrapperFunctions.go" 
    and compare.getExpr().getEnclosingFunction()  = cex.getTarget().getFuncDecl()
    and cex = ifstmt.getAChildExpr()
    and not getWrappedCond(cex.getTarget().getBody().getAChildStmt().getAChildExpr(), ifstmt).getAStmt() instanceof ReturnStmt
    select ifstmt

 

3.4. More conditionals

- 지금까지의 코드는 간단한 Equlity Test에 대해서만 작동하고 있습니다. 하지만 실제 코드에서는 !, &&, ||와 관련된 조건이 포함될 수 있습니다.

- logicalOperators.go에 존재하는 모든 Bad Example을 탐지할 수 있어야 합니다.

Hint

- ControlFlow::ConditionGuardNode를 확인해보십시오. 해당 Node는 Binary Logical Operators(!, &&, ||)를 포함하여 Passed 또는 Failed에 대한 결과를 표시합니다.

- control-flow graph에 대한 자세한 내용은 3.2를 참고하세요.

새 Database : https://github.com/github/codeql-ctf-go-return

 

여기서부터는 예제를 이해할 수가 없네요? 아래와 같이 동일한 결과를 반환하는 코드가 Bad, Good으로 나눠지는 걸 볼 수 있습니다....??

func logicalAndThenBranchSometimesBad() {

	if errorSource() != ErrNone && someOtherCondition() {
		// Bad: there is a route from a positive error test around the 'return' statement.
		return
	}
	doSomething()

}

func logicalAndThenBranchGood() {

	if someOtherCondition() && errorSource() != ErrNone {
		// Good: whenever an error is indicated we return (note errorSource() is not called until someOtherCondition() passes)
		return
	}
	doSomething()

}

 

Good, Bad Example의 조건을 잘 모르겠어서...  3.4 ~ 3.5는 안풀고 넘어가겠습니다. Example이 잘못되었다는 것을 깨닫기 전까지 짜고 있던 코드는 아래와 같습니다.

import go

predicate getNotCond(IfStmt ifstmt, Expr expr){
    if expr.(NotExpr).getAnOperand().getAChildExpr().(BinaryExpr).getOperator()="==" then not ifstmt.getThen().getAStmt() instanceof ReturnStmt else not ifstmt.getElse().(BlockStmt).getAStmt() instanceof ReturnStmt
}

predicate getAmperCond(IfStmt ifstmt, Expr expr) {
    if expr.(LandExpr).getAnOperand().(BinaryExpr).getOperator()="!="
        then not ifstmt.getThen().getAStmt() instanceof ReturnStmt
        else not ifstmt.getElse().(BlockStmt).getAStmt() instanceof ReturnStmt
}

predicate getMoreCondition(IfStmt ifstmt, Expr expr){
    expr instanceof NotExpr and getNotCond(ifstmt, expr)
    or expr instanceof LandExpr and getAmperCond(ifstmt, expr)
    // or expr instanceof LorExpr and getPipeCond(ifstmt, expr)
}

from ControlFlow::Node pred, ControlFlow::Node succ, IfStmt ifstmt, ControlFlow::ConditionGuardNode cgnode
    where
    succ.getFile().toString()="/Users/chris/codeql-ctf-go-return/logicalOperators.go"
    and pred.getFile().toString()="/Users/chris/codeql-ctf-go-return/logicalOperators.go"
    and ifstmt.getFile().toString()="/Users/chris/codeql-ctf-go-return/logicalOperators.go"
    and cgnode.getFile().toString()="/Users/chris/codeql-ctf-go-return/logicalOperators.go"
    and getMoreCondition(ifstmt, ifstmt.getAChildExpr())
    select ifstmt.getAChildExpr()

이걸 써봤다고 해도 될 지는 모르겠지만, 처음 CodeQL을 써봤는데 생각보다 Query를 짜는 게 쉽지 않았습니다.

각각의 케이스에 대한 공개된 샘플 코드들이 많지 않았고 변경된 내역도 많은 것으로 보여 공부하는 데 삽질을 많이 하게 되는 것 같습니다.

 

그래도 복잡한 로직에서 발생할 수 있는 취약점을 적은 양의 Query로도 찾을 수 있다는 점은 엄청난 메리트인 것 같습니다. 아직 CodeQL의 기능을 10%도 제대로 사용하지 못했지만요...!

 

추후 CodeQL의 사용 방법에 익숙해지면, 일반적으로 사용할 수 있는 취약 패턴을 모델링하고 이를 통해 신규 취약점들을 찾아볼 계획입니다. 아마도..

 

Security Github에서 공개한 정답 링크도 달아놓겠습니다. 코드가 완전 다르네요 ㅎㅎ..

 

Answers & Feedback - GitHub Security Lab CTF: Go and don’t return

Securing the world’s software, together

securitylab.github.com


Reference

 

Capture the flag

Securing the world’s software, together

securitylab.github.com

 

CodeQL standard libraries

CodeQL standard libraries Browse the classes, predicates, and modules included in the standard CodeQL libraries in the most recent release of CodeQL, or search the library for a specific language.

codeql.github.com

 

반응형

댓글