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.
- Install the Visual Studio Code IDE.
- 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. - Download this
CodeQL database of MinIO, corresponding to unpatched revision
a5efcbab51cc7127fd4f3aa9eae8fbe89c98c9d1
and import it into VS Code (useChoose database from archive
, do not choose directly from the URL). Check the documentation. - Test by running
the
example.ql
query that is in thecodeql-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 toisReqAuthenticated
inminio
), andErrNone
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 <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 thegetAPredecessor() / getASuccessor()
methods of its superclassControlFlow::Node
, which traverse control-flow graph edges. - A passing or failing
if
test is always followed by aConditionGuardNode
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()
andFunction::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.