skip to content
/
Research Advisories CodeQL Wall of Fame Events Get Involved
capture the flag
Go and don't return
Capture the flag Go and don't return
About this challenge
This CTF is now closed! You can still challenge yourself for fun! Check the reference answers
Start Date
March 1, 2021
Deadline
March 31, 2021
Language
Go
Difficulty

Looking for a vulnerability hunting challenge? Then this Capture The Flag challenge is for you! You will hone your bug finding skills and also learn about CodeQL's features. Take your chance at this challenge and you can be the winner of one of our prizes: The winner will get a 1-year subscription to Burp Suite Professional and the second and third placed participants will win streaming equipment (Blue Yeti X microphone and Logitech Brio Webcam).

Your mission, should you choose to accept it, is to hunt for a recently identified vulnerability in an object store. This authentication bypass vulnerability enabled attackers to perform admin API operations without knowing the admin secret key.

Using CodeQL, you'll learn how to detect this bug, and also how to generalize your query to catch a diverse range of related bugs.

This challenge is accessible to CodeQL beginners.

Prerequisite

  • Knowledge of the Go language

To complete this challenge, participants do not need prior knowledge of CodeQL; we’ll start with the basics and ramp-up slowly. However, there are plenty of CodeQL resources you can use to warm-up before this challenge if you prefer! You can try out

How to enter?

  • Create a secret GitHub gist or a private GitHub repository.
  • Submit your write-up, in your gist or in the README.md of your repo, or in another file of your repo.
  • You can add the responses either directly in the main write-up, or in separate files that you reference in your main write-up.
  • When you are ready to submit, just email ctf@github.com with the link to your gist or to your repo. (If you are using a private repo, first invite the user securitylab-ctf as a collaborator.)

Need help?

  • Your first stop should be the documentation. If you need more help on some CodeQL concepts, visit our forum. Be careful, though, don't give away your solution to other competitors! 😉
  • You can contact us at ctf@github.com
  • You can also join our Slack workspace.

Introduction

MinIO is an Amazon S3-compatible object store. In April 2020, the developers were alerted to a high severity security issue: an authentication bypass issue in the MinIO admin API. Given an admin access key, it was possible to perform admin API operations (e.g., creating new service accounts for existing access keys) without knowing the admin secret key. This permission-checking mistake was assigned CVE-2020-11012 and was fixed with this commit. MinIO published a GitHub Security Advisory to notify the open source ecosystem and ask them to upgrade,

In this CTF we'll detect the original mistake, then generalise our code to catch a diverse range of related bugs.

Challenge problem

As you can see in the fix commit, the problem is a missing return in an if s3Err != ErrNone { block. The function validateAdminSignature was then failing to return the result of the validation upstream. This is a simple mistake that can easily go undetected during code review, so let’s try to use CodeQL to automatically detect this kind of mistake, and then polish our query to only find those that really matter.

Setup instructions

CodeQL allows you to explore your code, by running queries on it as if it were data. The first step consists in analyzing the code and extracting information such as the AST into a relational database (the CodeQL database, that we’ll provide you). Once you have this database you’ll be able to query it with the CodeQL language, to explore your code.

  1. Install the Visual Studio Code IDE.
  2. Go to the CodeQL starter workspace repository, and follow the instructions in that repository's README. When you are done, you should have the CodeQL extension installed and the vscode-codeql-starter workspace open in Visual Studio Code.
  3. Download this CodeQL database of MinIO, corresponding to unpatched revision a5efcbab51cc7127fd4f3aa9eae8fbe89c98c9d1 and import it into VS Code (use Choose database from archive, do not choose directly from the URL). Check the documentation.
  4. Test by running the example.ql query that is in the codeql-custom-queries-go folder.

You are ready now to write your queries in the codeql-custom-queries-go folder.

Step 1: Let’s catch the bug!

First, let's put a query together to find blocks with our problem. We are looking for if blocks that test a variable against ErrNone and don’t contain a return statement. We’ll make baby steps through this search, to familiarize you with the concepts and the CodeQL Go library.

Note: As we are making baby steps, we sometimes ask you to write imperfect things that will be improved later. If you don’t get the right results, check the next steps, perhaps you went ahead and already implemented a future one.

Step 1.1: Finding references to ErrNone

In this first step, you’ll find all references to ErrNone in your code.

Your query should look like the example.ql query available in the codeql-custom-queries-go folder. The first line should be import go, which imports the CodeQL go library, and the body of the query is

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

Write the query that finds all identifiers named ErrNone. You will find in the documentation the relevant object types to query. Your query should return 231 results.

Tip: Use the features of the VSCode CodeQL extension: the auto-completion will give you a list of choices (for classes or predicates) as you start typing, and the inline documentation will tell you what each class represents, and what each predicate does.

Step 1.2: Finding equality tests against ErrNone

In this next step, write a query to find all equality test expressions where one of the operands is an identifier called ErrNone. Your query should give you 158 results.

Tip: Learn more in the documentation how to constrain the type of an expression to a specific type.

Step 1.3: Finding if-blocks making such a test

Write a query that finds all if statements, where the condition is an equality test similar as found in step 1.2. Your query should give you 133 results.

Tip: Search the documentation for the relevant statement type.

Step 1.4: Finding return statements

Write a query that finds all return statements. Your query should give 10,651 results.

Step 1.5: Finding if-blocks without return statements

Write a query that finds all if-blocks that don’t contain return statements in their then branch. Your query should return 3541 results. Remember, we are doing baby steps! We just care about the then branch for now!

Tip: You can perform a type check of your variable with instanceof.

Step 1.6: Putting it all together

Ok, time to find our bug! Combine steps 1.5 and 1.3 and write a query that finds the if-blocks testing for equality to ErrNone with no return.

You should get a total of 7 results. Check that the bug we're looking for is one of them.

Well done! You wrote a query that detects the bug! We hope you enjoyed this warm-up with CodeQL, as we’ll now continue with more complex concepts.

Step 2: Improving the precision

So we found the bug, but we can also see a few false positives: some deliberately-ignored non-fatal errors, some failures directly reported using writeErrorResponseJSON and related functions, and a few cases that respond directly, break from a loop, or use some other pattern to respond to an error. It’s good to be able to detect real bugs, but you might miss them if the results are too noisy. That is, if too many of the alerts are false positives.

One way we can be more precise is to only check return codes from isReqAuthenticated, which surely should not be ignored. We can do this using the data flow feature of CodeQL.

We recommend that you read more about data flow analysis in CodeQL, and how to write data flow queries in Go: local data flow and global data flow.

Step 2.1: Find conditionals that are fed from calls to isReqAuthenticated

Write a data flow configuration that tracks data flowing from any call to isReqAuthenticated to any equality test operand. Your query must select all equality tests -- Type: DataFlow::EqualityTestNode -- where the operand is a sink of the above configuration.

This gives us 64 potentially interesting conditionals to investigate. Note many of them are not direct calls to isReqAuthenticated, instead they test the result of some intermediate function which in turn calls isReqAuthenticated. The CodeQL global data flow analysis feature allows us to detect those.

Tip: Learn about the any aggregate.

Step 2.2: Find the true bug!

We can now put this dataflow query together with our query from step 1.6, and find all if statements that

  • Are one of the equality tests returned in 2.1
  • Are testing equality against ErrNone
  • Do not contain a return statement in their then branch

Bingo! One result, which is the bug we're looking for. No more false positives, no more noise.

Step 2.3: Final check

To make sure that your query can distinguish the bug, test it against the database for the fixed version of MinIO. It should show zero results. Download and import this database containing the fix into your VS code, and launch your query against it to check.

Step 3: Expanding the query

So we're done? Well, not quite -- this happens to work against this particular version of MinIO, but it's quite brittle, and in some cases incorrect. We've created a repository containing some other problems in the same spirit as the original MinIO CVE to illustrate the problems: try to improve your query to identify as many bugs and as few false-positives as possible. Import this new database containing all the problems in VS Code, to check your queries against it.

Note that you can solve these problems in any order, however the hints we are giving assume that you have read the previous ones.

Important: Because this is a different project, you will need to alter your query to recognise

  • errorSource as the source of error values to check (similar to isReqAuthenticated in minio), and
  • ErrNone in the same package as the unique value indicating no error.

Tips:

  • Re-test your code against the real MinIO, to check that your query doesn’t pick false-positives there. To do that, write your queries in a way they can run on both codebases, by recognizing both error sources.
  • We also encourage you to write extra tests to verify that your code is as general as possible. To do that, you’ll have to locally clone the repo, add your specific tests in there, build the database with GITHUB_REPOSITORY=github/codeql-ctf-go-return codeql database create &lt;your-database-directory> --language=go, and import this locally built database into VS Code. Note your database directory should be outside the repository.

Step 3.1: Conditional polarity

You might have noticed this in step 1.6: our code looking for equality tests encompases both x == ErrNone and x != ErrNone and checks the then block in both cases. This is wrong. It ought to check the "then" or "else" case of an if block, depending on which form of conditional is used. Modify your query to fix this problem. Your query should be able to detect all bad examples in conditionalPolarities.go.

Hint: Check out the predicate EqualityTestExpr.getPolarity

Step 3.2: More blocks

Let's detect more blocks that must return. For example, our query fails to detect a return statement in an else branch, and there are other such cases that we need to handle, such as cascading else or switch/case. Modify your query to find more blocks that don’t return. Your query should be able to detect all bad examples in moreWaysToReturn.go.

Hints:

  • While we could recursively inspect the control-flow structures inside the if block, it may help to use the control-flow graph. Check the documentation of the class IR::ReturnInstruction, a control-flow graph node corresponding to a return statement, and the getAPredecessor() / getASuccessor() methods of its superclass ControlFlow::Node, which traverse control-flow graph edges.
  • A passing or failing if test is always followed by a ConditionGuardNode that indicates which branch was taken.

Tip: Try creating a temporary query such as the one below to get an idea what the control flow graph looks like.

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
    

Step 3.3: Wrapped conditionals

Now we can have cases where our equality test against ErrNone is no longer directly used in a conditional statement, but is instead wrapped inside a utility function. Modify your query to handle this case. Your query should be able to detect all bad examples in wrapperFunctions.go.

Hint:

  • You can have several layers in your wrap!
  • Check out the predicates CallExpr::getTarget(), DataFlow::CallNode::getTarget() and Function::getFuncDecl() to navigate between a callsite and its callee.

Step 3.4: More conditionals

Our code works for simple equality tests, but there are cases where this test is part of a bigger test with conditionals involving !, &&, ||, that are not currently accounted for in our query. Improve your query to handle these cases. Your query should be able to detect all bad examples in logicalOperators.go.

Hint: Check out ControlFlow::ConditionGuardNode. This node flags a point in a control-flow graph where a particular test is known to have passed or failed, including those nested within the short-circuiting binary logical operators &&, ||. Its predicate ensures can already analyse some boolean expression structure. Even if you cannot use it directly, the implementation of ensures may be a useful inspiration for your solution. See the hints for Step 3.2 for more information about the control-flow graph.

Note: A contestant pointed out an inconsistency in the original version of the extension goals moreWaysToReturn.go and logicalOperators.go. logicalOperators.go considered paths that may circumvent a return statement as acceptable, while moreWaysToReturn.go wanted these maybe cases flagged as problems. In the updated version of the extension repository https://github.com/github/codeql-ctf-go-return we are considering moreWaysToReturn.go as correct and amending the good/bad labels in logicalOperators.go accordingly, also adding a few more test cases.

Step 3.5: Valid returns only

Ok, so now we make sure we return something when we check the permission. But is that enough? Just returning somehow isn't good enough, we may also need to return an appropriate value. The use of non-nil / nil error values is normal to indicate an error in Go, so let’s assume for this problem that non-nil is considered an appropriate return value. Modify your query to detect all bad examples in checkReturnValue.go.

Conclusion

That’s it, you made it! Congratulations!

Now you can submit your solutions, and you’ll hear from us around one week after the submissions deadline.

Did you like writing CodeQL to find security vulnerabilities? You can contribute to make open source more secure and get bounty rewards for it. Visit our bounty program.

To keep this community open and welcoming, please read our Code of Conduct.